2008-11-28 4 views
13

Pour ceux qui aiment un bon défi contraignant WPF:Comment pouvez-vous lier deux fois une case à cocher à un bit individuel d'une énumération de drapeaux?

J'ai un exemple presque fonctionnel de deux voies de liaison d'une case à cocher à un bit individuel d'une énumération des drapeaux (merci Ian Oakes, original MSDN post). Le problème est que la liaison se comporte comme si elle était à sens unique (UI to DataContext, et non l'inverse). La case à cocher ne s'initialise donc pas, mais si elle est activée, la source de données est correctement mise à jour. Attachée est la classe définissant certaines propriétés de dépendance attachées pour activer la liaison basée sur les bits. Ce que j'ai remarqué est que ValueChanged n'est jamais appelé, même quand je force le DataContext à changer.

Ce que j'ai essayé: Changer l'ordre des définitions de propriétés, l'aide d'une étiquette et zone de texte pour confirmer la DataContext bouillonne des mises à jour, toutes les FrameworkMetadataPropertyOptions plausibles (AffectsRender, BindsTwoWayByDefault), réglage Explicitement mode de liaison = TwoWay, Battant head on wall, Changer ValueProperty en EnumValueProperty en cas de conflit.

Toutes les suggestions ou idées seraient extrêmement appréciées, merci pour tout ce que vous pouvez offrir!

L'énumération:


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

L'utilisation de 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 classe:


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

Répondre

37

Vous pouvez utiliser un convertisseur de valeur. Voici une application très spécifique pour la cible Enum, mais ne serait pas difficile de voir comment rendre le convertisseur plus générique:

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

Et puis utilisez le convertisseur dans le 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 : Je n'ai pas assez de "rep" (encore!) Pour commenter ci-dessous donc je dois mettre à jour mon propre post :(

Dans le dernier commentaire, demwiz.myopenid.com dit "mais quand il s'agit de deux- manière contraignante le ConvertBack tombe en morceaux ", eh bien j'ai mis à jour d mon exemple de code ci-dessus pour gérer le scénario ConvertBack; J'ai également posté un exemple d'application de travail here (edit: note que l'exemple de téléchargement de code inclut également une version générique du convertisseur).

Personnellement, je pense que c'est beaucoup plus simple, j'espère que cela aide.

+0

Merci pour la suggestion Paul, mais s'il y a plusieurs cases à cocher alors le ConvertBack de l'un d'entre eux va remplacer et perdre les données pour les autres bits. C'est la partie ConvertBack qui en fait un problème délicat. –

+0

En effet, l'échantillon est un peu simpliste; cependant, je pense que cette solution s'applique toujours comme vous pourriez regarder le bool entrant? valeur, puis^= la valeur basée sur le masque fourni dans le paramètre ConverterParameter; avoir un sens? Si ce n'est pas lemme savoir et je vais poster du code quand je reçois un peu de temps pendant les vacances. – PaulJ

+0

Mise à jour du post pour inclure le scénario ConvertBack, notez également que j'ai posté un lien vers une copie de travail de l'application. – PaulJ

1

Vérifiez votre DataObject qui se lie au CheckBoxes contient la propriété Ministère a une INotifyPropertyChnaged. PropertyChanged a appelé son Setter?

+0

Je suis lié à un DataRow fortement typé qui publie avec succès les événements PropertyChanged. Je l'ai confirmé en le liant à d'autres contrôles de l'interface utilisateur (Label, TextBox) qui seraient mis à jour correctement. Merci pour la suggestion cependant. :) –

2

Merci pour l'aide de tout le monde, j'ai finalement compris.

Je suis lié à un DataSet fortement typé, de sorte que les énumérations sont stockées en tant que type System.Byte et non System.Enum. J'ai remarqué une exception de moulage de liaison silencieuse dans ma fenêtre de sortie de débogage qui m'a indiqué cette différence. La solution est la même que ci-dessus, mais avec ValueProperty étant de type Byte au lieu de Enum.

Voici la classe CheckBoxFlagsBehavior répétée dans sa révision finale. Merci encore à Ian Oakes pour la mise en œuvre 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 
+0

Cela semble terriblement complexe - pourquoi pas un convertisseur de valeur simple –

+1

Un convertisseur de valeur est idéal pour la liaison unidirectionnelle, mais en ce qui concerne la liaison bidirectionnelle, le ConvertBack s'effondre car vous ne pouvez pas savoir à quoi les autres bits sont affectés pour renvoyer une valeur valide. –

Questions connexes