基于带有标志的枚举的WPF ComboBox/ListBox多选。

6

我可能要稍微超出一点范围...

基本上,我有以下枚举,在C#代码中声明:

[Flags]
public enum FlaggedEnum : int
{
    Option1 = 1,
    Option2 = 2,
    Option3 = 4,
    Option4 = 8,
    ...
    Option16 = 32768,
    None = 0
}

这个枚举是一个对象的成员,我已经成功将它绑定到了DataGrid对象。成功意味着我已经成功地绑定了所有其他字段。 :)

我的目标是创建一个控件,其中上面的所有适当选项都被选中,并且行为和操作类似于ComboBox/ListBox。因此,您单击该字段,会弹出一个下拉菜单,可以“勾选”所需的任何选项。

该控件还必须能够从枚举中读取并写入枚举。

我是WPF的新手,除了创建ComboBox并绑定到列之外,我不知道该怎么做...希望得到帮助!

2个回答

6
我有一个可能可行的方法。我不为此做出任何贡献 - 我在网上找到了这种方法,但忘记保存地址。
在我的项目中,我需要将几个复选框绑定到标志枚举。为了帮助,我找到了一个简单值转换器的实现,以促进双向绑定。它不是通用的,一个转换器的单个实例只能同时处理一个目标(意味着一个值和其一组复选框的实例)。转换器使用存储的值引用作为一种转换方式,因此,如果您尝试在分离的对象实例之间重用它,它将无法工作。话虽如此,这是我对此类东西唯一的用途,而且它像魅力一样工作。
转换器:
/// <summary>
/// Provides for two way binding between a TestErrors Flag Enum property and a boolean value.
/// TODO: make this more generic and add it to the converter dictionary if possible
/// </summary>
public class TestActionFlagValueConverter : IValueConverter {
    private TestErrors target;

    public TestActionFlagValueConverter() {

    }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
        TestErrors mask = (TestErrors)parameter;
        this.target = (TestErrors)value;
        return ((mask & this.target) != 0);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
        this.target ^= (TestErrors)parameter;
        return this.target;
    }
}

XAML 中使用方式如下:

<StackPanel.Resources>
    <local:TestActionFlagValueConverter x:Key="TestActionFlagValueConverter"/>
</StackPanel.Resources>

<CheckBox IsChecked="{Binding Errors, Converter={StaticResource TestActionFlagValueConverter}, ConverterParameter={x:Static local:TestErrors.PowerFailure}...
<CheckBox IsChecked="{Binding Errors, Converter={StaticResource TestActionFlagValueConverter}, ConverterParameter={x:Static local:TestErrors.OpenCondition}...

在您的情况下,您可能会将此放入数据单元格模板中(尽管显然您可能更喜欢使用组合框而不是简单的堆栈面板)。确保在复选框组容器附近实例化转换器,以确保它们有自己的转换器实例。
编辑:
这里,我制作了一个小测试项目,演示如何在带有数据网格的组合框中使用它,它基于默认的WPF应用程序 - 只需确保引用了WPF工具包。
以下是Window1.xaml文件:
<Window 
    x:Class="FlagEnumTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Controls="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
    xmlns:FlagEnumTest="clr-namespace:FlagEnumTest"
    Title="Window1" Height="300" Width="300">

    <Window.Resources>
        <x:Array Type="{x:Type FlagEnumTest:TestObject}" x:Key="TestArray">
            <FlagEnumTest:TestObject Errors="OpenCondition" />
            <FlagEnumTest:TestObject />
        </x:Array>
    </Window.Resources>

    <StackPanel>

        <Controls:DataGrid ItemsSource="{Binding Source={StaticResource TestArray}}">
            <Controls:DataGrid.Columns>
                <Controls:DataGridTemplateColumn Header="Errors">
                    <Controls:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <ComboBox>
                                <ComboBox.Resources>
                                    <FlagEnumTest:TestErrorConverter x:Key="ErrorConverter" />
                                </ComboBox.Resources>
                                <CheckBox Content="PowerFailure" IsChecked="{Binding Path=Errors, Converter={StaticResource ErrorConverter}, ConverterParameter={x:Static FlagEnumTest:TestErrors.PowerFailure}}" />
                                <CheckBox Content="OpenCondition" IsChecked="{Binding Path=Errors, Converter={StaticResource ErrorConverter}, ConverterParameter={x:Static FlagEnumTest:TestErrors.OpenCondition}}" />
                            </ComboBox>
                        </DataTemplate>
                    </Controls:DataGridTemplateColumn.CellTemplate>
                </Controls:DataGridTemplateColumn>
            </Controls:DataGrid.Columns>
        </Controls:DataGrid>

    </StackPanel>
</Window>

这里是Window1.xaml.cs文件的代码后台。

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace FlagEnumTest {
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window {
        public Window1() {
            InitializeComponent();
        }
    }

    [Flags]
    public enum TestErrors {
        NoError = 0x0,
        PowerFailure = 0x1,
        OpenCondition = 0x2,
    }

    public class TestObject {
        public TestErrors Errors { get; set; }
    } 

    /// <summary>
    /// Provides for two way binding between a TestErrors Flag Enum property and a boolean value.
    /// TODO: make this more generic and add it to the converter dictionary if possible
    /// </summary>
    public class TestErrorConverter : IValueConverter {
        private TestErrors target;

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
            TestErrors mask = (TestErrors)parameter;
            this.target = (TestErrors)value;
            return ((mask & this.target) != 0);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
            this.target ^= (TestErrors)parameter;
            return this.target;
        }
    }

}

默认情况下,数据网格将创建其自己的列表示形式以及我指定的模板形式,因此您可以看到文本表示形式以及复选框表示形式。标志枚举会使默认文本表示形式混淆,但您仍然可以看到绑定正常工作(同时勾选两个,然后取消勾选最后一个 - 字符串值更改为另一个而不是0)。


Egor,我已经成功让它运行得很完美了。我想知道当复选框状态改变时,是否可以向DataGrid发送一行已被修改的信号?目前我依赖于绑定数据上的IEditableInterface实现来将更新写入后端数据库。然而,编辑复选框并没有触发这种行为。 - sohum
1
另外,更改组合框中显示的值是否可能?我注意到了SelectionBoxItem属性,但似乎这是只读的。 - sohum
我认为组合框中显示的值是当前选定的项目(在这种情况下,来自复选框集合)。您可能会更好地使用带有标题和展开器的模板(您可以将其模板化以看起来像组合框)-这样,您就不需要担心所选项目的语义,并且可以为标志属性提供自定义值转换器以显示为您的“当前”值。 - Egor
我想我可能不得不放弃组合框。我会试着调整一下。谢谢! - sohum
再次感谢,Egor。我已经成功使用Expander了。事件仍然引起混乱。我已经提出了另一个问题来解决这个问题。感谢您的帮助! - sohum
显示剩余2条评论

1
我创建了一个IValueConverter,支持直接绑定到枚举类型,无需使用codebehind或helper类。它只有两个限制:
  • 每个源属性必须使用一个转换器实例。如果模型包含多个相同枚举类型的属性,则每个属性都需要使用单独的转换器实例。这可以通过在XAML中实例化转换器来完成。
  • 当枚举类型为标志枚举时,它必须是整数。
该解决方案基于经验事实:始终会先进行Convert,然后才是ConvertBack。如果ConvertBack已更改值,则总是会执行Convert。仅当在模型上正确实现了INotifyPropertyChanged时,此方法才有效。因此,在两次调用之间,最后已知的值可以存储在转换器中,并在ConvertBack方法中使用。
转换器实例应获取枚举的类型以及它是否为标志枚举。
 <EnumToCheckedConverter x:Key="InstanceName" Type="{x:Type MyEnum}" Flags="True" />

然后可以使用这个转换器来绑定复选框。
 <CheckBox Content="ValueText" IsChecked="{Binding Source, Converter={StaticResource InstanceName}, ConverterParameter=Value}"/>

可以使用Flags="False"的实例来绑定单选按钮。

转换器的源代码

public class EnumToCheckedConverter : IValueConverter
{
    public Type Type { get; set; }
    public int? LastValue { get; private set; }
    public bool Flags { get; set; }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value != null && value.GetType() == Type)
        {
            try
            {
                var parameterValue = Enum.Parse(Type, parameter as string);

                if (Flags == true)
                {
                    var intParameter = (int)parameterValue;
                    var intValue = (int)value;
                    LastValue = intValue;

                    return (intValue & intParameter) == intParameter;
                }
                else
                {
                    return Equals(parameterValue, value);
                }
            }
            catch (ArgumentNullException)
            {
                return false;
            }
            catch (ArgumentException)
            {
                throw new NotSupportedException();
            }
        }
        else if (value == null)
        {
            return false;
        }

        throw new NotSupportedException();
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value != null && value is bool check)
        {
            if (check)
            {
                try
                {
                    if (Flags == true && LastValue.HasValue)
                    {
                        var parameterValue = Enum.Parse(Type, parameter as string);
                        var intParameter = (int)parameterValue;

                        return Enum.ToObject(Type, LastValue | intParameter);
                    }
                    else
                    {
                        return Enum.Parse(Type, parameter as string);
                    }
                }
                catch (ArgumentNullException)
                {
                    return Binding.DoNothing;
                }
                catch (ArgumentException)
                {
                    return Binding.DoNothing;
                }
            }
            else
            {
                try
                {
                    if (Flags == true && LastValue.HasValue)
                    {
                        var parameterValue = Enum.Parse(Type, parameter as string);
                        var intParameter = (int)parameterValue;

                        return Enum.ToObject(Type, LastValue ^ intParameter);
                    }
                    else
                    {
                        return Binding.DoNothing;
                    }
                }
                catch (ArgumentNullException)
                {
                    return Binding.DoNothing;
                }
                catch (ArgumentException)
                {
                    return Binding.DoNothing;
                }
            }
        }

        throw new NotSupportedException();
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接