WPF - 专用枚举器ListBox和ComboBox
在您的WPF应用程序中实现枚举器选择
引言
几天前,我发布了一篇文章,在其中描述了一种用于 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 - 初次发布。