MudTreeView: Add ITreeItemData and make Children IReadOnlyCollection#12090
MudTreeView: Add ITreeItemData and make Children IReadOnlyCollection#12090danielchalmers merged 4 commits intoMudBlazor:devfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces the ITreeItemData<T> interface to provide a contract for tree item data and changes the Children property type from List<TreeItemData<T>> to IReadOnlyCollection<ITreeItemData<T>> across the TreeView components. This enables custom implementations of tree item data while maintaining type safety and flexibility.
Key Changes:
- Added new
ITreeItemData<T>interface thatTreeItemData<T>now implements - Changed
Childrenproperty fromList<TreeItemData<T>>?toIReadOnlyCollection<ITreeItemData<T>>? - Updated TreeView components and examples to use two-way binding (
@bind-Items) instead of separateItems/ItemsChangedparameters - Modified related API signatures (
FilterFunc,ItemTemplate, etc.) to use the interface type
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| TreeItemData.cs | Introduced ITreeItemData<T> interface and updated TreeItemData<T> to implement it with IReadOnlyCollection for Children |
| MudTreeViewItem.razor.cs | Updated type signatures to use ITreeItemData<T> instead of TreeItemData<T> |
| MudTreeView.razor.cs | Updated FilterFunc, ItemTemplate, and Items parameters to use interface type |
| TreeViewTests.cs | Added test to verify server data binding works correctly with Children property |
| TreeViewServerTest.razor | Simplified to use @bind-Items instead of manual ItemsChanged callback |
| TreeViewServerTest2.razor | Added helper method to convert between interface and concrete types |
| TreeViewTest8.razor | Added type conversion logic in LoadServerData method |
| TreeViewServerExample.razor | Simplified to use @bind-Items pattern in documentation example |
| TreeViewFilteringExample.razor | Updated FilterFunc signature to use interface type |
| TreeViewCustomExample.razor | Updated method signature to use interface type |
| ApiTypeHierarchy.razor.cs | Updated to work with interface type, requiring ToList() conversion pattern for adding children |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// <summary> | ||
| /// The child items underneath this item. | ||
| /// </summary> | ||
| IReadOnlyCollection<ITreeItemData<T>>? Children { get; set; } |
There was a problem hiding this comment.
The Children property is declared as IReadOnlyCollection<ITreeItemData<T>>? with a setter, which creates an API design issue. IReadOnlyCollection<T> indicates immutability of the collection, but having a setter allows replacing the entire collection reference. This is semantically inconsistent - the interface name suggests read-only behavior, but the setter allows mutation at the reference level.
Consider one of these alternatives:
- Use
ICollection<ITreeItemData<T>>?if the collection is meant to be mutable - Make
Childrena get-only property if it should truly be read-only - Use
IEnumerable<ITreeItemData<T>>?if you only need enumeration with settability
| IReadOnlyCollection<ITreeItemData<T>>? Children { get; set; } | |
| IReadOnlyCollection<ITreeItemData<T>>? Children { get; } |
There was a problem hiding this comment.
The setter is necessary for Blazor’s @bind-Items="context.Children".
This is an intentional design choice. We don’t want the list to be modified using Add, because in that case we wouldn’t know the list has changed, and the DOM wouldn’t update automatically. The end user would have to trigger the update manually, which isn’t very friendly. We would need to introduce some sort of observable list, subscribe to change and call StateHasChanged, which we don’t want to do.
The idea is that if you modify Children, you create an entirely new list.
There was a problem hiding this comment.
This exactly why Add doesn't work well with Blazor #12079 (comment) which is why it's always best to push item as new collection, and IReadOnlyCollection helps to avoid it. Not end user can modify the list and wonder why something doesn't work or why it behaves strangely.
| /// The child items underneath this item. | ||
| /// </summary> | ||
| public virtual List<TreeItemData<T>>? Children { get; set; } | ||
| public virtual IReadOnlyCollection<ITreeItemData<T>>? Children { get; set; } |
There was a problem hiding this comment.
The same API design issue exists in the implementation. The Children property is declared as IReadOnlyCollection<ITreeItemData<T>>? with a setter, which creates semantic inconsistency. While the property can be set to a new collection, the IReadOnlyCollection interface suggests the collection itself should not be modifiable, creating confusion for API consumers.
This is particularly problematic because code like item.Children.Add(...) won't compile, forcing users to replace the entire collection even for simple additions, which is inefficient and awkward.
There was a problem hiding this comment.
Same comment like above with IReadOnlyCollection<ITreeItemData<T>>? Children { get; set; }
| var children = primaryItem.Children?.ToList() ?? new List<ITreeItemData<DocumentedType>>(); | ||
| children.Add(new TreeItemData<DocumentedType> | ||
| { | ||
| Children = [], | ||
| Text = descendant.NameFriendly, | ||
| Value = descendant | ||
| }); | ||
| primaryItem.Children = children; |
There was a problem hiding this comment.
This code demonstrates the awkwardness created by using IReadOnlyCollection for the Children property. To add a child item, you must:
- Convert
Childrento a mutable list with.ToList() - Add the new item to the list
- Reassign the entire collection back to
Children
This pattern is inefficient and awkward. If Children were declared as ICollection<ITreeItemData<T>>? or List<ITreeItemData<T>>? instead, you could simply call primaryItem.Children.Add(...) directly, which would be more intuitive and performant.
| var children = primaryItem.Children?.ToList() ?? new List<ITreeItemData<DocumentedType>>(); | |
| children.Add(new TreeItemData<DocumentedType> | |
| { | |
| Children = [], | |
| Text = descendant.NameFriendly, | |
| Value = descendant | |
| }); | |
| primaryItem.Children = children; | |
| primaryItem.Children?.Add(new TreeItemData<DocumentedType> | |
| { | |
| Children = [], | |
| Text = descendant.NameFriendly, | |
| Value = descendant | |
| }); |
There was a problem hiding this comment.
While this doesn't looks as friendly, it's quite common pattern when working with immutable list.
It doesn't mean the list will never modify and be in a "locked" state, the items might be added through creating new collection and supplying it, we just don't want the descender API to modify the collection unexpectedly.
Co-authored-by: Copilot <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 12 out of 12 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Closes #12083 and Closes #12082
Checklist:
dev).