How to deal with calculated values with Dependency Properties on a custom WPF control
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 exp开发者_高级运维oses 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.
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:
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:
- SelectedOptions changes, either via code or data binding
- The SelectedOptionsExpansion's EnumValue property is updated by the binding, which causes the EnumExpansion's collection to change.
- The CollectionChange event is picked up by the ListBoxHelper code which updates the selection in the ListBox.
ListBox to Enum:
- An item is selected or deselect in the ListBox
- ListBoxHelper picks it up and updates the EnumExpansion collection, which causes the EnumValue property to update.
- 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.
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!
精彩评论