MVVM パターンに厳密に準拠しながら、要素内ComboBox
にあり、常に default を持つWPF を取得するにはどうすればよいですか?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 ***
**実際のフィールドの「タイプ」とそこに含まれる実装は、表示の問題とは関係がないため、この質問には含まれていません。
上記の疑似実装に基づいて、フォームを生成するために必要な主要なクラスを次に示します。
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" }
}
}
};
}
}
フォーム.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
が流れているかどうかを確認し、そうであれば、を 0null
に設定します。これは機能します。SelectedIndex
アプリケーションを実行すると、それぞれComboBox
にデフォルトで選択された項目があります! しかし、待ってください。アイテムがFields
リストから削除され、いつでも再び追加された場合、アイテムはComboBox
再び選択されません。基本的に、フィールドが初めて作成されると、データトリガーは演算子リストの最初の項目を選択し、それをフィールドの として設定しますSelectedItem
。フィールドが削除されてから再度追加されると、元の DataTrigger が機能しなくSelectedItem
なります。null
奇妙なことに、明らかに SelectedItem プロパティのバインディングがあるにもかかわらず、現在選択されている項目が選択されていません。
要約:ComboBox
が DataTemplate 内で使用されている場合、 はそのバインドされたプロパティをデフォルト値として使用していませんSelectedItem
。ComboBox
私が試したこと:
SelectedItem
リスト内の最初の項目を選択するためのDataTriggerが null の場合。
結果: フィールドの作成時にアイテムが正しく選択されます。フィールドが表示から削除されてから再度追加されると、項目が失われます。1 と同じです
SelectedItem
が、リスト内の最初の項目を再選択するための DataTrigger が 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
フィールドが削除/再追加された後に値が失われたことを除いて、すべて同じ出力が提供されました。
どんな助けでも大歓迎です。