Architecture WPF Migration
Key Takeaways
  • 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 Patterns

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.

MVUX

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.

MVUX
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. Replaces ObservableCollection<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.

MVUX
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:

XAML
<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.

Translation Table

Pattern Translation Table

WPF PatternUno Platform MVUX Equivalent
INotifyPropertyChanged propertiesIState<T> (read-write) or IFeed<T> (read-only)
ObservableCollection<T>IListFeed<T> (read-only) or IListState<T> (read-write)
RelayCommand / DelegateCommandAuto-generated commands from public model methods
Prism RegionManagerUno Navigation extension with regions
EventAggregator / MVVMLight MessengerIMessenger from CommunityToolkit.Mvvm or scoped DI services
SimpleIoc / ServiceLocatorMicrosoft.Extensions.DependencyInjection via Uno hosting
Prism modulesProject references + DI registration
Before & After

Pattern-by-Pattern Code Translation

INotifyPropertyChanged to IState and IFeed

WPF
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...
}
MVUX
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

WPF
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; }
    }
}
MVUX
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

WPF Prism
// Shell.xaml
<ContentControl prism:RegionManager.RegionName="MainRegion" />
Uno Platform
<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.

Coexistence

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.

csproj
<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.

AI Refactor

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.

  1. Point the agent at a WPF ViewModel file
  2. The agent identifies patterns: INotifyPropertyChanged, commands, collections, service dependencies
  3. The agent generates the MVUX model record with equivalent feeds, states, and methods
  4. 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:

68 lines became 20
MVUX (AI-Generated, Reviewed)
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

Testing the Refactored Architecture

MVUX models are records that accept services through their primary constructor. This makes them straightforward to unit test.

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

BehaviorWPF ViewModelMVUX Model
Initial data loadConstructor calls async methodFeed evaluates on first access
Search filteringSetter triggers async reloadCriteria state change triggers feed re-evaluation
Loading indicatorManual IsLoading propertyFeedView.ProgressTemplate
Error displayManual try-catch with error propertyFeedView.ErrorTemplate
Delete itemCommand calls service, removes from collectionMethod calls service, feed refreshes
Button disablingManual CanExecute + RaiseCanExecuteChangedAuto-disabled during async execution
FAQ

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.