For those who like a good WPF binding challenge:
I have a nearly functional example of two-way binding a CheckBox
to an individual bit of a flags enumeration (thanks Ian Oakes, original MSDN post). The problem though is that the binding behaves as if it is one way (UI to DataContext
, not vice versa). So effectively the CheckBox
does not initialize, but if it is toggled the data source is correctly updated. Attached is the class defining some attached dependency properties to enable the bit-based binding. What I've noticed is that ValueChanged is never called, even when I force the DataContext
to change.
What I've tried: Changing the order of property definitions, Using a label and text box to confirm the DataContext
is bubbling out updates, Any plausible FrameworkMetadataPropertyOptions
(AffectsRender
, BindsTwoWayByDefault
), Explicitly setting Binding Mode=TwoWay
, Beating head on wall, Changing ValueProperty
to EnumValueProperty
in case of conflict.
Any suggestions or ideas would be extremely appreciated, thanks for anything you can offer!
The enumeration:
[Flags] public enum Department : byte { None = 0x00, A = 0x01, B = 0x02, C = 0x04, D = 0x08 } // end enum Department
The XAML usage:
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}"
The class:
/// <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
You could use a value converter. Here's a very specific implementation for the target Enum
, but would not be hard to see how to make the converter more generic:
[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; } }
And then use the converter in the 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>
EDIT: I don't have enough "rep" (yet!) to comment below so I have to update my own post :(
In the last comment Steve Cadwallader says: "but when it comes to two-way binding the ConvertBack falls apart", well I've updated my sample code above to handle the ConvertBack scenario; I've also posted a sample working application here (edit: note that the sample code download also includes a generic version of the converter).
Personally I think this is a lot simpler, I hope this helps.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With