如何使用自定义 WPF 控件上的依赖属性处理计算值

发布于 2024-08-29 02:50:59 字数 685 浏览 5 评论 0 原文

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

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

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

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

To summarize what I'm doing, I have a custom control that looks like a checked listbox and that has two dependency properties one that provides a list of available options and the other that represents a enum flag value that combines the selection options.

So as I mentioned my custom control exposes two different DependencyProperties, one of which is a list of options called Options and the other property called SelectedOptions is of a specific Enum type that uses the [Flags] attribute to allow combinations of values to be set. My UserControl then contains an ItemsControl similar to a ListBox that is used to display the options along with a checkbox. When the check box is checked or unchecked the SelectedOptions property should be updated accordingly by using the corresponding bitwise operation.

The problem I'm experiencing is that I have no way other than resorting to maintaining private fields and handling property change events to update my properties which just feels unatural in WPF. I have tried using ValueConverters but have run into the problem that I can't really using binding with the value converter binding so I would need to resort to hard coding my enum values as the ValueConverter parameter which is not acceptable. If anybody has seen a good example of how to do this sanely I would greatly appreciate any input.

Side Note:
This has been a problem I've had in the past too while trying to wrap my head around how dependency properties don't allow calculated or deferred values. Another example is when one may want to expose a property on a child control as a property on the parent. Most suggest in this case to use binding but that only works if the child controls property is a Dependency Property since placing the binding so that the target is the parent property it would be overwritten when the user of the parent control wants to set their own binding for that property.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

悲凉≈ 2024-09-05 02:50:59

如果不深入查看您的代码,我无法确切确定您想要做什么,但我认为我对您的场景有一个模糊的了解。我为您构建了一个示例,说明了与此类似的内容。为了便于演示,我没有构建新的控件,而是将所有代码放置在一个 Window 中。首先,让我们看一下窗口的 XAML:

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

窗口的 DataContext 设置为其自己的代码隐藏,因此我可以绑定到那里的属性。我有一些属性 - AvailableOptions 是您可以选择的所有选项。 SelectedOptions 是用户当前选择的选项。 SelectCommand 是一个 RelayCommand 用于向 SelectedOptions 添加标志或删除标志。

XAML 的其余部分应该非常简单。 ListBox 绑定到所有可用选项,每个选项都表示为单个 CheckBox。请特别注意CommandParameter,它绑定到选项项本身。现在让我们看一下隐藏的代码,魔法发生的地方:

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

从顶部开始,我们有枚举声明,后面是 SelectedOptions 依赖属性和 AvailableOptions 属性(可以是标准 CLR 属性,因为它永远不会改变)。然后我们就有了我们的命令,以及将为该命令执行的处理程序(每当选中或取消选中一个选项时)。首先注意命令是如何连接的 - 我们创建一个新的 RelayCommand 并告诉它运行 OnSelect,并传入命令参数。请记住,这与 XAML 中绑定的命令参数相同 - 这意味着它是当前选中或未选中的选项。我们使用按位运算符将该选项与 SelectedOptions 进行比较。如果该选项存在,则意味着我们要取消选中它,并且需要使用按位与将其清除。如果它不存在,我们使用按位或将其添加到所选中。

发生这种情况时,SelectedOptions 依赖项属性会自动更新,从而更新 XAML 中的 TextBlock 绑定。这是最终结果:

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

三生路 2024-09-05 02:50:59

不同的解决方案

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

EnumExpansion 类

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

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

EnumValue可以设置为任何枚举类型。设置 EnumValue 时,通过删除当前 EnumValue 中不存在的所有标志并添加当前 EnumValue 中的所有标志来更新内部 ObservableCollection。每当内部集合发生更改时,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 });
  ...

工作原理

枚举到 ListBox:

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

ListBox 到 Enum:

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

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

创建两个实用程序类后,绑定过程的其余部分就是简单的数据绑定。无需在应用程序代码中处理命令或更新集合 - 它全部隐藏在实用程序类中。

A different solution

I use a very different solution for this situation that I think is much cleaner. Using a couple of utility classes I created I can bind the SelectedOptions directly without having to write application code to deal with commands, collection updates, etc.

EnumExpansion class

I created a simple class with the following signature:

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

EnumValue can be set to any enum type. When EnumValue is set, the internal ObservableCollection is updated by removing all flags not in the current EnumValue and adding all flags in the current EnumValue. Whenever the internal collection is changed, EnumValue is updated.

BindableSelectedItems property

I also created a simple attached property that allows ListBox to bind its SelectedItems property. It is used like this:

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

The attached property is implemented by subscribing to SelectionChanged on the ListBox and CollectionChanged on the property value (which is of type INotifyCollectionChanged).

Initializing SelectedOptionsExpansion

You can do this in XAML but it is quite easy in code:

public EnumExpansion SelectedOptionsExpansion { get; set; }

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

How it works

Enum to ListBox:

  1. SelectedOptions changes, either via code or data binding
  2. The SelectedOptionsExpansion's EnumValue property is updated by the binding, which causes the EnumExpansion's collection to change.
  3. The CollectionChange event is picked up by the ListBoxHelper code which updates the selection in the ListBox.

ListBox to Enum:

  1. An item is selected or deselect in the ListBox
  2. ListBoxHelper picks it up and updates the EnumExpansion collection, which causes the EnumValue property to update.
  3. Since EnumValue is BindsTwoWayByDefault, the SelectedOptions value is updated.

Why I prefer this solution

Once the two utility classes are created, the rest of the binding process is straightforward data binding. There is no need to handle commands or update collections in your application code - it is all hidden within the utility classes.

零崎曲识 2024-09-05 02:50:59

为了支持默认值的概念,您需要在 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 会自动选中,并且所有绑定仍然有效。胜利!

To support the notion of defaults, you will need to set up a binding on the CheckBox.IsChecked property. You need both the current option (which is found on the DataContext of the relevant checkbox) as well as the SelectedOptions property, which is located on the window. So this binding becomes:

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

The FlagsToBoolConverter simply takes in these and checks to see if the current option is on the 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;
    }
}

Now try setting the SelectedOptions to some default value in the constructor. Notice that the relevant CheckBox is automatically checked, and all the bindings are still functional. Victory!

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文