【问题标题】:How can you two-way bind a checkbox to an individual bit of a flags enumeration?如何将复选框双向绑定到标志枚举的单个位?
【发布时间】:2010-09-24 12:24:58
【问题描述】:

对于那些喜欢良好的 WPF 绑定挑战的人:

我有一个几乎实用的示例,将CheckBox 双向绑定到标志枚举的单个位(感谢 Ian Oakes,original MSDN post)。但问题是绑定的行为好像是一种方式(UI 到 DataContext,反之亦然)。所以有效地CheckBox 不会初始化,但如果它被切换,则数据源会正确更新。 Attached 是定义一些附加依赖属性以启用基于位的绑定的类。我注意到 ValueChanged 永远不会被调用,即使我强制 DataContext 更改。

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

班级:

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

【问题讨论】:

    标签: c# wpf data-binding enums bit-manipulation


    【解决方案1】:

    您可以使用值转换器。这是目标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>
    

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

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

    我个人认为这要简单得多,希望对您有所帮助。

    【讨论】:

    • 感谢 Paul 的建议,但如果有多个复选框,则其中任何一个的 ConvertBack 都会覆盖并丢失其他位的数据。正是 ConvertBack 部分使这成为一个棘手的问题。
    • 确实,示例有点简单;但是,我认为这个解决方案仍然适用,因为您可以查看传入的布尔值?值,然后 ^= 基于 ConverterParameter 中提供的掩码的值;有道理?如果不知道的话,我会在假期有时间时发布一些代码。
    • 更新了帖子以包含 ConvertBack 场景,还请注意,我已经发布了指向应用程序工作副本的链接。
    • 你的建议确实看起来更干净。我避免使用转换器对象上的字段,因为我不清楚 WPF 如何处理创建单独的转换器实例。如果您有两组四个复选框用于枚举的两个副本 - 您认为您的解决方案会起作用吗?
    • @PaulJ 如果在 ItemsControl 内的 DataTemplate 中创建需要绑定到枚举标志位的复选框怎么办?我无法为每个标志创建单独的转换器实例,因为我不知道需要创建多少个,并且我无法使用 ConverterParameter 传入掩码,因为 ConverterParameter 不是 DependencyProperty。跨度>
    【解决方案2】:

    我还没有足够的代表发表评论,此解决方案针对 user99999991:
    “我猜,不能有多个复选框绑定到具有相同转换器的页面上的不同值。”
    另一个优点是,使用此解决方案,您还可以绑定标志掩码,而不是硬编码静态引用。

    使用 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是一个List,所以可以在运行时添加多个Checkbox):

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

    【讨论】:

      【解决方案3】:

      这是我想出的东西,它使 View 干净整洁(无需静态资源,无需填写新的附加属性,绑定中无需转换器或转换器参数),并使 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
      }
      

      【讨论】:

      • 喜欢它,简单且可重复使用(例如在共享项目中)。此外,您可以为每个枚举值添加本地化字符串作为奖励功能。唯一困扰我的是 XAML 中的缺失值检查(即输入错误的值)......
      【解决方案4】:

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

      我绑定到一个强类型数据集,所以枚举存储为 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
      

      【讨论】:

      • 这看起来非常复杂 - 为什么一个简单的值转换器不能完成这项工作?
      • 值转换器非常适合单向绑定,但是当涉及双向绑定时,ConvertBack 会崩溃,因为您无法知道其他位设置为什么以返回有效值。
      【解决方案5】:

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

      【讨论】:

      • 我正在绑定一个强类型的 DataRow,它确实成功地发布了 PropertyChanged 事件。我通过将其绑定到可以正确更新的其他 UI 控件(标签、文本框)来确认这一点。不过谢谢你的建议。 :)
      猜你喜欢
      • 2015-09-09
      • 2012-04-21
      • 2011-12-06
      • 2010-10-12
      • 2023-03-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多