.net c#使用 TreeView 的主/详细视图

Master/detail view using TreeView
2021-12-04
  •  译文(汉语)
  •  原文(英语)

我正在使用 TreeView 和自定义详细信息视图控件在我的应用程序中实现主/详细信息视图.我也在努力坚持 MVVM 模式.

现在 TreeView 绑定到包含所有详细信息的视图模型对象集合,详细信息视图绑定到 TreeView 的选定项.

这很好用……直到 TreeView 节点之一有 5,000 个子节点,并且应用程序突然占用了 500MB 的 RAM.

主窗口视图模型:

public class MainWindowViewModel
{
    private readonly List<ItemViewModel> rootItems;

    public List<ItemViewModel> RootItems { get { return rootItems; } } // TreeView is bound to this property.

    public MainWindowViewModel()
    {
        rootItems = GetRootItems();
    }

    // ...
}

项目视图模型:

public ItemViewModel
{
    private readonly ModelItem item; // Has a TON of properties
    private readonly List<ItemViewModel> children;

    public List<ItemViewModel> Children { get { return children; } }

    // ...
}

这是我绑定详细信息视图的方式:

<View:ItemDetails DataContext="{Binding SelectedItem, ElementName=ItemTreeView}" />

我对 WPF 和 MVVM 模式还很陌生,但是我想将 TreeView 绑定到一个较小的简化对象的集合似乎是一种浪费,该对象仅具有显示项目所需的属性(如名称和 ID),然后一旦选择它,就会加载所有详细信息.我将如何去做这样的事情?

解决过程1

概述

应该是将 TreeView 的 selected item 属性绑定到源上的某些内容的简单问题.但是,由于 TreeView 控件的构建方式,您必须使用开箱即用的 WPF 编写更多代码才能获得对 MVVM 友好的解决方案.

如果您使用的是 vanilla WPF(我假设您是),那么我建议您使用附加行为.附加的行为将绑定到主视图模型上的操作,当 TreeView 的选择更改时将调用该操作.您也可以调用命令而不是操作,但我将向您展示如何使用操作.

基本上,总体思路是使用详细视图模型的一个实例,该实例将作为主视图模型的属性提供.然后,您可以使用轻量级对象,而不是具有数百个视图模型实例的 RootItems 集合,这些对象仅具有节点的显示名称,并且可能在它们后面有某种 id 字段.当 TreeView 上的选择发生变化时,您希望通过调用方法或设置属性来通知您的详细信息视图模型.在下面的演示代码中,我在 DetailsViewModel 上设置了一个名为 Selection 的属性.

代码演练

这是附加行为的代码:

public static class TreeViewBehavior
{
    public static readonly DependencyProperty SelectionChangedActionProperty =
        DependencyProperty.RegisterAttached("SelectionChangedAction", typeof (Action<object>), typeof (TreeViewBehavior), new PropertyMetadata(default(Action), OnSelectionChangedActionChanged));

    private static void OnSelectionChangedActionChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var treeView = sender as TreeView;
        if (treeView == null) return;

        var action = GetSelectionChangedAction(treeView);

        if (action != null)
        {
            // Remove the next line if you don't want to invoke immediately.
            InvokeSelectionChangedAction(treeView);
            treeView.SelectedItemChanged += TreeViewOnSelectedItemChanged;
        }
        else
        {
            treeView.SelectedItemChanged -= TreeViewOnSelectedItemChanged;
        }
    }

    private static void TreeViewOnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        var treeView = sender as TreeView;
        if (treeView == null) return;

        InvokeSelectionChangedAction(treeView);

    }

    private static void InvokeSelectionChangedAction(TreeView treeView)
    {
        var action = GetSelectionChangedAction(treeView);
        if (action == null) return;

        var selectedItem = treeView.GetValue(TreeView.SelectedItemProperty);

        action(selectedItem);
    }

    public static void SetSelectionChangedAction(TreeView treeView, Action<object> value)
    {
        treeView.SetValue(SelectionChangedActionProperty, value);
    }

    public static Action<object> GetSelectionChangedAction(TreeView treeView)
    {
        return (Action<object>) treeView.GetValue(SelectionChangedActionProperty);
    }
}

然后,在XAML您的TreeView元素,应用以下:local:TreeViewBehavior.SelectionChangedAction="{Binding Path=SelectionChangedAction}".请注意,您必须用 local 替换TreeViewBehavior类的命名空间.

现在,将以下属性添加到 MainWindowViewModel:

public Action<object> SelectionChangedAction { get; private set; } 
public DetailsViewModel DetailsViewModel { get; private set; }

在 MainWindowViewModel 的构造函数中,您需要将 SelectionChangedAction 属性设置为某个值.SelectionChangedAction = item => DetailsViewModel.Selection = item;如果您的 DetailsViewModel 有一个 Selection 属性,您可能会这样做.这完全取决于你.

最后,在您的 XAML 中,将详细信息视图连接到其视图模型,如下所示:

<View:ItemDetails DataContext="{Binding Path=DetailsViewModel}" />

这是使用直接 WPF 的 MVVM 友好解决方案的基本架构.现在,话虽如此,如果您使用像 Caliburn.Micro 或 PRISM 这样的框架,您的方法可能与我在此处提供的方法不同.要时刻铭记在心.

I'm working on implementing a master/details view in my application using a TreeView and a custom details view control. I'm also trying to stick to the MVVM pattern.

Right now the TreeView is bound to a collection of view model objects that contain all of the details and the details view is bound to the selected item of the TreeView.

This works great... until one of the TreeView nodes has 5,000 children and the application is suddenly taking up 500MB of RAM.

Main window view model:

public class MainWindowViewModel
{
    private readonly List<ItemViewModel> rootItems;

    public List<ItemViewModel> RootItems { get { return rootItems; } } // TreeView is bound to this property.

    public MainWindowViewModel()
    {
        rootItems = GetRootItems();
    }

    // ...
}

Item view model:

public ItemViewModel
{
    private readonly ModelItem item; // Has a TON of properties
    private readonly List<ItemViewModel> children;

    public List<ItemViewModel> Children { get { return children; } }

    // ...
}

Here's how I'm binding the details view:

<View:ItemDetails DataContext="{Binding SelectedItem, ElementName=ItemTreeView}" />

I'm fairly new to WPF and the MVVM pattern, but it seems like a waste to I want to bind the TreeView to a collection of a smaller, simplified object that only has properties necessary for displaying the item (like Name and ID), then once it is selected have all of the details loaded. How would I go about doing something like this?

Solutions1

Overview

This should be a simple matter of binding the TreeView's selected item property to something on your source. However, because of the way the TreeView control was built, you have to write more code to get an MVVM-friendly solution, using out-of-the-box WPF.

If you're using vanilla WPF (which I'm assuming you are), then I'd recommend going with an attached behavior. The attached behavior would bind to an action on your main view model that would be invoked when the TreeView's selection changes. You could also invoke a command instead of an action, but I'm going to show you how to use an action.

Basically, the overall idea is to use one instance of your details view model that will be made available as a property of your master view model. Then, instead of your RootItems collection having hundreds of instances of view models, you can use light-weight objects that simply have a display name for the node and perhaps some kind of id field behind them. When the selection on your TreeView changes, you want to notify your details view model by either calling a method or setting a property. In the demonstration code below, I'm setting a property on the DetailsViewModel called Selection.

Walkthrough with Code

Here's the code for the attached behavior:

public static class TreeViewBehavior
{
    public static readonly DependencyProperty SelectionChangedActionProperty =
        DependencyProperty.RegisterAttached("SelectionChangedAction", typeof (Action<object>), typeof (TreeViewBehavior), new PropertyMetadata(default(Action), OnSelectionChangedActionChanged));

    private static void OnSelectionChangedActionChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
    {
        var treeView = sender as TreeView;
        if (treeView == null) return;

        var action = GetSelectionChangedAction(treeView);

        if (action != null)
        {
            // Remove the next line if you don't want to invoke immediately.
            InvokeSelectionChangedAction(treeView);
            treeView.SelectedItemChanged += TreeViewOnSelectedItemChanged;
        }
        else
        {
            treeView.SelectedItemChanged -= TreeViewOnSelectedItemChanged;
        }
    }

    private static void TreeViewOnSelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
    {
        var treeView = sender as TreeView;
        if (treeView == null) return;

        InvokeSelectionChangedAction(treeView);

    }

    private static void InvokeSelectionChangedAction(TreeView treeView)
    {
        var action = GetSelectionChangedAction(treeView);
        if (action == null) return;

        var selectedItem = treeView.GetValue(TreeView.SelectedItemProperty);

        action(selectedItem);
    }

    public static void SetSelectionChangedAction(TreeView treeView, Action<object> value)
    {
        treeView.SetValue(SelectionChangedActionProperty, value);
    }

    public static Action<object> GetSelectionChangedAction(TreeView treeView)
    {
        return (Action<object>) treeView.GetValue(SelectionChangedActionProperty);
    }
}

Then, in the XAML on your TreeView element, apply the following: local:TreeViewBehavior.SelectionChangedAction="{Binding Path=SelectionChangedAction}". Note that you will have to substitute local for the namespace of the TreeViewBehavior class.

Now, add the following properties to your MainWindowViewModel:

public Action<object> SelectionChangedAction { get; private set; } 
public DetailsViewModel DetailsViewModel { get; private set; }

In your MainWindowViewModel's constructor, you need to set the SelectionChangedAction property to something. You might do SelectionChangedAction = item => DetailsViewModel.Selection = item; if your DetailsViewModel has a Selection property on it. That's entirely up to you.

And finally, in your XAML, wire the details view up to its view model like so:

<View:ItemDetails DataContext="{Binding Path=DetailsViewModel}" />

That's the basic architecture of an MVVM friendly solution using straight WPF. Now, with that said, if you're using a framework like Caliburn.Micro or PRISM, your approach would probably be different than what I've provided here. Just keep that in mind.

转载于:https://stackoverflow.com/questions/15206671/master-detail-view-using-treeview

本人是.net程序员,因为英语不行,使用工具翻译,希望对有需要的人有所帮助
如果本文质量不好,还请谅解,毕竟这些操作还是比较费时的,英语较好的可以看原文

留言回复
我们只提供高质量资源,素材,源码,坚持 下了就能用 原则,让客户花了钱觉得值
上班时间 : 周一至周五9:00-17:30 期待您的加入