From 1c2e67df8d746443f86f5c514f12b8321ade536b Mon Sep 17 00:00:00 2001 From: Jonas Arnold Date: Sun, 26 Mar 2023 15:23:49 +0200 Subject: [PATCH] Finished implementation of SingleSelectSubMenu, removed MenuItemExtensions --- MultiTerm.Core/ViewModel/ShellViewModel.cs | 12 +- MultiTerm.Wpf/Controls/MenuItemExtensions.cs | 106 --------- MultiTerm.Wpf/Controls/SingleSelectSubMenu.cs | 224 ++++++++++++------ .../EnumDescriptionConverter.cs | 60 ----- .../EnumDescriptionToMenuItemConverter.cs | 144 +++++++++++ MultiTerm.Wpf/View/ShellView.xaml | 38 +-- 6 files changed, 321 insertions(+), 263 deletions(-) delete mode 100644 MultiTerm.Wpf/Controls/MenuItemExtensions.cs delete mode 100644 MultiTerm.Wpf/ValueConverters/EnumDescriptionConverter.cs create mode 100644 MultiTerm.Wpf/ValueConverters/EnumDescriptionToMenuItemConverter.cs diff --git a/MultiTerm.Core/ViewModel/ShellViewModel.cs b/MultiTerm.Core/ViewModel/ShellViewModel.cs index 0d32acb..43f644e 100644 --- a/MultiTerm.Core/ViewModel/ShellViewModel.cs +++ b/MultiTerm.Core/ViewModel/ShellViewModel.cs @@ -3,7 +3,6 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using MultiTerm.Core.Common; using System.Collections.ObjectModel; -using System.Runtime.CompilerServices; namespace MultiTerm.Core.ViewModel; @@ -59,4 +58,15 @@ public partial class ShellViewModel : ObservableObject Console.WriteLine($"Changed to {value}"); } + partial void OnSelectedSendNewlineSeparatorChanging(NewlineSeparatorType value) + { + Console.WriteLine($"Changed Send to {value}"); + } + + [RelayCommand] + public void TestButtonClicked() + { + this.SelectedReceiveNewlineSeparator = NewlineSeparatorType.CR_LF; + } + } diff --git a/MultiTerm.Wpf/Controls/MenuItemExtensions.cs b/MultiTerm.Wpf/Controls/MenuItemExtensions.cs deleted file mode 100644 index c527315..0000000 --- a/MultiTerm.Wpf/Controls/MenuItemExtensions.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Windows; -using System.Windows.Controls; - -namespace MultiTerm.Wpf.Controls -{ - /// - /// TODO ueberarbeiten - /// FROM: - /// https://stackoverflow.com/questions/3652688/mutually-exclusive-checkable-menu-items/11497189#11497189 - /// - public class MenuItemExtensions : MenuItem - { - private static MenuItem? previouslySelectedMenuItem = null; - public static Dictionary ElementToGroupNames = new Dictionary(); - - public static readonly DependencyProperty GroupNameProperty = - DependencyProperty.RegisterAttached("GroupName", - typeof(String), - typeof(MenuItemExtensions), - new PropertyMetadata(String.Empty, OnGroupNameChanged)); - - public static readonly RoutedEvent IsCheckedChangedEvent = - EventManager.RegisterRoutedEvent("IsCheckedChanged", - RoutingStrategy.Bubble, typeof(RoutedEventArgs), - typeof(MenuItemExtensions)); - - public event RoutedEventHandler IsCheckedChanged - { - add { this.AddHandler(IsCheckedChangedEvent, value); } - remove { this.RemoveHandler(IsCheckedChangedEvent, value); } - } - - public static void SetGroupName(MenuItem element, String value) - { - element.SetValue(GroupNameProperty, value); - } - - public static String GetGroupName(MenuItem element) - { - return element.GetValue(GroupNameProperty).ToString(); - } - - private static void OnGroupNameChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - //Add an entry to the group name collection - var menuItem = d as MenuItem; - var parent = menuItem.Parent as MenuItem; - - if (menuItem != null) - { - String newGroupName = e.NewValue.ToString(); - String oldGroupName = e.OldValue.ToString(); - if (String.IsNullOrEmpty(newGroupName)) - { - //Removing the toggle button from grouping - RemoveCheckboxFromGrouping(menuItem); - } - else - { - //Switching to a new group - if (newGroupName != oldGroupName) - { - if (!String.IsNullOrEmpty(oldGroupName)) - { - //Remove the old group mapping - RemoveCheckboxFromGrouping(menuItem); - } - ElementToGroupNames.Add(menuItem, e.NewValue.ToString()); - menuItem.IsCheckable = true; - menuItem.Checked += MenuItemChecked; - } - } - } - } - - private static void RemoveCheckboxFromGrouping(MenuItem checkBox) - { - ElementToGroupNames.Remove(checkBox); - checkBox.Checked -= MenuItemChecked; - } - - - static void MenuItemChecked(object sender, RoutedEventArgs e) - { - var menuItem = e.OriginalSource as MenuItem; - foreach (var item in ElementToGroupNames) - { - // uncheck all other menu items in group - if (item.Key != menuItem && item.Value == GetGroupName(menuItem)) - { - item.Key.IsChecked = false; - } - } - // raise routed event - var menuItemExtensions = menuItem as MenuItemExtensions; - if(previouslySelectedMenuItem != null && previouslySelectedMenuItem != menuItem) - { - RoutedEventArgs args = new RoutedEventArgs(IsCheckedChangedEvent); - menuItemExtensions.RaiseEvent(args); - } - previouslySelectedMenuItem = menuItem; - } - } -} \ No newline at end of file diff --git a/MultiTerm.Wpf/Controls/SingleSelectSubMenu.cs b/MultiTerm.Wpf/Controls/SingleSelectSubMenu.cs index dfc194f..aad2f3a 100644 --- a/MultiTerm.Wpf/Controls/SingleSelectSubMenu.cs +++ b/MultiTerm.Wpf/Controls/SingleSelectSubMenu.cs @@ -3,122 +3,206 @@ using System.Linq; using System.Collections; using System.Collections.Generic; using System.ComponentModel; -using System.Diagnostics; using System.Globalization; -using System.Runtime.CompilerServices; using System.Windows; using System.Windows.Controls; using System; -using System.Printing; namespace MultiTerm.Wpf.Controls; public class SingleSelectSubMenu : MenuItem { + #region Static Properties public static Dictionary RegisteredSubItemsAndParent = new Dictionary(); + #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), + typeof(IEnumerable), typeof(SingleSelectSubMenu), new PropertyMetadata(null, OnOptionsSourceChanged)); public static readonly DependencyProperty SelectedMenuItemProperty = DependencyProperty.Register("SelectedMenuItem", - typeof(object), - typeof(SingleSelectSubMenu), + typeof(object), typeof(SingleSelectSubMenu), new PropertyMetadata(null, OnSelectedMenuItemPropertyChanged)); + #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); } + } + #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 var sssm = d as SingleSelectSubMenu; - if (sssm != null) + if (sssm == null) { return; } + // extract instance of new Value and guard null + var menuItem = e.NewValue as MenuItem; + if (menuItem == null) { 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(e.NewValue != null) + if (String.Compare(associatedItem.Header.ToString(), menuItem.Header.ToString(), true) == 0) { - // get menu item with same name from registered items - MenuItem selectedMenuItem = null; - foreach (var item in RegisteredSubItemsAndParent) - { - if(String.Compare(item.Key.Header.ToString(), e.NewValue.ToString(), true) == 0) - { - selectedMenuItem = item.Key; - break; - } - } - // var selectedMenuItem = RegisteredSubItemsAndParent.Where(x => x.Key.Header == e.NewValue).FirstOrDefault(); - if(selectedMenuItem != null) - { - selectedMenuItem.IsChecked = true; - //OnAnyItemChecked(selectedMenuItem, new RoutedEventArgs()); - } + associatedItem.IsChecked = true; + break; } } } + /// + /// Title Changed Handler. + /// Hides SingleSelectSubMenu main entry in ItemsCollection (Visibility Collapsed). + /// Sets the header of the SingleSelectSubMenu instance to the Title, which helps to identify sssm while debugging. + /// + private static void OnTitleChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + // extract instance and guard null + var sssm = d as SingleSelectSubMenu; + if (sssm == null) + { + return; + } + + sssm.Visibility = Visibility.Collapsed; + sssm.Header = sssm.Title; + } + + /// + /// Options Source Changed Handler. + /// Builds list with options and adds them to parent MenuItem (must be Menu Item!). + /// 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 var sssm = d as SingleSelectSubMenu; - if(sssm != null) + if (sssm == null) { return; } + // extract parent instance of SSSM and guard null + var parent = sssm.Parent as MenuItem; + if (parent == null) { return; } + + // IMRPOVEMENT Delete currently associated values (using e.OldValue) from RegisteredSubItemsAndParent + // create and add title menu item + var titleMenuItem = new MenuItem { - if(e.NewValue != null) - { - var parent = sssm.Parent as MenuItem; - if(parent != null) - { - foreach (var item in (IEnumerable)e.NewValue) - { - var converter = new EnumDescriptionConverter(); - var newItem = new MenuItem() - { - Header = converter.Convert(item, typeof(object), null, CultureInfo.CurrentCulture), - IsCheckable = true - }; - newItem.Checked += OnAnyItemChecked; - RegisteredSubItemsAndParent.Add(newItem, sssm); - - parent.Items.Add(newItem); - Debug.WriteLine(item); - } - } - } + IsEnabled = false, + Header = sssm.Title + }; + parent.Items.Add(titleMenuItem); + + // 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; + + // assign to event handler and register in dictionary + newMenuItem.Checked += OnAnyItemChecked; + 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 var menuItem = sender as MenuItem; - if(menuItem != null) - { - Debug.WriteLine($"menuitem checked: {menuItem}"); + if (menuItem == null) { return; } + + // get associated menu items + var associatedMenuitems = GetAssociatedMenuItems(menuItem); - // uncheck others - foreach (var item in RegisteredSubItemsAndParent) + foreach (var associatedItem in associatedMenuitems) + { + // uncheck items that are not null and not the sender item + if (associatedItem != null && associatedItem != menuItem) { - if (item.Key != null && item.Key != menuItem) - { - item.Key.IsChecked = false; - } + associatedItem.IsChecked = false; } - - // set menu item for respective parent - RegisteredSubItemsAndParent[menuItem].SelectedMenuItem = menuItem; } - } + // update SelectedMenuItem for respective parent + GetParentSingleSelectSubMenu(menuItem).SelectedMenuItem = menuItem; + } - [Bindable(true)] - public IEnumerable OptionsSource + #region Helpers + private static IEnumerable GetAssociatedMenuItems(SingleSelectSubMenu sssm) { - get { return(IEnumerable)GetValue(OptionsSourceProperty); } - set { SetValue(OptionsSourceProperty, value); } + List menuItems = new(); + foreach(var item in RegisteredSubItemsAndParent.Where(x => x.Value == sssm)) + { + menuItems.Add(item.Key); + } + return menuItems; } - - - [Bindable(true)] - public object SelectedMenuItem + private static IEnumerable GetAssociatedMenuItems(MenuItem menuItem) { - get { return GetValue(SelectedMenuItemProperty); } - set { SetValue (SelectedMenuItemProperty, value); } + 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 + } diff --git a/MultiTerm.Wpf/ValueConverters/EnumDescriptionConverter.cs b/MultiTerm.Wpf/ValueConverters/EnumDescriptionConverter.cs deleted file mode 100644 index 758c6d9..0000000 --- a/MultiTerm.Wpf/ValueConverters/EnumDescriptionConverter.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System; -using System.ComponentModel; -using System.Globalization; -using System.Reflection; -using System.Windows.Data; - -namespace MultiTerm.Wpf.ValueConverters; - -public class EnumDescriptionConverter : IValueConverter -{ - private string GetEnumDescription(Enum enumObject) - { - FieldInfo fieldInfo = enumObject.GetType().GetField(enumObject.ToString()); - object[] attributeArray = fieldInfo.GetCustomAttributes(false); - if(attributeArray.Length == 0) - { - return enumObject.ToString(); - } - else - { - DescriptionAttribute attribute = attributeArray[0] as DescriptionAttribute; - return attribute.Description; - } - } - - /// - /// Convert from Data Source to Dependency Object type. - /// Here => Enum type to object. - /// - /// - /// - /// - /// - /// - public object Convert(object value, Type targetType, object parameter, CultureInfo culture) - { - Enum myEnum = (Enum)value; - string description = this.GetEnumDescription(myEnum); - return description; - } - - /// - /// Convert from Dependency Object type to Data Source type. - /// Here => object to Enum type. - /// - /// - /// - /// - /// - /// - public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) - { - int returnValue = 0; - if (parameter is Type) - { - returnValue = (int)Enum.Parse((Type)parameter, value.ToString()); - } - return returnValue; - } -} diff --git a/MultiTerm.Wpf/ValueConverters/EnumDescriptionToMenuItemConverter.cs b/MultiTerm.Wpf/ValueConverters/EnumDescriptionToMenuItemConverter.cs new file mode 100644 index 0000000..18048d7 --- /dev/null +++ b/MultiTerm.Wpf/ValueConverters/EnumDescriptionToMenuItemConverter.cs @@ -0,0 +1,144 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Reflection; +using System.Windows.Data; +using System.Collections.Generic; +using System.Windows.Controls; + +namespace MultiTerm.Wpf.ValueConverters; + +/// +/// Converts an Enum (using its Description Attribute) to a Menu Item. +/// The Menu Item has the Description assigned to its Header. +/// If the Enum has no Description, it will convert the Enum Value to string and use this. +/// +[ValueConversion(typeof(Enum), typeof(MenuItem))] +public class EnumDescriptionToMenuItemConverter : IValueConverter +{ + /// + /// Gets the description of an Enum value. + /// If there is no Description set, the Enum Value will be converted to string. + /// + /// Enum Object to get Description of. + /// String with Content of DescriptionAttribute of Enum object. + private string GetEnumDescription(Enum enumObject) + { + // guard argument null + if(enumObject == null) { throw new ArgumentNullException(nameof(enumObject)); } + + // get field info from enum type + FieldInfo? fieldInfo = enumObject.GetType().GetField(enumObject.ToString()); + // return string of enum value if there is no field info + if (fieldInfo == null) + { + return enumObject.ToString(); + } + + // get description attribute and return if it is present + DescriptionAttribute? descAttrib = (DescriptionAttribute?)fieldInfo.GetCustomAttribute(typeof(DescriptionAttribute), true); + if (descAttrib != null) + { + return descAttrib.Description; + } + + // if no description attribute was found => return string of enum value + return enumObject.ToString(); + } + + /// + /// Gets all Description Attributes of a Enum type. + /// Descriptions must be unique! + /// + /// An object of the Enum. Will be searched for other Descriptions + /// Key Value Pair of Description and respective Enum Value + private Dictionary GetAllEnumDescriptions(Enum enumObject) + { + // guard argument null + if (enumObject == null) { throw new ArgumentNullException(nameof(enumObject)); } + + Dictionary descriptionsToEnumValues = new(); + + // get members of enum type + var members = enumObject.GetType().GetMembers(); + foreach (var member in members) + { + // get description attributes of all members + DescriptionAttribute? descAttrib = (DescriptionAttribute?)member.GetCustomAttribute(typeof(DescriptionAttribute), true); + if(descAttrib != null) + { + // if a description exists, add the description and the enum value to the dictionary + descriptionsToEnumValues.Add(descAttrib.Description, (Enum)Enum.Parse(enumObject.GetType(), member.Name)); + } + } + + return descriptionsToEnumValues; + } + + /// + /// Convert from Data Source to Dependency Object type. + /// Here: Enum type to MenuItem. + /// + /// value to convert + /// type to convert to + /// no parameter required + /// most likely CultureInfo.CurrentCulture + /// New MenuItem with Enum description in Header property + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + Enum enumValue = (Enum)value; + string description = this.GetEnumDescription(enumValue); + MenuItem newMenuItem = new() + { + Header = description + }; + return newMenuItem; + } + + /// + /// Convert from Dependency Object type to Data Source type. + /// Here: MenuItem to Enum type. + /// + /// value to convert + /// type to convert to + /// no parameter required + /// most likely CultureInfo.CurrentCulture + /// Enum value of the MenuItem that was converted + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + Enum? enumObj; + + // guard argument null + if (targetType == null) { throw new ArgumentNullException(nameof(targetType)); } + if (value == null) { throw new ArgumentNullException(nameof(value)); } + + // extract menu item and guard null + MenuItem? menuObject = value as MenuItem; + if (menuObject == null) + { + throw new Exception($"Cannot convert value that is not of type {nameof(MenuItem)} with {nameof(EnumDescriptionToMenuItemConverter)}"); + } + + // generate instance of enum using target type + enumObj = Activator.CreateInstance(targetType) as Enum; + if (enumObj == null) + { + throw new Exception($"Could not instanciate Enum of targetType {targetType}."); + } + + // get all enum descriptions and iterate + var descriptionToEnumValue = this.GetAllEnumDescriptions(enumObj); + foreach (var kvp in descriptionToEnumValue) + { + // compare key (enum description) to menu header + if(String.Compare(kvp.Key, menuObject.Header.ToString(), true) == 0) + { + // if correct description is found, return according enum value + return kvp.Value; + } + } + + // if nothing worked => return value + return value; + } +} diff --git a/MultiTerm.Wpf/View/ShellView.xaml b/MultiTerm.Wpf/View/ShellView.xaml index c27f56f..a3c96f5 100644 --- a/MultiTerm.Wpf/View/ShellView.xaml +++ b/MultiTerm.Wpf/View/ShellView.xaml @@ -13,7 +13,7 @@ d:DesignHeight="600" d:DesignWidth="1200"> - + @@ -33,35 +33,21 @@ - - test - true - - + - + SelectedMenuItem="{Binding SelectedReceiveNewlineSeparator, + Mode=TwoWay, + Converter={StaticResource EnumDescriptionConverter}}"> + + - - -