【问题标题】:How to bind a wpf datagrid to a column that contains combination of combo box and test box via mvvm如何通过 mvvm 将 wpf 数据网格绑定到包含组合框和测试框组合的列
【发布时间】:2023-03-08 04:57:01
【问题描述】:

在我的数据网格中,我有一个文本框列和另一列应该包含组合框和文本框的组合,应该动态设置。例如,我让用户设置机器的状态。因此,State 和 Value 是每列的标题,其中 Value 可以包含组合框或文本框,具体取决于状态的类型。它的类型可以是布尔值或枚举。如果是枚举,则显示组合框,否则显示文本框。

我正在尝试通过视图模型执行此操作,但我不确定如何在 xaml 中设置 DataGridview。或者这种情况是否可能......?

<DataGrid Name="dataGridView" ItemsSource="{Binding Path=StateParametersList}" CanUserAddRows="False" 
                      IsReadOnly="True" >

                <DataGrid.Columns>
                    <DataGridTextColumn Binding="{Binding State}"/>
                    <DataGridTemplateColumn>
                        <DataGridTemplateColumn.CellEditingTemplate>
                            <DataTemplate>
                                <ComboBox ItemsSource="{Binding ValueCell}" SelectedItem="{Binding Value}"/>
                            </DataTemplate>
                        </DataGridTemplateColumn.CellEditingTemplate>
                    </DataGridTemplateColumn>
                </DataGrid.Columns>

            </DataGrid>

视图模型:

private ObservableCollection<StateParameters> StateParametersList =
        new ObservableCollection<StateParameters>();

    public ObservableCollection<StateParameters> StateParametersList
    {
        get { return StateParametersList; }
        set
        {
            StateParametersList = value;
            NotifyPropertyChanged(nameof(StateParametersList));
        }
    }
[Serializable]
public class StateParameters
{
    public string State { get; set; }
    public object Value { get; set; }
}

List<string> ValueCell = new List<string>();

其中 ValueCell 将是组合框中将在运行时填充的项目列表。

因此,我可以通过 xaml.cs 文件完成此操作,并根据其是否枚举创建组合框,但我想通过视图模型实现此目的。而且,每个组合框都将具有不同的值,这些值在运行时动态填充。我在这里挣扎,因此,如果有人能指出我正确的方向,将不胜感激。

【问题讨论】:

  • 为什么要一个代表布尔参数的文本框? CheckBox 不是表示布尔参数的更好选择吗?
  • 是的,复选框也很理想。但是如何在动态填充的数据网格中组合一个复选框和一个组合框?
  • 我已经知道如何解决您的问题,但请允许我再问一个问题,以便更好地处理可能的解决方案:正如您已经注意到的,您的简单 StateParamters 数据模型并没有真正帮助您的应用程序的需求。您已经使用 [Serialize] 属性装饰了您的 StateParameters,在我看来它将在(反)序列化期间使用。 (1/2)
  • (2/2) 将 StateParameters 设为接口或抽象类是否可以接受?然后是从 StateParameters 派生的其他类,每个类代表不同类别的参数(如可切换/布尔、基于选择/枚举、文本等)?这背后的动机是拥有一个更适合您的应用程序需求的数据模型(交互逻辑)
  • (旁注:无论如何我建议将类 StateParameters 重命名为 StateParameterStateParameters 实例仅代表单个状态参数,因此使用起来有点错误和误导复数 StateParameters 作为此类的名称... ;-) )

标签: c# wpf mvvm datagridviewcolumn


【解决方案1】:

1.组织状态参数数据模型

在查看所需的用户交互时,存在不同类别的状态参数,即它们将如何呈现/编辑给用户/由用户进行。在问题的范围内,我们可以确定以下类别:

  • 可切换参数 (bool)
  • 选择参数,其中参数的值是给定集合中的一个(实际上就像枚举或任何其他数据类型)
  • 还有一个文本参数 (string)


2.实现状态参数数据模型

状态参数具有状态名称/标识符和值。该值可以是不同的类型。这本质上就是问题中StateParameters类的定义。

但是,正如我稍后在回答中将变得更加明显的那样,具有表示上面列出的不同类别状态参数的不同类型/类将有利于在 UI 中连接表示和交互逻辑。

当然,不管是哪个类别,每个状态参数都应该用相同的基类型来表示。显而易见的选择是使状态参数基类型成为抽象类或接口。在这里,我选择了一个界面:

public interface IStateParameter
{
    string State { get; }
    object Value { get; set; }
}

现在我没有根据上面列出的类别直接创建具体的状态参数类,而是创建了一个额外的抽象基类。这个类将是通用的,使得以类型安全的方式处理状态参数更容易一些:

public abstract class StateParameter<T> : IStateParameter, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public string State { get; set; }

    public T Value
    {
        get { return _v; }
        set
        {
            if ((_v as IEquatable<T>)?.Equals(value) == true || ReferenceEquals(_v, value) || _v?.Equals(value) == true)
                return;

            _v = value;
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
        }
    }

    private T _v;

    object IStateParameter.Value
    {
        get { return this.Value; }
        set { this.Value = (T) value; }
    }
}

(虽然State 属性有一个setter,但该属性只能设置一次,因此不需要属性更改通知。从技术上讲,您可以随时更改属性;我只是选择使用setter这里是为了让我的答案中的代码相对简短。)

注意INotifyPropertyChanged 接口的实现,这是必要的,因为UI 将通过绑定来操作Value 属性。另请注意IStateParameter 接口属性Valueexplicit interface implementation,除非您将状态参数对象引用显式转换为IStateParameter,否则它将“隐藏”它。这是有意的,因为StateParameter&lt;T&gt; 提供了它自己的Value 属性,该属性与StateParameter 的泛型类型参数相匹配。此外,不幸的是,Value setter 中的相等比较有点不妥,因为这里的泛型类型参数T 完全不受约束,可以是某种值类型或某种引用类型。因此,相等比较必须涵盖所有可能性。

因此,在完成这些准备工作之后,是时候将我们的注意力重新集中到实际问题上了。我们现在要根据答案开头列出的类别来实现具体的状态参数类型:

    public class BoolStateParameter : StateParameter<bool>
    { }
    public class TextStateParameter : StateParameter<string>
    { }
    public class ChoiceStateParameter : StateParameter<object>
    {
        public Array Choices { get; set; }
    }

ChoiceStateParameter 类声明了一个附加属性,该属性用于保存具有可供选择的特定状态参数的可能值的数组。 (就像上面的 StateParameter.State 一样,这个属性只能设置一次,我在这里给它一个设置器的原因是为了让我的答案中的代码相对简短。 )

除了 ChoiceStateParameter 类之外,没有其他类在其中有任何声明。如果我们可以直接使用 StateParameter/StateParameter,为什么我们需要 BoolStateParameter/TextStateParameter,你问?这是个好问题。如果我们不必处理 XAML,我们可以轻松地直接使用 StateParameter/StateParameter(假设 _StateParameter 不是抽象类)。但是,尝试从 XAML 标记中引用泛型类型是一件非常痛苦和完全不可能的事情。至此,定义了非泛型具体状态参数类BoolStateParameterTextStateParameterChoiceStateParameter

哦,在我们忘记之前,由于我们已将公共状态参数基类型声明为名为@9​​87654343@ 的接口,因此必须相应调整视图模型中StateParametersList 属性的类型参数(及其支持字段,当然也是):

public ObservableCollection<IStateParameter> StateParametersList { get ..... set ..... }

完成后,我们完成了 C# 代码方面的部分,我们继续进行 DataGrid。


3.用户界面/XAML

由于不同的状态参数类别需要不同的交互元素(CheckBoxes、TextBoxes、ComboBoxes),我们将尝试利用 DataTemplates 来定义每个状态参数类别应如何在 DataGrid 单元格中表示。

现在我们为什么要努力定义这些类别并为每个类别声明不同的状态参数类型也将变得显而易见。因为 DataTemplates 可以与特定类型关联。我们现在将为BoolStateParameterTextStateParameterChoiceStateParameter 类型定义这些DataTemplate。

DataTemplates 将放置在 DataGrid 中,作为 DataGrid 资源字典的一部分:

<DataGrid Name="dataGridView" ItemsSource="{Binding Path=StateParametersList}" ... >

    <DataGrid.Resources>
        <DataTemplate DataType="{x:Type local:BoolStateParameter}">
            <CheckBox IsChecked="{Binding Value, UpdateSourceTrigger=PropertyChanged}" />
        </DataTemplate>

        <DataTemplate DataType="{x:Type local:TextStateParameter}">
            <TextBox Text="{Binding Value, UpdateSourceTrigger=PropertyChanged}" />
        </DataTemplate>

        <DataTemplate DataType="{x:Type local:ChoiceStateParameter}">
            <ComboBox ItemsSource="{Binding Choices}" SelectedItem="{Binding Value, UpdateSourceTrigger=PropertyChanged}" />
        </DataTemplate>
    </DataGrid.Resources>

(注意:您可能需要调整我在此处使用的 local: 命名空间,或者将其与映射到您声明状态参数类的 C# 命名空间的 XML 命名空间进行交换。)

下一步是使 DataGridTemplateColumn 根据它在给定列单元格中处理的状态参数的实际类型选择适当的 DataTemplate。但是,DataGridTemplateColumn 不能从资源字典本身中选择 DataTemplate,DataGrid 控件也不能代表 DataGridTemplateColumn。那么,现在呢?

幸运的是,WPF 中有一些 UI 元素使用资源字典中的 DataTemplate 呈现一些值/对象,并根据值/对象的类型选择 DataTemplate。一个这样的 UI 元素是 ContentPresenter,我们将在 DataGridTemplateColumn 中使用它:

    <DataGrid.Columns>
        <DataGridTextColumn Binding="{Binding State}"/>

        <DataGridTemplateColumn Width="*">
            <DataGridTemplateColumn.CellTemplate>
                <DataTemplate>
                    <ContentPresenter Content="{Binding}" />
                </DataTemplate>
            </DataGridTemplateColumn.CellTemplate>
        </DataGridTemplateColumn>
    </DataGrid.Columns>

</DataGrid>

就是这样。随着底层数据模型(状态参数类)的小幅扩展,XAML 问题就消失了(或者我希望如此)。


4.演示数据集

一个用于演示实际代码的快速测试数据集(使用随机选择的枚举类型作为示例):

StateParametersList = new ObservableCollection<IStateParameter>
{
    new BoolStateParameter
    {
        State = "Bool1",
        Value = false
    },
    new ChoiceStateParameter
    {
        State = "Enum FileShare",
        Value = System.IO.FileShare.ReadWrite,
        Choices = Enum.GetValues(typeof(System.IO.FileShare))
    },
    new TextStateParameter
    {
        State = "Text1",
        Value = "Hello"
    },
    new BoolStateParameter
    {
        State = "Bool2",
        Value = true
    },
    new ChoiceStateParameter
    {
        State = "Enum ConsoleKey",
        Value = System.ConsoleKey.Backspace,
        Choices = Enum.GetValues(typeof(System.ConsoleKey))
    },
    new TextStateParameter
    {
        State = "Text2",
        Value = "World"
    }
};

看起来像这样:

【讨论】:

  • 我试过这个,这就像一个魅力,这是一个很棒的解释。我一直在努力思考应该有一个直接的方法来处理它,但看起来这是要走的路。非常感谢您的时间和精力。非常感谢!
猜你喜欢
  • 2012-06-05
  • 2012-12-21
  • 2015-09-15
  • 1970-01-01
  • 2021-02-16
  • 2017-05-17
  • 2021-10-11
  • 2015-07-11
  • 2015-04-27
相关资源
最近更新 更多