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

4

13

I want to create a context menu where one of the menuItem would be a submenu with a choice among enum values.

I do not want to hard code any of the values from my enum into xaml because I want that any enum value changes would be automtically reflected in the UI without any intervention.

I want my menu to be a regular context menu without any artifact (I mean the appearance should be as a regular ContextMenu).

I've tried many ways without success. Each of my trial always misses something but mainly it seems that the main missing part is a converterParamter that could be bound to something.

I red:

This is my many trials and related code:

<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"
        xmlns:system="clr-namespace:System;assembly=mscorlib"
        xmlns:converter="clr-namespace:WpfContextMenuWithEnum.Converter"
        Title="MainWindow" Height="350" Width="525"
        Name="MyWindow">
    <Window.DataContext>
        <wpfContextMenuWithEnum:MainWindowModel></wpfContextMenuWithEnum:MainWindowModel>
    </Window.DataContext>

    <Window.Resources>
        <ObjectDataProvider x:Key="EnumChoiceProvider" MethodName="GetValues" ObjectType="{x:Type system:Enum}">
            <ObjectDataProvider.MethodParameters>
                <x:Type TypeName="wpfContextMenuWithEnum:EnumChoice"/>
            </ObjectDataProvider.MethodParameters>
        </ObjectDataProvider>

        <converter:EnumToBooleanConverter x:Key="EnumToBooleanConverter"></converter:EnumToBooleanConverter>
        <converter:MultiBind2ValueComparerConverter x:Key="MultiBind2ValueComparerConverter"></converter:MultiBind2ValueComparerConverter>
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"></RowDefinition>
        </Grid.RowDefinitions>

        <TextBox Text="Right click me">
            <TextBox.ContextMenu>
                <ContextMenu ItemsSource="{Binding Source={StaticResource EnumChoiceProvider}}">
                    <ContextMenu.ItemTemplate>
                        <DataTemplate>
                            <MenuItem IsCheckable="True" Header="{Binding Path=.}">
                                <MenuItem.IsChecked>
                                    <MultiBinding Converter="{StaticResource MultiBind2ValueComparerConverter}">
                                        <Binding Path="DataContext.ModelEnumChoice" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}" />
                                        <Binding Path="." Mode="OneWay"></Binding>
                                    </MultiBinding>
                                </MenuItem.IsChecked>
                            </MenuItem>
                        </DataTemplate>
                    </ContextMenu.ItemTemplate>
                </ContextMenu>
            </TextBox.ContextMenu>
        </TextBox>
    </Grid>
</Window>

Enum:

using System.ComponentModel;

    namespace WpfContextMenuWithEnum
    {
        public enum EnumChoice
        {
            [Description("Default")]
            ChoiceDefault = 0, // easier if the default have value = 0

            [Description("<1>")]
            Choice1 = 1,

            [Description("<2>")]
            Choice2 = 2,
        }
    }

Converters:

using System;
using System.Windows;
using System.Windows.Data;

namespace WpfContextMenuWithEnum.Converter
{
    public class ConverterWrapperWithDependencyParameterConverter : DependencyObject, IValueConverter
    {
        public static readonly DependencyProperty ParameterProperty = DependencyProperty.Register("Parameter",
            typeof(object), typeof(ConverterWrapperWithDependencyParameterConverter));

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (parameter != null)
            {
                throw new ArgumentException("The parameter should be set directly as a property not into the Binding object.");
            }

            return Converter.Convert(value, targetType, Parameter, culture);
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (parameter != null)
            {
                throw new ArgumentException("The parameter should be set directly as a property not into the Binding object.");
            }

            return Converter.ConvertBack(value, targetType, Parameter, culture);
        }

        public object Parameter
        {
            get { return GetValue(ParameterProperty); }
            set { SetValue(ParameterProperty, value); }
        }

        public IValueConverter Converter { get; set; }
    }
}





using System;
using System.Windows.Data;

namespace WpfContextMenuWithEnum.Converter
{
    public class EnumToBooleanConverter : IValueConverter
    {
        // **********************************************************************
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return value.Equals(parameter);
        }

        // **********************************************************************
        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return value.Equals(true) ? parameter : Binding.DoNothing;
        }

        // **********************************************************************
    }

}




   using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using System.Windows.Data;

    namespace WpfContextMenuWithEnum.Converter
    {
        public class MultiBind2ValueComparerConverter : IMultiValueConverter
        {
            public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                if (values.Length != 2)
                {
                    throw new ArgumentException("Can compare only 2 values together fo equality");
                }

                return (values[0].Equals(values[1]));
            }

            public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
            {
                // if ((bool)value == true)
                throw new NotImplementedException();
            }
        }
    }

Trial 1: MultiBindConverter ConvertBack can't work, it misses information.

<ContextMenu ItemsSource="{Binding Source={StaticResource EnumChoiceProvider}}">
            <ContextMenu.ItemTemplate>
                <DataTemplate>
                    <MenuItem IsCheckable="True" Header="{Binding Path=.}">
                        <MenuItem.IsChecked>
                            <MultiBinding Converter="{StaticResource MultiBind2ValueComparerConverter}">
                                <Binding Path="DataContext.ModelEnumChoice" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}" />
                                <Binding Path="."></Binding>
                            </MultiBinding>
                        </MenuItem.IsChecked>
                    </MenuItem>
                </DataTemplate>
            </ContextMenu.ItemTemplate>
        </ContextMenu>

Trial 2: My ConverterParameter bind did not work at all. It never received any value

<ContextMenu ItemsSource="{Binding Source={StaticResource EnumChoiceProvider}}">
                    <ContextMenu.ItemTemplate>
                        <DataTemplate>
                            <MenuItem IsCheckable="True" Header="{Binding Path=.}">
                                <MenuItem.IsChecked>
                                    <Binding Path="DataContext.ModelEnumChoice" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}">
                                        <Binding.Converter>
                                            <converter:ConverterWrapperWithDependencyParameterConverter Converter="{StaticResource EnumToBooleanConverter}"
                                                Parameter="{Binding Path=.}"/>
                                        </Binding.Converter>
                                    </Binding>
                                </MenuItem.IsChecked>
                            </MenuItem>
                        </DataTemplate>
                    </ContextMenu.ItemTemplate>
                </ContextMenu>

Trial 3:

With a listBox using template and SelectedItem but the UI is not as standard as it should be (an additional frame appears).

Sunglasses answered 27/8, 2015 at 17:49 Comment(2)
You could make a submenu in code instead of xaml.Doggo
Yes but its not there that lie the problem. The problem come from the fact to have a generic way to enumerate enum values and be able to select one. "Generic" mean with general re-usable code without any hard coded enum value.Sunglasses
A
12

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();
        }
    }
}
Affiant answered 27/8, 2015 at 20:9 Comment(2)
this is great, thanks, but is there any way to remove the MenuItem-in-a-MenuItem styling? I notice that you have to click on the inner MenuItem to select it and it looks a little unusual.Splenius
I played around with styles (margins etc) to make it look nicer but it was too much for an effort (for a SO answer).Affiant
S
3

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(); }
        }
    }
}
Sunglasses answered 1/9, 2015 at 18:25 Comment(0)
D
0

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>
Decury answered 30/6, 2021 at 6:22 Comment(0)
A
0

To finalize the addition of Daniel, here is the full code with the appropriates bindings that goes inside the MainWindow initially provided by Mikko.

<ContextMenu ItemsSource="{Binding EnumChoiceProvider}">
    <ContextMenu.ItemContainerStyle>
        <Style TargetType="MenuItem" BasedOn="{StaticResource {x:Type MenuItem}}">
            <Setter Property="IsCheckable" Value="True" />
            <Setter Property="Header" Value="{Binding Path=., Converter={StaticResource EnumDescriptionConverter}}" />
            <Setter Property="Command" Value="{Binding DataContext.ToggleEnumChoiceCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}}" />
            <Setter Property="CommandParameter" Value="{Binding}"/>
            <Setter Property="IsChecked">
                <Setter.Value>
                    <MultiBinding Mode="OneWay" 
                                  NotifyOnSourceUpdated="True"  
                                  UpdateSourceTrigger="PropertyChanged" 
                                  Converter="{StaticResource EnumCheckedConverter}">
                        <Binding Path="DataContext.RunEnv.ControllerViewModel.SimulationMode" 
                                 RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=ContextMenu}"  />
                        <Binding Path="."></Binding>
                    </MultiBinding>
                </Setter.Value>
            </Setter>
        </Style>
    </ContextMenu.ItemContainerStyle>
</ContextMenu>
Arcature answered 27/1, 2022 at 15:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.