using MultiTerm.Wpf.ValueConverters; using System.Linq; using System.Collections; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; using System.Windows; using System.Windows.Controls; using System; namespace MultiTerm.Wpf.Controls; public class SingleSelectSubMenu : MenuItem { /// /// Internal marker for when internal unchecking is ongoing. Unchecking events shall then be ignored. /// private bool internalUncheckingOngoing = false; #region Static Properties private static readonly Dictionary RegisteredSubItemsAndParent = new(); #endregion #region Dependency Properties public static readonly DependencyProperty TitleProperty = DependencyProperty.Register("Title", typeof(string), typeof(SingleSelectSubMenu), new PropertyMetadata(String.Empty, OnTitleChanged)); public static readonly DependencyProperty OptionsSourceProperty = DependencyProperty.Register("OptionsSource", typeof(IEnumerable), typeof(SingleSelectSubMenu), new PropertyMetadata(null, OnOptionsSourceChanged)); public static readonly DependencyProperty SelectedMenuItemProperty = DependencyProperty.Register("SelectedMenuItem", typeof(object), typeof(SingleSelectSubMenu), new PropertyMetadata(null, OnSelectedMenuItemPropertyChanged)); public static readonly DependencyProperty AllowNoneSelectedProperty = DependencyProperty.Register("AllowNoneSelected", typeof(bool), typeof(SingleSelectSubMenu), new PropertyMetadata(false, OnAllowNoneSelectedPropertyChanged)); #endregion #region Dotnet Properties /// /// .NET Property for OptionsSource. /// [Bindable(true)] public IEnumerable OptionsSource { get { return (IEnumerable)GetValue(OptionsSourceProperty); } set { SetValue(OptionsSourceProperty, value); } } /// /// .NET Property for SelectedMenuItem. /// [Bindable(true)] public object SelectedMenuItem { get { return GetValue(SelectedMenuItemProperty); } set { SetValue(SelectedMenuItemProperty, value); } } /// /// .NET Property for Title. /// [Bindable(false)] public string Title { get { return (string)GetValue(TitleProperty); } set { SetValue(TitleProperty, value); } } /// /// .NET Property for AllowNoneSelected. /// [Bindable(false)] public bool AllowNoneSelected { get { return (bool)GetValue(AllowNoneSelectedProperty); } set { SetValue(AllowNoneSelectedProperty, value); } } #endregion /// /// SelectedMenuItem Property Changed Handler. /// Updates IsChecked Property of according menu item. /// private static void OnSelectedMenuItemPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { // extract instance and guard null if (d is not SingleSelectSubMenu sssm) { return; } // extract instance of new Value and guard null if (e.NewValue is not MenuItem menuItem) { return; } // get associated menu items (same group) var associatedMenuitems = GetAssociatedMenuItems(sssm); // check menu item with same name in same group foreach (var associatedItem in associatedMenuitems) { if (String.Compare(associatedItem.Header.ToString(), menuItem.Header.ToString(), true) == 0) { associatedItem.IsChecked = true; break; } } } /// /// Title Changed Handler. /// Disables SingleSelectSubMenu and sets header to title. /// private static void OnTitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { // extract instance and guard null if (d is not SingleSelectSubMenu sssm) { return; } sssm.Header = sssm.Title; sssm.IsEnabled = false; } /// /// Options Source Changed Handler. /// Builds list with options and adds them to parent ItemsControl (must be subtype of ItemsControl). /// Registers menu Items in locally stored list. /// Cannot handle changing OptionsSources. Internal list will build up. /// private static void OnOptionsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { // extract instance and guard null if (d is not SingleSelectSubMenu sssm) { return; } // extract parent instance of SSSM and guard null if (sssm.Parent is not ItemsControl parent) { throw new ArgumentException($"Wrong parent type."); } // iterate through new OptionsSource Values and build up list of menuItems foreach (var item in (IEnumerable)e.NewValue) { // create new menu item var converter = new EnumDescriptionToMenuItemConverter(); var newMenuItem = (MenuItem)converter.Convert(item, typeof(MenuItem), new object(), CultureInfo.CurrentCulture); newMenuItem.IsCheckable = true; newMenuItem.StaysOpenOnClick = true; // assign to event handler and register in dictionary newMenuItem.Checked += OnAnyItemChecked; newMenuItem.Unchecked += OnAnyItemUnchecked; RegisteredSubItemsAndParent.Add(newMenuItem, sssm); // add to parent (which is expected to be a menu item) parent.Items.Add(newMenuItem); } } /// /// Event handler that handles registered menu items being checked. /// /// MenuItem that was checked /// routed event args private static void OnAnyItemChecked(object sender, RoutedEventArgs e) { // extract sender menuItem and guard null if (sender is not MenuItem menuItem) { return; } // get associated menu items var associatedMenuitems = GetAssociatedMenuItems(menuItem); // get parent sssm var parentSssm = GetParentSingleSelectSubMenu(menuItem); parentSssm.internalUncheckingOngoing = true; foreach (var associatedItem in associatedMenuitems) { // uncheck items that are not null and not the sender item if (associatedItem != null && associatedItem != menuItem) { associatedItem.IsChecked = false; } } parentSssm.internalUncheckingOngoing = false; // update SelectedMenuItem for respective parent parentSssm.SelectedMenuItem = menuItem; } /// /// Event handler that handles registered menu items being unchecked. /// /// MenuItem that was checked /// routed event args private static void OnAnyItemUnchecked(object sender, RoutedEventArgs e) { // extract sender menuItem and guard null if (sender is not MenuItem menuItem) { return; } // get parent sssm var parentSssm = GetParentSingleSelectSubMenu(menuItem); // if automated unchecking is ongoing => ignore events if (parentSssm.internalUncheckingOngoing) { return; } // if unselection is not allowed => recheck if (parentSssm.AllowNoneSelected == false) { menuItem.IsChecked = true; } } /// /// Event handler that handles if the property AllowNoneSelected changed. /// /// sender /// routed event args /// private static void OnAllowNoneSelectedPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { // extract instance and guard null // if (d is not SingleSelectSubMenu sssm) { return; } // nothing to do } #region Helpers private static IEnumerable GetAssociatedMenuItems(SingleSelectSubMenu sssm) { List menuItems = new(); foreach(var item in RegisteredSubItemsAndParent.Where(x => x.Value == sssm)) { menuItems.Add(item.Key); } return menuItems; } private static IEnumerable GetAssociatedMenuItems(MenuItem menuItem) { List menuItems = new(); SingleSelectSubMenu parent = GetParentSingleSelectSubMenu(menuItem); foreach (var item in RegisteredSubItemsAndParent.Where(x => x.Value == parent)) { menuItems.Add(item.Key); } return menuItems; } private static SingleSelectSubMenu GetParentSingleSelectSubMenu(MenuItem menuItem) { return RegisteredSubItemsAndParent[menuItem]; } #endregion }