65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF - 专用枚举器ListBox和ComboBox

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (5投票s)

2021年2月23日

CPOL

5分钟阅读

viewsIcon

6338

downloadIcon

229

在您的WPF应用程序中实现枚举器选择

screen shot

引言

几天前,我发布了一篇文章,在其中描述了一种用于 WPF 应用程序的可观察枚举值集合的创建方法。在这篇文章中,我将把这个想法推向下一个逻辑层面——创建用于选择枚举值的专用列表控件。

最初的想法是提供支持 C# 中任何 `System` 枚举器的控件。这绝对足够了,但在我看来,这只是朝着真正有用迈出的“半步”。因此,我也添加了对本地定义的枚举器的支持。

在我最近的几篇文章中,我提供了 .Net Framework 和 .Net Core 两个版本的代码,但说实话,我认为你们并不值得我付出那么多的努力(至少我这么认为)。转换为 .Net Core 是绰绰有余的,特别是如果你不在 .Net Framework 代码中使用任何 `System.Drawing` 或 ADO 的东西,所以如果你想/需要,可以自己转换。

提供的内容

由于枚举器本质上是“一招鲜”的,有用的属性数量恰好是一个(枚举器的名称),因此创建 `ListBox` 和 `ComboBox` 是有意义的,而忽略 `ListView`。除非另有说明,否则下面的功能集适用于这两个控件。

  • 对于系统枚举器,只需指定枚举类型名称。控件将创建枚举器集合并将其绑定到控件,而无需您在 XAML 中进行操作。以下代码片段是显示包含一周日期的 `ListBox`(或 `ComboBox`)所需的最小代码量。(我知道!太神奇了,对吧!?)
    <ctrls:EnumListBox x:Name="lbWeekDay" EnumTypeName="System.DayOfWeek" />
    
  • 如果枚举器代表标志(带有 `[Flags]` 属性),`ListBox` 将自动变为多选 `ListBox`,除非您通过将 `AutoSelectionMode` 属性设置为 `false`(默认值为 `true`)来指定不应如此。
    <ctrls:EnumListBox x:Name="lbWeekDay" EnumTypeName="System.DayOfWeek" AutoSelectMode="false" />
    
  • 您可以通过将 `ShowOrdinalWithName` 属性设置为 `true`(默认值为 `false`)来选择显示枚举器的序号值和名称。
    <ctrls:EnumListBox x:Name="lbWeekDay" EnumTypeName="System.DayOfWeek" ShowOrdinalWithName="true" />
    
  • 所有底层类型都受支持(但仍由开发人员决定是否要执行有意义的操作)。

本地定义的枚举器

我相信你们大多数人都实现过自己的枚举器,并且知道这一点,如果不能提供任何方法来使用这些自定义控件和你们自己的枚举器,那将是荒谬的。将自定义枚举器与全局可访问的自定义控件一起使用的主要问题是,控件无法知道特定应用程序定义的某些内容。

由于这些控件使用的实际集合定义在控件的命名空间中,您所要做的就是在您的窗口/用户控件中实例化它,如下所示:

给定以下枚举声明:

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

您将如此实例化集合:

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

然后您将手动将其绑定到控件:

<ctrls:EnumComboBox x:Name="cbLocalEnum1" ItemsSource="{Binding Path=Enum1}" />

代码

总的来说,`EnumComboBox` 和 `EnumListBox` 在底层是相同的。如果我能找到一种方法只用一个类编写代码,我会这样做。但是,C# 的性质要求我实际上在两个类中复制了所有代码。唯一的真正区别是组合框不支持多选。由于代码基本相同,我将只详细讨论 `EnumListBox`。我不会深入探讨创建 WPF 自定义控件的细微差别,因为互联网上有无数其他资源比我能解释得好得多。相反,我将简单地告诉您我做了什么,如果我认为“为什么”很重要,我甚至会告诉您。

EnumItemList 集合和 EnumItem 集合项

为了使实际项对使用该控件的窗体尽可能有用,枚举器将许多最有用的信息分解为易于访问的属性。这减轻了开发人员在其窗口/用户控件中后处理选定项的负担。

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;
    }
}

实际绑定到控件的 `EnumItemList ObservableCollection` 负责创建自己的项。它还可以自行确定控件是否允许多选。请记住,如果您要在控件中显示本地枚举器,您必须自己实例化此集合(已提供示例)。

public class EnumItemList : ObservableCollection
{
    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().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,
                });
            }
        }
    }
}

附加属性

几乎对于您将编写的每一个自定义控件,您都会添加一些基类中不存在的属性,其唯一目的是启用您的自定义功能,而 `EnumListBox` 当然也不例外。

//---------------------------------------------------------
// 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); }
}

还有几个辅助属性:

// 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;
    }
}

唯一剩下的就是控件在加载时的反应。我使用 `Loaded` 事件来确定如何绑定集合。当我尝试支持 XAML 中的 `ItemsSource` 绑定时,我认为有必要验证绑定的集合是否为 `EnumItemList` 类型,并发现我必须获取父窗口的 `DataContext` 才能做到这一点。

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(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;
            }
        }
    }
}

使用代码

使用这些控件非常容易,尤其是考虑到您不必实现它们使用的集合。您在窗口中创建的属性甚至不必使用 `INotifyPropertyChanged`,因为一旦实例化,集合就不会改变。事实上,如果您绑定到系统枚举器,您甚至不必实例化集合。

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);
    }
    ...
}

XAML 同样简单,除了样式元素和处理控件事件的愿望外,您所要做的就是应用适当的绑定:

<!-- 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}" />

结束语

很少有机会像我在这里一样,创建一个与数据如此紧密绑定的控件。大多数时候,您必须为更大的目的而编写。我最初只是想支持系统枚举器,但在考虑了一段时间后,我决定添加对本地枚举器的支持。通过这样做,我只添加了几行代码,就几乎使控件的可用性翻倍。

许多人可能会声称“面向未来”是浪费时间,因为您很可能永远不会需要支持该范例的代码。老实说?这是真的。但是,我认为“面向未来”比以后回来添加代码更容易,因为通常情况下,您几乎从未获得时间来回顾和改进代码。

历史

  • 2021.02.22 - 初次发布。
     
© . All rights reserved.