Привязка данных WPF к перечислению флагов (в PropertyGrid)

Мне нужно иметь возможность выбирать несколько значений, как это характерно для перечисления Flag из представления WPF (все в PropertyGrid).

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

Я опробовал ряд проприетарных сеток свойств с открытым исходным кодом для WPF, и, похоже, ни одна из них не поддерживает типы перечислений с атрибутами «Флаги» из коробки.

Решением этой проблемы может быть что угодно, что позволит мне привязать данные к + выбрать несколько значений для указанного Flags Enum для любого коммерческого WPF PropertyGrid или WPF с открытым исходным кодом.

Код:

Пример PropertyType:

public class PropertyTypeOne
{
    public PropertyTypeOne()
    {
        IntProp = 1;
        InProp2 = 2;
        BoolProp = true;
        Boolprop2 = false;
        StringProp = "string1";
        DoubleProp = 2.3;
        EnumProp = FlagEnumDataTYpe.MarketDepth;
    }

    public int IntProp { get; set; }

    public int InProp2 { get; set; }

    public bool BoolProp { get; set; }

    public bool BoolProp2 { get; set; }

    public string StringProp { get; set; }

    public double DoubleProp { get; set; }

    //This is the property in question
    public FlagEnumDataType EnumProp { get; set; }
}

Пример типа перечисления флагов:

[Flags]
public enum FlagEnumDataType : byte
{
    None = 0,
    Trade = 1,
    Quote = 2,
    MarketDepth = 4,
    All = 255
}

Примечание.

Если в решении используется WPF PropertyGrid с открытым исходным кодом (http://www.codeplex.com/wpg) Внесу изменения / дополнения обратно в элемент управления.

Спасибо.


person holsee    schedule 24.09.2009    source источник


Ответы (2)


Я не нашел по-настоящему элегантного способа сделать это, но поговорил с разработчиками в Mindscape, и вот кое-что грубое, но функциональное, которое работает с Mindscape PropertyGrid.

Сначала мы создаем шаблон для самого редактора flag-enum. Это ItemsControl, заполняемый с помощью EnumValuesConverter из библиотеки WPF Property Grid:

<ms:EnumValuesConverter x:Key="evc" />
<local:FlaggyConverter x:Key="fc" />

<DataTemplate x:Key="FlagEditorTemplate">
  <ItemsControl Name="ic" ItemsSource="{Binding Value, Converter={StaticResource evc}}">
    <ItemsControl.ItemTemplate>
      <DataTemplate>
        <CheckBox Content="{Binding}">
        </CheckBox>
      </DataTemplate>
    </ItemsControl.ItemTemplate>
  </ItemsControl>
</DataTemplate>

Теперь нам нужно отобразить флажки как отмеченные в зависимости от того, включен ли флаг. Для этого требуются две вещи: во-первых, IMultiValueConverter, чтобы он мог учитывать как имеющийся флаг, так и значение контекста, а во-вторых, способ для отдельных флажков читать значение контекста. (Под значением контекста я подразумеваю фактическое значение свойства. Например, значение контекста может быть Flag1 | Flag4 | Flag32.) Вот конвертер:

public class FlaggyConverter : IMultiValueConverter
{
  public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
  {
    int flagValue = (int)values[0];
    int propertyValue = (int)values[1];

    return (flagValue & propertyValue) == flagValue;
  }

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

Для распространения значения контекста я собираюсь использовать ярлык и использовать Tag. Вы можете предпочесть создать присоединенное свойство с более значимым именем.

Теперь элемент управления будет отображать проверки установленных флагов, но еще не будет обновлять значение, если вы установите или отключите флажок. К сожалению, я нашел единственный способ выполнить эту работу - обработать события Checked и Unchecked и вручную установить значение контекста. Для этого нам нужно поместить значение контекста в место, где его можно будет обновить с помощью обработчиков событий флажка. Это означает двустороннюю привязку свойства флажка к значению контекста. Я снова воспользуюсь тегом, хотя вам может понадобиться что-нибудь более чистое; Кроме того, я собираюсь использовать прямую обработку событий, хотя в зависимости от вашего дизайна вы можете заключить это в прикрепленное поведение (это будет особенно хорошо работать, если вы создавали присоединенные свойства для переноса значения контекста).

<DataTemplate x:Key="FlagEditorTemplate">
  <ItemsControl Name="ic" ItemsSource="{Binding Value, Converter={StaticResource evc}}" Tag="{Binding Value, Mode=TwoWay}">
    <ItemsControl.ItemTemplate>
      <DataTemplate>
        <CheckBox Content="{Binding}" Tag="{Binding Tag, ElementName=ic, Mode=TwoWay}" Checked="CheckBox_Checked" Unchecked="CheckBox_Unchecked">
          <CheckBox.IsChecked>
            <MultiBinding Converter="{StaticResource fc}" Mode="OneWay">
              <Binding />
              <Binding Path="Tag" ElementName="ic" />
            </MultiBinding>
          </CheckBox.IsChecked>
        </CheckBox>
      </DataTemplate>
    </ItemsControl.ItemTemplate>
  </ItemsControl>
</DataTemplate>

Обратите внимание на двустороннюю привязку тега: это так, что когда мы устанавливаем тег из нашего кода обработки событий, он распространяется обратно на ic.Tag, а оттуда - на значение свойства.

Обработчики событий в основном очевидны, но с одной оговоркой:

<DataTemplate x:Key="FlagEditorTemplate">
  <ItemsControl Name="ic" ItemsSource="{Binding Value, Converter={StaticResource evc}}" Tag="{Binding Value, Mode=TwoWay}">
    <ItemsControl.ItemTemplate>
      <DataTemplate>
        <CheckBox Content="{Binding}" Tag="{Binding Tag, ElementName=ic, Mode=TwoWay}" Checked="CheckBox_Checked" Unchecked="CheckBox_Unchecked">
          <CheckBox.IsChecked>
            <MultiBinding Converter="{StaticResource fc}" Mode="OneWay">
              <Binding />
              <Binding Path="Tag" ElementName="ic" />
            </MultiBinding>
          </CheckBox.IsChecked>
        </CheckBox>
      </DataTemplate>
    </ItemsControl.ItemTemplate>
  </ItemsControl>
</DataTemplate>

Обработчики событий:

private void CheckBox_Checked(object sender, RoutedEventArgs e)
{
  CheckBox cb = (CheckBox)sender;
  int val = (int)(cb.Tag);
  int flag = (int)(cb.Content);
  val = val | flag;
  cb.Tag = (Curses)val;
}

private void CheckBox_Unchecked(object sender, RoutedEventArgs e)
{
  CheckBox cb = (CheckBox)sender;
  int val = (int)(cb.Tag);
  int flag = (int)(cb.Content);
  val = val & ~flag;
  cb.Tag = (Curses)val;
}

Обратите внимание на приведение при установке cb.Tag. Без этого WPF внутренне не может преобразовать значение в тип перечисления при попытке передать его обратно в источник. Здесь Curses - это мой тип перечисления. Если вам нужен полностью гибкий, не зависящий от типа редактор, вы захотите предоставить его извне, например, как присоединенное свойство в поле для флажка. Вы можете сделать это с помощью конвертера или распространить его из редактора EditContext.

Наконец, нам нужно подключить это к сетке. Вы можете сделать это для каждого отдельного объекта недвижимости:

<ms:PropertyGrid>
  <ms:PropertyGrid.Editors>
    <ms:PropertyEditor PropertyName="Curses" EditorTemplate="{StaticResource FlagEditorTemplate}" />
  </ms:PropertyGrid.Editors>
</ms:PropertyGrid>

или с помощью объявления интеллектуального редактора для подключения всех свойств, типы которых имеют атрибут FlagsAttribute. Для получения информации о создании и использовании интеллектуальных редакторов см. http://www.mindscape.co.nz/blog/index.php/2008/04/30/smart-editor-declarations-in-the-wpf.-property-grid/.

Если вы хотите сэкономить место, вы можете изменить ItemsControl на ComboBox, хотя вам потребуется проделать дополнительную работу для обработки свернутого дисплея; Я не исследовал это подробно.

person holsee    schedule 19.10.2009

Нашел в Интернете, немного улучшил, но пока не успел протестировать.

/// <summary>
/// Two-way conversion from flags to bool and back using parameter as mask
/// Warning: The trick is in storing value locally between calls to Convert and ConvertBack
/// You must have a single instance of this converter per flags property per object
/// Do not share this converter between different objects or properties
/// Typical usage:
/// [Flags] enum FlagType { None = 0, Trade = 1, Quote = 2, Report = 4, All = 255 }
/// <local:EditableFlagsToBooleanConverter x:Key="FlagsToBooleanConverter" />
/// <CheckBox IsChecked="{Binding Prop1, Converter={StaticResource FlagsToBooleanConverter}, Mode=TwoWay, 
///     ConverterParameter={x:Static local:FlagType.Trade}}" >Trade</CheckBox>
/// <CheckBox IsChecked="{Binding Prop1, Converter={StaticResource FlagsToBooleanConverter}, Mode=TwoWay, 
///     ConverterParameter={x:Static local:FlagType.Quote}}" >Quote</CheckBox>
/// <CheckBox IsChecked="{Binding Prop1, Converter={StaticResource FlagsToBooleanConverter}, Mode=TwoWay, 
///     ConverterParameter={x:Static local:FlagType.Report}}" >Report</CheckBox>
/// </summary>
public class EditableFlagsToBooleanConverter : IValueConverter
{
    private ulong _target;

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (parameter is Enum && value is Enum)
        {
            var mask = (ulong) parameter;
            _target = (ulong) value;
            return ((mask & _target) != 0);
        }

        return Binding.DoNothing;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value is bool && parameter is Enum)
        {
            var mask = (ulong)parameter;
            if ((bool)value)
            {
                _target |= mask;
            }
            else
            {
                _target &= ~mask;
            }
            return _target;
        }

        return Binding.DoNothing;
    }
} 
person agroskin    schedule 01.12.2010
comment
Имейте в виду, что _target - это разделяемое значение. Поэтому вам нужно создать новый конвертер для каждой группы флажков! - person Sven; 08.05.2020
comment
/// Предупреждение: уловка заключается в локальном хранении значения между вызовами Convert и ConvertBack /// У вас должен быть один экземпляр этого конвертера для каждого свойства flags для каждого объекта /// Не используйте этот конвертер между разными объектами или свойствами - person agroskin; 09.05.2020