- MVUX eliminates INotifyPropertyChanged boilerplate entirely by using immutable records, reactive feeds, and auto-generated commands.
- Every mature WPF pattern (Prism regions, EventAggregator, MVVMLight Messenger, service locators, ObservableCollection) has a direct MVUX or Uno Platform Extensions equivalent.
- Traditional MVVM with CommunityToolkit.Mvvm and MVUX coexist in the same project. You do not have to rewrite everything at once.
- AI coding agents can read a WPF ViewModel, identify the MVVM pattern, and generate the MVUX model, accelerating the refactor from hours to minutes per screen.
Who this is for: Senior WPF developers whose applications use mature MVVM patterns (Prism, MVVMLight, custom frameworks) and who are evaluating Uno Platform 6.5 on .NET 10 as a modernization target.
Your WPF application has been in production for years. It uses Prism, or MVVMLight, or a hand-rolled MVVM framework your team built in 2014. The architecture works. ViewModels implement INotifyPropertyChanged. Commands are wired through DelegateCommand or RelayCommand. Navigation flows through a RegionManager. None of this is broken.
The question is whether modernizing these patterns yields enough benefit to justify the effort. This article answers that by mapping every common WPF architecture pattern to its Uno Platform MVUX equivalent, showing the before-and-after code, and giving you a concrete workflow for refactoring one ViewModel at a time.
WPF's Common Architecture Patterns
Most mature WPF applications share a common architectural DNA:
- MVVM with INotifyPropertyChanged and ObservableCollection. Every property setter fires PropertyChanged. Every list mutation fires CollectionChanged. The plumbing is repetitive but well understood.
- Prism: Modules, Regions, Navigation, EventAggregator. Modules define feature areas. RegionManager composes views into named regions. Navigation is URI-based. EventAggregator provides loosely-coupled pub/sub.
- MVVMLight: Messenger, SimpleIoc, RelayCommand. A lighter approach. Simple enough to understand in an afternoon.
- Custom frameworks. A ViewModelBase with SetProperty helpers. A ServiceLocator wrapping Unity or Autofac. These work well for the team that built them, but carry maintenance overhead.
- Command patterns. Whether DelegateCommand, RelayCommand from MVVMLight, or RelayCommand from CommunityToolkit.Mvvm: wrap a method in ICommand, optionally provide CanExecute, bind in XAML.
All of these patterns solve real problems. The issue is not that they are wrong. The issue is that they require significant boilerplate, make async data loading error-prone, and leave you responsible for managing loading states, error states, and thread marshaling manually.
What MVUX Is and Why It Matters
MVUX stands for Model-View-Update-eXtended. It is built into Uno Platform Extensions, designed specifically to work with XAML data binding.
Models Are Records
In MVUX, the Model replaces the ViewModel. Instead of a mutable class that implements INotifyPropertyChanged, you write a partial record. Records are immutable by default. When state needs to change, MVUX creates a new instance rather than mutating properties in place.
public partial record EmployeeListModel(IEmployeeService EmployeeService)
{
public IListFeed<Employee> Employees =>
ListFeed.Async(this.EmployeeService.GetAllAsync);
}That is the entire Model for a screen that loads and displays a list of employees. No ObservableCollection. No INotifyPropertyChanged. No OnPropertyChanged("Employees").
Feeds Provide Reactive Data Streams
IFeed<T>— A read-only stream of a single value. Use for service-driven data the user does not modify.IListFeed<T>— A read-only stream of a collection. ReplacesObservableCollection<T>for service-driven lists.IState<T>— A read-write reactive property. Use for user input, filters, selected items. Supports two-way binding.IListState<T>— A read-write reactive collection. Use when you need to add, remove, or edit items from the UI.
Feeds and states handle async loading automatically. When a feed is loading, the FeedView control shows a progress indicator. When it errors, an error template appears. When it returns no data, a "no results" template shows. You do not write any of this logic yourself.
Commands Are Generated Automatically
Any public method on your MVUX model becomes a bindable command. You write a method, and MVUX generates the IAsyncCommand for you.
public partial record EmployeeListModel(IEmployeeService EmployeeService)
{
public IListFeed<Employee> Employees =>
ListFeed.Async(this.EmployeeService.GetAllAsync);
public async ValueTask DeleteEmployee(Employee employee, CancellationToken ct)
{
await EmployeeService.DeleteAsync(employee.Id, ct);
}
}In XAML, bind directly to the method name. MVUX generates the command, auto-disables the button while the async operation runs, and passes the CancellationToken for you.
Built-in Async Data Loading
The FeedView control provides templates for every state your async data can be in:
<mvux:FeedView Source="{Binding Employees}">
<mvux:FeedView.ValueTemplate>
<DataTemplate>
<ListView ItemsSource="{Binding Data}" />
</DataTemplate>
</mvux:FeedView.ValueTemplate>
<mvux:FeedView.ProgressTemplate>
<DataTemplate><ProgressRing IsActive="True" /></DataTemplate>
</mvux:FeedView.ProgressTemplate>
<mvux:FeedView.ErrorTemplate>
<DataTemplate><TextBlock Text="Failed to load." /></DataTemplate>
</mvux:FeedView.ErrorTemplate>
<mvux:FeedView.NoneTemplate>
<DataTemplate><TextBlock Text="No employees found." /></DataTemplate>
</mvux:FeedView.NoneTemplate>
</mvux:FeedView>In traditional WPF MVVM, you would manage IsLoading, HasError, and IsEmpty boolean properties manually. With MVUX, the FeedView handles all of this.
Pattern Translation Table
| WPF Pattern | Uno Platform MVUX Equivalent |
|---|---|
INotifyPropertyChanged properties | IState<T> (read-write) or IFeed<T> (read-only) |
ObservableCollection<T> | IListFeed<T> (read-only) or IListState<T> (read-write) |
| RelayCommand / DelegateCommand | Auto-generated commands from public model methods |
| Prism RegionManager | Uno Navigation extension with regions |
| EventAggregator / MVVMLight Messenger | IMessenger from CommunityToolkit.Mvvm or scoped DI services |
| SimpleIoc / ServiceLocator | Microsoft.Extensions.DependencyInjection via Uno hosting |
| Prism modules | Project references + DI registration |
Pattern-by-Pattern Code Translation
INotifyPropertyChanged to IState and IFeed
public class EmployeeDetailViewModel : ViewModelBase
{
private string _firstName;
public string FirstName
{
get => _firstName;
set => SetProperty(ref _firstName, value);
}
private Employee _currentEmployee;
public Employee CurrentEmployee
{
get => _currentEmployee;
set => SetProperty(ref _currentEmployee, value);
}
// + constructor, async load, more properties...
}public partial record EmployeeDetailModel(IEmployeeService EmployeeService)
{
public IFeed<Employee> CurrentEmployee =>
Feed.Async(async ct => await EmployeeService.GetByIdAsync(1, ct));
public IState<string> FirstName => State<string>.Empty(this);
public IState<string> LastName => State<string>.Empty(this);
}ObservableCollection to IListFeed
public class EmployeeListViewModel : ViewModelBase
{
private ObservableCollection<Employee> _employees;
private bool _isLoading;
// + properties, try-catch, IsLoading management...
public async Task LoadAsync()
{
IsLoading = true;
try {
var result = await _service.GetAllAsync();
Employees = new ObservableCollection<Employee>(result);
} finally { IsLoading = false; }
}
}public partial record EmployeeListModel(IEmployeeService EmployeeService)
{
public IListFeed<Employee> Employees =>
ListFeed.Async(this.EmployeeService.GetAllAsync);
}The IListFeed<Employee> replaces both the ObservableCollection and the IsLoading property. The FeedView in XAML handles loading and error states automatically.
Commands: DelegateCommand to Auto-Generated
MVUX generates the IAsyncCommand from the DeleteEmployee method. The button auto-disables while the async call runs. No RaiseCanExecuteChanged(). No manual command wiring.
Prism RegionManager to Uno Navigation Regions
// Shell.xaml
<ContentControl prism:RegionManager.RegionName="MainRegion" /><Grid uen:Region.Attached="True">
<Grid uen:Region.Attached="True"
uen:Region.Navigator="Visibility">
<Grid uen:Region.Name="EmployeeList" />
<Grid uen:Region.Name="EmployeeDetail" />
</Grid>
</Grid>Uno Platform's Navigation extension uses Region.Attached and Region.Name to define composable content areas, directly analogous to Prism's RegionManager.RegionName.
EventAggregator / Messenger
Two options. CommunityToolkit.Mvvm IMessenger for drop-in pub/sub replacement, or scoped DI services for cleaner explicit dependencies. For most scenarios, scoped DI services are cleaner. Reserve IMessenger for cases where decoupled cross-module communication is genuinely needed.
SimpleIoc to Microsoft.Extensions.DependencyInjection
Services registered with the Uno Platform host builder are resolved through constructor injection. No service locator pattern needed. If you need the organizational boundary that Prism modules provided, use separate class library projects with an IServiceCollection extension method per feature.
Incremental Adoption: MVVM and MVUX Coexist
You do not have to convert every screen to MVUX on day one. Uno Platform supports both traditional MVVM (using CommunityToolkit.Mvvm) and MVUX in the same project.
<UnoFeatures>
Material;
Hosting;
Toolkit;
MVUX;
Mvvm;
Navigation;
</UnoFeatures>Naming conflict prevention: MVUX generates a ViewModel for any class ending in "Model". If you have an existing MainViewModel and create a MainModel in the same namespace, you'll get a conflict. Annotate your MVVM ViewModels with [ReactiveBindable(false)] to tell MVUX to skip them.
This means you can migrate screen by screen. Start with data-heavy list screens that benefit most from MVUX (loading states, search, filtering). Leave stable screens on MVVM until you have time and reason to convert them.
Using AI to Refactor ViewModel by ViewModel
AI coding agents excel at mechanical pattern translation. Reading a WPF ViewModel, identifying the MVVM patterns, and generating the MVUX equivalent is exactly the kind of structured refactoring where an agent provides the most value.
- Point the agent at a WPF ViewModel file
- The agent identifies patterns: INotifyPropertyChanged, commands, collections, service dependencies
- The agent generates the MVUX model record with equivalent feeds, states, and methods
- You review, adjust, and test
The Full Before/After
A realistic WPF ViewModel with search, filtering, three commands, manual loading state, and try-catch error handling:
public partial record EmployeeCriteria(string? SearchTerm, string? DepartmentFilter);
public partial record EmployeeListModel(IEmployeeService EmployeeService)
{
public IState<EmployeeCriteria> Criteria =>
State.Value(this, () => new EmployeeCriteria(SearchTerm: null, DepartmentFilter: null));
public IListFeed<Employee> Employees =>
Criteria
.SelectAsync(async (criteria, ct) =>
await EmployeeService.SearchAsync(
criteria.SearchTerm, criteria.DepartmentFilter, ct))
.AsListFeed();
public async ValueTask DeleteEmployee(Employee employee, CancellationToken ct)
{
await EmployeeService.DeleteAsync(employee.Id, ct);
}
public async ValueTask AddEmployee(CancellationToken ct)
{
var newEmployee = new Employee { FirstName = "New", LastName = "Employee" };
await EmployeeService.CreateAsync(newEmployee, ct);
}
}The entire IsLoading / try-catch-finally / RaiseCanExecuteChanged ceremony disappears. SearchTerm and DepartmentFilter collapse into an IState<EmployeeCriteria>. Three commands become two public methods. Error handling and loading states move to the FeedView. ObservableCollection becomes IListFeed, which is reactive: it re-executes whenever Criteria changes.
Testing the Refactored Architecture
MVUX models are records that accept services through their primary constructor. This makes them straightforward to unit test.
[Fact]
public async Task DeleteEmployee_Calls_Service()
{
var mockService = Substitute.For<IEmployeeService>();
var model = new EmployeeListModel(mockService);
var employee = new Employee { Id = 1, FirstName = "Jane" };
await model.DeleteEmployee(employee, CancellationToken.None);
await mockService.Received(1).DeleteAsync(1, Arg.Any<CancellationToken>());
}No DelegateCommand to invoke. No CanExecute to check. No PropertyChanged event subscriptions. Just call the method and verify the service interaction. Immutable records are easy to assert: record equality is value-based, so Assert.Equal works on entire records.
Behavior Comparison
| Behavior | WPF ViewModel | MVUX Model |
|---|---|---|
| Initial data load | Constructor calls async method | Feed evaluates on first access |
| Search filtering | Setter triggers async reload | Criteria state change triggers feed re-evaluation |
| Loading indicator | Manual IsLoading property | FeedView.ProgressTemplate |
| Error display | Manual try-catch with error property | FeedView.ErrorTemplate |
| Delete item | Command calls service, removes from collection | Method calls service, feed refreshes |
| Button disabling | Manual CanExecute + RaiseCanExecuteChanged | Auto-disabled during async execution |
FAQ
Does MVUX replace MVVM entirely?
No. Uno Platform supports both. Choose MVUX for screens with heavy async data loading, filtering, and reactive state. Keep MVVM for screens where the overhead is minimal or where your existing ViewModel is stable.
Can I use Prism with Uno Platform instead of MVUX?
Uno Platform includes a Prism UnoFeature. However, Prism for WinUI/Uno Platform does not have the same RegionManager capabilities as Prism for WPF. The Uno Navigation extension with regions provides the closest equivalent.
What happens to my existing service layer?
Your service interfaces and implementations transfer directly. MVUX models accept services through constructor injection, the same way MVVM ViewModels do. Only the presentation layer changes.
How does IListFeed handle large collections?
IListFeed<T> supports pagination through MVUX pagination operators. Your service method returns pages, and the feed manages incremental loading. The FeedView can work with ListView incremental loading to fetch pages as the user scrolls.
What about complex inter-property dependencies?
Use the SelectAsync operator to compose feeds from states. When IState<T> A changes, any IFeed<T> that depends on A re-evaluates automatically. This replaces the WPF pattern of calling a refresh method from every property setter.
Is there a performance cost to immutable records?
For typical ViewModel state (a few dozen properties, updated on user interaction), the allocation cost is negligible. The thread-safety benefits of immutability far outweigh the cost of creating new record instances.
Can I gradually migrate from Prism's EventAggregator?
Yes. Use IMessenger from CommunityToolkit.Mvvm as a drop-in replacement. Over time, consider replacing messaging with explicit service dependencies registered through DI. Direct service calls are easier to trace and test.
Subscribe to Our Blog
Subscribe via RSS
Back to Top