Общий способ создания настраиваемого контекстного меню из списка значений перечисления

Я хочу создать контекстное меню, в котором один из элементов menuItem будет подменю с выбором между значениями перечисления.

Я не хочу жестко кодировать любые значения из моего перечисления в xaml, потому что я хочу, чтобы любые изменения значения enum автоматически отражались в пользовательском интерфейсе без каких-либо вмешательств.

Я хочу, чтобы мое меню было обычным контекстным меню без какого-либо артефакта (я имею в виду, что внешний вид должен быть как обычный ContextMenu).

Я пробовал много способов без успеха. Каждое из моих пробных случаев всегда пропускает что-то, но главным образом кажется, что основная недостающая часть - это конвертер, который может быть связан с чем-то.

I красный:

Это мои многочисленные испытания и связанный код:

<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,
        }
    }

Конвертеры:

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 не может работать, он пропускает информацию.

<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>

Судебное разбирательство 2: Связывание с My ConverterParameter не работает вообще. Он никогда не получал никакой ценности

<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>

Испытание 3:

С помощью listBox с использованием шаблона и SelectedItem, но пользовательский интерфейс не является таким стандартным, как он должен быть (появляется дополнительный кадр).

Ответ 1

Итак, вы хотите иметь возможность

  • Привяжите любые Enum к ContextMenu и покажите его Description attribute
  • Установите флажок перед выбранным Enum, только один может быть "активным" в любой момент времени
  • Сохранять выбранное значение в ViewModel и вызывать некоторую логику при изменении выбора

Что-то вроде следующего?

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();
        }
    }
}

Ответ 2

Я добавляю свое решение в качестве ссылки. Оба решения (принятый ответ и мои работы прекрасны). Я создал один, тем временем, я ожидал действительного полного ответа. Я думаю, что у Микко есть более стандартный способ выполнения работы, и, вероятно, его легче поддерживать. Решение Mikko также демонстрирует хорошие применения нескольких трюков WPF (Relaycommand, MultiBinding,...).

Основным преимуществом моего решения является абстракция "сложности", используя общий код, который имитирует коллекцию элемента, представляющую каждое значение перечисления и их свойства (IsChecked, Name, DisplayName). Все это скрыто и ничего не требует в модели. Но в любом случае, как дополнительная информация...

<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>

Общие классы, которые можно использовать где угодно:

    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(); }
        }
    }
}