Master/detail view using TreeView

2019-07-04 03:50发布

问题:

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?

回答1:

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.



标签: c# wpf mvvm