如何在严格遵守 MVVM 模式的同时,获得一个位于元素ComboBox
内的 WPF并始终拥有一个 default ?DataTemplate
ItemsControl
SelectedItem
我的目标是定义一个“表单域”列表,然后通过模板将其翻译成实际的表单域(即 - TextBox
、ComboBox
、DatePicker
等)。字段列表是 100% 动态的,并且可以随时(由用户)添加和删除字段。
伪实现是:
MainWindow
-> Sets FormViewModel as DataContext
FormViewModel (View Model)
-> Populated the `Fields` Property
Form (View)
-> Has an `ItemsControl` element with the `ItemsSource` bound to FormViewModel's `Fields` Property
-> `ItemsControl` element uses an `ItemTemplateSelector` to select correct template based on current field's type**
FormField
-> Class that has a `DisplayName`, `Value`, and list of `Operators` (=, <, >, >=, <=, etc.)
Operator
-> Class that has an `Id` and `Label` (i.e.: Id = "<=", Label = "Less Than or Equal To")
DataTemplate
-> A `DataTemplate` element for each field type* that creates a form field with a label, and a `ComboBox` with the field's available Operators
*** The `Operators` ComboBox is where the issue occurs ***
** 实际字段的“类型”和其中包含的实现不包含在此问题中,因为它与显示问题无关。
以下是生成表单所需的主要类,基于上面的伪实现:
窗体视图模型.cs
public class FormViewModel : INotifyPropertyChanged {
protected ObservableCollection<FormField> _fields;
public ObservableCollection<FormField> Fields {
get { return _fields; }
set { _fields = value; _onPropertyChanged("Fields"); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected void _onPropertyChanged(string propertyName) {
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public FormViewModel() {
// create a sample field that has a list of operators
Fields = new ObservableCollection<FormField>() {
new FormField() {
DisplayName = "Field1",
Value = "Default Value",
Operators = new ObservableCollection<Operator>() {
new Operator() { Id = "=", Label = "Equals" },
new Operator() { Id = "<", Label = "Less Than" },
new Operator() { Id = ">", Label = "Greater Than" }
}
}
};
}
}
表单.xaml
<UserControl.Resources>
<ResourceDictionary Source="DataTemplates.xaml" />
</UserControl.Resources>
<ItemsControl
ItemsSource="{Binding Fields}"
ItemTemplateSelector="{StaticResource fieldTemplateSelector}">
<ItemsControl.Template>
<ControlTemplate TargetType="ItemsControl">
<ItemsPresenter />
</ControlTemplate>
</ItemsControl.Template>
</ItemsControl>
表单.xaml.cs
public partial class Form : UserControl {
public static readonly DependencyProperty FieldsProperty = DependencyProperty.RegisterAttached("Fields", typeof(ObservableCollection<FormField>), typeof(Form));
public ObservableCollection<FormField> Fields {
get { return ((ObservableCollection<FormField>)GetValue(FieldsProperty)); }
set { SetValue(FieldsProperty, ((ObservableCollection<FormField>)value)); }
}
public Form() {
InitializeComponent();
}
}
FieldTemplateSelector.cs
public class FieldTemplateSelector : DataTemplateSelector {
public DataTemplate DefaultTemplate { get; set; }
public override DataTemplate SelectTemplate(object item, DependencyObject container) {
FrameworkElement element = (container as FrameworkElement);
if ((element != null) && (item != null) && (item is FormField)) {
return (element.FindResource("defaultFieldTemplate") as DataTemplate);
}
return DefaultTemplate;
}
}
数据模板.xaml
<local:FieldTemplateSelector x:Key="fieldTemplateSelector" />
<DataTemplate x:Key="defaultFieldTemplate">
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Path=DisplayName}" />
<TextBox Text="{Binding Path=Value, UpdateSourceTrigger=PropertyChanged}" />
<ComboBox
ItemsSource="{Binding Path=Operators}"
DisplayMemberPath="Label" SelectedValuePath="Id"
SelectedItem="{Binding SelectedOperator, Mode=TwoWay}"
HorizontalAlignment="Right"
/>
</StackPanel>
</DataTemplate>
表单域.cs
public class FormField : INotifyPropertyChanged {
public string DisplayName { get; set; }
public string Value { get; set; }
protected ObservableCollection<Operator> _operators;
public ObservableCollection<Operator> Operators {
get { return _operators; }
set {
_operators = value;
_onPropertyChanged("Operators");
}
}
protected Operator _selectedOperator;
public Operator SelectedOperator {
get { return _selectedOperator; }
set { _selectedOperator = value; _onPropertyChanged("SelectedOperator"); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected void _onPropertyChanged(string propertyName) {
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
运算符.cs
public class Operator {
public string Id { get; set; }
public string Label { get; set; }
}
表格已正确生成;列表中的所有“表单字段”Fields
都被创建为TextBox
元素,其名称显示为标签,并且它们每个都有ComboBox
完整的运算符。但是,ComboBox
默认情况下没有选择项目。
我解决这个问题的第一步是SelectedIndex=0
在ComboBox
; 这没有用。经过反复试验,我选择使用DataTrigger
如下:
<ComboBox
ItemsSource="{Binding Path=Operators}"
DisplayMemberPath="Label" SelectedValuePath="Id"
SelectedItem="{Binding SelectedOperator, Mode=TwoWay}"
HorizontalAlignment="Right">
<ComboBox.Style>
<Style TargetType="{x:Type ComboBox}">
<Style.Triggers>
<!-- select the first item by default (if no other is selected) -->
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Self}, Path=SelectedItem}" Value="{x:Null}">
<Setter Property="SelectedIndex" Value="0"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
我添加的触发器将检查当前SelectedItem
是否是null
,如果是,则将其设置SelectedIndex
为 0。这有效!当我运行应用程序时,ComboBox
默认情况下每个项目都有一个选项!但是等等,还有更多:如果一个项目随后从Fields
列表中删除并在任何时候添加回来,ComboBox
则没有再次选择项目。基本上,发生的事情是,当第一次创建该字段时,数据触发器选择操作符列表中的第一项并将其设置为该字段的SelectedItem
. 当该字段被删除然后重新添加时,原来的 DataTrigger 不再起作用SelectedItem
。null
奇怪的是,尽管 SelectedItem 属性显然存在绑定,但当前选定的项目并未被选中。
总结:在ComboBox
DataTemplate 中使用 a 时,SelectedItem
forComboBox
不使用其绑定属性作为默认值。
我试过的:
DataTrigger
SelectedItem
为 null 时选择列表中的第一项。
结果:创建字段时正确选择了项目;当字段从显示中删除然后重新添加时丢失项目。与 1 相同,加上一个 DataTrigger for when
SelectedItem
is not null 以重新选择列表中的第一项。
结果:与#1 结果相同 + 当字段从显示中删除然后添加回来时,正确选择列表中的第一项;如果使用已创建的项目重新创建整个Fields
列表本身FormField
,则所选项目再次为空。此外,最好预先选择先前选择的运算符(虽然不是必需的)。用于
SelectedIndex
代替SelectedItem
, 带 - 和不带 - DataTriggers(如 #1 和 #2 中所示)。
结果:在这两种情况下都没有成功选择默认项目,几乎就像SelectedIndex
在ItemsSource
.使用 DataTrigger 检查
Items.Count
属性;如果它大于零,则将 设置SelectedItem
为列表中的第一个元素。
结果:没有成功选择项目。与 4 相同,但使用
SelectedIndex
而不是SelectedItem
.
结果:与 #1 结果相同IsSynchronizedWithCurrentItem
与True
和False
值一起使用。
结果:未选择任何内容。将 XAML 属性重新排序以放置
SelectedItem
(并且SelectedIndex
在使用时)放在ItemsSource
. 这是为每个测试完成的,因为我在网上看到它有帮助。
结果:没有帮助。为该
Operators
属性尝试了不同类型的集合。我用过List
,IEnumerable
,ICollectionView
, 目前正在使用ObservableCollection
.
结果:所有都提供了相同的输出,除了IEnumerable
- 它在字段被删除/重新添加后丢失了值。
任何帮助将不胜感激。