WPF 枚举标志的多选控件





5.00/5 (5投票s)
设置枚举标志的可自定义方式
引言
在我最近创建的一个应用程序中,我发现我需要一个紧凑的控件,能够为带有 `Flags` 属性的 `enum` 选择多个值;我希望通过 GUI 设置 `myFlags = Flag2 | Flag5 | Flag7`。这可以通过多种方式完成,都可以通过在表单上显示所有可选值来实现,但我没有那么大的空间。 `ComboBox` 有一个多选模式,非常接近,但它需要一个可选值的列表作为其 `InputSource`。我想要一个能从 `enum` 类型创建自己的列表并绑定到 `enum` 值的控件。下面描述了我的解决方案。
代码使用 .NET Core 3.1 开发,但可以轻松地重新定位到 .NET Framework 4.5.2 或更高版本。尽管我是在 Visual Studio 2019 中开发它和演示项目的,但我认为我没有使用任何 VS 2015 中不提供的功能。
使用控件
在你的 Window 节点中添加一个命名空间引用,然后像插入 `Button` 一样将 `FlagsEnumButton` 插入你的 XAML 中。(请参见演示应用程序。)可用的自定义属性的完整集合如下例所示。
<uc:FlagsEnumButton EnumValue="{Binding InputSelection, Mode=TwoWay,
Converter={StaticResource FlagsIntConverter}}"
Check="LogUserActivity"
ButtonLabelStyle="FixedText"
EmptySelectionLabel="Inactive">
<uc:FlagsEnumButton.ButtonLabel>Activated</uc:FlagsEnumButton.ButtonLabel>
<uc:FlagsEnumButton.ChoicesSource>
<x:Type Type="models:Inputs"/>
</uc:FlagsEnumButton.ChoicesSource>
</uc:FlagsEnumButton>
自定义属性说明如下:
ChoicesSource
– 设置 `Enum` 的 `Type`。这必须是一个带有 `Flags` 属性的 `enum`。它用于创建下拉列表中的条目。绑定的字段假定为此 `Type`。这是唯一必需的属性。ButtonLabelStyle
(Dependency Property) – 选择按钮文本的显示格式。可用值如下:Indexes
。每个选中标志的 1 基索引以逗号分隔的列表显示,例如“1, 3, 4
”。这是默认值。索引与下拉列表中显示的顺序匹配。Values
。每个选中标志的值以逗号分隔的列表显示,例如“1, 4, 8
”。回过头来看,这似乎没什么用,但我还是保留了它。你永远不知道什么时候它可能正是你需要的。Names
。标志值的文本表示形式,每行一个。我认为这个看起来最好,而且对我来说非常合适,但当设置了很多标志时,它可能会变得混乱。FixedText
。当设置了任何标志时,按钮标签(Button.Content
)保持不变。请注意,这与 `EmptySelectionLabel` 的设置无关。固定文本默认为“Button
”。
ButtonLabel
- 设置当 `ButtonLabelStyle` 为 `FixedText` 时显示的固定文本字符串。否则忽略。Check
(Dependency Property) – 设置一个路由事件处理程序,当标志选择更改时调用。点击的 `MenuItem` 会被传递给处理程序。EmptySelectionLabel
(Dependency Property) – 设置当没有选择值时显示为按钮标签的文本字符串。适用于所有 `ButtonLabelStyle` 的值。默认为“None
”。EnumValue
(Dependency Property) – 获取/设置绑定的 `Enum` 字段的值。该属性定义为 `int`,因此你的应用程序需要一个转换器来将 `int` 与 `enum` 值相互转换。(WPF 找不到它们像 .NET 那样等效。)我找不到一种方法可以消除对转换器的需求。
最少的 XAML 只需要将 `ChoicesSource` 属性设置为要表示的 `enum` 的 `Type`。虽然不是必需的,但如果不将 `EnumValue` 依赖属性绑定,该控件几乎无用。毕竟,你选择使用这个控件就是因为它。
绑定到 `EnumValue` 需要一个简单的转换器。将值转换器添加到你的 `ResourceDictionary` 中,并带有合适的键,例如上面示例中使用的“FlagsIntConverter
”。
<ResourceDictionary>
<local:FlagsIntConverter x:Key="FlagsIntConverter"/>
</ResourceDictionary>
编写你自己的转换器,或者复制演示项目中的那个,如下所示。
public class FlagsIntConverter : System.Windows.Data.IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
int val = -1;
if (value is Enum)
val = (int)value;
return val;
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (!targetType.IsEnum)
throw new InvalidCastException(targetType.Name + " is not an Enum type.");
return Enum.ToObject(targetType, (int)value);
}
}
控件内部
XAML 非常简单。我通过 `ContextMenu` 和一个名为 `TextBlock`(包含按钮标签)来扩展 `Button`。
<Button x:Class="UserControls.FlagsEnumButton"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:UserControls"
mc:Ignorable="d"
Height="Auto"
Padding="10,1,10,3"
Click="Button_Click">
<Button.ContextMenu>
<ContextMenu x:Name="menu" Closed="Menu_Closed" />
</Button.ContextMenu>
<Button.Content>
<TextBlock x:Name="buttonLabel" Text="FlagsEnumButton"/>
</Button.Content>
</Button>
关注点
代码隐藏中有趣的部分是:
- 初始化 `ChoicesSource` 时,将使用每个定义的 `enum` 值在 `ObservableCollection` 中创建一个 `MenuItem`,该集合在构造函数中分配给 `ContextMenu.ItemsSource`。通过将 `MenuItem.IsCheckable` 设置为 `true`,勾选标记会自动生效。我使用 `MenuItem.Tag` 来存储原始 `enum` 值。它以 `enum` 的类型存储,但 `MenuItem.Tag` 是一个 `object`,因此在所有地方仍然必须显式转换。每个 `MenuItem` 的 `Click` 事件都会调用对 `EnumValue` 和按钮标签的重新计算。
我有些惊讶地发现,当控件位于可展开的网格行(DataGrid.RowDetailsTemplate
)中时,`EnumValue` 在 `ChoicesSource` 设置之前就被设置了。这会导致值和显示的标签不同步。为了解决这个问题,如果在创建 `MenuItems` 时 `EnumValue` 不为零,它会被冗余地赋给自身,这会导致显示和值重新同步。 - 每次 `enum` 值更改时都必须更新按钮标签。我将生成字符串的所有逻辑放在 `ButtonLabel` 属性的 getter 中,在 `switch` 语句中使用一些简单的 LINQ 表达式。在检查 `ButtonLabelStyles.FixedText` 之前测试“无选择”条件,可以使 `EmptySelectionLabel` 具有优先权,并实现双值标签。
- 按钮的 `Click` 事件用于打开上下文菜单。这里的“陷阱”是,此时菜单的 `IsOpen` 属性始终为 `false`,无论菜单状态如何。而是使用 `IsVisible` 属性来检查菜单状态。
- 当上下文菜单关闭时,显式更新绑定到 `EnumValue` 的源。这并非对所有操作都必需,但我遇到了至少一种情况,源没有被更新,所以我添加了这一步。
- `int` 类型为你提供了 31 个独立的标志空间,你可以通过更改为 `uint` 来使其达到 32 个,但我发现一次选择超过八个标志用 Names 样式看起来不太好。请明智地规划。
润色
Description 属性
在初始化期间,会检索标志的名称并用于为上下文菜单创建下拉列表。我想要比 `enum` 名称允许的更自由的文本,所以我给它们添加了 `Description` 属性,并使用其值(当存在时)用于下拉列表。一个 `GetDescription()` 扩展方法(包含在控件库中)会检查该属性并返回适当的 `string`。请参见演示应用程序,其中有一个使用此属性的示例。
/// <summary>Returns the value of the DescriptionAttribute associated with the enum
/// value, or the results of value.ToString() if it has no DescriptionAttribute.
/// </summary>
public static string GetDescription(this System.Enum value)
{
var fieldInfo = value.GetType().GetField(value.ToString());
if (fieldInfo == null)
return value.ToString();
var attribArray = fieldInfo.GetCustomAttributes(
typeof(System.ComponentModel.DescriptionAttribute), false);
if (attribArray.Length == 0)
return value.ToString();
else
return ((System.ComponentModel.DescriptionAttribute)attribArray[0]).Description;
}
Material Design
Material Design 在公司得到了推广,所以我将 MaterialDesignInXaml Nuget 包添加到我的应用程序中,看看效果如何。效果相当不错。但是,如果你使用它,你可能需要为这个控件禁用 `RippleAssist`。如果保留启用状态,行为会显得有点奇怪。将以下内容添加到 FlagsEnumButton.xaml 的 `Button` 节点中。
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
materialDesign:RippleAssist.IsDisabled="True"
总结
在此过程中,我尝试了各种功能想法。有些保留了,有些则没有。禁用下拉列表中单个标志值的概念仍然保留在代码的注释中,如果你想为你的应用程序恢复它。我在确定应用程序如何使用它时遇到了困难,最终只是消除了这个需求。
这个控件可能不如我最初设想的那样通用,但它是一次有趣的学习经历,尤其是在我添加了 MaterialDesignInXaml 包之后。我很乐意听到你对其进行的改进。
历史
- 2020年1月15日:初始版本