如何在ComboBox
元素内的DataTemplate
内获得并且始终拥有默认ItemsControl
的WPF SelectedItem
,同时严格遵守到MVVM模式?
我的目标是定义一个“表单字段”列表,然后通过模板将其翻译成实际的表单字段(即 - 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 ***
**实际字段的“类型”及其中包含的实现不包含在此问题中,因为它与显示问题无关。
以下是生成表单所需的主要类,基于上面的伪实现:
FormViewModel.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" }
}
}
};
}
}
Form.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>
Form.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;
}
}
DataTemplates.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>
FormField.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));
}
}
Operator.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
。删除该字段然后重新添加后,SelectedItem
不再是null
,因此原始DataTrigger不起作用。奇怪的是,即使明确存在SelectedItem属性的绑定,也不会选择当前选定的项目。
总结:在DataTemplate中使用ComboBox
时,SelectedItem
的{{1}}未使用其绑定属性作为默认值。
我尝试了什么:
DataTrigger选择列表中的第一项 结果:在创建字段时正确选择项目;当字段从显示中移除然后重新添加时丢失该项目。
与1相同,加上一个DataTrigger,当ComboBox
不为空时重新选择列表中的第一项。
结果:与#1结果相同+当从显示中删除字段然后再添加时,正确选择列表中的第一个项目;如果使用已创建的SelectedItem
项重新创建整个SelectedItem
列表本身,则所选项目将再次为空。此外,预先选择之前选择的运算符会很好(尽管不是要求)。
使用Fields
代替FormField
,使用 - 而不使用 - DataTriggers(如#1和#2)。
结果:在两种情况下均未成功选择默认项目,几乎就像SelectedIndex
之前正在阅读SelectedItem
一样。
使用DataTrigger检查SelectedIndex
属性;如果它大于零,则将ItemsSource
设置为列表中的第一个元素
结果:未成功选择项目。
与4相同,但使用Items.Count
代替SelectedItem
结果:与#1结果相同
SelectedIndex
使用了SelectedItem
和IsSynchronizedWithCurrentItem
值
结果:未选择任何内容。
重新排序XAML属性,使True
(以及False
在使用时)位于SelectedItem
之前。这是为每次测试完成的,因为我在网上看到它有帮助
结果:没有帮助。
尝试了SelectedIndex
属性的不同类型的集合。我使用过ItemsSource
,Operators
,List
,目前正在使用IEnumerable
。
结果:除了ICollectionView
之外,所有提供了相同的输出 - 在删除/重新添加字段后它丢失了值。
非常感谢任何帮助。
答案 0 :(得分:1)
虽然我重新构建了我的应用程序并且上述问题已不复存在,但我也找到了解决问题的解决方案!
步骤:
通过Will的评论提示,我更新了Form
的代码隐藏,为FieldsProperty
添加PropertyMetadata
回调。
来自#1的回调遍历整个字段列表,并使用Dispatcher.BeginInvoke()
调用Input
-priority level上的委托操作,该操作将当前字段的SelectedOperator
设置为字段Operators
列表中的第一个运算符。
.BeginInvoke()
或任何其他较低优先级的情况下,更新将尝试在GUI生成之前访问该字段,并且会失败。从DataTriggers
中的Operators
ComboBox
移除DataTemplate
(现在,它与DataTemplates.xaml
的第一个代码示例相同在我的问题中)。
新的工作代码(仅限更新):
Form.cs
...
public static readonly DependencyProperty FieldsProperty =
DependencyProperty.RegisterAttached("Fields", typeof(ObservableCollection<FormField>), typeof(Form), new PropertyMetadata(_fieldsListUpdated));
...
// PropertyMetaData-callback for when the FieldsProperty is updated
protected static void _fieldsListUpdated(DependencyObject sender, DependencyPropertyChangedEventArgs args) {
foreach (FormField field in ((Form)sender).Fields) {
// check to see if the current field has valid operators
if ((field.Operators != null) && (field.Operators.Count > 0)) {
Dispatcher.CurrentDispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input, (Action)(() => {
// set the current field's SelectedOperator to the first in the list
field.SelectedOperator = field.Operators[0];
}));
}
}
}
对上述内容的一点警告是SelectedOperator
将始终设置为列表中的第一个。对我来说,这不是一个问题 - 但我可以看到一个案例,其中“最后选择的运算符”将要重新选择。
调试后,当Field
被添加回Fields
列表时,它仍会保留之前的SelectedItem
值 - 然后是ComboBox
的{{1}} 1}}立即设置为SelectedIndex
。在-1
(并尝试FormField.SelectedOperator
/ SelectedItem
)的设置器中阻止此操作无济于事。
相反,在名为SelectedIndex
的{{1}}中创建第二个“占位符”属性,并在设置者传递FormField
时将其设置为LastOperator
,然后更新{ SelectedOperator
中的{1}}行似乎有效:
FormField.cs
null
Form.cs
field.Operator =
答案 1 :(得分:0)
尝试以下方法:
FormField.cs
protected ObservableCollection<Operator> _operators;
public ObservableCollection<Operator> Operators {
get { return _operators; }
set {
_operators = value;
_onPropertyChanged("Operators");
}
}
private QuestionOption _selectedItem;
public QuestionOption SelectedItem
{
get
{
return _selectedItem;
}
set
{
if (_selectedItem != value)
{
if (SelectedIndex == -1)
SelectedIndex = Operators.IndexOf(value);
_selectedItem = value;
_onPropertyChanged("SelectedItem");
}
}
}
private int _selectedIndex = -1;
public int SelectedIndex
{
get { return _selectedIndex; }
set
{
if (_selectedIndex != value)
{
_selectedIndex = value;
_onPropertyChanged("SelectedIndex");
}
}
}
DataTemplate.xaml
<ComboBox Width="Auto"
ItemsSource="{Binding Operators}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}"
SelectedIndex="{Binding SelectedIndex, Mode=TwoWay}"
DisplayMemberPath="Label" SelectedValuePath="Id">
至于确保对Fields的更改触发PropertyChanged事件,请尝试以下方法强制触发事件:
// Set the changes to the modifiedFormField placeholder
ObservableCollection<FormField> modifiedFormField;
this.Fields = new ObservableCollection<FormField>(modifiedFormField);
我在使用MVVM Silverlight 5应用程序时遇到了类似的问题,并做了类似的事情以使绑定工作。这些概念应该可以与WPF互换。希望这会有所帮助。
答案 2 :(得分:0)
使用带有数据绑定SelectedItem的ComboBox,在DataTemplate中很棘手.. 我通过以下方法解决了这个问题:而不是使用SelectedItem,(TwoWay)仅绑定 SelectedValue (到您的自定义类型属性 - SelectedOperator)并设置 DisplayMemberPath (但不是SelectedValuePath - 以使其具有整个自定义类型实例作为值)