Reputation: 5763
I want to have an Enum on my ViewModel, let's say to represent a person's Gender. The View representing that ViewModel should be able to present a way of supplying that value; whether that is a group of Radio Buttons or a Combo Box (if there are lots). And there are plenty of examples out there where you hard-code Radio Buttons in the XAML each one saying which value it represents. And the better ones will also use the Display Attribute's Name to provide the text for the radio button.
I'm looking to go a step further. I'd like it to generate the RadioButtons dynamically based on the Enum's values and things like the DisplayAttribute's Name and Description. Ideally, I'd like it to choose to create a ComboBox (rather than RadioButtons) if it's more than 6 items (perhaps implemented as a Control of some sort); but let's see if we can walk before we try to run. :)
My googling has got me pretty close... here's what I've got:
public enum Gender
{
[Display(Name="Gentleman", Description = "Slugs and snails and puppy-dogs' tails")]
Male,
[Display(Name = "Lady", Description = "Sugar and spice and all things nice")]
Female
}
Window:
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication2"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:EnumMultiConverter x:Key="EnumMultiConverter"/>
<ObjectDataProvider
MethodName="GetValues"
ObjectType="{x:Type local:EnumDescriptionProvider}"
x:Key="AdvancedGenderTypeEnum">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="local:Gender"/>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</Window.Resources>
<StackPanel>
<ItemsControl ItemsSource="{Binding Source={StaticResource AdvancedGenderTypeEnum}}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton GroupName="{Binding GroupName}" Content="{Binding Name}" ToolTip="{Binding Description}">
<RadioButton.IsChecked>
<MultiBinding Converter="{StaticResource EnumMultiConverter}" Mode="TwoWay">
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="DataContext.Gender" Mode="TwoWay" />
<Binding Path="Value" Mode="OneWay"/>
</MultiBinding>
</RadioButton.IsChecked>
</RadioButton>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Window>
EnumDescriptionProvider:
public static class EnumDescriptionProvider
{
public static IList<EnumerationItem> GetValues(Type enumType)
{
string typeName = enumType.Name;
var typeList = new List<EnumerationItem>();
foreach (var value in Enum.GetValues(enumType))
{
FieldInfo fieldInfo = enumType.GetField(value.ToString());
var displayAttribute = (DisplayAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(DisplayAttribute));
if (displayAttribute == null)
{
typeList.Add(new EnumerationItem
{
GroupName = typeName,
Value = value,
Name = value.ToString(),
Description = value.ToString()
});
}
else
{
typeList.Add(new EnumerationItem
{
GroupName = typeName,
Value = value,
Name = displayAttribute.Name,
Description = displayAttribute.Description
});
}
}
return typeList;
}
}
EnumerationItem:
public class EnumerationItem
{
public object GroupName { get; set; }
public object Value { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
And the MultiConverter (because IValueConverter can't take a Binding for the ConverterParameter):
public class EnumMultiConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values[0].Equals(values[1]);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
So the only problem I've got is that I can't do the ConvertBack. But maybe someone out there has a brilliant solution. As I say, ideally, I would just want some magical control that I can Bind to the Enum on my ViewModel, and for it to dynamically create RadioButtons for each value for that enum. But I'll take any suggestions that I can get.
Upvotes: 2
Views: 2698
Reputation: 5763
It's been a few years since I posted my other answer, so I thought I should post the benefit of my experience of taking that approach, and a newer, better solution.
I definitely had the right idea of wanting to have a single control to represent the collection of RadioButton
s (for example, so that you could trivially swap back and forth between having a set of radio buttons, or a ComboBox
. However, it was a mistake in my other answer to cram the generation of the items into that control. It's far more WPF-y to allow the user of the control to bind whatever they like into your control. (It also caused threading issues when I reached the point of wanting to tinker with which values were shown at a particular time.)
This new solution seems much cleaner, although it is (by necessity) made up of quite a few parts; but it does achieve that goal of having a single control to represent a collection of radio buttons. For example, you will be able to do:
<local:EnumRadioButtons SelectedValue="{Binding Gender, Mode=TwoWay}" ItemsSource="{Binding Genders}"/>
where the ViewModel has...
public ObservableCollection<IEnumerationItem> Genders { get; }
public Gender? Gender
{
get => _gender;
set => SetProperty(ref _gender, value); // common implementation of INotifyPropertyChanged, as seen on ViewModels.
}
So settle in, and I'll walk you through it... and apologies if I'm teaching you to suck eggs.
The control itself is basically an extension of an ItemsControl
, which gives it the ability to contain a collection of other controls. It allows you to control the overall layout of the individual items (e.g. if you want them sideways instead of vertically) in the same way that you would with an ItemsControl (via the ItemsPanel
).
using System.Windows;
using System.Windows.Controls;
public class EnumRadioButtons : ItemsControl
{
public static readonly DependencyProperty SelectedValueProperty =
DependencyProperty.Register(nameof(SelectedValue), typeof(object), typeof(EnumRadioButtons));
public object SelectedValue
{
get { return GetValue(SelectedValueProperty); }
set { SetValue(SelectedValueProperty, value); }
}
}
We will need to set up the default styling of that; but I'll come back to that later. Let's take a look at the individual EnumRadioButton
control. The biggest problem here is the same one posed in my original question... that converters cannot take a ConverterParameter
via a Binding
. This means I can't leave it up to the caller, so I need to know what type the collection of items is. So I've defined this interface to represent each item...
public interface IEnumerationItem
{
string Name { get; set; }
object Value { get; set; }
string Description { get; set; }
bool IsEnabled { get; set; }
}
and here's an example implementation...
using System.Diagnostics;
// I'm making the assumption that although the values can be set at any time, they will not be changed after these items are bound,
// so there is no need for this class to implement INotifyPropertyChanged.
[DebuggerDisplay("Name={Name}")]
public class EnumerationItem : IEnumerationItem
{
public object Value { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public bool IsEnabled { get; set; }
}
Clearly it would be useful to have something to help you create these things, so here is the interface...
using System;
using System.Collections.Generic;
public interface IEnumerationItemProvider
{
IList<IEnumerationItem> GetValues(Type enumType);
}
and implementation...
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
internal class EnumerationItemProvider : IEnumerationItemProvider
{
public IList<IEnumerationItem> GetValues(Type enumType)
{
var result = new List<IEnumerationItem>();
foreach (var value in Enum.GetValues(enumType))
{
var item = new EnumerationItem { Value = value };
FieldInfo fieldInfo = enumType.GetField(value.ToString());
var obsoleteAttribute = (ObsoleteAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(ObsoleteAttribute));
item.IsEnabled = obsoleteAttribute == null;
var displayAttribute = (DisplayAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(DisplayAttribute));
item.Name = displayAttribute?.Name ?? value.ToString();
item.Description = displayAttribute?.Description ?? value.ToString();
result.Add(item);
}
return result;
}
}
The idea is that this would give you the starting point, and you could tinker with the items and their properties (if you need to) before you put them into an ObservableCollection
and bind it to EnumRadioButtons.ItemsSource
. After that point, you can add/remove items to/from the collection; but changing the properties will not be reflected (because I haven't made it implement INotifyPropertyChanged
, because I don't expect to need to change them after that). I think that's reasonable; but you can change the implementation if you disagree.
So, back to the individual EnumRadioButton
. Basically it is just a RadioButton
, which will set up the Binding when the DataContext
is set. As I mentioned before we have to do it this way this because the ConverterParameter
can't be a Binding
, and a MultiConverter
won't be able to ConvertBack
to one of its sources.
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
public class EnumRadioButton : RadioButton
{
private static readonly Lazy<IValueConverter> ConverterFactory = new Lazy<IValueConverter>(() => new EnumToBooleanConverter());
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property == DataContextProperty)
{
SetupBindings();
}
}
/// <summary>
/// This entire method would not be necessary if I could have used a Binding for "ConverterParameter" - I could have done it all in XAML.
/// </summary>
private void SetupBindings()
{
var enumerationItem = DataContext as IEnumerationItem;
if (enumerationItem != null)
{
// I'm making the assumption that the properties of an IEnumerationItem won't change after this point
Content = enumerationItem.Name;
IsEnabled = enumerationItem.IsEnabled;
ToolTip = enumerationItem.Description;
//// Note to self, I used to expose GroupName on IEnumerationItem, so that I could set that property here; but there is actually no need...
//// You can have two EnumRadioButtons controls next to each other, bound to the same collection of values, each with SelectedItem bound
//// to different properties, and they work independently without setting GroupName.
var binding = new Binding
{
Mode = BindingMode.TwoWay,
RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(EnumRadioButtons), 1),
Path = new PropertyPath(nameof(EnumRadioButtons.SelectedValue)),
Converter = ConverterFactory.Value, // because we can reuse the same instance for everything rather than having one for each individual value
ConverterParameter = enumerationItem.Value,
};
SetBinding(IsCheckedProperty, binding);
}
}
}
As you've seen above, we're still going to need a Converter, and you've probably already got one like this; but for completeness, here it is...
using System;
using System.Globalization;
using System.Windows.Data;
public class EnumToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value?.Equals(parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value.Equals(true) ? parameter : Binding.DoNothing;
}
}
The only thing left is to set up the default styling for those controls. (Note that if you already have default styles defined for RadioButton
and ItemsControl
, then you will want to add the BasedOn
clause.)
<DataTemplate x:Key="EnumRadioButtonItem" DataType="{x:Type local:EnumerationItem}">
<local:EnumRadioButton/>
</DataTemplate>
<Style TargetType="local:EnumRadioButton">
<!-- Put your preferred stylings in here -->
</Style>
<Style TargetType="local:EnumRadioButtons">
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="ItemTemplate" Value="{StaticResource EnumRadioButtonItem}"/>
<!-- Put your preferred stylings in here -->
</Style>
Hope this helps.
Upvotes: 0
Reputation: 5763
I eventually found this post: How to bind RadioButtons to an enum? and if you look a long, long way down to the answer by artiom, he proposes a solution, and gives a link (which is now broken) before being chastised for giving a link that could become broken :) I contacted him, and he instantly sent me the information. For example, it allows me to just have this in the XAML:
<local:EnumRadioButton
SelectedItem="{Binding Path=Gender, Mode=TwoWay}"
EnumType="{x:Type local:Gender}"
RadioButtonStyle="{StaticResource MyStyle}"/>
So rather than the MultiConverter that I mentioned in the original post, you need:
public class EnumToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value?.Equals(parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value.Equals(true) ? parameter : Binding.DoNothing;
}
}
and here is the magic bit:
public class EnumRadioButton : ItemsControl
{
public static readonly DependencyProperty EnumTypeProperty =
DependencyProperty.Register(nameof(EnumType), typeof(Type), typeof(EnumRadioButton), new PropertyMetadata(null, EnumTypeChanged));
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register(nameof(SelectedItem), typeof(object), typeof(EnumRadioButton));
public static readonly DependencyProperty RadioButtonStyleProperty =
DependencyProperty.Register(nameof(RadioButtonStyle), typeof(Style), typeof(EnumRadioButton));
public Type EnumType
{
get { return (Type)GetValue(EnumTypeProperty); }
set { SetValue(EnumTypeProperty, value); }
}
public object SelectedItem
{
get { return GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
public Style RadioButtonStyle
{
get { return (Style)GetValue(RadioButtonStyleProperty); }
set { SetValue(RadioButtonStyleProperty, value); }
}
private static void EnumTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
EnumRadioButton enumRadioButton = (EnumRadioButton)d;
enumRadioButton.UpdateItems(e.NewValue as Type);
}
private void UpdateItems(Type newValue)
{
Items.Clear();
if (!newValue.IsEnum)
{
throw new ArgumentOutOfRangeException(nameof(newValue), $"Only enum types are supported in {GetType().Name} control");
}
var enumerationItems = EnumerationItemProvider.GetValues(newValue);
foreach (var enumerationItem in enumerationItems)
{
var radioButton = new RadioButton { Content = enumerationItem.Name, ToolTip = enumerationItem.Description };
SetCheckedBinding(enumerationItem, radioButton);
SetStyleBinding(radioButton);
Items.Add(radioButton);
}
}
private void SetStyleBinding(RadioButton radioButton)
{
var binding = new Binding
{
Source = this,
Mode = BindingMode.OneWay,
Path = new PropertyPath(nameof(RadioButtonStyle))
};
radioButton.SetBinding(StyleProperty, binding);
}
private void SetCheckedBinding(EnumerationItem enumerationItem, RadioButton radioButton)
{
var binding = new Binding
{
Source = this,
Mode = BindingMode.TwoWay,
Path = new PropertyPath(nameof(SelectedItem)),
Converter = new EnumToBooleanConverter(), // would be more efficient as a singleton
ConverterParameter = enumerationItem.Value
};
radioButton.SetBinding(ToggleButton.IsCheckedProperty, binding);
}
}
Upvotes: 0
Reputation: 16119
You're almost there, the key is the realization that a RadioButton's Command
event always gets fired when the user clicks on it, even when the IsChecked
property is bound. All you need to do is make your IsChecked
multi-value binding OneWay
and add a command handler that gets invoked when the radio button is checked by the user, i.e. something like this:
<DataTemplate>
<RadioButton Content="{Binding Name}" ToolTip="{Binding Description}"
Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}, Path=DataContext.CheckedCommand}"
CommandParameter="{Binding}">
<RadioButton.IsChecked>
<MultiBinding Converter="{StaticResource EnumMultiConverter}" Mode="OneWay">
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="DataContext.Gender" />
<Binding Path="Value" />
</MultiBinding>
</RadioButton.IsChecked>
</RadioButton>
</DataTemplate>
Then back in your view model you provide a handler for the command which sets the value of Gender manually rather than relying on the radio buttons to propagate the value back themselves:
public ICommand CheckedCommand { get { return new RelayCommand<Gender>(value => this.Gender = value); } }
Note that you don't even need a GroupName, it's all handled automatically according to which property and command you bind to in the view model (which is better for testing purposes anyway).
Upvotes: 0
Reputation: 20451
I would recommend that you use a custom behaviour, which will enable you to put all the Enum to ViewModel logic into a single reusable piece of code. That way you don't have to wrangle complex ValueConverters
There is a great article and GitHub sample that demonstrates a solution to this very problem, see the link below
WPF – Enum ItemsSource With Custom Behavior - Article
GitHub repository for sample code
I hope that gives you what you are looking for
Upvotes: 1