2

总结一下我正在做的事情,我有一个自定义控件,它看起来像一个选中的列表框,它有两个依赖属性,一个提供可用选项列表,另一个表示组合选择选项的枚举标志值。

因此,正如我提到的,我的自定义控件公开了两个不同的 DependencyProperties,其中一个是名为Options的选项列表,另一个名为SelectedOptions的属性是特定的 Enum 类型,它使用 [Flags] 属性来允许设置值的组合。然后,我的 UserControl 包含一个类似于 ListBox 的 ItemsControl,用于显示选项和复选框。当检查或未选中复选框时,应使用相应的位置操作相应地更新选定的属性。

我遇到的问题是,除了求助于维护私有字段和处理属性更改事件来更新我的属性之外,我别无他法,这在 WPF 中感觉不自然。我曾尝试使用 ValueConverters 但遇到了一个问题,即我无法真正使用与值转换器绑定的绑定,因此我需要将我的枚举值硬编码为 ValueConverter 参数,这是不可接受的。如果有人看到了如何明智地做到这一点的好例子,我将不胜感激任何输入。

旁注:这也是我过去在试图了解依赖属性如何不允许计算或延迟值时遇到的一个问题。另一个示例是,当人们可能希望将子控件上的属性公开为父控件上的属性时。大多数人在这种情况下建议使用绑定,但仅当子控件属性是依赖属性时才有效,因为放置绑定以便目标是父属性,当父控件的用户想要设置自己的绑定时,它将被覆盖对于那个属性。

4

3 回答 3

2

I can't be sure exactly what you're trying to do without looking at your code in-depth, but I think I have a vague idea of your scenario. I have constructed an example for you, illustrating something similar to this. Rather than build a new control, I have placed all of the code in a single Window, for ease of demonstration. For starters, let's look at the XAML for the window:

<Window x:Class="TestWpfApplication.DataBoundFlags"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TestWpfApplication"
Title="DataBoundFlags" Height="300" Width="300"
DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>

    <ListBox ItemsSource="{Binding AvailableOptions}" Grid.Row="0">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <CheckBox Content="{Binding}" CommandParameter="{Binding}"
                          Command="{Binding RelativeSource={RelativeSource FindAncestor, 
                          AncestorType={x:Type Window}}, Path=SelectCommand}"/>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>

    <TextBlock Text="{Binding SelectedOptions}" Grid.Row="1"/>
</Grid>

The window's DataContext is set to its own code-behind, so I can bind to properties there. I have a handful of properties- AvailableOptions is all the options you can choose from. SelectedOptions are the options that the user has currently selected. SelectCommand is a RelayCommand that is used to either add a flag to the SelectedOptions or remove one.

The rest of the XAML should be very straightforward. The ListBox is bound to all of the available options, and each option is represented as a single CheckBox. Pay careful attention to the CommandParameter, which is bound to the option item itself. Now let's take a look at the code-behind, where the magic happens:

[Flags()]
public enum Options
{
    Plain = 0,
    Ketchup = 1,
    Mustard = 2,
    Mayo = 4,
    HotSauce = 8
}

public partial class DataBoundFlags : Window
{
    public static readonly DependencyProperty SelectedOptionsProperty =
        DependencyProperty.Register("SelectedOptions", typeof(Options), typeof(DataBoundFlags));

    public Options SelectedOptions
    {
        get { return (Options)GetValue(SelectedOptionsProperty); }
        set { SetValue(SelectedOptionsProperty, value); }
    }

    public List<Options> AvailableOptions
    {
        get
        {
            return new List<Options>()
            {
                Options.Ketchup,
                Options.Mustard,
                Options.Mayo,
                Options.HotSauce
            };
        }
    }

    public ICommand SelectCommand
    {
        get;
        private set;
    }

    /// <summary>
    /// If the option is selected, unselect it.
    /// Otherwise, select it.
    /// </summary>
    private void OnSelect(Options option)
    {
        if ((SelectedOptions & option) == option)
            SelectedOptions = SelectedOptions & ~option;
        else
            SelectedOptions |= option;
    }

    public DataBoundFlags()
    {
        SelectCommand = new RelayCommand((o) => OnSelect((Options)o));
        InitializeComponent();
    }
}

Beginning from the top, we have the enum declaration, followed by the SelectedOptions dependency property, and the AvailableOptions property (which can be a standard CLR property since it will never change). We then have our command, and the handler which will be executed for the command (whenever an option is checked or unchecked). First notice how the command is wired up- we create a new RelayCommand and tell it to run OnSelect, passing in the command parameter. Remember this is the same command parameter that was bound in the XAML- that means it is the current option being checked or unchecked. We compare that option to the SelectedOptions using bitwise operators. If the option exists, that means we are unchecking it and we need to clear it off using a bitwise AND. If it doesn't exist, we add it to selected using a bitwise OR.

When that happens, the SelectedOptions dependency property is automatically updated, which updates the TextBlock binding in the XAML. Here is the final result:

alt text

于 2010-04-13T20:47:33.257 回答
1

不同的解决方案

对于这种情况,我使用了一种非常不同的解决方案,我认为它更干净。使用我创建的几个实用程序类,我可以直接绑定 SelectedOptions,而无需编写应用程序代码来处理命令、集合更新等。

枚举扩展类

我创建了一个具有以下签名的简单类:

public class EnumExpansion : DependencyObject, IList, INotifyCollectionChanged
{
  public object EnumValue   { ... // DependencyProperty
  ... // IList & INotifyCollectionChanged implementation
}

EnumValue 可以设置为任何枚举类型。当设置 EnumValue 时,内部 ObservableCollection 会通过删除所有不在当前 EnumValue 中的标志并添加当前 EnumValue 中的所有标志来更新。每当更改内部集合时,都会更新 EnumValue。

BindableSelectedItems 属性

我还创建了一个简单的附加属性,允许 ListBox 绑定其 SelectedItems 属性。它是这样使用的:

<ListBox ItemsSource="{Binding Options}"
  edf:ListBoxHelper.BindableSelectedItems="{Binding SelectedOptionsExpansion}" />

附加属性是通过订阅 ListBox 上的 SelectionChanged 和属性值(类型为 INotifyCollectionChanged)的 CollectionChanged 来实现的。

初始化 SelectedOptionsExpansion

您可以在 XAML 中执行此操作,但在代码中非常简单:

public EnumExpansion SelectedOptionsExpansion { get; set; }

  ...
  SelectedOptionsExpansion = new EnumExpansion();
  BindingOperations.SetBinding(SelectedOptionsExpansion, EnumExpansion.EnumValueProperty,
    new Binding { Path = "SelectedOptions", Source = this });
  ...

这个怎么运作

枚举到列表框:

  1. SelectedOptions 更改,通过代码或数据绑定
  2. SelectedOptionsExpansion 的 EnumValue 属性由绑定更新,这会导致 EnumExpansion 的集合发生变化。
  3. CollectionChange 事件由 ListBoxHelper 代码获取,该代码更新 ListBox 中的选择。

列表框到枚举:

  1. 在 ListBox 中选择或取消选择项目
  2. ListBoxHelper 拾取它并更新 EnumExpansion 集合,这会导致 EnumValue 属性更新。
  3. 由于 EnumValue 是 BindsTwoWayByDefault,因此 SelectedOptions 值会更新。

为什么我更喜欢这个解决方案

一旦创建了两个实用程序类,绑定过程的其余部分就是直接的数据绑定。无需在应用程序代码中处理命令或更新集合 - 它们都隐藏在实用程序类中。

于 2010-04-14T21:48:47.120 回答
0

CheckBox.IsChecked为了支持默认值的概念,您需要在属性上设置绑定。您需要当前选项(DataContext位于相关复选框的 )以及SelectedOptions位于窗口上的属性。所以这个绑定变成了:

<CheckBox.IsChecked>
    <MultiBinding Converter="{StaticResource FlagsToBoolConverter}">
        <Binding RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Window}}" 
                 Path="SelectedOptions"/>
        <Binding RelativeSource="{RelativeSource Self}" Path="DataContext"/>
    </MultiBinding>
</CheckBox.IsChecked>

只需接受这些FlagsToBoolConverter并检查当前选项是否在SelectedOptions

public class FlagsToBoolConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        Options selected = (Options)values[0];
        Options current = (Options)values[1];

        return ((selected & current) == current);
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        return null;
    }
}

现在尝试SelectedOptions在构造函数中设置一些默认值。请注意,CheckBox自动检查相关内容,并且所有绑定仍然有效。胜利!

于 2010-04-14T15:34:30.823 回答