WPFツリービューのSelectedItemへのデータバインディング


240

WPFツリービューで選択されているアイテムを取得するにはどうすればよいですか?バインドしたいので、これをXAMLで行いたいと思います。

存在するとは思われるかもしれませんがSelectedItem、明らかに存在しないのは読み取り専用であるため、使用できません

これは私がしたいことです:

<TreeView ItemsSource="{Binding Path=Model.Clusters}" 
            ItemTemplate="{StaticResource ClusterTemplate}"
            SelectedItem="{Binding Path=Model.SelectedCluster}" />

SelectedItemモデルのプロパティにバインドします。

しかし、これは私にエラーを与えます:

'SelectedItem'プロパティは読み取り専用であり、マークアップから設定することはできません。

編集: OK、これは私がこれを解決した方法です:

<TreeView
          ItemsSource="{Binding Path=Model.Clusters}" 
          ItemTemplate="{StaticResource HoofdCLusterTemplate}"
          SelectedItemChanged="TreeView_OnSelectedItemChanged" />

そして私のxamlのcodebehindfileで:

private void TreeView_OnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
    Model.SelectedCluster = (Cluster)e.NewValue;
}

50
これは最悪だ。それも私を襲った。きちんとした方法があると私はここに来て、私はばかです。これは..私は、私は馬鹿ではないよという悲しいのは初めてのことである
アンドレイRînea

6
これは本当に拘束力のある概念を吸収し、混乱させます
Delta

希望これは、ツリービュー項目にバインドするためにいくつかのいずれかを助けることができるのICommandの上で変更コールバックを選択jacobaloysious.wordpress.com/2012/02/19/...
ジェイコブaloysious

9
バインディングとMVVMに関しては、分離コードは「禁止」されていません。むしろ、分離コードはビューをサポートする必要があります。私が見た他のすべてのソリューションからの私の意見では、背後のコードは、ビューをビューモデルに「バインド」することを扱っているため、非常に優れたオプションです。唯一の欠点は、XAMLでのみ機能する設計者がいるチームがある場合、背後のコードが破損または無視される可能性があることです。ソリューションの実装に10秒かかる費用はわずかです。
nrjohnstone 2014

おそらく最も簡単な解決策の一つ:stackoverflow.com/questions/1238304/...
JoanComasFdz

回答:


240

これはすでに回答が受け入れられていることに気づきましたが、これをまとめて問題を解決しました。Deltaのソリューションと同様のアイデアを使用しますが、TreeViewをサブクラス化する必要はありません。

public class BindableSelectedItemBehavior : Behavior<TreeView>
{
    #region SelectedItem Property

    public object SelectedItem
    {
        get { return (object)GetValue(SelectedItemProperty); }
        set { SetValue(SelectedItemProperty, value); }
    }

    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register("SelectedItem", typeof(object), typeof(BindableSelectedItemBehavior), new UIPropertyMetadata(null, OnSelectedItemChanged));

    private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var item = e.NewValue as TreeViewItem;
        if (item != null)
        {
            item.SetValue(TreeViewItem.IsSelectedProperty, true);
        }
    }

    #endregion

    protected override void OnAttached()
    {
        base.OnAttached();

        this.AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();

        if (this.AssociatedObject != null)
        {
            this.AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
        }
    }

    private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        this.SelectedItem = e.NewValue;
    }
}

次に、これをXAMLで次のように使用できます。

<TreeView>
    <e:Interaction.Behaviors>
        <behaviours:BindableSelectedItemBehavior SelectedItem="{Binding SelectedItem, Mode=TwoWay}" />
    </e:Interaction.Behaviors>
</TreeView>

うまくいけば、それは誰かを助けるでしょう!


5
ブレントが指摘したように、バインディングにMode = TwoWayを追加する必要もありました。私は「ブレンダー」ではないので、System.Windows.InteractivityのBehavior <>クラスに慣れていません。アセンブリは、Expression Blendの一部です。試用版を購入/インストールしてこのアセンブリを入手したくない場合は、System.Windows.Interactivityを含むBlendSDKをダウンロードできます。BlendSDK 3 for 3.5 ... 4.0のBlendSDK 4だと思います。注:これは、選択されているアイテムを取得することのみを可能にし、選択されたアイテムを設定することを許可しません
Mike Rowley

4
UIPropertyMetadataをFrameworkPropertyMetadata(null、FrameworkPropertyMetadataOptions.BindsTwoWayByDefault、OnSelectedItemChanged));で置き換えることもできます。
Filimindji 2011

3
これは問題を解決するためのアプローチになります:stackoverflow.com/a/18700099/4227
bitbonk

2
上記のXAMLコードスニペットに示されているとおりの@Lukas。次のものに置き換え{Binding SelectedItem, Mode=TwoWay}てください{Binding MyViewModelField, Mode=TwoWay}
Steve Greatrex

4
@Pascal it'sxmlns:e="http://schemas.microsoft.com/expression/2010/interactivity"
Steve Greatrex、

46

このプロパティは存在します:TreeView.SelectedItem

しかし、それは読み取り専用であるため、バインディングを介して割り当てることはできず、取得するだけです。


私はこの回答を受け入れます。これは、このリンクを見つけたためです。これにより、独自の回答が得られます。msdn.microsoft.com
us

1
TreeView.SelectedItemユーザーがアイテム(別名OneWayToSource)を選択したときに、モデルのプロパティに影響を与えることができますか?
Shimmy Weitzhandler 2017年

43

必要に応じて、添付プロパティと外部依存関係なしで回答してください!

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

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}">

    </TreeView>

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


4
+1、このスレッドimhoのベストアンサー。System.Windows.Interactivityに依存せず、双方向バインディング(MVVM環境でプログラムによって設定)を許可します。完璧です。
クリス・レイ

5
このアプローチの問題は、バインディングによって(つまり、ViewModelから)選択された項目が1度設定されると、動作が機能し始めることだけです。VMの初期値がnullの場合、バインディングはDP値を更新せず、動作はアクティブ化されません。別のデフォルトで選択された項目(無効な項目など)を使用してこれを修正できます。
マーク

6
@Mark:添付プロパティのUIPropertyMetadataをインスタンス化するときに、上記のnullの代わりにnew object()を使用するだけです。問題は
barnacleboy

2
データ型によってリソースから適用されたHierarchicalDataTemplateを使用しているため、TreeViewItemへのキャストは失敗します。ただし、ChangeSelectedItemを削除すると、ビューモデルにバインドしてアイテムを取得することは正常に機能します。
Casey Sebben、2015年

1
TreeViewItemへのキャストにも問題があります。その時点では、ItemContainerGeneratorにはルートアイテムへの参照しか含まれていませんが、非ルートアイテムも取得できるようにする必要があります。参照を渡した場合、キャストは失敗し、nullが返されます。これをどのように修正できるかわかりませんか?
Bob Tway 2015年

39

さて、私は解決策を見つけました。それは混乱を移動させ、MVVMが機能するようにします。

最初にこのクラスを追加します。

public class ExtendedTreeView : TreeView
{
    public ExtendedTreeView()
        : base()
    {
        this.SelectedItemChanged += new RoutedPropertyChangedEventHandler<object>(___ICH);
    }

    void ___ICH(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        if (SelectedItem != null)
        {
            SetValue(SelectedItem_Property, SelectedItem);
        }
    }

    public object SelectedItem_
    {
        get { return (object)GetValue(SelectedItem_Property); }
        set { SetValue(SelectedItem_Property, value); }
    }
    public static readonly DependencyProperty SelectedItem_Property = DependencyProperty.Register("SelectedItem_", typeof(object), typeof(ExtendedTreeView), new UIPropertyMetadata(null));
}

これをxamlに追加します。

 <local:ExtendedTreeView ItemsSource="{Binding Items}" SelectedItem_="{Binding Item, Mode=TwoWay}">
 .....
 </local:ExtendedTreeView>

3
これは、これまでのところ私のために働くようになった唯一のものです。私はこのソリューションが本当に好きです。
レイチェル2013年

1
ツリーの外側から選択した項目を変更する- 。私はその逆ツリーから選択した項目を取得することに成功しなく:(なぜ知らないが、それは私のために仕事をしませんでしたか
エレズ

依存関係プロパティをBindsTwoWayByDefaultとして設定する方が少しすっきりしているので、XAMLでTwoWayを指定する必要はありません
Stephen Holt

これが最良のアプローチです。対話性参照を使用せず、コードビハインドを使用せず、一部の動作のようにメモリリークが発生しません。ありがとうございました。
Alexandru Dicu

前述のように、このソリューションは双方向のバインディングでは機能しません。viewmodelに値を設定した場合、変更はTreeViewに反映されません。
Richard Moore

25

それはOPが期待するよりも少し多く答えます...しかし、私はそれが少なくとも誰かを助けることができると思います。

変更があったICommandときにいつでも実行したい場合SelectedItemは、イベントにコマンドをバインドSelectedItemし、ViewModelもう必要ありません。

そうするために:

1-への参照を追加 System.Windows.Interactivity

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

2-コマンドをイベントにバインドする SelectedItemChanged

<TreeView x:Name="myTreeView" Margin="1"
            ItemsSource="{Binding Directories}">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="SelectedItemChanged">
            <i:InvokeCommandAction Command="{Binding SomeCommand}"
                                   CommandParameter="
                                            {Binding ElementName=myTreeView
                                             ,Path=SelectedItem}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
    <TreeView.ItemTemplate>
           <!-- ... -->
    </TreeView.ItemTemplate>
</TreeView>

3
リファレンスSystem.Windows.InteractivityはNuGetからインストールできます:nuget.org/packages/System.Windows.Interactivity.WPF
Junle Li

私はこれを何時間もこの問題を解決しようとしました、私はこれを実装しましたが、私のコマンドは機能していません、私を助けていただけませんか?
Alfie

1
2018年の終わりにMicrosoftによってWPFXAML Behaviorsが導入されました 。これはの代わりに使用できますSystem.Windows.Interactivity。それは私のために働いた(.NET Coreプロジェクトで試した)。設定するには、Microsoft.Xaml.Behaviors.Wpf nugetパッケージを追加し 、名前空間をに変更しますxmlns:i="http://schemas.microsoft.com/xaml/behaviors"。詳細については、ブログ
rychlmoj

19

これは、バインディングとGalaSoft MVVM LightライブラリのEventToCommandのみを使用して、「より良い」方法で実現できます。VMに、選択したアイテムが変更されたときに呼び出されるコマンドを追加し、必要なアクションを実行するためにコマンドを初期化します。この例では、RelayCommandを使用して、SelectedClusterプロパティを設定します。

public class ViewModel
{
    public ViewModel()
    {
        SelectedClusterChanged = new RelayCommand<Cluster>( c => SelectedCluster = c );
    }

    public RelayCommand<Cluster> SelectedClusterChanged { get; private set; } 

    public Cluster SelectedCluster { get; private set; }
}

次に、xamlにEventToCommand動作を追加します。これはblendを使うと本当に簡単です。

<TreeView
      x:Name="lstClusters"
      ItemsSource="{Binding Path=Model.Clusters}" 
      ItemTemplate="{StaticResource HoofdCLusterTemplate}">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="SelectedItemChanged">
            <GalaSoft_MvvmLight_Command:EventToCommand Command="{Binding SelectedClusterChanged}" CommandParameter="{Binding ElementName=lstClusters,Path=SelectedValue}"/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</TreeView>

これは、特にMvvmLightツールキットを既に使用している場合は、優れたソリューションです。ただし、選択されたノードの設定の問題は解決されず、ツリービューで選択を更新します。
keft

12

すべてが複雑に... Caliburn Micro(http://caliburnmicro.codeplex.com/)で行く

見る:

<TreeView Micro:Message.Attach="[Event SelectedItemChanged] = [Action SetSelectedItem($this.SelectedItem)]" />

ViewModel:

public void SetSelectedItem(YourNodeViewModel item) {}; 

5
はい...そしてTreeViewの SelectedItem を設定する部分はどこですか?
mnn 2013年

カリバーンは素敵でエレガントです。ネストされた階層に対して非常に簡単に機能します
Purusartha

8

私は元の著者と同じ答えを探してこのページに出くわしました、そしてそれを行う方法は常に複数あることを証明しました、私のための解決策はこれまでにここで提供された答えよりもさらに簡単だったので、私は追加したいと思った山に。

バインディングの動機は、MVVMを適切に保つことです。ViewModelのおそらくの使用法は、「CurrentThingy」などの名前を持つプロパティを使用することであり、他の場所では、DataContextが「CurrentThingy」にバインドされています。

TreeViewから私のモデルへの、そして何か他のものから私のモデルへの素敵なバインディングをサポートするために必要な追加の手順(例:カスタム動作、サードパーティコントロール)を経由するのではなく、私の解決策は、他のものをバインディングする単純な要素を使用することでした。 TreeView.SelectedItem。他のものをViewModelにバインドするのではなく、必要な追加の作業をスキップします。

XAML:

<TreeView x:Name="myTreeView" ItemsSource="{Binding MyThingyCollection}">
.... stuff
</TreeView>

<!-- then.. somewhere else where I want to see the currently selected TreeView item: -->

<local:MyThingyDetailsView 
       DataContext="{Binding ElementName=myTreeView, Path=SelectedItem}" />

もちろん、これは現在選択されている項目を読み取るのに最適ですが、設定する必要はありません。


1
local:MyThingyDetailsViewとは何ですか?local:MyThingyDetailsViewが選択されたアイテムを保持していることを確認しましたが、ビューモデルはどのようにこの情報を取得しますか?これは、これを行うための素晴らしい、すっきりした方法のように見えますが、もう少し情報が必要です...
ボブホーン

local:MyThingyDetailsViewは、1つの「もの」インスタンスに関する詳細ビューを構成する、XAMLで完全なUserControlです。これは、別のビューの中央にコンテンツとして埋め込まれます。このビューのDataContextは、要素バインディングを使用して、現在選択されているツリービューアイテムです。
ウェス

6

TreeViewItem.IsSelectedプロパティを使用することもできます


これが正解かもしれません。しかし、ItemsのIsSelectedプロパティがTreeViewにどのように渡されるかについての例またはベストプラクティスの推奨を見てみたいと思います。
anhoppe 2016

3

Interaction.Behaviorsを使用せずにXAMLバインド可能なSelectedItemプロパティを作成する方法もあります。

public static class BindableSelectedItemHelper
{
    #region Properties

    public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.RegisterAttached("SelectedItem", typeof(object), typeof(BindableSelectedItemHelper),
        new FrameworkPropertyMetadata(null, OnSelectedItemPropertyChanged));

    public static readonly DependencyProperty AttachProperty = DependencyProperty.RegisterAttached("Attach", typeof(bool), typeof(BindableSelectedItemHelper), new PropertyMetadata(false, Attach));

    private static readonly DependencyProperty IsUpdatingProperty = DependencyProperty.RegisterAttached("IsUpdating", typeof(bool), typeof(BindableSelectedItemHelper));

    #endregion

    #region Implementation

    public static void SetAttach(DependencyObject dp, bool value)
    {
        dp.SetValue(AttachProperty, value);
    }

    public static bool GetAttach(DependencyObject dp)
    {
        return (bool)dp.GetValue(AttachProperty);
    }

    public static string GetSelectedItem(DependencyObject dp)
    {
        return (string)dp.GetValue(SelectedItemProperty);
    }

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

    private static bool GetIsUpdating(DependencyObject dp)
    {
        return (bool)dp.GetValue(IsUpdatingProperty);
    }

    private static void SetIsUpdating(DependencyObject dp, bool value)
    {
        dp.SetValue(IsUpdatingProperty, value);
    }

    private static void Attach(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        TreeListView treeListView = sender as TreeListView;
        if (treeListView != null)
        {
            if ((bool)e.OldValue)
                treeListView.SelectedItemChanged -= SelectedItemChanged;

            if ((bool)e.NewValue)
                treeListView.SelectedItemChanged += SelectedItemChanged;
        }
    }

    private static void OnSelectedItemPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        TreeListView treeListView = sender as TreeListView;
        if (treeListView != null)
        {
            treeListView.SelectedItemChanged -= SelectedItemChanged;

            if (!(bool)GetIsUpdating(treeListView))
            {
                foreach (TreeViewItem item in treeListView.Items)
                {
                    if (item == e.NewValue)
                    {
                        item.IsSelected = true;
                        break;
                    }
                    else
                       item.IsSelected = false;                        
                }
            }

            treeListView.SelectedItemChanged += SelectedItemChanged;
        }
    }

    private static void SelectedItemChanged(object sender, RoutedEventArgs e)
    {
        TreeListView treeListView = sender as TreeListView;
        if (treeListView != null)
        {
            SetIsUpdating(treeListView, true);
            SetSelectedItem(treeListView, treeListView.SelectedItem);
            SetIsUpdating(treeListView, false);
        }
    }
    #endregion
}

次に、これをXAMLで次のように使用できます。

<TreeView  helper:BindableSelectedItemHelper.Attach="True" 
           helper:BindableSelectedItemHelper.SelectedItem="{Binding SelectedItem, Mode=TwoWay}">

3

私はこの質問のすべての解決策を試しました。誰も私の問題を完全に解決しませんでした。そのため、このような継承されたクラスを、SelectedItemプロパティを再定義して使用する方が良いと思います。GUIからツリー要素を選択し、コードでこのプロパティ値を設定すると、完全に機能します

public class TreeViewEx : TreeView
{
    public TreeViewEx()
    {
        this.SelectedItemChanged += new RoutedPropertyChangedEventHandler<object>(TreeViewEx_SelectedItemChanged);
    }

    void TreeViewEx_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        this.SelectedItem = e.NewValue;
    }

    #region SelectedItem

    /// <summary>
    /// Gets or Sets the SelectedItem possible Value of the TreeViewItem object.
    /// </summary>
    public new object SelectedItem
    {
        get { return this.GetValue(TreeViewEx.SelectedItemProperty); }
        set { this.SetValue(TreeViewEx.SelectedItemProperty, value); }
    }

    // Using a DependencyProperty as the backing store for MyProperty.  This enables animation, styling, binding, etc...
    public new static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register("SelectedItem", typeof(object), typeof(TreeViewEx),
        new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, SelectedItemProperty_Changed));

    static void SelectedItemProperty_Changed(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        TreeViewEx targetObject = dependencyObject as TreeViewEx;
        if (targetObject != null)
        {
            TreeViewItem tvi = targetObject.FindItemNode(targetObject.SelectedItem) as TreeViewItem;
            if (tvi != null)
                tvi.IsSelected = true;
        }
    }                                               
    #endregion SelectedItem   

    public TreeViewItem FindItemNode(object item)
    {
        TreeViewItem node = null;
        foreach (object data in this.Items)
        {
            node = this.ItemContainerGenerator.ContainerFromItem(data) as TreeViewItem;
            if (node != null)
            {
                if (data == item)
                    break;
                node = FindItemNodeInChildren(node, item);
                if (node != null)
                    break;
            }
        }
        return node;
    }

    protected TreeViewItem FindItemNodeInChildren(TreeViewItem parent, object item)
    {
        TreeViewItem node = null;
        bool isExpanded = parent.IsExpanded;
        if (!isExpanded) //Can't find child container unless the parent node is Expanded once
        {
            parent.IsExpanded = true;
            parent.UpdateLayout();
        }
        foreach (object data in parent.Items)
        {
            node = parent.ItemContainerGenerator.ContainerFromItem(data) as TreeViewItem;
            if (data == item && node != null)
                break;
            node = FindItemNodeInChildren(node, item);
            if (node != null)
                break;
        }
        if (node == null && parent.IsExpanded != isExpanded)
            parent.IsExpanded = isExpanded;
        if (node != null)
            parent.IsExpanded = true;
        return node;
    }
} 

一部のノードでUpdateLayout()およびIsExpandedが呼び出されない場合は、はるかに高速になります。UpdateLayout()とIsExpandedを呼び出す必要がない場合は?以前にツリーアイテムを訪問したとき。それを知る方法は?ContainerFromItem()は、訪問されていないノードに対してnullを返します。したがって、ContainerFromItem()が子に対してnullを返す場合にのみ、親ノードを展開できます。
CoperNick 2013年

3

私の要件は、TreeViewが必要であり、バインドされたオブジェクトがCollection <>タイプであるため、HierarchicalDataTemplateが必要なPRISM-MVVMベースのソリューションでした。デフォルトのBindableSelectedItemBehaviorは子TreeViewItemを識別できません。このシナリオで機能するようにします。

public class BindableSelectedItemBehavior : Behavior<TreeView>
{
    #region SelectedItem Property

    public object SelectedItem
    {
        get { return (object)GetValue(SelectedItemProperty); }
        set { SetValue(SelectedItemProperty, value); }
    }

    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register("SelectedItem", typeof(object), typeof(BindableSelectedItemBehavior), new UIPropertyMetadata(null, OnSelectedItemChanged));

    private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var behavior = sender as BindableSelectedItemBehavior;
        if (behavior == null) return;
        var tree = behavior.AssociatedObject;
        if (tree == null) return;
        if (e.NewValue == null)
            foreach (var item in tree.Items.OfType<TreeViewItem>())
                item.SetValue(TreeViewItem.IsSelectedProperty, false);
        var treeViewItem = e.NewValue as TreeViewItem;
        if (treeViewItem != null)
            treeViewItem.SetValue(TreeViewItem.IsSelectedProperty, true);
        else
        {
            var itemsHostProperty = tree.GetType().GetProperty("ItemsHost", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
            if (itemsHostProperty == null) return;
            var itemsHost = itemsHostProperty.GetValue(tree, null) as Panel;
            if (itemsHost == null) return;
            foreach (var item in itemsHost.Children.OfType<TreeViewItem>())
            {
                if (WalkTreeViewItem(item, e.NewValue)) 
                    break;
            }
        }
    }

    public static bool WalkTreeViewItem(TreeViewItem treeViewItem, object selectedValue)
    {
        if (treeViewItem.DataContext == selectedValue)
        {
            treeViewItem.SetValue(TreeViewItem.IsSelectedProperty, true);
            treeViewItem.Focus();
            return true;
        }
        var itemsHostProperty = treeViewItem.GetType().GetProperty("ItemsHost", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
        if (itemsHostProperty == null) return false;
        var itemsHost = itemsHostProperty.GetValue(treeViewItem, null) as Panel;
        if (itemsHost == null) return false;
        foreach (var item in itemsHost.Children.OfType<TreeViewItem>())
        {
            if (WalkTreeViewItem(item, selectedValue))
                break;
        }
        return false;
    }
    #endregion

    protected override void OnAttached()
    {
        base.OnAttached();
        this.AssociatedObject.SelectedItemChanged += OnTreeViewSelectedItemChanged;
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();
        if (this.AssociatedObject != null)
        {
            this.AssociatedObject.SelectedItemChanged -= OnTreeViewSelectedItemChanged;
        }
    }

    private void OnTreeViewSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        this.SelectedItem = e.NewValue;
    }
}

これにより、レベルに関係なく、すべての要素を反復処理できます。


ありがとうございました!これは、あなたのシナリオと同じように、私のシナリオで機能する唯一のものでした。
ロバート

非常にうまく機能し、選択/拡張されたバインディングが混乱することはありません。
Rusty

2

Steve Greatrexが提供する動作に追加することをお勧めします。TreeViewItemのコレクションではない可能性があるため、彼の動作はソースからの変更を反映していません。したがって、どのdatacontextがソースからのselectedValueであるかをTreeViewItemで見つけるだけです。TreeViewには、TreeViewItemコレクションを保持する "ItemsHost"という保護プロパティがあります。リフレクションを介してそれを取得し、選択したアイテムを検索するツリーを歩くことができます。

private static void OnSelectedItemChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var behavior = sender as BindableSelectedItemBehaviour;

        if (behavior == null) return;

        var tree = behavior.AssociatedObject;

        if (tree == null) return;

        if (e.NewValue == null) 
            foreach (var item in tree.Items.OfType<TreeViewItem>())
                item.SetValue(TreeViewItem.IsSelectedProperty, false);

        var treeViewItem = e.NewValue as TreeViewItem; 
        if (treeViewItem != null)
        {
            treeViewItem.SetValue(TreeViewItem.IsSelectedProperty, true);
        }
        else
        {
            var itemsHostProperty = tree.GetType().GetProperty("ItemsHost", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);

            if (itemsHostProperty == null) return;

            var itemsHost = itemsHostProperty.GetValue(tree, null) as Panel;

            if (itemsHost == null) return;

            foreach (var item in itemsHost.Children.OfType<TreeViewItem>())
                if (WalkTreeViewItem(item, e.NewValue)) break;
        }
    }

    public static bool WalkTreeViewItem(TreeViewItem treeViewItem, object selectedValue) {
        if (treeViewItem.DataContext == selectedValue)
        {
            treeViewItem.SetValue(TreeViewItem.IsSelectedProperty, true);
            treeViewItem.Focus();
            return true;
        }

        foreach (var item in treeViewItem.Items.OfType<TreeViewItem>())
            if (WalkTreeViewItem(item, selectedValue)) return true;

        return false;
    }

このようにして、動作は双方向バインディングに対して機能します。または、ItemsHostの取得をBehaviorのOnAttachedメソッドに移動して、バインディングが更新されるたびにリフレクションを使用するオーバーヘッドを節約することもできます。


2

WPF MVVM TreeView SelectedItem

...より良い答えですが、ViewModelでSelectedItemを取得/設定する方法については触れていません。

  1. IsSelectedブールプロパティをItemViewModelに追加し、TreeViewItemのスタイルセッターでバインドします。
  2. SelectedItemプロパティを、TreeViewのDataContextとして使用されるViewModelに追加します。これは上記のソリューションで欠けている部分です。
    'ItemVM ...
    パブリックプロパティIsSelected As Boolean
        取得する
            _func.SelectedNode Is Meを返す
        終了取得
        Set(value As Boolean)
            IsSelected値の場合
                _func.SelectedNode = If(value、Me、Nothing)
            終了する場合
            RaisePropertyChange()
        エンドセット
    終了プロパティ
    'TreeVM ...
    パブリックプロパティSelectedItem As ItemVM
        取得する
            _selectedItemを返す
        終了取得
        Set(ItemVMとしての値)
            If _selectedItem Is value Then
                戻る
            終了する場合
            Dim prev = _selectedItem
            _selectedItem = value
            もし前の場合は、その後、何もない
                prev.IsSelected = False
            終了する場合
            _selectedItemが何もない場合
                _selectedItem.IsSelected = True
            終了する場合
        エンドセット
    終了プロパティ
<TreeView ItemsSource="{Binding Path=TreeVM}" 
          BorderBrush="Transparent">
    <TreeView.ItemContainerStyle>
        <Style TargetType="TreeViewItem">
            <Setter Property="IsExpanded" Value="{Binding IsExpanded}"/>
            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
        </Style>
    </TreeView.ItemContainerStyle>
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding Children}">
            <TextBlock Text="{Binding Name}"/>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

1

1日間インターネットを調べた後、通常の WPF / C#環境で通常のツリービューを作成した後、アイテムを選択するための独自のソリューションを見つけました

private void BuildSortTree(int sel)
        {
            MergeSort.Items.Clear();
            TreeViewItem itTemp = new TreeViewItem();
            itTemp.Header = SortList[0];
            MergeSort.Items.Add(itTemp);
            TreeViewItem prev;
            itTemp.IsExpanded = true;
            if (0 == sel) itTemp.IsSelected= true;
            prev = itTemp;
            for(int i = 1; i<SortList.Count; i++)
            {

                TreeViewItem itTempNEW = new TreeViewItem();
                itTempNEW.Header = SortList[i];
                prev.Items.Add(itTempNEW);
                itTempNEW.IsExpanded = true;
                if (i == sel) itTempNEW.IsSelected = true;
                prev = itTempNEW ;
            }
        }

1

TreeViewアイテムのIsSelectedプロパティを使用して行うこともできます。これが私の管理方法です

public delegate void TreeviewItemSelectedHandler(TreeViewItem item);
public class TreeViewItem
{      
  public static event TreeviewItemSelectedHandler OnItemSelected = delegate { };
  public bool IsSelected 
  {
    get { return isSelected; }
    set 
    { 
      isSelected = value;
      if (value)
        OnItemSelected(this);
    }
  }
}

次に、TreeViewがバインドされているデータを含むViewModelで、TreeViewItemクラスのイベントをサブスクライブします。

TreeViewItem.OnItemSelected += TreeViewItemSelected;

最後に、このハンドラを同じViewModelに実装します。

private void TreeViewItemSelected(TreeViewItem item)
{
  //Do something
}

もちろんバインディングは

<Setter Property="IsSelected" Value="{Binding IsSelected}" />    

これは実際には過小評価されたソリューションです。考え方を変更し、各ツリービュー要素のIsSelectedプロパティをバインドし、IsSelectedイベントをバブルアップすることにより、双方向バインディングでうまく機能する組み込み機能を使用できます。私はこの問題に対して提案された多くの解決策を試しましたが、これがうまくいった最初のものです。配線するには少し複雑です。ありがとう。
Richard Moore

1

このスレッドは10年前のものですが、問題はまだ存在しています。

元の質問は、選択したアイテムを「取得する」ことでした。ビューモデルで選択したアイテムを「取得」する必要もあります(設定しないでください)。このスレッドのすべての回答のうち、「Wes」による回答が、問題に別の方法でアプローチする唯一の回答です。「選択された項目」をデータバインディングのターゲットとして使用できる場合は、それをデータバインディングのソースとして使用します。別のビュープロパティに対してそれを行いました。私はビューモデルプロパティに対してそれを行います。

次の2つが必要です。

  • ビューモデルで依存関係プロパティを作成します(私のツリービューは 'MyObject'タイプのオブジェクトにバインドされているため、タイプ 'MyObject'の場合)。
  • Treeview.SelectedItemからビューのコンストラクターのこのプロパティにバインドします(はい、これはコードビハインドですが、そこでデータコンテキストも初期化する可能性があります)。

ビューモデル:

public static readonly DependencyProperty SelectedTreeViewItemProperty = DependencyProperty.Register("SelectedTreeViewItem", typeof(MyObject), typeof(MyViewModel), new PropertyMetadata(OnSelectedTreeViewItemChanged));

    private static void OnSelectedTreeViewItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        (d as MyViewModel).OnSelectedTreeViewItemChanged(e);
    }

    private void OnSelectedTreeViewItemChanged(DependencyPropertyChangedEventArgs e)
    {
        //do your stuff here
    }

    public MyObject SelectedWorkOrderTreeViewItem
    {
        get { return (MyObject)GetValue(SelectedTreeViewItemProperty); }
        set { SetValue(SelectedTreeViewItemProperty, value); }
    }

ビューコンストラクタ:

Binding binding = new Binding("SelectedItem")
        {
            Source = treeView, //name of tree view in xaml
            Mode = BindingMode.OneWay
        };

        BindingOperations.SetBinding(DataContext, MyViewModel.SelectedTreeViewItemProperty, binding);

0

(TreeViewがこの問題に関して明らかに無効化されていることにすべて同意します。SelectedItemへのバインドは明白だったでしょう。 ため息

TreeViewItemのIsSelectedプロパティと適切に対話するためのソリューションが必要だったので、次のようにしました。

// the Type CustomThing needs to implement IsSelected with notification
// for this to work.
public class CustomTreeView : TreeView
{
    public CustomThing SelectedCustomThing
    {
        get
        {
            return (CustomThing)GetValue(SelectedNode_Property);
        }
        set
        {
            SetValue(SelectedNode_Property, value);
            if(value != null) value.IsSelected = true;
        }
    }

    public static DependencyProperty SelectedNode_Property =
        DependencyProperty.Register(
            "SelectedCustomThing",
            typeof(CustomThing),
            typeof(CustomTreeView),
            new FrameworkPropertyMetadata(
                null,
                FrameworkPropertyMetadataOptions.None,
                SelectedNodeChanged));

    public CustomTreeView(): base()
    {
        this.SelectedItemChanged += new RoutedPropertyChangedEventHandler<object>(SelectedItemChanged_CustomHandler);
    }

    void SelectedItemChanged_CustomHandler(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        SetValue(SelectedNode_Property, SelectedItem);
    }

    private static void SelectedNodeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var treeView = d as CustomTreeView;
        var newNode = e.NewValue as CustomThing;

        treeView.SelectedCustomThing = (CustomThing)e.NewValue;
    }
}

このXAMLの場合:

<local:CustonTreeView ItemsSource="{Binding TreeRoot}" 
    SelectedCustomThing="{Binding SelectedNode,Mode=TwoWay}">
    <TreeView.ItemContainerStyle>
        <Style TargetType="TreeViewItem">
            <Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
        </Style>
    </TreeView.ItemContainerStyle>
</local:CustonTreeView>

0

以下の機能を提供する私のソリューションをお届けします。

  • 2つの方法のバインディングをサポート

  • TreeViewItem.IsSelectedプロパティを自動的に更新します(SelectedItemに従って)

  • TreeViewサブクラス化なし

  • ViewModelにバインドされたアイテムは任意のタイプ(nullも含む)にすることができます

1 / CSに次のコードを貼り付けます。

public class BindableSelectedItem
{
    public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.RegisterAttached(
        "SelectedItem", typeof(object), typeof(BindableSelectedItem), new PropertyMetadata(default(object), OnSelectedItemPropertyChangedCallback));

    private static void OnSelectedItemPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var treeView = d as TreeView;
        if (treeView != null)
        {
            BrowseTreeViewItems(treeView, tvi =>
            {
                tvi.IsSelected = tvi.DataContext == e.NewValue;
            });
        }
        else
        {
            throw new Exception("Attached property supports only TreeView");
        }
    }

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

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

    public static void BrowseTreeViewItems(TreeView treeView, Action<TreeViewItem> onBrowsedTreeViewItem)
    {
        var collectionsToVisit = new System.Collections.Generic.List<Tuple<ItemContainerGenerator, ItemCollection>> { new Tuple<ItemContainerGenerator, ItemCollection>(treeView.ItemContainerGenerator, treeView.Items) };
        var collectionIndex = 0;
        while (collectionIndex < collectionsToVisit.Count)
        {
            var itemContainerGenerator = collectionsToVisit[collectionIndex].Item1;
            var itemCollection = collectionsToVisit[collectionIndex].Item2;
            for (var i = 0; i < itemCollection.Count; i++)
            {
                var tvi = itemContainerGenerator.ContainerFromIndex(i) as TreeViewItem;
                if (tvi == null)
                {
                    continue;
                }

                if (tvi.ItemContainerGenerator.Status == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
                {
                    collectionsToVisit.Add(new Tuple<ItemContainerGenerator, ItemCollection>(tvi.ItemContainerGenerator, tvi.Items));
                }

                onBrowsedTreeViewItem(tvi);
            }

            collectionIndex++;
        }
    }

}

2 / XAMLファイルでの使用例

<TreeView myNS:BindableSelectedItem.SelectedItem="{Binding Path=SelectedItem, Mode=TwoWay}" />  

0

私は、ビューの選択されたアイテムからビューモデルの選択されたアイテムを更新するために完全に機能するこのソリューション(私は最も簡単でメモリリークがないと考えています)を提案します。

ViewModelから選択したアイテムを変更しても、ビューの選択したアイテムは更新されないことに注意してください。

public class TreeViewEx : TreeView
{
    public static readonly DependencyProperty SelectedItemExProperty = DependencyProperty.Register("SelectedItemEx", typeof(object), typeof(TreeViewEx), new FrameworkPropertyMetadata(default(object))
    {
        BindsTwoWayByDefault = true // Required in order to avoid setting the "BindingMode" from the XAML
    });

    public object SelectedItemEx
    {
        get => GetValue(SelectedItemExProperty);
        set => SetValue(SelectedItemExProperty, value);
    }

    protected override void OnSelectedItemChanged(RoutedPropertyChangedEventArgs<object> e)
    {
        SelectedItemEx = e.NewValue;
    }
}

XAMLの使用

<l:TreeViewEx ItemsSource="{Binding Path=Items}" SelectedItemEx="{Binding Path=SelectedItem}" >
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.