Come si può associare una casella di controllo bidirezionale a un singolo bit di una enumerazione di bandiere?

Per coloro a cui piace una buona sfida vincolante WPF:

Ho un esempio quasi funzionale di associazione bidirezionale di una casella di controllo a un singolo bit di un’enumerazione di bandiere (grazie a Ian Oakes, post originale MSDN ). Il problema però è che l’associazione si comporta come se fosse unidirezionale (da UI a DataContext, non viceversa). In modo efficace, la casella di controllo non viene inizializzata, ma se viene triggersta, l’origine dati viene aggiornata correttamente. In allegato è la class che definisce alcune proprietà di dipendenza allegate per abilitare l’associazione basata su bit. Quello che ho notato è che ValueChanged non viene mai chiamato, anche quando imposto il cambio DataContext.

Cosa ho provato: Modifica dell’ordine delle definizioni delle proprietà, Utilizzo di un’etichetta e una casella di testo per confermare che DataContext stia saltando fuori aggiornamenti, Qualsiasi FrameworkMetadataPropertyOptions plausibile (AffectsRender, BindsTwoWayByDefault), Impostazione esplicita Modalità binding = TwoWay, Beating head on wall, Changing ValueProperty to EnumValueProperty in caso di conflitto.

Qualsiasi suggerimento o idea sarebbe estremamente apprezzato, grazie per tutto ciò che puoi offrire!

L’enumerazione:

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

L’utilizzo di 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}" 

La 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 

Potresti usare un convertitore di valori. Ecco una implementazione molto specifica per l’objective Enum, ma non sarebbe difficile vedere come rendere il convertitore più generico:

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

E quindi utilizzare il convertitore in XAML:

        

EDIT: non ho abbastanza “rep” (ancora!) Per commentare qui, quindi devo aggiornare il mio post 🙁

Nell’ultimo commento, demwiz.myopenid.com dice “ma quando si tratta di associazione a due vie la conversione di ConvertBack”, ho aggiornato il mio codice di esempio sopra per gestire lo scenario ConvertBack; Ho anche pubblicato qui un’applicazione di esempio ( modifica: si noti che il download del codice di esempio include anche una versione generica del convertitore).

Personalmente penso che sia molto più semplice, spero che questo aiuti.

Grazie per l’aiuto di tutti, finalmente l’ho capito.

Sono vincolato a un DataSet fortemente tipizzato, quindi le enumerazioni vengono archiviate come tipo System.Byte e non System.Enum. Mi è capitato di notare un’eccezione silenziosa di colaggio vincolante nella finestra di output del debug che mi ha indicato questa differenza. La soluzione è la stessa di sopra, ma con ValueProperty di tipo Byte anziché Enum.

Ecco la class CheckBoxFlagsBehavior ripetuta nella sua revisione finale. Grazie ancora a Ian Oakes per l’implementazione originale!

 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 

Controlla il tuo DataObject che si lega ai CheckBoxes contiene la proprietà Department ha un INotifyPropertyChnaged.PropertyChanged chiamato sul suo Setter?

Ecco una cosa che mi è venuta in mente che lascia la View bella e pulita (niente risorse statiche necessarie, nessuna nuova proprietà allegata da compilare, nessun convertitore o parametro del convertitore richiesto nell’associazione) e lascia il ViewModel pulito (nessuna proprietà aggiuntiva da associare a )

La vista assomiglia a questa:

     

Il ViewModel si presenta così:

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

Se hai intenzione di assegnare un nuovo valore alla proprietà Department, non farlo. Lascia il Dipartimento da solo. Scrivi invece il nuovo valore su Department.Value.

Qui è dove avviene la magia (questa class generica può essere riutilizzata per qualsiasi enumerazione di bandiera)

 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 }