Click here to Skip to main content
15,881,173 members
Articles / Desktop Programming / WPF
Article

WPF - Dedicated Enumerator ListBox and ComboBox

Rate me:
Please Sign up or sign in to vote.
4.75/5 (5 votes)
23 Feb 2021CPOL5 min read 5.5K   203   10   1
Implement enumerator selection in your WPF apps

screen shot

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!?)
    XML
    <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):
    XML
    <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).
    XML
    <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:

XML
public enum EnumTest1 { One=1, Two, Three, Four, Fifty=50, FiftyOne, FiftyTwo }

You would instantiate the collection like so:

XML
public EnumItemList Enum1 { get; set; }
...
this.Enum1 = new EnumItemList(typeof(EnumTest1), true);

And then you would manually bind it to the control:

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

C#
public class EnumItem
{
    // The actual value of the enumerator (i.e., DayOfWeek.Monday)
    public object Value               { get; set; }
    // The name of the enumerator value (i.e., "Monday")
    public string Name                { get; set; }
    // The enumerator type (i.e., DayOfWeek)
    public Type   EnumType            { get; set; }
    // The underlying enumerator type (i.e., Int32)
    public Type   UnderlyingType      { get; set; }
    // A helper property that determines how the enumartor value is 
    // displayed in the control
    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).

C#
public class EnumItemList : ObservableCollection<enumitem>
{
    public bool CanMultiSelect { get; set; }

    public EnumItemList(Type enumType, bool showOrd)
    {
        // if the enumerator is decorated with the "Flags" attribute, 
        // more than one item can be selected at a time.
        this.CanMultiSelect = enumType.GetCustomAttributes<flagsattribute>().Any();
        // find all of the enumerator's members
        this.AsObservableEnum(enumType, showOrd);
    }

    public void AsObservableEnum(Type enumType, bool showOrd)
    {
        // if the specified type is not null AND it is actually an 
        // enum type, we can create the collection
        if (enumType != null && enumType.IsEnum)
        {
            // discover the underlying type (int, long, byte, etc)
            Type underlyingType = Enum.GetUnderlyingType(enumType);

            // get each enum item and add it to the list
            foreach (Enum item in enumType.GetEnumValues())
            {
                this.Add(new EnumItem()
                { 
                    // the name that will probably be displayed in the 
                    // UI component
                    Name           = item.ToString(), 
                    // the actual enum value (DayofWeek.Monday)
                    Value          = item, 
                    // the enum type
                    EnumType       = enumType,
                    // the underlying type (int, long, byte, etc)
                    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.

C#
//---------------------------------------------------------
// This property allows you to specify the type name for system 
// enumerators (it's pointless to try using local enumerators 
// here because the control won't be able to discover its 
// members.)
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); }
}

//---------------------------------------------------------
// This property allows you to turn off the automatic 
// determination of whether or not to use multiple selection. 
// This only affects list boxes because combo boxes do not 
// support multiple-selection. The default value is true.
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); }
}

//---------------------------------------------------------
// This property causes the displayed enumerator name to be 
// pre-pended with the ordnial value of the enumerator. The 
// default value is false.
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:

C#
// This property represents the auto-created collection (for 
// system enums only).
public EnumItemList EnumList { get; set; }

// This property provides the actual Type based on the enum 
// type name
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.

C#
private void EnumListBox_Loaded(object sender, RoutedEventArgs e)
{
    // avoid errors being displayed in designer
    if (!DesignerProperties.GetIsInDesignMode(this))
    {
        // if the enum type is not null, the enum must be a system enum, so we can 
        // populate/bind automatically
        if (this.EnumType != null)
        {
            // create the list of enums
            this.EnumList = new EnumItemList(this.EnumType, this.ShowOrdinalWithName);

            // create and set the binding
            Binding binding    = new Binding() { Source=this.EnumList };
            this.SetBinding(ListBox.ItemsSourceProperty, binding);
        }
        else
        {
            // otherwise, the developer specifically set the binding, so we have 
            // to get the datacontext from the parent content control (window or 
            // usercontrol) so we can use the specified collection
            this.DataContext = EnumGlobal.FindParent<contentcontrol>(this).DataContext;

            // before we use it, make sure it's the correct type (it must be a 
            // EnumItemList object)
            if (!(this.ItemsSource is EnumItemList))
            {
                throw new InvalidCastException("The bound collection must be of type EnumItemList.");
            }
        }
        // no matter what happens, see if we can set the list to mult5iple selection
        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.

C#
public partial class MainWindow : Window
{
    public EnumItemList Enum1 { get; set; }

    public MainWindow()
    {
        this.InitializeComponent();
        this.DataContext = this;

        // we only have to do this for locally implemented enumerators.
        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

C#
<!-- 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.
     

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Paddedwall Software
United States United States
I've been paid as a programmer since 1982 with experience in Pascal, and C++ (both self-taught), and began writing Windows programs in 1991 using Visual C++ and MFC. In the 2nd half of 2007, I started writing C# Windows Forms and ASP.Net applications, and have since done WPF, Silverlight, WCF, web services, and Windows services.

My weakest point is that my moments of clarity are too brief to hold a meaningful conversation that requires more than 30 seconds to complete. Thankfully, grunts of agreement are all that is required to conduct most discussions without committing to any particular belief system.

Comments and Discussions

 
GeneralMy vote of 5 Pin
r_hyde23-Feb-21 14:40
r_hyde23-Feb-21 14:40 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.