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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/326802/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-03 23:19:45  来源:igfitidea点击:

How can you two-way bind a checkbox to an individual bit of a flags enumeration?

c#wpfdata-bindingenumsbit-manipulation

提问by Steve Cadwallader

For those who like a good WPF binding challenge:

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

I have a nearly functional example of two-way binding a CheckBoxto 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 CheckBoxdoes 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 DataContextto change.

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

What I've tried:Changing the order of property definitions, Using a label and text box to confirm the DataContextis bubbling out updates, Any plausible FrameworkMetadataPropertyOptions(AffectsRender, BindsTwoWayByDefault), Explicitly setting Binding Mode=TwoWay, Beating head on wall, Changing ValuePropertyto EnumValuePropertyin case of conflict.

我尝试过的:更改属性定义的顺序,使用标签和文本框确认DataContext正在冒泡更新,任何合理FrameworkMetadataPropertyOptionsAffectsRenderBindsTwoWayByDefault),明确设置Binding Mode=TwoWay,在墙上敲头,在发生冲突时更改ValuePropertyEnumValueProperty

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:

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

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

采纳答案by PaulJ

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:

您可以使用值转换器。这是 target 的一个非常具体的实现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;
    }
}

And then use the converter in the XAML:

然后在 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 Cadwalladersays: "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).

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

Personally I think this is a lot simpler, I hope this helps.

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

回答by Jobi Joy

Check your DataObject which binds to the CheckBoxes contains Department property has an INotifyPropertyChnaged.PropertyChanged called on its Setter?

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

回答by Steve Cadwallader

Thanks for everyone's help, I finally figured it out.

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

I am binding to a strongly typed DataSet, so the enumerations are stored as type System.Byte and not System.Enum. I happened to notice a silent binding casting exception in my debug output window which pointed me to this difference. The solution is the same as above, but with the ValueProperty being of type Byte instead of Enum.

我绑定到强类型数据集,因此枚举存储为 System.Byte 类型而不是 System.Enum。我碰巧在调试输出窗口中注意到一个静默绑定转换异常,它指出了这种差异。解决方案与上述相同,但 ValueProperty 的类型为 Byte 而不是 Enum。

Here is the CheckBoxFlagsBehavior class repeated in its final revision. Thanks again to Ian Oakes for the original implementation!

这是在其最终修订版中重复的 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

回答by Nick

Here's something I came up with that leaves the View nice and clean (no static resources necessary, no new attached properties to fill out, no converters or converter parameters required in the binding), and leaves the ViewModel clean (no extra properties to bind to)

这是我想出的一些东西,它使 View 保持干净整洁(不需要静态资源,不需要填写新的附加属性,绑定中不需要转换器或转换器参数),并使 ViewModel 保持干净(没有要绑定到的额外属性) )

The View looks like this:

视图如下所示:

<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]}"/>

The ViewModel looks like this:

ViewModel 看起来像这样:

public class ViewModel : ViewModelBase
{
  private Department department;

  public ViewModel()
  {
    Department = new EnumFlags<Department>(department);
  }

  public Department Department { get; private set; }
}

If you're ever going to assign a new value to the Department property, don't. Leave Department alone. Write the new value to Department.Value instead.

如果您打算为 Department 属性分配一个新值,请不要这样做。离开部门。改为将新值写入 Department.Value。

This is where the magic happens (this generic class can be reused for any flag enum)

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

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
}

回答by Skelvir

I do not have enough rep to comment yet, this solution is targeting user99999991:
"Cant have multiple checkboxes binding to different values on a page with the same converter I guess."
Another advantage, with this solution you can also bind the Flag mask instead of hardcoding a static reference.

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

Using a IMultiValueConverter:

使用 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 is a List, so you can add multiple Checkboxes during Runtime):

ItemsControl(CheckBoxTemplates 是一个列表,因此您可以在运行时添加多个复选框):

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

Important: When declaring the converter, set x:Shared="False" so that multiple instances are created:

重要提示:声明转换器时,设置 x:Shared="False" 以便创建多个实例:

<UserControl.Resources>
    <ui:FlagToBoolConverter x:Key="FlagToBoolConverter" x:Shared="False"></ui:FlagToBoolConverter>
</UserControl.Resources>