WPF Treeview에서 SelectedItem에 대한 데이터 바인딩


241

WPF-treeview에서 선택된 항목을 어떻게 검색합니까? 바인딩하기 때문에 XAML 에서이 작업을 수행하고 싶습니다.

당신은 것을 생각 SelectedItem하는하지만 분명히 존재하지 않는 읽기 전용 따라서 사용할 수없는 것입니다.

이것이 내가하고 싶은 일입니다.

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

SelectedItem내 모델의 속성에 바인딩하고 싶습니다 .

그러나 이것은 나에게 오류를 준다 :

'SelectedItem'속성은 읽기 전용이며 마크 업에서 설정할 수 없습니다.

편집 : 좋아, 이것이 내가 해결 한 방법입니다.

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

내 xaml의 코드 숨김 파일에서 :

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

51
이거 짜증나 나도 때렸다. 나는 괜찮은 방법이 있다는 것을 알기 위해 여기에 왔으며 나는 단지 바보입니다. 내가 바보가 아니라는 것이 슬프다 ..
Andrei Rînea

6
이건 정말 짜증과 결합 개념까지 엉망
델타

이 ICommand의의에 트리 뷰 항목 선택 변경 콜 다시에 바인딩하는 몇 가지 중 하나 도움이 될 희망 jacobaloysious.wordpress.com/2012/02/19/...
aloysious 야곱

9
바인딩 및 MVVM의 경우 코드 숨김이 "금지"되지 않고 코드 숨김이 뷰를 지원해야합니다. 내가 본 다른 모든 솔루션에 대한 견해로는, 코드 뒤의 뷰는 뷰 모델에 뷰를 "바인딩"하는 것을 다루기 때문에 훨씬 더 나은 옵션입니다. 유일한 단점은 XAML에서만 작업하는 디자이너가있는 팀이있는 경우 코드가 깨지거나 무시 될 수 있다는 것입니다. 구현하는 데 10 초가 걸리는 솔루션에 대한 비용은 저렴합니다.
nrjohnstone

아마 가장 쉬운 솔루션 중 하나 stackoverflow.com/questions/1238304/...
JoanComasFdz

답변:


240

나는 이것이 이미 대답을 받아 들였다는 것을 알고 있지만 문제를 해결하기 위해 이것을 합쳤다. 델타의 솔루션과 비슷한 아이디어를 사용하지만 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 = TWay를 추가해야했습니다. 저는 "블렌더"가 아니므로 System.Windows.Interactivity의 Behavior <> 클래스에 익숙하지 않았습니다. 어셈블리는 Expression Blend의 일부입니다. 이 어셈블리를 얻기 위해 평가판을 구매 / 설치하지 않으려는 경우 System.Windows.Interactivity가 포함 된 BlendSDK를 다운로드 할 수 있습니다. 3.5 용 BlendSDK 3 ... 4.0 용 BlendSDK 4 인 것 같습니다. 참고 : 선택한 항목 만 가져올 수 있으며 선택한 항목을 설정할 수 없습니다
Mike Rowley

4
UIPropertyMetadata를 FrameworkPropertyMetadata (null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged))로 대체 할 수도 있습니다.
Filimindji

3
이것은 문제를 해결하기위한 접근법입니다. stackoverflow.com/a/18700099/4227
bitbonk

2
위의 XAML 코드 스 니펫에 표시된대로 @Lukas. 그냥 교체 {Binding SelectedItem, Mode=TwoWay}와 함께{Binding MyViewModelField, Mode=TwoWay}
스티브 Greatrex

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


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
이 스레드 imho에서 +1, 최고의 답변. System.Windows.Interactivity에 종속되지 않으며 양방향 바인딩 (MVVM 환경에서 프로그래밍 방식으로 설정)을 허용합니다. 완전한.
Chris Ray

5
이 접근 방식의 문제점은 선택한 항목이 바인딩을 통해 한 번 설정된 경우 (예 : ViewModel) 동작이 작동하기 시작한다는 것입니다. VM의 초기 값이 null 인 경우 바인딩은 DP 값을 업데이트하지 않으며 동작이 활성화되지 않습니다. 다른 기본 선택된 항목 (예 : 유효하지 않은 항목)을 사용하여이 문제를 해결할 수 있습니다.
Mark

6
@Mark : 연결된 속성의 UIPropertyMetadata를 인스턴스화 할 때 위의 null 대신 new object ()를 사용하십시오. 문제는 그때
barnacleboy

2
데이터 유형별로 리소스에서 적용된 HierarchicalDataTemplate을 사용하고 있기 때문에 TreeViewItem으로 캐스트하지 못했습니다. 그러나 ChangeSelectedItem을 제거하면 뷰 모델에 바인딩하고 항목을 검색하는 것이 좋습니다.
Casey Sebben

1
TreeViewItem으로 캐스트하는 데 문제가 있습니다. 이 시점에서 ItemContainerGenerator에는 루트 항목에 대한 참조 만 포함되어 있지만 루트가 아닌 항목도 가져올 수 있어야합니다. 참조를 전달하면 캐스트가 실패하고 null을 반환합니다. 이 문제를 어떻게 해결할 수 있는지 잘 모르시겠습니까?
밥 트웨이

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
이것은 지금까지 나를 위해 일하는 것에 가깝습니다. 나는이 솔루션을 정말 좋아한다.
Rachael

1
이유를 모르지만 저에게는 효과가 없었습니다. (선택한 항목을 트리에서 가져 왔지만 그 반대의 경우는 그렇지 않았습니다.
Erez

종속성 속성을 BindsTwoWayByDefault로 설정하는 것이 약간 더 깔끔 할 것입니다. 그러면 XAML에서 TwoWay를 지정할 필요가 없습니다
Stephen Holt

이것이 가장 좋은 방법입니다. 상호 작용 참조를 사용하지 않고 코드 숨김을 사용하지 않으며 일부 동작과 같이 메모리 누수가 없습니다. 감사합니다.
Alexandru Dicu

언급 했듯이이 솔루션은 양방향 바인딩에서는 작동하지 않습니다. 뷰 모델에서 값을 설정하면 변경 사항이 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.InteractivityNuGet에서 참조 를 설치할 수 있습니다 : nuget.org/packages/System.Windows.Interactivity.WPF
Junle Li

나는이 문제를 몇 시간 동안 해결하려고 노력했지만 이것을 구현했지만 내 명령이 작동하지 않습니다. 제발 도와 줄 수 있습니까?
Alfie

1
2018 년 말 Microsoft에서 WPF 용 XAML 동작을 도입했습니다. 대신 XAML 동작을 사용할 수 있습니다 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 동작을 추가하십시오. 블렌드를 사용하면 정말 쉽습니다.

<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)]" />

뷰 모델 :

public void SetSelectedItem(YourNodeViewModel item) {}; 

5
예. 그리고 TreeView에서 SelectedItem 을 설정 하는 부분은 어디에 있습니까?
mnn

Caliburn은 훌륭하고 우아합니다. 중첩 된 계층 구조에서 매우 쉽게 작동
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에 선택한 항목이 있지만 뷰 모델은 어떻게이 정보를 얻습니까? 이 작업을 수행하는 좋은 깔끔한 방법처럼 보이지만 조금 더 정보가 필요합니다.
Bob Horn

local : MyThingyDetailsView는 하나의 "thingy"인스턴스에 대한 세부 정보보기를 구성하는 XAML로 가득 찬 UserControl입니다. 다른 뷰의 중간에 내용으로 포함되어 있습니다.이 뷰의 DataContext는 현재 요소 트리를 사용하여 선택된 트리 뷰 항목입니다.
웨스

6

TreeViewItem.IsSelected 속성을 사용할 수도 있습니다.


나는 이것이 정답이라고 생각합니다. 그러나 Items의 IsSelected 속성이 TreeView에 전달되는 방법에 대한 예제 또는 모범 사례를 확인하고 싶습니다.
anhoppe

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

3

내 요구 사항은 TreeView가 필요하고 바인딩 된 개체가 Collection <> 유형이므로 PRISM-MVVM 기반 솔루션에 대한 요구 사항이므로 HierarchicalDataTemplate이 필요합니다. 기본 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;
    }
}

레벨에 관계없이 모든 요소를 ​​반복 할 수 있습니다.


감사합니다! 이것은 내 시나리오에서 작동하는 유일한 것이 었으며 귀하와 다릅니다.
Robert

매우 잘 작동하며 선택 / 확장 된 바인딩이 혼동 되지 않습니다 .
Rusty

2

Steve Greatrex가 제공 한 행동에 추가 할 것을 제안합니다. 그의 동작은 TreeViewItems의 컬렉션이 아니기 때문에 소스의 변경 사항을 반영하지 않습니다. 따라서 데이터 컨텍스트가 소스에서 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;
    }

이런 식으로 양방향 바인딩에 대해 동작이 작동합니다. 또는 항목 호스트 획득을 동작의 OnAttached 메소드로 이동하여 바인딩이 업데이트 될 때마다 리플렉션 사용의 오버 헤드를 줄일 수 있습니다.


2

WPF MVVM TreeView 선택 항목

... 더 나은 대답이지만 ViewModel에서 SelectedItem을 가져 오거나 설정하는 방법은 언급하지 않았습니다.

  1. IsSelected 부울 속성을 ItemViewModel에 추가하고 TreeViewItem의 스타일 설정 기에서 바인딩합니다.
  2. TreeView의 DataContext로 사용되는 ViewModel에 SelectedItem 속성을 추가하십시오. 위의 솔루션에서 누락 된 부분입니다.
    'ItemVM ...
    부울로 선택된 공용 속성
        가져 오기
            _func.SelectedNode가 나 반환
        종료
        설정 (부울 값)
            IsSelected 값이면
                _func.SelectedNode = If (value, Me, Nothing)
            끝 경우
            RaisePropertyChange ()
        엔드 세트
    최종 재산
    'TreeVM ...
    Public 속성 SelectedItem을 ItemVM으로
        가져 오기
            _selectedItem 반환
        종료
        설정 (ItemVM으로 값)
            _selectedItem이 값이면
                반환
            끝 경우
            희미한 prev = _selectedItem
            _selectedItem = 값
            prev가 아무것도 아닌 경우
                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

인터넷을 하루 동안 공부 한 후 일반 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}" />    

이것은 실제로 저평가 된 솔루션입니다. 사고 방식을 변경하고 각 treeview 요소의 IsSelected 속성을 바인딩하고 IsSelected 이벤트를 버블 링하면 양방향 바인딩과 잘 작동하는 기본 제공 기능을 사용할 수 있습니다. 이 문제에 대해 제안 된 많은 솔루션을 시도했지만 이것이 가장 먼저 해결되었습니다. 배선하기가 조금 복잡합니다. 감사.
Richard Moore

1

이 스레드는 10 살이지만 문제는 여전히 존재한다는 것을 알고 있습니다 ....

원래 질문은 선택한 항목을 '검색'하는 것이 었습니다. 또한 뷰 모델에서 선택한 항목을 "가져와야"했습니다 (설정하지 않음). 이 스레드의 모든 답변 중에서 'Wes'의 답변은 문제에 다르게 접근하는 유일한 방법입니다. '선택한 항목'을 데이터 바인딩의 대상으로 사용할 수 있으면 데이터 바인딩의 소스로 사용하십시오. 우리는 그것을 다른 뷰 속성으로했는데, 나는 viewmodel 속성으로하겠습니다 :

우리는 두 가지가 필요합니다 :

  • 뷰 모델에서 의존성 속성을 만듭니다 (내 경우에는 'MyObject'유형의 객체에 바인딩되어 있기 때문에 'MyObject'유형의 경우)
  • Treeview.SelectedItem에서 View의 생성자 에서이 속성에 바인딩합니다 (예, 코드 뒤에 있지만 데이터 컨텍스트를 초기화 할 가능성이 있습니다)

뷰 모델 :

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에 바인딩하는 것이 명백했을 것입니다. Sigh )

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

View의 선택된 항목에서 ViewModel의 선택된 항목을 업데이트하는 데 완벽하게 작동하는이 솔루션 (가장 쉬운 메모리 누출을 고려합니다)을 제안합니다.

ViewModel에서 선택한 항목을 변경해도 View의 선택된 항목은 업데이트되지 않습니다.

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.