9.4. .NET MAUI Integration¶
Wire a ComponentVM<M> to a .NET MAUI page (desktop + mobile XAML)
through the same INotifyPropertyChanged adapter used for WPF.
9.4.1. Reactivity primitive¶
MAUI's XAML data binding observes INotifyPropertyChanged.PropertyChanged
and ICommand. ComponentVMBase already implements INotifyPropertyChanged:
a setter such as Model raises it after publishing its PropertyChangedMessage
to the hub, and every lifecycle transition raises it for Status after
publishing a ConstructionStatusChangedMessage. The hub never carries a
PropertyChangedMessage for Status.
9.4.2. Mapping¶
| MAUI | VMx |
|---|---|
INotifyPropertyChanged |
ComponentVMBase.PropertyChanged (implemented by every VM) |
ICommand |
RelayCommand / RelayCommand<T> (already implements ICommand) |
ObservableCollection<T> |
ServicedObservableCollection<T> or wrap an ObservableList<T> |
MainThread.BeginInvokeOnMainThread |
SynchronizationContextScheduler created on the UI thread (host) |
9.4.3. Adapter skeleton¶
MAUI uses the WPF adapter unchanged. It is the exact file that VMx.Tests
executes (Integration/XamlRecipeTests.cs). CI has no MAUI host, so the
adapter runs there against a scheduler-driven host thread and, for WPF, a real
Dispatcher.
using System.ComponentModel;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using VMx.Components;
using VMx.Lifecycle;
/// <summary>
/// Binds a borrowed <see cref="ComponentVM{M}"/> to XAML. The VM's creator
/// constructs and disposes it; this adapter only observes it.
/// </summary>
public sealed class BindableVm<M> : INotifyPropertyChanged, IDisposable
{
private readonly ComponentVM<M> _vm;
private readonly IDisposable _subscription;
/// <param name="vm">The VM to show.</param>
/// <param name="host">
/// The UI thread's scheduler, for example a
/// <see cref="SynchronizationContextScheduler"/> created on the UI thread.
/// </param>
public BindableVm(ComponentVM<M> vm, IScheduler host)
{
_vm = vm;
// ComponentVM<M> implements INotifyPropertyChanged: it raises Model on
// assignment and Status on every lifecycle transition, on the thread
// that made the change. Forward each notification on the UI thread.
// Hub messages are not forwarded: the hub carries no Status change,
// and its Model message would notify a second time.
_subscription = Observable
.FromEventPattern<PropertyChangedEventHandler, PropertyChangedEventArgs>(
handler => vm.PropertyChanged += handler,
handler => vm.PropertyChanged -= handler)
.ObserveOn(host)
.Subscribe(change => PropertyChanged?.Invoke(this, change.EventArgs));
}
/// <summary>The VM's model; assigning it replaces the VM's model.</summary>
public M Model
{
get => _vm.Model;
set => _vm.Model = value;
}
/// <summary>The VM's name.</summary>
public string Name => _vm.Name;
/// <summary>The VM's lifecycle status.</summary>
public ConstructionStatus Status => _vm.Status;
/// <inheritdoc/>
public event PropertyChangedEventHandler? PropertyChanged;
/// <summary>Stops observing the VM without changing its lifecycle.</summary>
public void Dispose() => _subscription.Dispose();
}
Create the adapter on the main thread, for example in the page constructor,
and set it as the page's BindingContext:
BindingContext = new BindableVm<Note>(
vm, new SynchronizationContextScheduler(SynchronizationContext.Current!));
- One notification path. Forward
PropertyChangedonly. Forwarding the hub'sPropertyChangedMessageas well notifiesModeltwice, and the hub never carriesStatus. - Current values at once.
Model,Name, andStatusread through to the VM, so a binding shows the VM's current values as soon as it attaches. - UI thread only. The VM raises
PropertyChangedon the thread that made the change, such as a worker or a background construct. The adapter re-raises each notification throughhost, so bound state is only touched on the UI thread. The dispatcher'sForegroundscheduler decides where VMx completes lifecycle work; it does not move your subscriptions. - Borrowed lifetime.
Dispose()detaches the adapter and nothing else. The code that created the VM constructs and disposes it.
9.4.4. Fuller example¶
No MAUI Notes-Showcase ships yet. The WPF and Avalonia adapters share the same shape — see wpf.md and avalonia.md for copyable starting points. Microsoft's MAUI MVVM docs cover the framework-side mechanics.