Как можно двусторонним образом привязать флажок к отдельному биту перечисления флагов?

Для тех, кто любит хорошую задачу привязки WPF:

У меня есть почти функциональный пример двусторонней привязки CheckBox к отдельному биту перечисления флагов (спасибо Иану Оуксу, исходное сообщение MSDN). Однако проблема в том, что привязка ведет себя так, как если бы она была односторонней (пользовательский интерфейс к DataContext, а не наоборот). Таким образом, CheckBox не инициализируется, но если он включен, источник данных обновляется правильно. Прикреплен класс, определяющий некоторые присоединенные свойства зависимостей для включения привязки на основе битов. Я заметил, что ValueChanged никогда не вызывается, даже когда я заставляю DataContext измениться.

Что я пробовал: изменение порядка определений свойств, использование метки и текстового поля для подтверждения DataContext всплывающих обновлений, любые правдоподобные FrameworkMetadataPropertyOptions (AffectsRender, BindsTwoWayByDefault), явная установка Binding Mode=TwoWay, избиение головы на стене, замена ValueProperty на EnumValueProperty в случае конфликта.

Мы будем очень благодарны за любые предложения или идеи, спасибо за все, что вы можете предложить!

Перечисление:

[Flags]
public enum Department : byte
{
    None = 0x00,
    A = 0x01,
    B = 0x02,
    C = 0x04,
    D = 0x08
} // end enum Department

Использование XAML:

CheckBox Name="studentIsInDeptACheckBox"
         ctrl:CheckBoxFlagsBehaviour.Mask="{x:Static c:Department.A}"
         ctrl:CheckBoxFlagsBehaviour.IsChecked="{Binding Path=IsChecked, RelativeSource={RelativeSource Self}}"
         ctrl:CheckBoxFlagsBehaviour.Value="{Binding Department}"

Класс:

/// <summary>
/// A helper class for providing bit-wise binding.
/// </summary>
public class CheckBoxFlagsBehaviour
{
    private static bool isValueChanging;

    public static Enum GetMask(DependencyObject obj)
    {
        return (Enum)obj.GetValue(MaskProperty);
    } // end GetMask

    public static void SetMask(DependencyObject obj, Enum value)
    {
        obj.SetValue(MaskProperty, value);
    } // end SetMask

    public static readonly DependencyProperty MaskProperty =
        DependencyProperty.RegisterAttached("Mask", typeof(Enum),
        typeof(CheckBoxFlagsBehaviour), new UIPropertyMetadata(null));

    public static Enum GetValue(DependencyObject obj)
    {
        return (Enum)obj.GetValue(ValueProperty);
    } // end GetValue

    public static void SetValue(DependencyObject obj, Enum value)
    {
        obj.SetValue(ValueProperty, value);
    } // end SetValue

    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.RegisterAttached("Value", typeof(Enum),
        typeof(CheckBoxFlagsBehaviour), new UIPropertyMetadata(null, ValueChanged));

    private static void ValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        isValueChanging = true;
        byte mask = Convert.ToByte(GetMask(d));
        byte value = Convert.ToByte(e.NewValue);

        BindingExpression exp = BindingOperations.GetBindingExpression(d, IsCheckedProperty);
        object dataItem = GetUnderlyingDataItem(exp.DataItem);
        PropertyInfo pi = dataItem.GetType().GetProperty(exp.ParentBinding.Path.Path);
        pi.SetValue(dataItem, (value & mask) != 0, null);

        ((CheckBox)d).IsChecked = (value & mask) != 0;
        isValueChanging = false;
    } // end ValueChanged

    public static bool? GetIsChecked(DependencyObject obj)
    {
        return (bool?)obj.GetValue(IsCheckedProperty);
    } // end GetIsChecked

    public static void SetIsChecked(DependencyObject obj, bool? value)
    {
        obj.SetValue(IsCheckedProperty, value);
    } // end SetIsChecked

    public static readonly DependencyProperty IsCheckedProperty =
        DependencyProperty.RegisterAttached("IsChecked", typeof(bool?),
        typeof(CheckBoxFlagsBehaviour), new UIPropertyMetadata(false, IsCheckedChanged));

    private static void IsCheckedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (isValueChanging) return;

        bool? isChecked = (bool?)e.NewValue;
        if (isChecked != null)
        {
            BindingExpression exp = BindingOperations.GetBindingExpression(d, ValueProperty);
            object dataItem = GetUnderlyingDataItem(exp.DataItem);
            PropertyInfo pi = dataItem.GetType().GetProperty(exp.ParentBinding.Path.Path);

            byte mask = Convert.ToByte(GetMask(d));
            byte value = Convert.ToByte(pi.GetValue(dataItem, null));

            if (isChecked.Value)
            {
                if ((value & mask) == 0)
                {
                    value = (byte)(value + mask);
                }
            }
            else
            {
                if ((value & mask) != 0)
                {
                    value = (byte)(value - mask);
                }
            }

            pi.SetValue(dataItem, value, null);
        }
    } // end IsCheckedChanged

    /// <summary>
    /// Gets the underlying data item from an object.
    /// </summary>
    /// <param name="o">The object to examine.</param>
    /// <returns>The underlying data item if appropriate, or the object passed in.</returns>
    private static object GetUnderlyingDataItem(object o)
    {
        return o is DataRowView ? ((DataRowView)o).Row : o;
    } // end GetUnderlyingDataItem
} // end class CheckBoxFlagsBehaviour

person Steve Cadwallader    schedule 28.11.2008    source источник


Ответы (5)


Вы можете использовать конвертер значений. Вот очень конкретная реализация для цели Enum, но нетрудно понять, как сделать преобразователь более универсальным:

[Flags]
public enum Department
{
    None = 0,
    A = 1,
    B = 2,
    C = 4,
    D = 8
}

public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();

        this.DepartmentsPanel.DataContext = new DataObject
        {
            Department = Department.A | Department.C
        };
    }
}

public class DataObject
{
    public DataObject()
    {
    }

    public Department Department { get; set; }
}

public class DepartmentValueConverter : IValueConverter
{
    private Department target;

    public DepartmentValueConverter()
    {
    }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        Department mask = (Department)parameter;
        this.target = (Department)value;
        return ((mask & this.target) != 0);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        this.target ^= (Department)parameter;
        return this.target;
    }
}

А затем используйте конвертер в XAML:

<Window.Resources>
    <l:DepartmentValueConverter x:Key="DeptConverter" />
</Window.Resources>

 <StackPanel x:Name="DepartmentsPanel">
    <CheckBox Content="A"
              IsChecked="{Binding 
                            Path=Department,
                            Converter={StaticResource DeptConverter},
                            ConverterParameter={x:Static l:Department.A}}"/>
    <!-- more -->
 </StackPanel>

РЕДАКТИРОВАТЬ: У меня недостаточно репутации (пока!) для комментариев ниже, поэтому мне нужно обновить свой собственный пост :(

В последнем комментарии Стив Кадвалладер говорит: "но когда дело доходит до двусторонней привязки, ConvertBack падает отдельно ", я обновил приведенный выше пример кода для обработки сценария ConvertBack; Я также разместил образец рабочего приложения здесь < / a> (edit: обратите внимание, что загружаемый образец кода также включает общую версию конвертера).

Лично я считаю, что это намного проще, надеюсь, это поможет.

person PaulJ    schedule 17.12.2008
comment
Спасибо за предложение, Пол, но если есть несколько флажков, ConvertBack из любого из них переопределит и потеряет данные для других бит. Это часть ConvertBack, которая делает эту проблему сложной. - person Steve Cadwallader; 20.12.2008
comment
Действительно, образец немного упрощен; однако я думаю, что это решение все еще применимо, поскольку вы могли бы посмотреть на входящий bool? значение, а затем ^ = значение на основе маски, предоставленной в ConverterParameter; имеет смысл? Если нет, дайте мне знать, и я отправлю код, когда у меня будет время на праздниках. - person PaulJ; 22.12.2008
comment
Обновлен пост, чтобы включить сценарий ConvertBack, также обратите внимание, что я разместил ссылку на рабочую копию приложения. - person PaulJ; 23.12.2008
comment
Ваше предложение определенно кажется более чистым. Я избегаю полей в объекте конвертера, потому что не понимаю, как WPF обрабатывает создание отдельных экземпляров конвертеров. Если бы у вас было два набора из четырех флажков для двух копий перечисления - как вы думаете, ваше решение будет работать? - person Steve Cadwallader; 26.12.2008
comment
Абсолютно! Чтобы все это работало хорошо, вам нужно создать экземпляр конвертера для каждого значения флагов, которое вам нужно преобразовать. В моем примере кода выше вы просто добавляете еще один преобразователь в раздел Windows.Resources XAML, и все готово (очевидно, с другим значением x: Key). - person PaulJ; 27.12.2008
comment
В методе преобразования измените возвращаемое значение на: return (mask == 0 && this.target == 0) ? true : ((mask & this.target) != 0); для обработки флага со значением 0 - person Wiesław Šoltés; 06.05.2015
comment
@PaulJ Что делать, если мои флажки, которые необходимо привязать к битам флага перечисления, создаются в DataTemplate внутри ItemsControl? Я не могу создать отдельный экземпляр конвертера для каждого флага, потому что я не знаю, сколько нужно создать, и я не могу использовать ConverterParameter для передачи маски, потому что ConverterParameter не является DependencyProperty. - person Nick; 19.04.2016
comment
На этот вопрос все еще нет ответа: (Я думаю, у меня не может быть несколько флажков, привязанных к разным значениям на странице с одним и тем же преобразователем. - person user99999991; 05.05.2016
comment
@ user999999928 ответ выше работает с несколькими флажками - person artman; 02.02.2017

Спасибо всем за помощь, наконец-то разобрался.

Я привязываюсь к строго типизированному DataSet, поэтому перечисления хранятся как тип System.Byte, а не System.Enum. Я случайно заметил исключение беззвучного литья привязки в моем окне вывода отладки, которое указывало мне на эту разницу. Решение такое же, как указано выше, но ValueProperty имеет тип Byte вместо Enum.

Вот класс CheckBoxFlagsBehavior, повторенный в его последней редакции. Еще раз спасибо Яну Оуксу за оригинальную реализацию!

public class CheckBoxFlagsBehaviour
{
    private static bool isValueChanging;

    public static Enum GetMask(DependencyObject obj)
    {
        return (Enum)obj.GetValue(MaskProperty);
    } // end GetMask

    public static void SetMask(DependencyObject obj, Enum value)
    {
        obj.SetValue(MaskProperty, value);
    } // end SetMask

    public static readonly DependencyProperty MaskProperty =
        DependencyProperty.RegisterAttached("Mask", typeof(Enum),
        typeof(CheckBoxFlagsBehaviour), new UIPropertyMetadata(null));

    public static byte GetValue(DependencyObject obj)
    {
        return (byte)obj.GetValue(ValueProperty);
    } // end GetValue

    public static void SetValue(DependencyObject obj, byte value)
    {
        obj.SetValue(ValueProperty, value);
    } // end SetValue

    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.RegisterAttached("Value", typeof(byte),
        typeof(CheckBoxFlagsBehaviour), new UIPropertyMetadata(default(byte), ValueChanged));

    private static void ValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        isValueChanging = true;
        byte mask = Convert.ToByte(GetMask(d));
        byte value = Convert.ToByte(e.NewValue);

        BindingExpression exp = BindingOperations.GetBindingExpression(d, IsCheckedProperty);
        object dataItem = GetUnderlyingDataItem(exp.DataItem);
        PropertyInfo pi = dataItem.GetType().GetProperty(exp.ParentBinding.Path.Path);
        pi.SetValue(dataItem, (value & mask) != 0, null);

        ((CheckBox)d).IsChecked = (value & mask) != 0;
        isValueChanging = false;
    } // end ValueChanged

    public static bool? GetIsChecked(DependencyObject obj)
    {
        return (bool?)obj.GetValue(IsCheckedProperty);
    } // end GetIsChecked

    public static void SetIsChecked(DependencyObject obj, bool? value)
    {
        obj.SetValue(IsCheckedProperty, value);
    } // end SetIsChecked

    public static readonly DependencyProperty IsCheckedProperty =
        DependencyProperty.RegisterAttached("IsChecked", typeof(bool?),
        typeof(CheckBoxFlagsBehaviour), new UIPropertyMetadata(false, IsCheckedChanged));

    private static void IsCheckedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        if (isValueChanging) return;

        bool? isChecked = (bool?)e.NewValue;
        if (isChecked != null)
        {
            BindingExpression exp = BindingOperations.GetBindingExpression(d, ValueProperty);
            object dataItem = GetUnderlyingDataItem(exp.DataItem);
            PropertyInfo pi = dataItem.GetType().GetProperty(exp.ParentBinding.Path.Path);

            byte mask = Convert.ToByte(GetMask(d));
            byte value = Convert.ToByte(pi.GetValue(dataItem, null));

            if (isChecked.Value)
            {
                if ((value & mask) == 0)
                {
                    value = (byte)(value + mask);
                }
            }
            else
            {
                if ((value & mask) != 0)
                {
                    value = (byte)(value - mask);
                }
            }

            pi.SetValue(dataItem, value, null);
        }
    } // end IsCheckedChanged

    private static object GetUnderlyingDataItem(object o)
    {
        return o is DataRowView ? ((DataRowView)o).Row : o;
    } // end GetUnderlyingDataItem
} // end class CheckBoxFlagsBehaviour
person Steve Cadwallader    schedule 21.12.2008
comment
Это кажется ужасно сложным - почему бы с этой задачей не справился простой преобразователь значений? - person Daniel Paull; 22.12.2008
comment
Конвертер значений отлично подходит для односторонней привязки, но когда дело доходит до двусторонней привязки, ConvertBack разваливается, потому что вы не можете знать, какие другие биты установлены для возврата действительного значения. - person Steve Cadwallader; 23.12.2008

Вот кое-что, что я придумал, что оставляет View красивым и чистым (нет необходимости в статических ресурсах, нет новых прикрепленных свойств для заполнения, никаких преобразователей или параметров преобразователя, необходимых в привязке), и оставляет ViewModel чистым (нет дополнительных свойств для привязки к )

Вид выглядит так:

<CheckBox Content="A" IsChecked="{Binding Department[A]}"/>
<CheckBox Content="B" IsChecked="{Binding Department[B]}"/>
<CheckBox Content="C" IsChecked="{Binding Department[C]}"/>
<CheckBox Content="D" IsChecked="{Binding Department[D]}"/>

ViewModel выглядит так:

public class ViewModel : ViewModelBase
{
  private Department department;

  public ViewModel()
  {
    Department = new EnumFlags<Department>(department);
  }

  public Department Department { get; private set; }
}

Если вы когда-нибудь собираетесь присвоить новое значение свойству Department, не делайте этого. Оставьте Департамент в покое. Вместо этого запишите новое значение в Department.Value.

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

public class EnumFlags<T> : INotifyPropertyChanged where T : struct, IComparable, IFormattable, IConvertible
{
  private T value;

  public EnumFlags(T t)
  {
    if (!typeof(T).IsEnum) throw new ArgumentException($"{nameof(T)} must be an enum type"); // I really wish they would just let me add Enum to the generic type constraints
    value = t;
  }

  public T Value
  {
    get { return value; }
    set
    {
      if (this.value.Equals(value)) return;
      this.value = value;
      OnPropertyChanged("Item[]");
    }
  }

  [IndexerName("Item")]
  public bool this[T key]
  {
    get
    {
      // .net does not allow us to specify that T is an enum, so it thinks we can't cast T to int.
      // to get around this, cast it to object then cast that to int.
      return (((int)(object)value & (int)(object)key) == (int)(object)key);
    }
    set
    {
      if ((((int)(object)this.value & (int)(object)key) == (int)(object)key) == value) return;

      this.value = (T)(object)((int)(object)this.value ^ (int)(object)key);

      OnPropertyChanged("Item[]");
    }
  }

  #region INotifyPropertyChanged
  public event PropertyChangedEventHandler PropertyChanged;

  private void OnPropertyChanged([CallerMemberName] string memberName = "")
  {
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(memberName));
  }
  #endregion
}
person Nick    schedule 29.03.2018
comment
Люблю его, простой и пригодный для повторного использования (например, в общем проекте). Кроме того, вы можете добавить локализованные строки для каждого значения перечисления в качестве бонусной функции. Единственное, что меня беспокоит, это проверка отсутствующего значения в XAML (т.е. ошибочные значения) ... - person MHolzmayr; 26.09.2019

Проверьте, что ваш объект DataObject, который привязывается к CheckBoxes, содержит свойство Department имеет INotifyPropertyChnaged.PropertyChanged, вызванный его сеттером?

person Jobi Joy    schedule 29.11.2008
comment
Я привязываюсь к строго типизированному DataRow, который успешно публикует события PropertyChanged. Я подтвердил это, привязав его к другим элементам управления пользовательского интерфейса (Label, TextBox), которые будут обновляться правильно. Спасибо за предложение. :) - person Steve Cadwallader; 29.11.2008

У меня пока недостаточно комментариев для комментариев, это решение нацелено на user99999991:
«Я полагаю, что нельзя иметь несколько флажков, привязанных к разным значениям на странице с одним и тем же преобразователем».
Еще одно преимущество: с этим решением вы можете также привяжите маску флага вместо жесткого кодирования статической ссылки.

Использование IMultiValueConverter:

public class FlagToBoolConverter : IMultiValueConverter

{
    private YourFlagEnum selection;
    private YourFlagEnum mask;

    public static int InstanceCount = 0;

    public FlagToBoolConverter()
    {
        InstanceCount++;
    }

    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        mask = (YourFlagEnum ) values[1];
        selection = (YourFlagEnum ) values[0];
        return (mask & selection) != 0;
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value.Equals(true))
        {
            selection |= mask;
        }
        else
        {
            selection &= ~mask;
        }

        object[] o = new object[2];
        o[0] = selection;
        o[1] = mask;
        return o;
    }
}

ItemsControl (CheckBoxTemplates - это список, поэтому вы можете добавить несколько флажков во время выполнения):

                            <ItemsControl ItemsSource="{Binding CheckBoxTemplates}">
                                <ItemsControl.ItemsPanel>
                                    <ItemsPanelTemplate>
                                        <StackPanel Orientation="Vertical" Margin="40,0,0,0"></StackPanel>
                                    </ItemsPanelTemplate>
                                </ItemsControl.ItemsPanel>
                                <ItemsControl.ItemTemplate>
                                    <DataTemplate>
                                    <CheckBox Content="{Binding Path=Content}" >
                                        <CheckBox.Style>
                                            <Style TargetType="CheckBox">
                                                <Setter Property="IsChecked">
                                                    <Setter.Value>
                                                        <MultiBinding Converter="{StaticResource FlagToBoolConverter}">
                                                            <Binding Path="MyEnumProperty" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged"></Binding>
                                                            <Binding Path="MyEnumPropertyMask"></Binding>
                                                        </MultiBinding>
                                                    </Setter.Value>
                                                </Setter>
                                            </Style>
                                        </CheckBox.Style>
                                    </CheckBox>
                                    </DataTemplate>
                                </ItemsControl.ItemTemplate>
                            </ItemsControl>

Важно: при объявлении конвертера установите x: Shared = "False", чтобы было создано несколько экземпляров:

<UserControl.Resources>
    <ui:FlagToBoolConverter x:Key="FlagToBoolConverter" x:Shared="False"></ui:FlagToBoolConverter>
</UserControl.Resources>
person Skelvir    schedule 04.12.2018