web-dev-qa-db-ja.com

WPF MVVM TreeView SelectedItem

これは難しいことではありません。 WPFのTreeViewでは、プロパティが読み取り専用であることを示すSelectedItemを設定できません。 TreeViewにデータを追加し、データバインドされたコレクションが変更されたときに更新します。

どのアイテムが選択されているかを知る必要があります。 MVVMを使用しているため、ツリービューを参照するための分離コードや変数はありません。 これが唯一の解決策です 私は発見しましたが、明らかなハックです.ElementNameバインディングを使用してツリービューの選択されたアイテムに自分自身を設定する別の要素をXAMLで作成し、Viewmodelもバインドする必要があります。 複数 その他 質問 これについて尋ねられますが、他の有効なソリューションは提供されていません。

この質問 を見ましたが、与えられた答えを使用するとコンパイルエラーが発生します。何らかの理由でブレンドsdk System.Windows.Interactivityへの参照をプロジェクトに追加できません。 「不明なエラーsystem.windowsがプリロードされていません」と表示されますが、それをどのように乗り越えるかはまだわかりません。

ボーナスポイントの場合:なぜマイクロソフトはこの要素のSelectedItemプロパティをReadOnlyにしたのですか?

30
Kyeotic

SelectedItemプロパティを直接処理する必要はありません。 IsSelected をビューモデルのプロパティにバインドし、そこで選択されたアイテムを追跡します。

スケッチ:

<TreeView ItemsSource="{Binding TreeData}">
    <TreeView.ItemContainerStyle>
        <Style TargetType="{x:Type TreeViewItem}">
            <Setter Property="IsSelected" Value="{Binding IsSelected}" />
        </Style>
    </TreeView.ItemContainerStyle>
</TreeView>
public class TViewModel : INotifyPropertyChanged
{
    private static object _selectedItem = null;
    // This is public get-only here but you could implement a public setter which
    // also selects the item.
    // Also this should be moved to an instance property on a VM for the whole tree, 
    // otherwise there will be conflicts for more than one tree.
    public static object SelectedItem
    {
        get { return _selectedItem; }
        private set
        {
            if (_selectedItem != value)
            {
                _selectedItem = value;
                OnSelectedItemChanged();
            }
        }
    }

    static virtual void OnSelectedItemChanged()
    {
        // Raise event / do other things
    }

    private bool _isSelected;
    public bool IsSelected
    {
        get { return _isSelected; }
        set
        {
            if (_isSelected != value)
            {
                _isSelected = value;
                OnPropertyChanged("IsSelected");
                if (_isSelected)
                {
                    SelectedItem = this;
                }
            }
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(string propertyName)
    {
        var handler = this.PropertyChanged;
        if (handler != null)
            handler(this, new PropertyChangedEventArgs(propertyName));
    }
}
47
H.B.

バインド可能で、ゲッターとセッターを持つ添付プロパティを作成できます。

public class TreeViewHelper
{
    private static Dictionary<DependencyObject, TreeViewSelectedItemBehavior> behaviors = new Dictionary<DependencyObject, TreeViewSelectedItemBehavior>();

    public static object GetSelectedItem(DependencyObject obj)
    {
        return (object)obj.GetValue(SelectedItemProperty);
    }

    public static void SetSelectedItem(DependencyObject obj, object value)
    {
        obj.SetValue(SelectedItemProperty, value);
    }

    // Using a DependencyProperty as the backing store for SelectedItem.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.RegisterAttached("SelectedItem", typeof(object), typeof(TreeViewHelper), new UIPropertyMetadata(null, SelectedItemChanged));

    private static void SelectedItemChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
    {
        if (!(obj is TreeView))
            return;

        if (!behaviors.ContainsKey(obj))
            behaviors.Add(obj, new TreeViewSelectedItemBehavior(obj as TreeView));

        TreeViewSelectedItemBehavior view = behaviors[obj];
        view.ChangeSelectedItem(e.NewValue);
    }

    private class TreeViewSelectedItemBehavior
    {
        TreeView view;
        public TreeViewSelectedItemBehavior(TreeView view)
        {
            this.view = view;
            view.SelectedItemChanged += (sender, e) => SetSelectedItem(view, e.NewValue);
        }

        internal void ChangeSelectedItem(object p)
        {
            TreeViewItem item = (TreeViewItem)view.ItemContainerGenerator.ContainerFromItem(p);
            item.IsSelected = true;
        }
    }
}

そのクラスを含む名前空間宣言をXAMLに追加し、次のようにバインドします(ローカルは名前空間宣言の命名方法です)。

<TreeView ItemsSource="{Binding Path=Root.Children}"
          local:TreeViewHelper.SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}"/>

これで、選択したアイテムをバインドできます。また、ビューモデルに設定して、要件が発生した場合にプログラムで変更することもできます。もちろん、これは、その特定のプロパティにINotifyPropertyChangedを実装することを前提としています。

12
Bas

MVVMで許容可能な方法でこれを解決する非常に珍しいが非常に効果的な方法は次のとおりです。

  1. TreeViewと同じビューに可視性が折りたたまれたContentControlを作成します。適切な名前を付け、そのコンテンツをビューモデルのSelectedSomethingプロパティにバインドします。このContentControlは、選択されたオブジェクトを「保持」し、そのバインディングであるOneWayToSourceを処理します。
  2. TreeViewでSelectedItemChangedをリッスンし、コードビハインドでハンドラーを追加して、ContentControl.Contentを新しく選択した項目に設定します。

XAML:

<ContentControl x:Name="SelectedItemHelper" Content="{Binding SelectedObject, Mode=OneWayToSource}" Visibility="Collapsed"/>
<TreeView ItemsSource="{Binding SomeCollection}"
    SelectedItemChanged="TreeView_SelectedItemChanged">

コードビハインド:

    private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        SelectedItemHelper.Content = e.NewValue;
    }

ViewModel:

    public object SelectedObject  // Class is not actually "object"
    {
        get { return _selected_object; }
        set
        {
            _selected_object = value;
            RaisePropertyChanged(() => SelectedObject);
            Console.WriteLine(SelectedObject);
        }
    }
    object _selected_object;
6
heltonbiker

OneWayToSource バインディングモードを使用します。 これは機能しません。編集を参照してください。

Editこの質問 ;によれば、これはMicrosoftのバグまたは「設計による」動作のようです。ただし、いくつかの回避策が投稿されています。それらのいずれかがあなたのTreeViewで動作しますか?

Microsoft Connectの問題: https://connect.Microsoft.com/WPF/feedback/details/523865/read-only-dependency-properties-does-not-support-onewaytosource-bindings

Microsoftによる2010年1月10日午後2時46分

DependencyPropertiesではないプロパティのバインドをサポートできないのと同じ理由で、今日WPFでこれを行うことはできません。バインディングのインスタンスごとのランタイム状態はBindingExpressionに保持され、ターゲットDependencyObjectのEffectiveValueTableに格納されます。ターゲットプロパティがDPでない場合、またはDPが読み取り専用の場合、BindingExpressionを格納する場所はありません。

バインド機能をこれら2つのシナリオに拡張することをいつか選択する可能性があります。それらについてかなり頻繁に尋ねられます。言い換えれば、あなたの要求は、将来のリリースで検討する機能のリストに既にあります。

ご意見をいただきありがとうございます。

4
Aphex

コードビハインドとビューモデルコードの組み合わせを使用することにしました。 xamlは次のようになります。

<TreeView 
                    Name="tvCountries"
                ItemsSource="{Binding Path=Countries}"
                ItemTemplate="{StaticResource ResourceKey=countryTemplate}"   
                    SelectedValuePath="Name"
                    SelectedItemChanged="tvCountries_SelectedItemChanged">

コードビハインド

private void tvCountries_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        var vm = this.FindResource("vm") as ViewModels.CoiEditorViewModel;
        if (vm != null)
        {
            var treeItem = sender as TreeView;
            vm.TreeItemSelected = treeItem.SelectedItem;
        }
    }

また、ビューモデルにはTreeItemSelectedオブジェクトがあり、ビューモデルでアクセスできます。

2
allan

ICommandを使用するDependencyPropertyをいつでも作成し、TreeViewのSelectedItemChangedイベントをリッスンできます。これは、IsSelectedをバインドするよりも少し簡単ですが、他の理由でIsSelectedをバインドすることになります。 IsSelectedにバインドするだけの場合、IsSelectedが変更されるたびにアイテムにメッセージを送信させることができます。その後、プログラムのどこからでもこれらのメッセージを聞くことができます。

1
stricq