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 接口属性Value 的explicit interface implementation,除非您将状态参数对象引用显式转换为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 类声明了一个附加属性,该属性用于保存具有可供选择的特定状态参数的可能值的数组。 (就像上面的 StateParameter.State 一样,这个属性只能设置一次,我在这里给它一个设置器的原因是为了让我的答案中的代码相对简短。 )
除了 ChoiceStateParameter 类之外,没有其他类在其中有任何声明。如果我们可以直接使用 StateParameter/StateParameter,为什么我们需要 BoolStateParameter/TextStateParameter,你问?这是个好问题。如果我们不必处理 XAML,我们可以轻松地直接使用 StateParameter/StateParameter(假设 _StateParameter 不是抽象类)。但是,尝试从 XAML 标记中引用泛型类型是一件非常痛苦和完全不可能的事情。至此,定义了非泛型具体状态参数类BoolStateParameter、TextStateParameter和ChoiceStateParameter。
哦,在我们忘记之前,由于我们已将公共状态参数基类型声明为名为@987654343@ 的接口,因此必须相应调整视图模型中StateParametersList 属性的类型参数(及其支持字段,当然也是):
public ObservableCollection<IStateParameter> StateParametersList { get ..... set ..... }
完成后,我们完成了 C# 代码方面的部分,我们继续进行 DataGrid。
3.用户界面/XAML
由于不同的状态参数类别需要不同的交互元素(CheckBoxes、TextBoxes、ComboBoxes),我们将尝试利用 DataTemplates 来定义每个状态参数类别应如何在 DataGrid 单元格中表示。
现在我们为什么要努力定义这些类别并为每个类别声明不同的状态参数类型也将变得显而易见。因为 DataTemplates 可以与特定类型关联。我们现在将为BoolStateParameter、TextStateParameter 和ChoiceStateParameter 类型定义这些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"
}
};
看起来像这样: