2

如何在严格遵守 MVVM 模式的同时,获得一个位于元素ComboBox内的 WPF并始终拥有一个 default ?DataTemplateItemsControlSelectedItem

我的目标是定义一个“表单域”列表,然后通过模板将其翻译成实际的表单域(即 - TextBoxComboBoxDatePicker等)。字段列表是 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=0ComboBox; 这没有用。经过反复试验,我选择使用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 不再起作用SelectedItemnull奇怪的是,尽管 SelectedItem 属性显然存在绑定,但当前选定的项目并未被选中。

总结:在ComboBoxDataTemplate 中使用 a 时,SelectedItemforComboBox不使用其绑定属性作为默认值。

我试过的:

  1. DataTriggerSelectedItem为 null 时选择列表中的第一项。
    结果:创建字段时正确选择了项目;当字段从显示中删除然后重新添加时丢失项目。

  2. 与 1 相同,加上一个 DataTrigger for when SelectedItemis not null 以重新选择列表中的第一项。
    结果:与#1 结果相同 + 当字段从显示中删除然后添加回来时,正确选择列表中的第一项;如果使用已创建的项目重新创建整个Fields列表本身FormField,则所选项目再次为空。此外,最好预先选择先前选择的运算符(虽然不是必需的)。

  3. 用于SelectedIndex代替SelectedItem, 带 - 和不带 - DataTriggers(如 #1 和 #2 中所示)。
    结果:在这两种情况下都没有成功选择默认项目,几乎就像SelectedIndexItemsSource.

  4. 使用 DataTrigger 检查Items.Count属性;如果它大于零,则将 设置SelectedItem为列表中的第一个元素。
    结果:没有成功选择项目。

  5. 与 4 相同,但使用SelectedIndex而不是SelectedItem.
    结果:与 #1 结果相同

  6. IsSynchronizedWithCurrentItemTrueFalse值一起使用。
    结果:未选择任何内容。

  7. 将 XAML 属性重新排序以放置SelectedItem(并且SelectedIndex在使用时)放在ItemsSource. 这是为每个测试完成的,因为我在网上看到它有帮助。
    结果:没有帮助。

  8. 为该Operators属性尝试了不同类型的集合。我用过List, IEnumerable, ICollectionView, 目前正在使用ObservableCollection.
    结果:所有都提供了相同的输出,除了IEnumerable- 它在字段被删除/重新添加后丢失了值。

任何帮助将不胜感激。

4

3 回答 3

1

虽然我重组了我的应用程序并且上述问题不再存在,但我也找到了解决它的解决方案!

步骤:

  1. 从 Will 的评论中得到提示,我更新了Form's 代码隐藏以PropertyMetadataFieldsProperty.

  2. 来自 #1 的回调遍历整个字段列表,并用于在-priority 级别Dispatcher.BeginInvoke()上调用 Delegate-Action ,它将当前字段设置为字段列表中的第一个运算符。InputSelectedOperatorOperators

    • 如果不使用.BeginInvoke()或任何其他较低优先级,更新将在 GUI 生成之前尝试访问该字段并且会失败。
  3. DataTriggers从中删除了Operators ComboBoxDataTemplate现在,它与DataTemplates.xaml我的问题中的第一个代码示例相同)。

新的工作代码(仅限更新):

表格.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'sSelectedIndex设置为-1。在设置器中防止这种情况FormField.SelectedOperator(并通过尝试SelectedItem/ SelectedIndex)没有帮助。

FormField相反,在命名中创建第二个“占位符”属性LastOperator并将其设置为SelectedOperator当 setter 被传递null时,然后更新field.Operator =Form.cs似乎有效:

表单域.cs

...
public Operator SelectedOperator {
    get { return _selectedOperator; }
    set {
        if (value == null) LastOperator = _selectedOperator;
        _selectedOperator = value; _onPropertyChanged("SelectedOperator");
    }
}

public Operator LastOperator { get; set; }

表格.cs

...
field.SelectedOperator = ((field.LastOperator != null) ? field.LastOperator : field.Operators[0]);
...
于 2012-08-21T16:51:10.863 回答
0

尝试以下操作:

表单域.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");
            }
        }
    }

数据模板.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 互换。希望这可以帮助。

于 2012-08-12T17:10:17.747 回答
0

在 DataTemplate 内部使用带有数据绑定 SelectedItem 的 ComboBoxes 很棘手。我通过以下方式解决了这个问题:而不是使用 SelectedItem,(TwoWay)仅绑定SelectedValue(到您的自定义类型属性 - SelectedOperator)并设置DisplayMemberPath(但不是 SelectedValuePath - 拥有整个自定义类型实例作为值)

于 2015-02-02T14:19:50.077 回答