如何基于MVVM用无限层级EF模型填充WPF TreeView
Hey there! The problem you're facing is that your current code only manually loads two levels of categories, but we can adjust this to support unlimited nesting without much hassle. Let's start with fixing the model first, then move on to loading the hierarchical data correctly, and confirm the WPF setup is right.
Step 1: Correct the Entity Framework Model
First, there's a small issue with your Category model that might be causing navigation property confusion. The SubCategories collection shouldn't have the ForeignKey attribute—instead, that attribute belongs to a single ParentCategory property, which represents the parent of the current category. Here's the corrected model:
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 points to the parent category entity [ForeignKey("ParentID")] public virtual Category ParentCategory { get; set; } // Collection of child categories public virtual ObservableCollection<Category> SubCategories { get; set; } }
This fixes the EF navigation property mapping so it correctly understands the parent-child relationship.
Step 2: Load Infinite Hierarchical Data
We have two solid approaches to load unlimited category levels, depending on your data size:
Option 1: Recursive Loading (Simple, Small Datasets)
If your category tree isn't too large, a recursive method will work perfectly. It loads root categories first, then recursively loads each node's children:
// Initialize root categories (ParentID is null) Categories = new ObservableCollection<Category>(db.Categories.Where(x => x.ParentID == null)); // Recursively load child categories for each root foreach (var rootCat in Categories) { LoadChildCategories(rootCat); } // Recursive helper method private void LoadChildCategories(Category parentCategory) { // Get all direct children of the current parent var childCategories = db.Categories.Where(x => x.ParentID == parentCategory.Id).ToList(); foreach (var childCat in childCategories) { parentCategory.SubCategories.Add(childCat); // Recurse to load this child's children LoadChildCategories(childCat); } }
Option 2: Single Database Query with CTE (Better for Large Datasets)
For larger category trees, recursive database queries (using Common Table Expressions, CTE) are more efficient—they pull all categories in one go, then we build the hierarchy in memory:
// Use a CTE to fetch all categories with their hierarchy level var allCategories = db.Categories.FromSqlRaw(@" WITH CategoryHierarchy AS ( SELECT Id, Name, ParentID, 0 AS Level FROM Categories WHERE ParentID IS NULL UNION ALL SELECT c.Id, c.Name, c.ParentID, ch.Level + 1 FROM Categories c INNER JOIN CategoryHierarchy ch ON c.ParentID = ch.Id ) SELECT * FROM CategoryHierarchy ").ToList(); // Build the hierarchy in memory using a lookup for fast access var categoryLookup = allCategories.ToLookup(c => c.ParentID); var rootCategories = new ObservableCollection<Category>(allCategories.Where(c => c.ParentID == null)); foreach (var category in allCategories) { // Add all children to the current category's SubCategories collection foreach (var childCat in categoryLookup[category.Id]) { category.SubCategories.Add(childCat); } } Categories = rootCategories;
This approach minimizes database round-trips, which is crucial for performance with large datasets.
Step 3: Fix the WPF TreeView XAML
Your TreeView setup is almost right—just a small typo in the DataType (it should be data:Category, singular, since your model class is named Category):
<TreeView Grid.Row="0" ItemsSource="{Binding Categories}" MinWidth="220"> <TreeView.Resources> <HierarchicalDataTemplate DataType="{x:Type data:Category}" 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>
The HierarchicalDataTemplate will automatically render child nodes recursively as long as the SubCategories collection is properly populated—no extra work needed here!
Bonus: Lazy Loading (Optional)
If you enable lazy loading in EF (it's enabled by default in some versions), you could avoid manual loading entirely. EF will automatically load child categories when you access the SubCategories collection. However, this can lead to N+1 database queries (one query for each node), which is not ideal for large trees. Stick with the recursive or CTE methods for better performance.
内容的提问来源于stack exchange,提问作者Ahmed Mohammed

