22

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

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

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

任何建议或想法将不胜感激,感谢您提供的任何东西!

枚举:

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

5 回答 5

53

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

然后在 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 场景;我还在这里发布了一个示例工作应用程序(编辑:请注意示例代码下载还包括转换器的通用版本)。

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

于 2008-12-17T17:13:32.393 回答
4

这是我想出的东西,它使 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
}
于 2018-03-29T14:51:17.550 回答
2

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

我绑定到一个强类型的 DataSet,所以枚举存储为 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
于 2008-12-21T18:19:58.727 回答
1

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

于 2008-11-29T06:42:54.327 回答
0

我还没有足够的代表发表评论,这个解决方案的目标是 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>
于 2018-12-04T11:45:44.370 回答