Как можно двухсторонне связать флажок с отдельным битом перечисления флагов? - PullRequest
17 голосов
/ 29 ноября 2008

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

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

Что я пробовал: Изменение порядка определений свойств, Использование метки и текстового поля для подтверждения того, что DataContext пузырится обновлениями, Любые правдоподобные FrameworkMetadataPropertyOptions (AffectsRender, BindsTwoWayByDefault), Явная установка режима привязки = TwoWay, Удар головой на стену, Изменение ValueProperty в EnumValuePro в случае конфликт.

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

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


    [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}"

Класс:


    /// 
    /// A helper class for providing bit-wise binding.
    /// 
    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

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

Ответы [ 5 ]

44 голосов
/ 17 декабря 2008

Вы можете использовать конвертер значений. Вот очень конкретная реализация для целевого 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>

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

В последнем комментарии demwiz.myopenid.com говорит «но когда дело доходит до двухстороннего связывания, ConvertBack разваливается», я обновил мой пример кода выше для обработки сценария ConvertBack; Я также разместил пример рабочего приложения здесь ( edit: ), обратите внимание, что загрузка примера кода также включает в себя общую версию конвертер).

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

2 голосов
/ 21 декабря 2008

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

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

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

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
1 голос
/ 29 марта 2018

Вот кое-что, что я придумал, которое оставляет представление красивым и чистым (не требуются статические ресурсы, не требуются новые присоединенные свойства, не требуются конвертеры или параметры конвертера в привязке) и оставляет 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
}
1 голос
/ 29 ноября 2008

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

0 голосов
/ 04 декабря 2018

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

Использование 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>
...