
Introduction
A few days ago, I posted this article, where I described a method for creating an observable collection of enumerator values for use in a WPF application. In this article, I will be taking that idea to the next logical level - the creation of list controls dedicated to allowing the selection of enumerator values.
Initially, the idea was to provide controls with support for any of tthe System
enumerators in C#. That would certainly have been adequate, but as I see it, would have been a mere "half-step" toward actually being useful. So, I added support for locally defined enumarators as well.
In my last few articles, I provided both a .Net Framework and a .Net Core version of the code, but quite frankly, I don't think you guys are really worth that kind of effort (at least, not on my part). Converting to .Net Core is beyond trivial, especially if you don't use any of the System.Drawing or ADO stuff in the .Net framework code, so you guys feel free to convert it if you want/need to.
What is Provided
Since enumerators are essentially one-trick ponies, where the number of useful properties is exactly one (the name of the enumerator), it made sense to create a ListBox
and a ComboBox
, and ignore the ListView
. Unless otherwise noted, the feature set below applies to both controls.
- For system enumerators, simply specify the enum type name. The control will create and bind the resulting enumerator collection to the control without you having to do it in the XAML. The following snippet is the minimal amount of code you need to write to display a
ListBox
(or ComboBox
) that contains the days of the week. (I know! Amazing, right!?)
<ctrls:EnumListBox x:Name="lbWeekDay" EnumTypeName="System.DayOfWeek" />
- If an enumerator represents flags (decorated with the [Flags] attribute), the
ListBox
will automatically become a multi-select ListBox
, unless you specify that it shouldn't by setting the AutoSelectionMode
property to false
(default value is true
):
<ctrls:EnumListBox x:Name="lbWeekDay" EnumTypeName="System.DayOfWeek" AutoSelectMode="false" />
- You can optionally display the ordinal value of the enumerator with the name by setting the
ShowOrdinalWithName
property to true
(default value is false
).
<ctrls:EnumListBox x:Name="lbWeekDay" EnumTypeName="System.DayOfWeek" ShowOrdinalWithName="true" />
- All underlying types are suppoprted (but it's still up to the develeper to make sure he does stuff that makes sense).
Locally Defined Enumerators
I'm sure most of you have implemented your own enumerators, and knowing this, it would be ludicrous to not provide some way to use these custom controls with your own enumerators. The primary problem with using your own enumerators with a globally accessible custom control is that the control can't know about something that is defined for a specific application.
Since the actual collection used by these controls is defined in the controls' namespace, all you have to do is instantiate it in your window/user control, like so:
Given the following enum declaration:
public enum EnumTest1 { One=1, Two, Three, Four, Fifty=50, FiftyOne, FiftyTwo }
You would instantiate the collection like so:
public EnumItemList Enum1 { get; set; }
...
this.Enum1 = new EnumItemList(typeof(EnumTest1), true);
And then you would manually bind it to the control:
<ctrls:EnumComboBox x:Name="cbLocalEnum1" ItemsSource="{Binding Path=Enum1}" />
The Code
Generally speaking, the EnumComboBox
and EnumListBox
are the same under the hood. If I could have found a way to write the code in just one class, I would have. However, the nature of C# required me to essentially duplicate all of the code in both classes. The only real difference is that the combo box doesn't support multiple-selection. Because the code is essentially the same, I'm only going to discuss the EnumListBox
in detail. I'm not going to go into the nuances regarding creating a custom control in WPF because there are countless other offerings available on the internet that describe the process MUCH better than I ever could. Instead, I'm simply going to tell you what I did, and maybe even why I did it, if I think the why is important.
The EnumItemList Collection and EnumItem Collection Item
To make the actual item as useful as possible to the form that uses the control, the enumerator breaks out a lot of the most useful info into readily accessible properties. This mitigates the burden on the developer to post-process the selected item in his window/user control.
public class EnumItem
{
public object Value { get; set; }
public string Name { get; set; }
public Type EnumType { get; set; }
public Type UnderlyingType { get; set; }
public bool ShowOrdinalWithName { get; set; }
public EnumItem()
{
this.ShowOrdinalWithName = false;
}
public override string ToString()
{
return (this.ShowOrdinalWithName) ? string.Format("({0}) {1}",
Convert.ChangeType(this.Value, this.UnderlyingType),
Name)
: this.Name;
}
}
The EnumItemList ObservableCollection
that is actually bound to the control is responsible for creating its own items. It also self-determines whether or not the control can allow multiple selection. Remember, if you're going to present a local enumartor in the control, you have to instantiate this collection yourself (an example of doing so has already been provided).
public class EnumItemList : ObservableCollection<enumitem>
{
public bool CanMultiSelect { get; set; }
public EnumItemList(Type enumType, bool showOrd)
{
this.CanMultiSelect = enumType.GetCustomAttributes<flagsattribute>().Any();
this.AsObservableEnum(enumType, showOrd);
}
public void AsObservableEnum(Type enumType, bool showOrd)
{
if (enumType != null && enumType.IsEnum)
{
Type underlyingType = Enum.GetUnderlyingType(enumType);
foreach (Enum item in enumType.GetEnumValues())
{
this.Add(new EnumItem()
{
Name = item.ToString(),
Value = item,
EnumType = enumType,
UnderlyingType = underlyingType,
ShowOrdinalWithName = showOrd,
});
}
}
}
}
</flagsattribute></enumitem>
Attached Properties
For pretty much every custom control you'll ever write, you're going to add some properties that are not available in the base class, fo the sole purpose of enabling your custom functionality, and EnumListBox
is certainly no different.
public static DependencyProperty EnumTypeNameProperty =
DependencyProperty.Register("EnumTypeName",
typeof(string),
typeof(EnumListBox),
new PropertyMetadata(null));
public string EnumTypeName
{
get { return (string)GetValue(EnumTypeNameProperty); }
set { SetValue(EnumTypeNameProperty, value); }
}
public static DependencyProperty AutoSelectionModeProperty =
DependencyProperty.Register("AutoSelectionMode",
typeof(bool),
typeof(EnumListBox),
new PropertyMetadata(true));
public bool AutoSelectionMode
{
get { return (bool)GetValue(AutoSelectionModeProperty); }
set { SetValue(AutoSelectionModeProperty, value); }
}
public static DependencyProperty ShowOrdinalWithNameProperty =
DependencyProperty.Register("ShowOrdinalWithName",
typeof(bool),
typeof(EnumListBox),
new PropertyMetadata(false));
public bool ShowOrdinalWithName
{
get { return (bool)GetValue(ShowOrdinalWithNameProperty); }
set { SetValue(ShowOrdinalWithNameProperty, value); }
}
There are a couple of helper properties as well:
public EnumItemList EnumList { get; set; }
public Type EnumType
{
get
{
Type value = (string.IsNullOrEmpty(this.EnumTypeName))
? null : Type.GetType(this.EnumTypeName);
return value;
}
}
The only thing left is how the control reacts to being loaded. I use the Loaded event to determine what to do as far as binding the collection. When I went to add support for in-XAML binding to the ItemsSource
, I felt like it was necessary to verify that the bound collection was of the type EnumItemList
, and found that I had to get the parent window's DataContext
in order to do that.
private void EnumListBox_Loaded(object sender, RoutedEventArgs e)
{
if (!DesignerProperties.GetIsInDesignMode(this))
{
if (this.EnumType != null)
{
this.EnumList = new EnumItemList(this.EnumType, this.ShowOrdinalWithName);
Binding binding = new Binding() { Source=this.EnumList };
this.SetBinding(ListBox.ItemsSourceProperty, binding);
}
else
{
this.DataContext = EnumGlobal.FindParent<contentcontrol>(this).DataContext;
if (!(this.ItemsSource is EnumItemList))
{
throw new InvalidCastException("The bound collection must be of type EnumItemList.");
}
}
if (this.ItemsSource != null)
{
if (this.AutoSelectionMode)
{
this.SelectionMode = (((EnumItemList)(this.ItemsSource)).CanMultiSelect)
? SelectionMode.Multiple : SelectionMode.Single;
}
}
}
}
</contentcontrol>
Using the code
Using the controls is pretty easy, especially considering you don't have to implement the collection they use. The property you create in your window doesn't even have to use INotifyPropertyChanged
, because once instantiated, the collection never changes. In cfact, I'f you're binding to a system enumerator, you don't even have to instantiate the collection.
public partial class MainWindow : Window
{
public EnumItemList Enum1 { get; set; }
public MainWindow()
{
this.InitializeComponent();
this.DataContext = this;
this.Enum1 = new EnumItemList(typeof(EnumTest1), true);
}
...
}
The XAML is equally simple, in that short of styling elements and the desire to handle events from the control, all you have to do is apply the appropriate binding
<!-- presenting a System.enum - note that I included the namespace -->
<ctrls:EnumListBox x:Name="lbWeekDay" EnumTypeName="System.DayOfWeek" />
<!-- presenting a locally defined enum - instead of specifying the enum type
name, you bind the collection that you instantiated in the parent window/control -->
>ctrls:EnumListBox x:Name="lbLocalEnum1" ItemsSource="{Binding Path=Enum1}" />
Closing Statements
It's rare that you have an opportunity to create a control as specifically tied to the data as I have here. Most of the time, you have to write to a larger purpose. I was originally just going to allow System enumerators, but after thinking about it for a while, I decided to add support for local enumerators. In doing so, I added just a few lines of code, and I practically doubled the controls' usefulness.
Many people may claim that "future-proofing" is a waste of time, because there's a good chance you'll never need the code that supports the paradign. Honestly? That's true. However, I think it's easier to future-proof than to come back later and add code, because as a rule, you are almost never afforded the time to go back and improve code.
History
- 2021.02.22 - Initial publication.