如何双向将复选框绑定到标志枚举的单个位?

对于喜欢WPF绑定挑战的人:

我有一个近乎function的例子,双向绑定一个复选框到一个标志枚举的单个位(感谢Ian Oakes, 原始的MSDNpost )。 但问题是绑定的行为就好像它是一种方式(UI到DataContext,反之亦然)。 因此,复选框不会初始化,但如果切换,则数据源会正确更新。 Attached是定义一些附加依赖项属性的类,以启用基于位的绑定。 我注意到的是,即使我强制DataContext更改,也永远不会调用ValueChanged。

我尝试过:更改属性定义的顺序,使用标签和文本框确认DataContext正在冒泡更新,任何合理的FrameworkMetadataPropertyOptions(AffectsRender,BindsTwoWayByDefault),显式设置绑定模式= 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}" 

class级:

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

您可以使用值转换器。 这是目标枚举的一个非常具体的实现,但不难看出如何使转换器更通用:

 [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中使用转换器:

        

编辑:我没有足够的“代表”(尚未!)在下面发表评论所以我必须更新自己的post:(

在最后一条评论中,demwiz.myopenid.com说“但是当谈到双向绑定时,ConvertBack会崩溃”,我已经更新了上面的示例代码来处理ConvertBack场景; 我还在这里发布了一个示例工作应用程序( 编辑:请注意,示例代码下载还包括转换器的通用版本)。

我个人认为这更简单,我希望这会有所帮助。

谢谢大家的帮助,我终于明白了。

我绑定到强类型的DataSet,因此枚举存储为System.Byte类型而不是System.Enum。 我碰巧在我的调试输出窗口中发现了一个静默的绑定转换exception,它指出了这个区别。 解决方案与上面相同,但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 

检查绑定到CheckBoxes的DataObject包含Department属性是否在其Setter上调用了一个INotifyPropertyChnaged.PropertyChanged?

这是我想出的一些东西,它让View看起来干净整洁(没有必要的静态资源,没有新的附加属性来填充,没有转换器或转换器参数在绑定中所需),并且使ViewModel保持干净(没有额外的属性可以绑定到)

View看起来像这样:

     

ViewModel看起来像这样:

 public class ViewModel : ViewModelBase { private Department department; public ViewModel() { Department = new EnumFlags(department); } public Department Department { get; private set; } } 

如果您要为Department属性分配新值,请不要。 单独留下部门。 将新值写入Department.Value。

这就是魔术发生的地方(这个generics类可以重用于任何标志枚举)

 public class EnumFlags : 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 }