如何使用 "infinte" 级别模型中的 mvvm 填充 wpf 树视图

How to fill a wpf tree view using mvvm from a model with "infinte" levels

我有那个 entity framework 内部模型,用户可以根据需要将子类别分成多个子类别。

public class Category
{
    public Category()
    {
        SubCategories = new ObservableCollection<Category>();
    }

    [Column("id")]
    public int Id { get; set; }

    [StringLength(100)]
    public string Name { get; set; }

    [Column("ParentID")]
    public int? ParentID { get; set; }


    [ForeignKey("ParentID")]
    public virtual ObservableCollection<Category> SubCategories { get; set; }
}

我正在考虑像这样使用 foreach 用它填充树视图:

Categories = new ObservableCollection<Category>(db.Categories.Where(x => x.ParentID == null));

foreach (var item in Categories)
{
    SubCategoriesModel = new ObservableCollection<Category>(db.Categories.Where(x => x.ParentID == item.Id));
    foreach (var subitem in SubCategoriesModel)
    {
        item.SubCategories.Add(subitem);
    }
}
<TreeView Grid.Row="0" ItemsSource="{Binding Categories}" MinWidth="220">
    <TreeView.Resources>
        <HierarchicalDataTemplate DataType="{x:Type data:Categories}" ItemsSource="{Binding SubCategories}">
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="{Binding Id}" Margin="3 2" />
                <TextBlock Text=" - "/>
                <TextBlock Text="{Binding Name}" Margin="3 2" />
            </StackPanel>
        </HierarchicalDataTemplate>
    </TreeView.Resources>
    <e:Interaction.Behaviors>
        <behaviours:BindableSelectedItemBehavior SelectedItem="{Binding SelectedTreeCategory, Mode=TwoWay}" />
    </e:Interaction.Behaviors>
</TreeView>

我意识到这行不通。有更好的方法吗?

方法一

当您处理可能无限数量的 sub-levels 时(例如,因为项目可以相互引用并会在递归期间导致无限循环),我建议在项目第一次出现时填充它们展开。

方法二

如果您没有递归并且想一次加载所有数据,您可以简单地通过以递归方法加载它来实现(不过要小心 - 如果级别太深,您可能会得到 WhosebugException)


示例方法 1

针对这种情况的一个非常简单的视图模型可能如下所示:

public class Node
{
    public uint NodeId { get; set; }
    public string DisplayName { get; set; }
    public bool ChildrenLoaded { get; set; }

    public ObservableCollection<Node> Children { get; set; }

    public Node()
    {
        ChildrenLoaded = false;
        Children = new ObservableCollection<Node>();
    }

    public void LoadChildNodes()
    {
        if (ChildrenLoaded) return;

        // e.g. Every SubCategory with a parentId of this NodeId
        var newChildren = whereverYourDataComesFrom.LoadChildNodes(NodeId);

        Children.Clear();
        foreach (Node child in newChildren)
            Children.Add(child);

        ChildrenLoaded = true;
    }
}

像这样设置树视图,其中 Nodes 是您首先加载的一个或多个根节点。 (Categories 在您的示例中 ParentId = null)

<TreeView TreeViewItem.Expanded="TreeViewItem_Expanded" ItemsSource="{Binding Nodes}">
    <TreeView.ItemTemplate>
        <HierarchicalDataTemplate ItemsSource="{Binding Children}">
            <TextBlock Text="{Binding DisplayName}"/>
        </HierarchicalDataTemplate>
    </TreeView.ItemTemplate>
</TreeView>

TreeViewItem.Expanded event is a so called RoutedEvent 顺便说一句。它不是由 TreeView 触发的,而是由 TreeViewItems 本身触发的,只是 bubbles 你的视觉树(这是它的实际技术术语,还有 tunneling 直接 ).

每当第一次展开节点时,您只需在 TreeViewItem_Expanded 事件处理程序中加载所有子节点。

private void TreeViewItem_Expanded(object sender, RoutedEventArgs e)
{
    Node node = ((FrameworkElement)e.OriginalSource).DataContext as Node;
    if (node == null) return;

    node.LoadChildNodes();
}

所以不管你有多少项目,如果相互引用,你只加载根节点,其他一切都会发生on-demand。

将该原则转化为您的具体示例,我只是将数据的加载方法拆分为根 Category 条目,并在扩展事件处理程序中加载 SubCategories 而不是 pre-loading一切。

由于你的大部分代码已经几乎相同,我认为这应该是一个相当容易的修改。


示例方法 2

private void LoadRootCategories()
{
    Categories = new ObservableCollection<Category>(db.Categories.Where(x => x.ParentID == null));

    foreach (var item in Categories)
    {
        LoadSubCategories(item)
    }
}

private void LoadSubCategories(Category item)
{
    item.SubCategories = new ObservableCollection<Category>(db.Categories.Where(x => x.ParentID == item.Id));
    
    foreach (var subitem in item.SubCategories)
    {
        // Recursive call
        LoadSubCategories(subitem);
    }
}