如何通过 mvvm 将 wpf 数据网格绑定到包含组合框和测试框组合的列
How to bind a wpf datagrid to a column that contains combination of combo box and test box via mvvm
在我的数据网格中,我有一个文本框列和另一个应该包含组合框和文本框组合的列,它们应该动态设置。例如,我让用户设置机器的状态。因此,State 和 Value 是每列的 headers,其中 Value 可以包含组合框或 TextBox,具体取决于 State 的类型。它的类型可以是布尔值或枚举。如果是枚举,则显示组合框,否则显示文本框。
我正在尝试通过视图模型执行此操作,但我不确定如何在 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 文件完成此操作,并根据其是否为枚举创建组合框,但我想通过视图模型实现此目的。而且,每个 comboBox 都有不同的值,这些值在 运行 时间动态填充。我在这里苦苦挣扎,因此,如果有人能指出我正确的方向,将不胜感激。
1.组织状态参数数据模型
在查看所需的用户交互时,存在不同类别的状态参数,这取决于它们如何成为 presented/edited to/by 用户。在问题范围内,我们可以识别出以下几类:
- 一个可切换的参数 (
bool
)
- 一个选择参数,其中参数的值是给定集合中的一个(实际上就像枚举或任何其他数据类型)
- 为了更好的衡量,一个文本参数 (
string
)
2。实现状态参数数据模型
一个状态参数有一个状态 name/identifier 和一个值。该值可以是不同的类型。这本质上是问题中 StateParameters class 的定义。
然而,正如我稍后的回答中将变得更加明显的那样,用不同的 types/classes 表示上面列出的不同类别的状态参数将有利于在 [=168] 中连接表示和交互逻辑=].
当然,无论属于哪个类别,每个状态参数都应该由相同的基类型表示。显而易见的选择是使状态参数基类型成为抽象 class 或接口。在这里,我选择了一个界面:
public interface IStateParameter
{
string State { get; }
object Value { get; set; }
}
我现在没有根据上面列出的类别直接创建具体状态参数 classes,而是创建了一个额外的抽象基础 class。 class 将是通用的,使得以 type-safe 方式处理状态参数更容易一些:
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 以使我的答案中的代码相对简短。)
注意 IStateParameter
接口 属性 Value
的 INotifyPropertyChanged
interface, which is necessary since the UI is going to manipulate the Value
property through bindings. Also note the explicit interface implementation 的实现,它将 "hide" 它除非你显式地转换状态参数对象引用为 IStateParameter
。这是有意为之,因为 StateParameter<T>
提供了自己的 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 class 声明了一个额外的 属性 用于保存数组,其中包含可供特定状态参数选择的可能值。 (就像上面的 StateParameter.State 一样,这个 属性 只能设置一次,我在这里给它一个 setter 的原因是让我的回答中的代码相对简短。)
除了 ChoiceStateParameter class 之外,其他 class 中没有任何声明。如果我们可以直接使用 BoolStateParameter/TextStateParameter 为什么我们需要 StateParameter/StateParameter ?这是个好问题。如果我们不必处理 XAML,我们可以轻松地直接使用 StateParameter/StateParameter (假设 _StateParameter 不是抽象 class).但是,尝试从 XAML 标记中引用泛型类型是一件介于非常痛苦和完全不可能之间的事情。因此,non-generic 具体状态参数 classes BoolStateParameter、TextStateParameter 和 ChoiceStateParameter 已定义。
哦,在我们忘记之前,因为我们已经将公共状态参数基类型声明为名为 IStateParameter
的接口,视图模型中 StateParametersList
属性 的类型参数必须相应地调整(当然还有它的支持字段):
public ObservableCollection<IStateParameter> StateParametersList { get ..... set ..... }
完成后,我们就完成了 C# 代码方面的部分,接下来我们将转向 DataGrid。
3。 UI / XAML
由于不同的状态参数类别需要不同的交互元素(CheckBoxes、TextBoxes、ComboBoxes),我们将尝试利用 DataTemplates 来定义每个状态参数类别应如何在 DataGrid 单元格中表示。
现在,我们努力定义这些类别并为每个类别声明不同的状态参数类型的原因也将变得显而易见。因为 DataTemplates 可以与特定类型相关联。我们现在要定义为每个 BoolStateParameter
、TextStateParameter
和 ChoiceStateParameter
类型设置数据模板。
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 命名空间交换状态参数 classes.)
下一步是使 DataGridTemplateColumn 根据它在给定列单元格中处理的状态参数的实际类型选择适当的数据模板。但是,DataGridTemplateColumn 无法从资源字典本身选择 DataTemplate,DataGrid 控件也不会代表 DataGridTemplateColumn。那么,现在怎么办?
幸运的是,WPF 中有 UI 个元素,它们使用资源字典中的 DataTemplate 呈现一些 value/object,DataTemplate 是根据 value/object 的类型选择的。 UI 元素之一是 ContentPresenter
,我们将在 DataGridTemplateColumn:
中使用它
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding State}"/>
<DataGridTemplateColumn Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
就是这样。随着底层数据模型(状态参数 classes)的小幅扩展,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"
}
};
它看起来像这样:
在我的数据网格中,我有一个文本框列和另一个应该包含组合框和文本框组合的列,它们应该动态设置。例如,我让用户设置机器的状态。因此,State 和 Value 是每列的 headers,其中 Value 可以包含组合框或 TextBox,具体取决于 State 的类型。它的类型可以是布尔值或枚举。如果是枚举,则显示组合框,否则显示文本框。
我正在尝试通过视图模型执行此操作,但我不确定如何在 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 文件完成此操作,并根据其是否为枚举创建组合框,但我想通过视图模型实现此目的。而且,每个 comboBox 都有不同的值,这些值在 运行 时间动态填充。我在这里苦苦挣扎,因此,如果有人能指出我正确的方向,将不胜感激。
1.组织状态参数数据模型
在查看所需的用户交互时,存在不同类别的状态参数,这取决于它们如何成为 presented/edited to/by 用户。在问题范围内,我们可以识别出以下几类:
- 一个可切换的参数 (
bool
) - 一个选择参数,其中参数的值是给定集合中的一个(实际上就像枚举或任何其他数据类型)
- 为了更好的衡量,一个文本参数 (
string
)
2。实现状态参数数据模型
一个状态参数有一个状态 name/identifier 和一个值。该值可以是不同的类型。这本质上是问题中 StateParameters class 的定义。
然而,正如我稍后的回答中将变得更加明显的那样,用不同的 types/classes 表示上面列出的不同类别的状态参数将有利于在 [=168] 中连接表示和交互逻辑=].
当然,无论属于哪个类别,每个状态参数都应该由相同的基类型表示。显而易见的选择是使状态参数基类型成为抽象 class 或接口。在这里,我选择了一个界面:
public interface IStateParameter
{
string State { get; }
object Value { get; set; }
}
我现在没有根据上面列出的类别直接创建具体状态参数 classes,而是创建了一个额外的抽象基础 class。 class 将是通用的,使得以 type-safe 方式处理状态参数更容易一些:
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 以使我的答案中的代码相对简短。)
注意 IStateParameter
接口 属性 Value
的 INotifyPropertyChanged
interface, which is necessary since the UI is going to manipulate the Value
property through bindings. Also note the explicit interface implementation 的实现,它将 "hide" 它除非你显式地转换状态参数对象引用为 IStateParameter
。这是有意为之,因为 StateParameter<T>
提供了自己的 Value
属性 类型,该类型匹配 StateParameterValue
setter 中的相等比较有点尴尬,因为这里的泛型类型参数 T
是完全不受约束的,可以是某个值类型或某个引用类型。因此,平等比较必须涵盖所有可能发生的情况。
因此,完成这些准备工作后,是时候将我们的注意力放回实际问题上了。我们现在将根据答案开头概述的类别来实现具体的状态参数类型:
public class BoolStateParameter : StateParameter<bool>
{ }
public class TextStateParameter : StateParameter<string>
{ }
public class ChoiceStateParameter : StateParameter<object>
{
public Array Choices { get; set; }
}
ChoiceStateParameter class 声明了一个额外的 属性 用于保存数组,其中包含可供特定状态参数选择的可能值。 (就像上面的 StateParameter
除了 ChoiceStateParameter class 之外,其他 class 中没有任何声明。如果我们可以直接使用 BoolStateParameter/TextStateParameter 为什么我们需要 StateParameter
哦,在我们忘记之前,因为我们已经将公共状态参数基类型声明为名为 IStateParameter
的接口,视图模型中 StateParametersList
属性 的类型参数必须相应地调整(当然还有它的支持字段):
public ObservableCollection<IStateParameter> StateParametersList { get ..... set ..... }
完成后,我们就完成了 C# 代码方面的部分,接下来我们将转向 DataGrid。
3。 UI / XAML
由于不同的状态参数类别需要不同的交互元素(CheckBoxes、TextBoxes、ComboBoxes),我们将尝试利用 DataTemplates 来定义每个状态参数类别应如何在 DataGrid 单元格中表示。
现在,我们努力定义这些类别并为每个类别声明不同的状态参数类型的原因也将变得显而易见。因为 DataTemplates 可以与特定类型相关联。我们现在要定义为每个 BoolStateParameter
、TextStateParameter
和 ChoiceStateParameter
类型设置数据模板。
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 命名空间交换状态参数 classes.)
下一步是使 DataGridTemplateColumn 根据它在给定列单元格中处理的状态参数的实际类型选择适当的数据模板。但是,DataGridTemplateColumn 无法从资源字典本身选择 DataTemplate,DataGrid 控件也不会代表 DataGridTemplateColumn。那么,现在怎么办?
幸运的是,WPF 中有 UI 个元素,它们使用资源字典中的 DataTemplate 呈现一些 value/object,DataTemplate 是根据 value/object 的类型选择的。 UI 元素之一是 ContentPresenter
,我们将在 DataGridTemplateColumn:
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding State}"/>
<DataGridTemplateColumn Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<ContentPresenter Content="{Binding}" />
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
就是这样。随着底层数据模型(状态参数 classes)的小幅扩展,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"
}
};
它看起来像这样: