2

MVVM パターンに厳密に準拠しながら、要素内ComboBoxにあり、常に default を持つWPF を取得するにはどうすればよいですか?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 ***

**実際のフィールドの「タイプ」とそこに含まれる実装は、表示の問題とは関係がないため、この質問には含まれていません。

上記の疑似実装に基づいて、フォームを生成するために必要な主要なクラスを次に示します。

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 内で使用されている場合、 はそのバインドされたプロパティをデフォルト値として使用していませんSelectedItemComboBox

私が試したこと:

  1. SelectedItemリスト内の最初の項目を選択するためのDataTriggerが null の場合。
    結果: フィールドの作成時にアイテムが正しく選択されます。フィールドが表示から削除されてから再度追加されると、項目が失われます。

  2. 1 と同じですSelectedItemが、リスト内の最初の項目を再選択するための DataTrigger が null でない場合。
    結果: #1 結果と同じ + フィールドが表示から削除されてから再度追加されたときに、リストの最初の項目が正しく選択されます。Fields既に作成された項目を使用してリスト全体が再作成さFormFieldれると、選択された項目は再び空になります。また、以前に選択したオペレーターを事前に選択しておくと便利です (必須ではありません)。

  3. SelectedIndexの代わりにSelectedItem、DataTriggers の有無にかかわらず使用されます (#1 と #2 のように)。
    結果: どちらの場合も、が のSelectedIndex前に読み取られたかのように、既定のアイテムを正常に選択できませんでしたItemsSource

  4. DataTrigger を使用してプロパティを確認しましたItems.Count。ゼロより大きい場合はSelectedItem、リストの最初の要素に設定します。
    結果: アイテムを正常に選択できませんでした。

  5. 4 と同じですが、SelectedIndex代わりに を使用しSelectedItemます。
    結果: #1の結果と同じ

  6. および値IsSynchronizedWithCurrentItemの両方で使用されます。 結果: 何も選択されていません。TrueFalse

  7. XAML プロパティの順序を変更してSelectedItem(およびSelectedIndex、使用する場合は ) を の前に配置しItemsSourceました。オンラインで役立つと読んだので、これはすべてのテストで行われました。
    結果:役に立ちません。

  8. プロパティのさまざまなタイプのコレクションを試しましたOperatorsListIEnumerable、を使用しておりICollectionView、現在 を使用してObservableCollectionいます。
    結果:IEnumerableフィールドが削除/再追加された後に値が失われたことを除いて、すべて同じ出力が提供されました。

どんな助けでも大歓迎です。

4

3 に答える 3

1

アプリケーションを再構築したところ、上記の問題はなくなりましたが、それを解決するための解決策も見つけました。

手順:

  1. Will のコメントからヒントを得て、 の分離コードを更新して、 にコールバックをForm追加しました。PropertyMetadataFieldsProperty

  2. #1 からのコールバックは、フィールドのリスト全体を反復し、現在のフィールドをフィールドのリストの最初の演算子に設定する優先度レベルDispatcher.BeginInvoke()でDelegate-Action を呼び出すために使用します。InputSelectedOperatorOperators

    • .BeginInvoke()またはその他の優先度の低いものを使用しないと、更新は GUI によって生成される前にフィールドにアクセスしようとし、失敗します。
  3. 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れ、がすぐに に設定されます。これを for セッターで(および/を試すことによって) 防止しても役に立ちません。ComboBoxSelectedIndex-1FormField.SelectedOperatorSelectedItemSelectedIndex

FormField代わりに、 namedで 2 番目の「プレースホルダー」プロパティを作成し、LastOperatorそれをSelectedOperatorwhen the setter is passed に設定してから、行をnull更新するとうまくいくようです。field.Operator =Form.cs

FormField.cs

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

public Operator LastOperator { get; set; }

Form.cs

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

DataTemplate 内でデータバインドされた SelectedItem で ComboBoxes を使用するのは難しい..私はこれを次のように解決しました: SelectedItem を使用する代わりに、(TwoWay) SelectedValueのみを (カスタム タイプ プロパティ - SelectedOperator に) バインドし、 DisplayMemberPathを設定します(ただし、SelectedValuePath ではありません - カスタム全体を持つようにします)。型インスタンスを値として)

于 2015-02-02T14:19:50.077 に答える
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 と互換性がある必要があります。お役に立てれば。

于 2012-08-12T17:10:17.747 に答える