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

WPF 枚举标志的多选控件

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2021年1月18日

CPOL

6分钟阅读

viewsIcon

8518

downloadIcon

245

设置枚举标志的可自定义方式

Button dropdown, no selection   Button dropdown, with selection

引言

在我最近创建的一个应用程序中,我发现我需要一个紧凑的控件,能够为带有 `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> 

关注点

代码隐藏中有趣的部分是:

  1. 初始化 `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` 不为零,它会被冗余地赋给自身,这会导致显示和值重新同步。
  2. 每次 `enum` 值更改时都必须更新按钮标签。我将生成字符串的所有逻辑放在 `ButtonLabel` 属性的 getter 中,在 `switch` 语句中使用一些简单的 LINQ 表达式。在检查 `ButtonLabelStyles.FixedText` 之前测试“无选择”条件,可以使 `EmptySelectionLabel` 具有优先权,并实现双值标签。
  3. 按钮的 `Click` 事件用于打开上下文菜单。这里的“陷阱”是,此时菜单的 `IsOpen` 属性始终为 `false`,无论菜单状态如何。而是使用 `IsVisible` 属性来检查菜单状态。
  4. 当上下文菜单关闭时,显式更新绑定到 `EnumValue` 的源。这并非对所有操作都必需,但我遇到了至少一种情况,源没有被更新,所以我添加了这一步。
  5. `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日:初始版本
© . All rights reserved.