A Generic way to create a checkable context menu from a list of enum values A Generic way to create a checkable context menu from a list of enum values wpf wpf

A Generic way to create a checkable context menu from a list of enum values


So you want to be able to

  • Bind any Enum to ContextMenu and display it's Description attribute
  • Have a checkmark in front of selected Enum, only one can be "active" at any given time
  • Store selected value in ViewModel & excute some logic when selection changes

Something like the following?

imgur


MainWindow.xaml

<Window x:Class="WpfApplication1.View.MainWindow"        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"        xmlns:viewModel="clr-namespace:WpfApplication1.ViewModel"        xmlns:local="clr-namespace:WpfApplication1"        Title="MainWindow"        Height="300"        Width="250">    <!-- Set data context -->            <Window.DataContext>      <viewModel:MainViewModel />    </Window.DataContext>    <!-- Converters -->    <Window.Resources>      <local:EnumDescriptionConverter x:Key="EnumDescriptionConverter" />      <local:EnumCheckedConverter x:Key="EnumCheckedConverter" />    </Window.Resources>    <!-- Element -->        <TextBox Text="Right click me">      <!-- Context menu -->      <TextBox.ContextMenu>        <ContextMenu ItemsSource="{Binding EnumChoiceProvider}">          <ContextMenu.ItemTemplate>            <DataTemplate>              <!-- Menu item header bound to enum converter -->              <!-- IsChecked bound to current selection -->              <!-- Toggle bound to a command, setting current selection -->              <MenuItem                 IsCheckable="True"                Width="150"                Header="{Binding Path=., Converter={StaticResource EnumDescriptionConverter}}"                Command="{Binding DataContext.ToggleEnumChoiceCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}}"                CommandParameter="{Binding}">                <MenuItem.IsChecked>                  <MultiBinding Mode="OneWay"                                 NotifyOnSourceUpdated="True"                                 UpdateSourceTrigger="PropertyChanged"                                 Converter="{StaticResource EnumCheckedConverter}">                    <Binding Path="DataContext.SelectedEnumChoice"                              RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}"  />                    <Binding Path="."></Binding>                  </MultiBinding>                </MenuItem.IsChecked>                  </MenuItem>            </DataTemplate>          </ContextMenu.ItemTemplate>        </ContextMenu>      </TextBox.ContextMenu>    </TextBox></Window>

MainViewModel.cs

namespace WpfApplication1.ViewModel{    public class MainViewModel : ViewModelBase // where base implements INotifyPropertyChanged    {        private EnumChoice? _selectedEnumChoice;        public MainViewModel()        {            EnumChoiceProvider = new ObservableCollection<EnumChoice>                (Enum.GetValues(typeof(EnumChoice)).Cast<EnumChoice>());            ToggleEnumChoiceCommand = new RelayCommand<EnumChoice>                (arg => SelectedEnumChoice = arg);        }        // Selections            public ObservableCollection<EnumChoice> EnumChoiceProvider { get; set; }        // Current selection            public EnumChoice? SelectedEnumChoice        {            get            {                return _selectedEnumChoice;            }            set            {                _selectedEnumChoice = value != _selectedEnumChoice ? value : null;                RaisePropertyChanged();            }        }        // "Selection changed" command            public ICommand ToggleEnumChoiceCommand { get; private set; }    }}

EnumChoice.cs

namespace WpfApplication1{    public enum EnumChoice    {        [Description("Default")]        ChoiceDefault,        [Description("<1>")]        Choice1,        [Description("<2>")]        Choice2    }}

EnumDescriptionConverter.cs

namespace WpfApplication1{    // Extract enum description     public class EnumDescriptionConverter : IValueConverter    {        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)        {            MemberInfo[] memberInfos = value.GetType().GetMember(value.ToString());            if (memberInfos.Length > 0)            {                object[] attrs = memberInfos[0].GetCustomAttributes(typeof (DescriptionAttribute), false);                if (attrs.Length > 0)                    return ((DescriptionAttribute) attrs[0]).Description;            }            return value;            // or maybe just            //throw new InvalidEnumArgumentException(string.Format("no description found for enum {0}", value));        }        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)        {            throw new NotImplementedException();        }    }}

EnumCheckedConverter.cs

namespace WpfApplication1{    // Check if currently selected     public class EnumCheckedConverter : IMultiValueConverter    {        public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)        {            return !values.Contains(null) && values[0].ToString().Equals(values[1].ToString(), StringComparison.OrdinalIgnoreCase);        }        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)        {            throw new NotImplementedException();        }    }}


I add my solution as reference. Both solution (accepted answer and mine works fine). I created one in the meantime I was waiting for a valid complete answer. I think that Mikko has a more standard way of doing the job and should probably be easier to maintain. Mikko solution also show nice usages of few WPF tricks (Relaycommand, MultiBinding, ...).

The main advantage of my solution is the abstraction of the "complexity" by using generic code that simulate a collection of item representing each enum value and their properties (IsChecked, Name, DisplayName). All of that is hidden and does not require anything in the model.But anyway, just as additional information...

<Window x:Class="WpfContextMenuWithEnum.MainWindow"        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"        xmlns:wpfContextMenuWithEnum="clr-namespace:WpfContextMenuWithEnum"        Title="MainWindow" Height="350" Width="525"        Name="MyWindow">    <Window.DataContext>        <wpfContextMenuWithEnum:MainWindowModel></wpfContextMenuWithEnum:MainWindowModel>    </Window.DataContext>    <Window.Resources>        <wpfContextMenuWithEnum:EnumWrapperIteratorAndSelector x:Key="EnumWrapperIteratorAndSelector"                                                                Enum="{Binding DataContext.SelectedEnumChoice, Mode=TwoWay, ElementName=MyWindow}" />    </Window.Resources>    <Grid>        <TextBox Text="Right click me">            <TextBox.ContextMenu>                <ContextMenu ItemsSource="{Binding Source={StaticResource EnumWrapperIteratorAndSelector}}">                    <ContextMenu.ItemTemplate>                        <DataTemplate>                            <MenuItem IsCheckable="True" Header="{Binding DisplayName}" IsChecked="{Binding IsChecked}">                            </MenuItem>                        </DataTemplate>                    </ContextMenu.ItemTemplate>                </ContextMenu>            </TextBox.ContextMenu>        </TextBox>    </Grid></Window>

Generic classes that could be used anywhere:

    using System;    using System.Collections;    using System.Collections.Generic;    using System.Collections.ObjectModel;    using System.Collections.Specialized;    using System.ComponentModel;    using System.Reflection;    using System.Windows;    namespace WpfContextMenuWithEnum    {        /// <summary>        /// Note: Freezable is necessary otherwise binding will never occurs if EnumWrapperIteratorAndSelector is defined        /// as resources. See article for more info:         /// http://www.thomaslevesque.com/2011/03/21/wpf-how-to-bind-to-data-when-the-datacontext-is-not-inherited/        ///  </summary>        public class EnumWrapperIteratorAndSelector : Freezable, IEnumerable<EnumWrapperIteratorAndSelectorChoice>, INotifyCollectionChanged        {            // ******************************************************************            public static readonly DependencyProperty EnumProperty =                DependencyProperty.Register("Enum", typeof(Enum), typeof(EnumWrapperIteratorAndSelector), new PropertyMetadata(null, PropertyChangedCallback));            ObservableCollection<EnumWrapperIteratorAndSelectorChoice> _allEnumValue = new ObservableCollection<EnumWrapperIteratorAndSelectorChoice>();            // ******************************************************************            private static void PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)            {                if (!(dependencyPropertyChangedEventArgs.NewValue is Enum))                {                    throw new ArgumentException("Only enum are supported.");                }                var me = dependencyObject as EnumWrapperIteratorAndSelector;                if (me != null)                {                    if (dependencyPropertyChangedEventArgs.OldValue == null)                    {                        me.ResetWithNewEnum(dependencyPropertyChangedEventArgs.NewValue);                    }                    else                    {                        foreach(EnumWrapperIteratorAndSelectorChoice enumWrapperIteratorAndSelectorChoice in me._allEnumValue)                        {                            enumWrapperIteratorAndSelectorChoice.RaiseChangeIfAppropriate(dependencyPropertyChangedEventArgs);                        }                    }                }            }            // ******************************************************************            private void ResetWithNewEnum(object enumValue)            {                _allEnumValue.Clear();                var enumType = Enum.GetType();                foreach (Enum enumValueIter in Enum.GetValues(enumValue.GetType()))                {                    MemberInfo[] memberInfos = enumType.GetMember(enumValueIter.ToString());                    if (memberInfos.Length > 0)                    {                        var desc = memberInfos[0].GetCustomAttribute<DescriptionAttribute>();                        if (desc != null)                        {                            _allEnumValue.Add(new EnumWrapperIteratorAndSelectorChoice(this, enumValueIter, desc.Description));                        }                        else                        {                            _allEnumValue.Add(new EnumWrapperIteratorAndSelectorChoice(this, enumValueIter));                        }                    }                }            }            // ******************************************************************            public Enum Enum            {                get { return (Enum)GetValue(EnumProperty); }                set                {                    SetValue(EnumProperty, value);                }            }            // ******************************************************************            internal void SetCurrentValue(Enum enumValue)            {                SetCurrentValue(EnumProperty, enumValue);            }            // ******************************************************************            public IEnumerator GetEnumerator()            {                return _allEnumValue.GetEnumerator();            }            // ******************************************************************            IEnumerator<EnumWrapperIteratorAndSelectorChoice> IEnumerable<EnumWrapperIteratorAndSelectorChoice>.GetEnumerator()            {                return _allEnumValue.GetEnumerator();            }            // ******************************************************************            public event NotifyCollectionChangedEventHandler CollectionChanged            {                add { _allEnumValue.CollectionChanged += value; }                remove { _allEnumValue.CollectionChanged -= value; }            }            // ******************************************************************            protected override Freezable CreateInstanceCore()            {                return new EnumWrapperIteratorAndSelector();            }            // ******************************************************************        }    }    using System;    using System.ComponentModel;    using System.Windows;    namespace WpfContextMenuWithEnum    {        public class EnumWrapperIteratorAndSelectorChoice : INotifyPropertyChanged        {            public event PropertyChangedEventHandler PropertyChanged;            private EnumWrapperIteratorAndSelector _enumWrapperIteratorAndSelector;            public Enum EnumValueRef { get; private set; }            public string Name { get; set; }            public string Description { get; set; }            public bool IsChecked            {                get                {                    return _enumWrapperIteratorAndSelector.Enum.Equals(EnumValueRef);                }                set                {                    if (value) // Can only set value                    {                        _enumWrapperIteratorAndSelector.SetCurrentValue(EnumValueRef);                    }                }            }            internal void RaiseChangeIfAppropriate(DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)            {                if (EnumValueRef.Equals(dependencyPropertyChangedEventArgs.OldValue) ||                    EnumValueRef.Equals(dependencyPropertyChangedEventArgs.NewValue))                {                    var propertyChangeLocal = PropertyChanged;                    if (propertyChangeLocal != null)                    {                        propertyChangeLocal(this, new PropertyChangedEventArgs("IsChecked"));                    }                }            }            public EnumWrapperIteratorAndSelectorChoice(EnumWrapperIteratorAndSelector enumWrapperIteratorAndSelector,                Enum enumValueRef, string description = null)            {                _enumWrapperIteratorAndSelector = enumWrapperIteratorAndSelector;                EnumValueRef = enumValueRef;                Name = enumValueRef.ToString();                Description = description;            }            public string DisplayName            {                get { return Description ?? Name; }            }        }    }using System;using System.Collections.ObjectModel;using System.Linq;using System.Windows.Input;using GalaSoft.MvvmLight;using GalaSoft.MvvmLight.CommandWpf;namespace WpfContextMenuWithEnum{    public class MainWindowModel : ViewModelBase    {        private EnumChoice _selectedEnumChoice;        public EnumChoice SelectedEnumChoice        {            get { return _selectedEnumChoice; }            set { _selectedEnumChoice = value; RaisePropertyChanged(); }        }    }}


To add to the rest of the answers: you can get rid of the "menu in a menu" style problem by setting ItemContainerStyle instead of ItemTemplate:

DataTemplate

<MenuItem.ItemTemplate>    <DataTemplate>        <MenuItem IsCheckable="True"                  Header="{Binding DisplayName}"                  IsChecked="{Binding IsChecked}">        </MenuItem>    </DataTemplate></MenuItem.ItemTemplate>

ItemContainerStyle

<MenuItem.ItemContainerStyle>    <Style TargetType="MenuItem" BasedOn="{StaticResource {x:Type MenuItem}}">        <Setter Property="IsCheckable" Value="True" />        <Setter Property="Header" Value="{Binding DisplayName}" />        <Setter Property="IsChecked" Value="{Binding IsChecked}" />    </Style></MenuItem.ItemContainerStyle>