This post assumes you're familiar with MVVM (Model-View-View Model) as
applied to WPF. If you're familiar with WPF but not MVVM, I found Jason Dolinger's presentation a very nice introduction. If
you're not familiar with WPF, you'd better skip this altogether, as it's too
esoteric.
Let's start off with a trivial UI, like so:
Let's start off with a trivial UI, like so:
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication2"
Title="MainWindow"
Height="350"
Width="525">
<Grid>
<TextBlock Height="23"
HorizontalAlignment="Left"
Margin="12,12,0,0"
VerticalAlignment="Top"
Text="{Binding Foo}" />
<RadioButton Content="Bar"
Height="16"
HorizontalAlignment="Left"
Margin="12,35,0,0"
VerticalAlignment="Top" />
<RadioButton Content="Baz"
Height="16"
HorizontalAlignment="Left"
Margin="12,50,0,0"
VerticalAlignment="Top" />
<RadioButton Content="Quux"
Height="16"
HorizontalAlignment="Left"
Margin="12,65,0,0"
VerticalAlignment="Top" />
</Grid>
</Window>
As is usual with MVVM, the
code-behind for this is extremely bare-bones (I'm ignoring stuff like
dependency injection for purposes of illustration):
public partial class MainWindow : Window {
private ViewModel viewModel = new ViewModel();
public MainWindow() {
DataContext = viewModel;
InitializeComponent();
}
}
And let's start off with a
view model that looks like this:
enum Foo { None, Bar, Baz, Quux };
class ViewModel : INotifyPropertyChanged {
Foo foo;
public Foo Foo {
get { return foo; }
set {
if (foo != value) {
foo = value;
PropertyChanged(this, new PropertyChangedEventArgs("Foo"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
The problem, as you might have
guessed, is how we are supposed to bind the radio buttons to our view model in
such a way that the check state corresponds to the enum value and vice versa.
A standard WPF solution for situations like this, if we ignore MVVM for a moment, is to implement a value converter. We need something that can map an enum constant to a boolean and back. This isn't hard. First, introduce the converter:
A standard WPF solution for situations like this, if we ignore MVVM for a moment, is to implement a value converter. We need something that can map an enum constant to a boolean and back. This isn't hard. First, introduce the converter:
class FooToBoolConverter : IValueConverter {
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return value == parameter;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return (bool) value ? parameter : Foo.None;
}
}
Then change the XAML to:
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication2"
Title="MainWindow"
Height="350"
Width="525">
<Window.Resources>
<local:FooToBoolConverter x:Key="FooToBoolConverter" />
Window.Resources>
<Grid>
<TextBlock Height="23"
HorizontalAlignment="Left"
Margin="12,12,0,0"
Name="textBlock1"
VerticalAlignment="Top"
Text="{Binding Foo}" />
<RadioButton Content="Bar"
Height="16"
HorizontalAlignment="Left"
Margin="12,35,0,0"
VerticalAlignment="Top"
IsChecked="{Binding Foo, Converter={StaticResource FooToBoolConverter}, ConverterParameter={x:Static local:Foo.Bar}}" />
<RadioButton Content="Baz"
Height="16"
HorizontalAlignment="Left"
Margin="12,50,0,0"
VerticalAlignment="Top"
IsChecked="{Binding Foo, Converter={StaticResource FooToBoolConverter}, ConverterParameter={x:Static local:Foo.Baz}}" />
<RadioButton Content="Quux"
Height="16"
HorizontalAlignment="Left"
Margin="12,65,0,0"
VerticalAlignment="Top"
IsChecked="{Binding Foo, Converter={StaticResource FooToBoolConverter}, ConverterParameter={x:Static local:Foo.Quux}}" />
</Grid>
</Window>
And we're done.
We can polish up the binding syntax a little by allowing our converter to take strings and map them back to enum tags, instead of having to specify actual typed enum values in our XAML. While we're at it, let's make the class generic enough to handle all enumerations so we don't need a separate class for every enum:
We can polish up the binding syntax a little by allowing our converter to take strings and map them back to enum tags, instead of having to specify actual typed enum values in our XAML. While we're at it, let's make the class generic enough to handle all enumerations so we don't need a separate class for every enum:
class EnumToBoolConverter : IValueConverter {
public Type EnumType { get; set; }
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
return Object.Equals(value, Enum.Parse(EnumType, parameter.ToString()));
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
return (bool) value ? Enum.Parse(EnumType, parameter.ToString()) : Enum.ToObject(EnumType, 0);
}
}
As a homework exercise: why do we need to use Object.Equals() here? Why doesn't a simple equality comparison work, like in the previous version?
Our XAML now becomes
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication2"
Title="MainWindow"
Height="350"
Width="525">
<Window.Resources>
<local:EnumToBoolConverter EnumType="local:Foo" x:Key="FooToBoolConverter" />
Window.Resources>
<Grid>
<TextBlock Height="23"
HorizontalAlignment="Left"
Margin="12,12,0,0"
Name="textBlock1"
VerticalAlignment="Top"
Text="{Binding Foo}" />
<RadioButton Content="Bar"
Height="16"
HorizontalAlignment="Left"
Margin="12,35,0,0"
VerticalAlignment="Top"
IsChecked="{Binding Foo, Converter={StaticResource FooToBoolConverter}, ConverterParameter=Bar}" />
<RadioButton Content="Baz"
Height="16"
HorizontalAlignment="Left"
Margin="12,50,0,0"
VerticalAlignment="Top"
IsChecked="{Binding Foo, Converter={StaticResource FooToBoolConverter}, ConverterParameter=Baz}" />
<RadioButton Content="Quux"
Height="16"
HorizontalAlignment="Left"
Margin="12,65,0,0"
VerticalAlignment="Top"
IsChecked="{Binding Foo, Converter={StaticResource FooToBoolConverter}, ConverterParameter=Quux}" />
</Grid>
</Window>
One inherent limitation of
this technique is that you cannot use it to bind checkboxes to flag enums,
because the converter can only convert a single value, not the combined state
of all checkboxes. You can't get around this by using IMultiValueConverter and MultiBinding, at least not in an intuitive
fashion, because the binding is "the wrong way around": MultiBinding
takes different values to produce one new value, so we'd need to bind the enum
property in the view model to the checkboxes, rather than the other way around.
If we do that, however, the parameter value becomes useless -- binding to the
"IsChecked" property only tells us that the checkbox is checked, not
which enum flag it represents. Frankly, thinking about this approach gives me
headaches.
A far more approachable alternative is to break down the enum into separate boolean properties and bind those to the checkboxes:
A far more approachable alternative is to break down the enum into separate boolean properties and bind those to the checkboxes:
[Flags]
enum Foo { None = 0, Bar = 1, Baz = 2, Quux = 4 };
class ViewModel : INotifyPropertyChanged {
Foo foo;
private void fooChanged() {
PropertyChanged(this, new PropertyChangedEventArgs("Foo"));
PropertyChanged(this, new PropertyChangedEventArgs("FooBar"));
PropertyChanged(this, new PropertyChangedEventArgs("FooBaz"));
PropertyChanged(this, new PropertyChangedEventArgs("FooQuux"));
}
public Foo Foo {
get { return foo; }
set {
if (foo != value) {
foo = value;
fooChanged();
}
}
}
public bool FooBar {
get { return foo.HasFlag(Foo.Bar); }
set {
if (foo.HasFlag(Foo.Bar) == value) return;
if (value) {
foo |= Foo.Bar;
} else {
foo &= ~Foo.Bar;
}
fooChanged();
}
}
// And similarly for Baz and Quux
public event PropertyChangedEventHandler PropertyChanged;
}
And
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication2"
Title="MainWindow"
Height="350"
Width="525">
<Grid>
<TextBlock Height="23"
HorizontalAlignment="Left"
Margin="12,12,0,0"
Name="textBlock1"
VerticalAlignment="Top"
Text="{Binding Foo}" />
<CheckBox Content="Bar"
Height="16"
HorizontalAlignment="Left"
Margin="12,35,0,0"
VerticalAlignment="Top"
IsChecked="{Binding FooBar}" />
<CheckBox Content="Baz"
Height="16"
HorizontalAlignment="Left"
Margin="12,50,0,0"
VerticalAlignment="Top"
IsChecked="{Binding FooBaz}" />
<CheckBox Content="Quux"
Height="16"
HorizontalAlignment="Left"
Margin="12,65,0,0"
VerticalAlignment="Top"
IsChecked="{Binding FooQuux}" />
</Grid>
</Window>
Of course, you can apply the same solution (slightly simplified) to the original problem of binding radio buttons.
This solution is nice and very MVVM-y, as opposed to the value converter, but it does have the unfortunate drawback of requiring boilerplate code. If you have lots of flags, this solution is not particularly attractive. On the other hand, if you have lots of flags, presenting this in the interface as a mass of radio buttons or checkboxes is not attractive either. In this case, you should probably find another way of representing these choices, like two listboxes (one representing the items that are not present, the other the items that are present, with buttons between them for transferring them).
I want to round things out with a fully generic way of handling two-way binding from enums to radio buttons or checkboxes, without the need for writing more than a single line per enum property. This requires WPF 4.0 (and correspondingly .NET 4.0) as we now have the ability to introduce dynamic objects: objects whose member accesses are resolved with custom code at runtime. The idea is that, just like in the example above, we bind our controls to individual boolean properties, but instead of defining these statically, we look them up at runtime. This means we lose static typing and IntelliSense, but since WPF binding is already inherently dynamic, this doesn't matter much.
Here's what the XAML for that looks like:
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication2"
Title="MainWindow"
Height="350"
Width="525">
<Grid>
<TextBlock Height="23"
HorizontalAlignment="Left"
Margin="12,12,0,0"
Name="textBlock1"
VerticalAlignment="Top"
Text="{Binding Foo}" />
<CheckBox Content="Bar"
Height="16"
HorizontalAlignment="Left"
Margin="12,35,0,0"
VerticalAlignment="Top"
IsChecked="{Binding Path=Foo.Bar}" />
<CheckBox Content="Baz"
Height="16"
HorizontalAlignment="Left"
Margin="12,50,0,0"
VerticalAlignment="Top"
IsChecked="{Binding Path=Foo.Baz}" />
<CheckBox Content="Quux"
Height="16"
HorizontalAlignment="Left"
Margin="12,65,0,0"
VerticalAlignment="Top"
IsChecked="{Binding Path=Foo.Quux}" />
</Grid>
</Window>
Almost the same as our
previous approach. The view model code is quite different:
class ViewModel : INotifyPropertyChanged {
public ViewModel() {
foo = ModelEnumProperty.For<Foo>(() => { PropertyChanged(this, new PropertyChangedEventArgs("Foo")); });
}
ModelEnumProperty<Foo> foo;
public dynamic Foo {
get { return foo; }
}
public event PropertyChangedEventHandler PropertyChanged;
}
The trick here is that the
property "Foo" is of type "dynamic". When a property like
"Foo.Bar" is bound, a call to DynamicObject.TryGetMember() is produced to look up the
value. As you can imagine, the real magic is in the class ModelEnumProperty:
public class ModelEnumProperty : DynamicObject, INotifyPropertyChanged where TEnum : struct {
long value;
Dictionary<string, long> constants;
bool isFlags;
public ModelEnumProperty() {
if (!typeof(TEnum).IsEnum) throw new InvalidOperationException();
constants = new Dictionary<string, long>();
var names = Enum.GetNames(typeof(TEnum));
var values = (TEnum[])Enum.GetValues(typeof(TEnum));
for (int i = 0; i != names.Length; ++i) {
constants.Add(names[i], Convert.ToInt64(values[i]));
}
isFlags = typeof(TEnum).GetCustomAttributes(typeof(FlagsAttribute), false).Length == 1;
}
public override IEnumerable<string> GetDynamicMemberNames() {
return Enum.GetNames(typeof(TEnum));
}
public override bool TryGetMember(GetMemberBinder binder, out object result) {
long longResult;
if (constants.TryGetValue(binder.Name, out longResult)) {
result = (value & longResult) != 0;
return true;
} else {
result = null;
return false;
}
}
public override bool TrySetMember(SetMemberBinder binder, object value) {
if (value == null) return false;
if (value.GetType() != typeof(bool)) return false;
long longResult;
if (!constants.TryGetValue(binder.Name, out longResult)) return false;
if ((bool)value) {
if (isFlags && longResult != 0) {
this.value |= longResult;
} else {
this.value = longResult;
}
} else {
if (isFlags) {
this.value &= ~longResult;
} else if (this.value == longResult) {
this.value = 0;
}
}
var propertyChanged = PropertyChanged;
if (propertyChanged != null) propertyChanged(this, new PropertyChangedEventArgs(binder.Name));
return true;
}
public override bool TryConvert(ConvertBinder binder, out object result) {
result = null;
Type underlyingType = Enum.GetUnderlyingType(typeof(TEnum));
if (binder.Type == typeof(TEnum)) {
result = Enum.ToObject(typeof(TEnum), value);
return true;
} else if (binder.Type == underlyingType) {
if (underlyingType == typeof(Int16)) {
result = (Int16)value;
return true;
} else if (underlyingType == typeof(Int32)) {
result = (Int32)value;
return true;
} else if (underlyingType == typeof(Int64)) {
result = (Int64)value;
return true;
} else {
return false;
}
} else {
return false;
}
}
public override string ToString() {
return Enum.ToObject(typeof(TEnum), value).ToString();
}
public event PropertyChangedEventHandler PropertyChanged;
}
This code looks more complicated than it is. Let's break it down to the
interesting pieces.
First note that we need to check explicitly if we're dealing with an enum type. It's not possible to do this with a type constraint. The previous examples omitted this check altogether.
In the constructor, we set up a quick lookup from tag names to values, not so much for performance but because it makes the lookup a lot less tedious. We also record whether or not the enum is a flags enum (as can be determined by checking for the "Flags" attribute on the type) to vary the logic later.
GetDynamicMemberNames() and TryGetMember() are fairly straightforward. TrySetMember() is more interesting because it needs to handle a few special cases:
First note that we need to check explicitly if we're dealing with an enum type. It's not possible to do this with a type constraint. The previous examples omitted this check altogether.
In the constructor, we set up a quick lookup from tag names to values, not so much for performance but because it makes the lookup a lot less tedious. We also record whether or not the enum is a flags enum (as can be determined by checking for the "Flags" attribute on the type) to vary the logic later.
GetDynamicMemberNames() and TryGetMember() are fairly straightforward. TrySetMember() is more interesting because it needs to handle a few special cases:
- If the enum is not a flags enum, setting the value simply overwrites the old one, but clearing the value only has effect if the value is set in the first place. If Foo has value "Bar", setting "Baz" to false has no effect.
- If the enum is a flags enum, we have to handle the special case of the "None" value being set, as this should clear the enum (rather than do nothing, which is what would happen if we simply "added" the flag).
ModelEnumProperty implements
INotifyPropertyChanged, but as we will see later, the actual property names
aren't used.
TryConvert() is more complicated than necessary because, in addition to allowing conversion to the enum type, it also allows conversion to the underlying integer type of the enum (but not arbitrary integer types). This is not strictly necessary.
Finally, there's a little static helper class:
TryConvert() is more complicated than necessary because, in addition to allowing conversion to the enum type, it also allows conversion to the underlying integer type of the enum (but not arbitrary integer types). This is not strictly necessary.
Finally, there's a little static helper class:
public static class ModelEnumProperty {
public static ModelEnumProperty For(Action notifyChanged) where TEnum : struct {
var result = new ModelEnumProperty();
result.PropertyChanged += delegate { notifyChanged(); };
return result;
}
}
This binds a delegate straight
to the event so we can initialize the property in one go, because we will
always want to notify the parent of changes (anything binding directly to the
enum property rather than its members should get property change notifications
as well). You can see that this ignores the specific member that was changed
altogether.
Although neat, I don't really recommend this solution in production code over the previous solutions. There are a few reasons. First, this code is not as accessible as the previous approaches. Second, there is significant memory and runtime overhead associated with maintaining these dynamic property lookups -- you wouldn't want to bind lots of enum properties this way. Perhaps most significantly, you lose strong typing and IntelliSense for users of the view model other than the XAML (which can't use type safety).
In other to set a ModelEnumProperty to a particular value, you have to use the "Foo.Bar = true" syntax, which may require multiple statements. It's possible to work around this by making the class more intelligent (for example, by providing a dynamic "SetValue" method), but you can't get around the fact that the property must be of type "dynamic". Of course you can declare a second, statically typed property and name it something like "FooValue" (or rename the dynamic property to "FooFlags") but overall, the solution is too much of a clever hack. I thought I'd present it anyway.
Although neat, I don't really recommend this solution in production code over the previous solutions. There are a few reasons. First, this code is not as accessible as the previous approaches. Second, there is significant memory and runtime overhead associated with maintaining these dynamic property lookups -- you wouldn't want to bind lots of enum properties this way. Perhaps most significantly, you lose strong typing and IntelliSense for users of the view model other than the XAML (which can't use type safety).
In other to set a ModelEnumProperty to a particular value, you have to use the "Foo.Bar = true" syntax, which may require multiple statements. It's possible to work around this by making the class more intelligent (for example, by providing a dynamic "SetValue" method), but you can't get around the fact that the property must be of type "dynamic". Of course you can declare a second, statically typed property and name it something like "FooValue" (or rename the dynamic property to "FooFlags") but overall, the solution is too much of a clever hack. I thought I'd present it anyway.
1 comment:
Hi Jeroen! I've just seen your code. I'm very interested in your last approach. Although it's complex, it may be useful for things like a "flagged enum editor": an custom control that is able to set/unset the flags in an enum.
However, I need your opinion. Would it be easy to do it? Some ideas?
Thanks a lot.
Post a Comment