Finished implementation of SingleSelectSubMenu,

removed MenuItemExtensions
master
Jonas Arnold 3 years ago
parent 07a86117e1
commit 1c2e67df8d
  1. 12
      MultiTerm.Core/ViewModel/ShellViewModel.cs
  2. 106
      MultiTerm.Wpf/Controls/MenuItemExtensions.cs
  3. 206
      MultiTerm.Wpf/Controls/SingleSelectSubMenu.cs
  4. 60
      MultiTerm.Wpf/ValueConverters/EnumDescriptionConverter.cs
  5. 144
      MultiTerm.Wpf/ValueConverters/EnumDescriptionToMenuItemConverter.cs
  6. 38
      MultiTerm.Wpf/View/ShellView.xaml

@ -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;
}
}

@ -1,106 +0,0 @@
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
namespace MultiTerm.Wpf.Controls
{
/// <summary>
/// TODO ueberarbeiten
/// FROM:
/// https://stackoverflow.com/questions/3652688/mutually-exclusive-checkable-menu-items/11497189#11497189
/// </summary>
public class MenuItemExtensions : MenuItem
{
private static MenuItem? previouslySelectedMenuItem = null;
public static Dictionary<MenuItem, String> ElementToGroupNames = new Dictionary<MenuItem, String>();
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;
}
}
}

@ -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<MenuItem, SingleSelectSubMenu> RegisteredSubItemsAndParent = new Dictionary<MenuItem, SingleSelectSubMenu>();
#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
private static void OnSelectedMenuItemPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
#region Dotnet Properties
/// <summary>
/// .NET Property for OptionsSource.
/// </summary>
[Bindable(true)]
public IEnumerable OptionsSource
{
var sssm = d as SingleSelectSubMenu;
if (sssm != null)
get { return (IEnumerable)GetValue(OptionsSourceProperty); }
set { SetValue(OptionsSourceProperty, value); }
}
/// <summary>
/// .NET Property for SelectedMenuItem.
/// </summary>
[Bindable(true)]
public object SelectedMenuItem
{
if(e.NewValue != null)
get { return GetValue(SelectedMenuItemProperty); }
set { SetValue(SelectedMenuItemProperty, value); }
}
/// <summary>
/// .NET Property for Title.
/// </summary>
[Bindable(false)]
public string Title
{
// get menu item with same name from registered items
MenuItem selectedMenuItem = null;
foreach (var item in RegisteredSubItemsAndParent)
get { return (string)GetValue(TitleProperty); }
set { SetValue(TitleProperty, value); }
}
#endregion
/// <summary>
/// SelectedMenuItem Property Changed Handler.
/// Updates IsChecked Property of according menu item.
/// </summary>
private static void OnSelectedMenuItemPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if(String.Compare(item.Key.Header.ToString(), e.NewValue.ToString(), true) == 0)
// extract instance and guard null
var sssm = d as SingleSelectSubMenu;
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)
{
selectedMenuItem = item.Key;
if (String.Compare(associatedItem.Header.ToString(), menuItem.Header.ToString(), true) == 0)
{
associatedItem.IsChecked = true;
break;
}
}
// var selectedMenuItem = RegisteredSubItemsAndParent.Where(x => x.Key.Header == e.NewValue).FirstOrDefault();
if(selectedMenuItem != null)
{
selectedMenuItem.IsChecked = true;
//OnAnyItemChecked(selectedMenuItem, new RoutedEventArgs());
}
}
/// <summary>
/// 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.
/// </summary>
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;
}
/// <summary>
/// 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.
/// </summary>
private static void OnOptionsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// extract instance and guard null
var sssm = d as SingleSelectSubMenu;
if(sssm != null)
{
if(e.NewValue != null)
{
if (sssm == null) { return; }
// extract parent instance of SSSM and guard null
var parent = sssm.Parent as MenuItem;
if(parent != null)
if (parent == null) { return; }
// IMRPOVEMENT Delete currently associated values (using e.OldValue) from RegisteredSubItemsAndParent
// create and add title menu item
var titleMenuItem = new MenuItem
{
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)
{
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);
// create new menu item
var converter = new EnumDescriptionToMenuItemConverter();
var newMenuItem = (MenuItem)converter.Convert(item, typeof(MenuItem), new object(), CultureInfo.CurrentCulture);
newMenuItem.IsCheckable = true;
parent.Items.Add(newItem);
Debug.WriteLine(item);
}
}
}
// 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);
}
}
/// <summary>
/// Event handler that handles registered menu items being checked.
/// </summary>
/// <param name="sender">MenuItem that was checked</param>
/// <param name="e">routed event args</param>
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; }
// uncheck others
foreach (var item in RegisteredSubItemsAndParent)
// get associated menu items
var associatedMenuitems = GetAssociatedMenuItems(menuItem);
foreach (var associatedItem in associatedMenuitems)
{
if (item.Key != null && item.Key != menuItem)
// uncheck items that are not null and not the sender item
if (associatedItem != null && associatedItem != 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<MenuItem> GetAssociatedMenuItems(SingleSelectSubMenu sssm)
{
get { return(IEnumerable)GetValue(OptionsSourceProperty); }
set { SetValue(OptionsSourceProperty, value); }
List<MenuItem> menuItems = new();
foreach(var item in RegisteredSubItemsAndParent.Where(x => x.Value == sssm))
{
menuItems.Add(item.Key);
}
[Bindable(true)]
public object SelectedMenuItem
return menuItems;
}
private static IEnumerable<MenuItem> GetAssociatedMenuItems(MenuItem menuItem)
{
get { return GetValue(SelectedMenuItemProperty); }
set { SetValue (SelectedMenuItemProperty, value); }
List<MenuItem> 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
}

@ -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;
}
}
/// <summary>
/// Convert from Data Source to Dependency Object type.
/// Here => Enum type to object.
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
Enum myEnum = (Enum)value;
string description = this.GetEnumDescription(myEnum);
return description;
}
/// <summary>
/// Convert from Dependency Object type to Data Source type.
/// Here => object to Enum type.
/// </summary>
/// <param name="value"></param>
/// <param name="targetType"></param>
/// <param name="parameter"></param>
/// <param name="culture"></param>
/// <returns></returns>
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;
}
}

@ -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;
/// <summary>
/// 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.
/// </summary>
[ValueConversion(typeof(Enum), typeof(MenuItem))]
public class EnumDescriptionToMenuItemConverter : IValueConverter
{
/// <summary>
/// Gets the description of an Enum value.
/// If there is no Description set, the Enum Value will be converted to string.
/// </summary>
/// <param name="enumObject">Enum Object to get Description of.</param>
/// <returns>String with Content of DescriptionAttribute of Enum object.</returns>
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();
}
/// <summary>
/// Gets all Description Attributes of a Enum type.
/// Descriptions must be unique!
/// </summary>
/// <param name="enumObject">An object of the Enum. Will be searched for other Descriptions</param>
/// <returns>Key Value Pair of Description and respective Enum Value</returns>
private Dictionary<string, Enum> GetAllEnumDescriptions(Enum enumObject)
{
// guard argument null
if (enumObject == null) { throw new ArgumentNullException(nameof(enumObject)); }
Dictionary<string, Enum> 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;
}
/// <summary>
/// Convert from Data Source to Dependency Object type.
/// Here: Enum type to MenuItem.
/// </summary>
/// <param name="value">value to convert</param>
/// <param name="targetType">type to convert to</param>
/// <param name="parameter">no parameter required</param>
/// <param name="culture">most likely CultureInfo.CurrentCulture</param>
/// <returns>New MenuItem with Enum description in Header property</returns>
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;
}
/// <summary>
/// Convert from Dependency Object type to Data Source type.
/// Here: MenuItem to Enum type.
/// </summary>
/// <param name="value">value to convert</param>
/// <param name="targetType">type to convert to</param>
/// <param name="parameter">no parameter required</param>
/// <param name="culture">most likely CultureInfo.CurrentCulture</param>
/// <returns>Enum value of the MenuItem that was converted</returns>
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;
}
}

@ -13,7 +13,7 @@
d:DesignHeight="600" d:DesignWidth="1200">
<UserControl.Resources>
<!-- Value Converters -->
<conv:EnumDescriptionConverter x:Key="EnumDescriptionConverter"/>
<conv:EnumDescriptionToMenuItemConverter x:Key="EnumDescriptionConverter"/>
</UserControl.Resources>
@ -33,35 +33,21 @@
</MenuItem>
</MenuItem>
<MenuItem Header="Test">
<MenuItem Header="Item1">
<controls:MenuItemExtensions.GroupName>test</controls:MenuItemExtensions.GroupName>
<MenuItem.IsCheckable>true</MenuItem.IsCheckable>
</MenuItem>
<MenuItem controls:MenuItemExtensions.GroupName="test" Header="Item2" IsCheckable="true"/>
<MenuItem Header="Item1" Command="{Binding TestButtonClickedCommand}"/>
</MenuItem>
<MenuItem Header="_Settings">
<controls:SingleSelectSubMenu Header="Newline Separator (Receive) TEST"
<controls:SingleSelectSubMenu Title="Newline Separator (Receive)"
OptionsSource="{Binding NewlineSeparatorTypeValues}"
SelectedMenuItem="{Binding Path=SelectedReceiveNewlineSeparator, Converter={StaticResource EnumDescriptionConverter}}">
SelectedMenuItem="{Binding SelectedReceiveNewlineSeparator,
Mode=TwoWay,
Converter={StaticResource EnumDescriptionConverter}}">
</controls:SingleSelectSubMenu>
<controls:SingleSelectSubMenu Title="Newline Separator On Send"
OptionsSource="{Binding NewlineSeparatorTypeValues}"
SelectedMenuItem="{Binding SelectedSendNewlineSeparator,
Mode=TwoWay,
Converter={StaticResource EnumDescriptionConverter}}">
</controls:SingleSelectSubMenu>
<!--<MenuItem Header="Newline Separator (Receive)"
ItemsSource="{Binding NewlineSeparatorTypeValues}">
--><!--<controls:RadioButtonMenuItem.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Converter={StaticResource EnumDescriptionConverter}}"/>
</DataTemplate>
</controls:RadioButtonMenuItem.ItemTemplate>--><!--
<MenuItem.ItemContainerStyle>
<Style TargetType="{x:Type MenuItem}">
<Setter Property="IsCheckable" Value="True"/>
<Setter Property=""
--><!--<Setter Property="GroupName" Value="receive"/>--><!--
<Setter Property="Header" Value="{Binding Converter={StaticResource EnumDescriptionConverter}}"/>
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>-->
<MenuItem Header="Newline Separator On Send" x:Name="newlineSendMenuItem"/>
</MenuItem>
<MenuItem Header="_View"/>
<MenuItem Header="_About"/>

Loading…
Cancel
Save