2015-08-27 19 views
12

Chcę utworzyć menu kontekstowe, w którym jedno z elementów menu będzie podmenu z możliwością wyboru wartości wyliczeniowych.Ogólny sposób tworzenia sprawdzalnego menu kontekstowego z listy wartości wyliczeniowych

Nie chcę sztywno zakodować żadnej wartości z mojego wyliczenia na xaml, ponieważ chcę, aby wszelkie zmiany wartości wyliczeniowej były automatycznie odzwierciedlane w interfejsie użytkownika bez żadnej interwencji.

Chcę, aby moje menu było zwykłym menu kontekstowym bez artefaktów (chodzi mi o to, że powinien wyglądać jak zwykłe menu kontekstowe).

Próbowałem na wiele sposobów bez powodzenia. Każda moja próba zawsze pomija coś, ale głównie wydaje się, że główną brakującą częścią jest konwerter Paramter, który może być związany z czymś.

I CZERWONY:

To moi wiele prób i powiązany kod:

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

Konwertery:

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

próbne 1: MultiBindConverter ConvertBack nie może pracować, to nie zdobywa informacje.

<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 nie działał w ogóle. To nigdy nie otrzymał żadnej wartości

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

Próba 3:

Z pola listy przy użyciu szablonu i SelectedItem ale interfejs użytkownika nie jest w standardzie, jak powinno być (pojawia się dodatkowa ramka).

+0

można zrobić podmenu w kodzie zamiast XAML. – Dialecticus

+0

Tak, ale nie ma tam problemu. Problem wynika z faktu, że istnieje ogólny sposób wyliczania wartości wyliczonych i możliwość wybrania jednego z nich. "Ogólny" oznacza ogólny kod wielokrotnego użytku bez wartości zakodowanej na stałe. –

Odpowiedz

8

Więc chcesz, aby móc

  • przypisać dowolny Enum do ContextMenu i wyświetlić to Description atrybut
  • są zaznaczone przed wybranym Enum, tylko jeden może być „aktywny” w danym momencie
  • Store wybrana wartość w ViewModel & excute pewnej logiki, gdy wybór zmienia

Coś jak poniżej?

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

Wielkie dzięki .. działa idealnie !!! –

+0

to jest świetne, dziękuję, ale czy istnieje sposób na usunięcie stylu MenuItem-in-a-MenuItem? Zauważam, że musisz kliknąć na wewnętrzny MenuItem, aby go wybrać i wygląda to trochę niecodziennie. – Dutts

+0

Grałem ze stylami (marginesy itp.), Aby wyglądać ładniej, ale było to za dużo na wysiłek (dla odpowiedzi na SO). –

3

dodam moje rozwiązanie jako odniesienie. Oba rozwiązania (zaakceptowana odpowiedź i moja działa dobrze). Stworzyłem jeden w międzyczasie czekałem na prawidłową kompletną odpowiedź. Myślę, że Mikko ma bardziej standardowy sposób wykonywania pracy i prawdopodobnie powinien być łatwiejszy w utrzymaniu. Rozwiązanie Mikko pokazuje również użyteczne zastosowania kilku sztuczek WPF (Relaycommand, MultiBinding, ...).

Główną zaletą mojego rozwiązania jest abstrakcja "złożoności" za pomocą kodu generycznego, który symuluje kolekcję elementów reprezentujących każdą wartość wyliczeniową i ich właściwości (IsChecked, Name, DisplayName). Wszystko to jest ukryte i nie wymaga niczego w modelu. Ale w każdym razie, tak Informacje dodatkowe ...

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

klas generycznych, które mogą być stosowane w dowolnym miejscu:

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(); } 
     } 
    } 
} 
Powiązane problemy