managing-wpf-collectionview-mvvm
npx skills add https://github.com/christian289/dotnet-with-claudecode --skill managing-wpf-collectionview-mvvm
Agent 安装分布
Skill 文档
5.6 MVVM Pattern with CollectionView
Project Structure
The templates folder contains a WPF project example (use latest .NET per version mapping).
templates/
âââ WpfCollectionViewSample.Core/ â Pure C# models and interfaces
â âââ Member.cs
â âââ IMemberCollectionService.cs
â âââ WpfCollectionViewSample.Core.csproj
âââ WpfCollectionViewSample.ViewModels/ â ViewModel (no WPF references)
â âââ MainViewModel.cs
â âââ GlobalUsings.cs
â âââ WpfCollectionViewSample.ViewModels.csproj
âââ WpfCollectionViewSample.WpfServices/ â WPF Service Layer
â âââ MemberCollectionService.cs
â âââ GlobalUsings.cs
â âââ WpfCollectionViewSample.WpfServices.csproj
âââ WpfCollectionViewSample.App/ â WPF Application
âââ Views/
â âââ MainWindow.xaml
â âââ MainWindow.xaml.cs
âââ App.xaml
âââ App.xaml.cs
âââ GlobalUsings.cs
âââ WpfCollectionViewSample.App.csproj
5.6.1 Problem Scenario
When a single source collection needs to be filtered with different conditions across multiple Views while adhering to MVVM principles
5.6.2 Core Principles
- ViewModel must not reference WPF-related assemblies (MVVM violation)
- Encapsulate
CollectionViewSourceaccess through Service Layer - ViewModel uses only
IEnumerableor pure BCL types
5.6.3 Architecture Layer Structure
View (XAML)
â DataBinding
ViewModel Layer (uses IEnumerable, no WPF assembly reference)
â IEnumerable interface
Service Layer (uses CollectionViewSource directly)
â
Data Layer (ObservableCollection<T>)
5.6.4 Implementation Pattern
1. Service Layer (CollectionViewFactory/Store)
// Services/MemberCollectionService.cs
// This class can reference PresentationFramework
namespace MyApp.Services;
public sealed class MemberCollectionService
{
private ObservableCollection<Member> Source { get; } = [];
// Factory Method: Create filtered view
// Returns IEnumerable so ViewModel doesn't know WPF types
public IEnumerable CreateView(Predicate<object>? filter = null)
{
var viewSource = new CollectionViewSource { Source = Source };
var view = viewSource.View;
if (filter is not null)
{
view.Filter = filter;
}
return view; // ICollectionView inherits IEnumerable
}
public void Add(Member item) => Source.Add(item);
public void Remove(Member? item)
{
if (item is not null)
Source.Remove(item);
}
public void Clear() => Source.Clear();
}
2. ViewModel Layer
// ViewModel uses only IEnumerable (pure BCL type)
namespace MyApp.ViewModels;
public abstract class BaseFilteredViewModel
{
public IEnumerable? Members { get; }
protected BaseFilteredViewModel(Predicate<object> filter)
{
// Receives IEnumerable from Service
Members = memberService.CreateView(filter);
}
}
// Each filtered ViewModel
public sealed class WalkerViewModel : BaseFilteredViewModel
{
public WalkerViewModel()
: base(item => (item as Member)?.Type == DeviceTypes.Walker)
{
}
}
// Or use with direct type casting
public sealed class AppViewModel : ObservableObject
{
public IEnumerable? Members { get; }
public AppViewModel()
{
Members = memberService.CreateView();
}
// Manipulate collection with LINQ when needed
private void ProcessMembers()
{
var memberList = Members?.Cast<Member>().ToList();
// Processing logic...
}
}
3. Initialize CollectionView from View (Alternative Approach)
This approach keeps ViewModel completely independent from WPF, but requires initialization logic in View’s Code-Behind.
// ViewModel - Uses pure BCL only
namespace MyApp.ViewModels;
public sealed partial class MainViewModel : ObservableObject
{
[ObservableProperty] private ObservableCollection<Person> _people = [];
private ICollectionView? _peopleView;
// Injected from View
public void InitializeCollectionView(ICollectionView collectionView)
{
_peopleView = collectionView;
_peopleView.Filter = FilterPerson;
}
private bool FilterPerson(object item)
{
// Filtering logic
return true;
}
}
// MainWindow.xaml.cs - View's Code-Behind
namespace MyApp.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var viewModel = new MainViewModel();
DataContext = viewModel;
// Create CollectionViewSource in View layer
ICollectionView collectionView =
CollectionViewSource.GetDefaultView(viewModel.People);
// Inject into ViewModel
viewModel.InitializeCollectionView(collectionView);
}
}
Note: This approach requires ViewModel to know the ICollectionView type, so WindowsBase.dll reference is needed. For complete independence, use the Service Layer approach.
5.6.5 Project Structure (Strict MVVM)
MyApp.Models/ // Pure C# models, BCL only
MyApp.ViewModels/ // Pure C# ViewModel
// No WPF assembly references
// Uses IEnumerable only
MyApp.Services/ // PresentationFramework reference: YES
// WindowsBase reference: YES
// Uses CollectionViewSource
MyApp.Views/ // References all WPF assemblies
5.6.6 Assembly Reference Rules
Assemblies that ViewModel project must NOT reference:
- â
WindowsBase.dll(contains ICollectionView) - â
PresentationFramework.dll(contains CollectionViewSource) - â
PresentationCore.dll
Assemblies that ViewModel project CAN reference:
- â BCL (Base Class Library) types only
- â
System.Collections.IEnumerable - â
System.Collections.ObjectModel.ObservableCollection<T> - â
System.ComponentModel.INotifyPropertyChanged
Assemblies that Service project CAN reference:
- â
WindowsBase.dll - â
PresentationFramework.dll - â All WPF-related assemblies
5.6.7 Key Advantages
- Single source maintenance: All Views share one
ObservableCollection - Automatic synchronization: Source changes automatically reflect in all filtered Views
- MVVM compliance: ViewModel is completely independent from UI framework
- Reusability: Multiple Views can be created with various filter conditions
- Testability: ViewModel can be unit tested without WPF
5.6.8 Utilizing CollectionView Features in Service Layer
Service Layer can encapsulate various CollectionView features.
// Services/MemberCollectionService.cs
namespace MyApp.Services;
public sealed class MemberCollectionService
{
private ObservableCollection<Member> Source { get; } = [];
public IEnumerable CreateView(Predicate<object>? filter = null)
{
var viewSource = new CollectionViewSource { Source = Source };
var view = viewSource.View;
if (filter is not null)
{
view.Filter = filter;
}
return view;
}
// Create sorted view
public IEnumerable CreateSortedView(
string propertyName,
ListSortDirection direction = ListSortDirection.Ascending)
{
var viewSource = new CollectionViewSource { Source = Source };
var view = viewSource.View;
view.SortDescriptions.Add(
new SortDescription(propertyName, direction)
);
return view;
}
// Create grouped view
public IEnumerable CreateGroupedView(string groupPropertyName)
{
var viewSource = new CollectionViewSource { Source = Source };
var view = viewSource.View;
view.GroupDescriptions.Add(
new PropertyGroupDescription(groupPropertyName)
);
return view;
}
public void Add(Member item) => Source.Add(item);
public void Remove(Member? item) { if (item is not null) Source.Remove(item); }
public void Clear() => Source.Clear();
}
5.6.9 When Applying DI/IoC
// Interface definition (uses pure BCL types only)
namespace MyApp.Services;
public interface IMemberCollectionService
{
IEnumerable CreateView(Predicate<object>? filter = null);
void Add(Member member);
void Remove(Member? member);
void Clear();
}
// DI container registration
services.AddSingleton<IMemberCollectionService, MemberCollectionService>();
// ViewModel constructor injection
namespace MyApp.ViewModels;
public sealed partial class AppViewModel(IMemberCollectionService memberService)
: ObservableObject
{
public IEnumerable? Members { get; } = memberService.CreateView();
}
5.6.10 Practical Recommendations
- Project separation: Separate ViewModel and Service into different projects
- Interface usage: Define Services with interfaces for testability
- Singleton or DI: Manage Services as Singleton or through DI container
- Naming conventions:
MemberCollectionService(Service suffix)MemberViewFactory(Factory suffix)MemberStore(Store suffix)
5.6.11 Grouping UI in XAML
When using grouped CollectionView, display groups using GroupStyle in ListBox or ItemsControl.
XAML – Grouped ListBox:
<ListBox ItemsSource="{Binding GroupedMembers}">
<!-- Group header template -->
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.HeaderTemplate>
<DataTemplate>
<Border Background="#E0E0E0" Padding="5">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"
FontWeight="Bold"
FontSize="14"/>
<TextBlock Text=" ("
Foreground="Gray"/>
<TextBlock Text="{Binding ItemCount}"
Foreground="Gray"/>
<TextBlock Text=" items)"
Foreground="Gray"/>
</StackPanel>
</Border>
</DataTemplate>
</GroupStyle.HeaderTemplate>
<!-- Optional: Group container style -->
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Margin" Value="0,0,0,10"/>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListBox.GroupStyle>
<!-- Item template -->
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" Padding="10,5"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
XAML – Expander Style Grouping:
<ListBox ItemsSource="{Binding GroupedMembers}">
<ListBox.GroupStyle>
<GroupStyle>
<GroupStyle.ContainerStyle>
<Style TargetType="{x:Type GroupItem}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type GroupItem}">
<Expander IsExpanded="True">
<Expander.Header>
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Name}"
FontWeight="Bold"/>
<TextBlock Text="{Binding ItemCount,
StringFormat=' ({0})'}"
Foreground="Gray"/>
</StackPanel>
</Expander.Header>
<ItemsPresenter/>
</Expander>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</GroupStyle.ContainerStyle>
</GroupStyle>
</ListBox.GroupStyle>
</ListBox>
Service Layer – Multiple Sort and Group:
// Create view with sorting and grouping combined
public IEnumerable CreateSortedGroupedView(
string sortProperty,
ListSortDirection sortDirection,
string groupProperty)
{
var viewSource = new CollectionViewSource { Source = Source };
var view = viewSource.View;
// Apply sort first, then group
view.SortDescriptions.Add(new SortDescription(sortProperty, sortDirection));
view.GroupDescriptions.Add(new PropertyGroupDescription(groupProperty));
return view;
}