Author and review Uno Platform code that follows MVUX (Model-View-Update-eXtended) best practices. Use when creating, editing, or reviewing presentation models in an Uno app — anything involving Feed, ListFeed, State, ListState, the Bindable* generated wrappers, async loaders, navigation between MVUX models, or commands invoked from XAML. Triggers on prompts like "add a page model", "create a view model", "expose a state", "wire up a command", "MVUX list", "feed vs state", "two-way binding in MVUX", "navigate with data", "convert MVVM to MVUX", or whenever generated code lives next to a `partial record SomethingModel`.
Installation
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Author and review Uno Platform code that follows MVUX (Model-View-Update-eXtended) best practices. Use when creating, editing, or reviewing presentation models in an Uno app — anything involving Feed, ListFeed, State, ListState, the Bindable* generated wrappers, async loaders, navigation between MVUX models, or commands invoked from XAML. Triggers on prompts like "add a page model", "create a view model", "expose a state", "wire up a command", "MVUX list", "feed vs state", "two-way binding in MVUX", "navigate with data", "convert MVVM to MVUX", or whenever generated code lives next to a `partial record SomethingModel`.
Uno Platform MVUX Best Practices
MVUX (Model-View-Update-eXtended) is Uno Platform's reactive presentation pattern. It replaces the hand-written ViewModel : ObservableObject + RelayCommand boilerplate of MVVM with immutable records, async data feeds, observable states, and source-generated bindable proxies. The compiler does the change-notification plumbing; the developer writes pure data and pure async functions.
This skill enforces the patterns that work and flags the ones that look like MVVM but break in MVUX.
When to Use This Skill
Creating or editing a *Model.cs file that is a partial record (MVUX page/component model)
Adding a Feed, ListFeed, State, or ListState property
Wiring a method on a model as a command in XAML
Sharing state across pages via a service event
Navigating from one MVUX model to another, especially when passing data
Reviewing PRs that mix MVVM idioms (ObservableObject, RelayCommand, Set(ref ...)) into an MVUX project
Resolving build errors KE0001 (record with Id not partial) or runtime "command does not fire" issues
Core Mental Model
MVVM
MVUX equivalent
Notes
class FooViewModel : ObservableObject
public partial record FooModel(...)
Records are immutable; the source generator emits a BindableFooModel wrapper for the View.
[ObservableProperty] private string _title;
IState<string> Title { get; }
A state is an observable cell. The bindable proxy exposes it as a normal property to XAML.
IRelayCommand RefreshCommand { get; }
public ValueTask Refresh(CancellationToken ct)
Public methods become commands automatically. The generator emits IAsyncCommand Refresh on the bindable proxy.
The feed handles loading state, errors, and cancellation.
The mental shift: the model is data; refresh is a function; the framework binds. You don't push values into the View — you describe how to compute them.
Mutable scalar: form fields, toggles, computed display strings refreshed on events
IListState<T>
yes
yes
Mutable list: filterable lists, lists that need to react to external events
Decision rule: start with the most restrictive (IFeed/IListFeed). Promote to IState/IListState only when you need to call UpdateAsync from an event handler or another method.
Async loaders
// Read-only feed from a service callpublic IFeed<Room?> Room { get; }
= Feed.Async(ct => sessions.GetRoomAsync(entity.RoomId, ct));
// Mutable list seeded asynchronously — note the `this` argumentpublic IListState<Item> Items { get; }
= ListState.Async(this, LoadAsync);
// Mutable scalar with a synchronous initial valuepublic IState<bool> IsFavorite { get; }
= State.Value(this, () => favorites.IsFavorite(entity.Id));
The this argument scopes the state's lifetime to the model — without it, two model instances will share the same state cell.
The lambda receives the current value (which may be null for first-load) and returns the next value. Never assign directly: Items = newList; won't compile and wouldn't notify if it did.
// In code-behind (rare — only for non-bindable controls like a custom back link)privateasyncvoidOnBackClick(object sender, RoutedEventArgs e)
=> await ((FrameworkElement)sender).Navigator()!
.NavigateViewModelAsync<SessionsModel>(this);
Records with an Id property MUST be partial
The MVUX IKeyEquatable source generator emits equality based on Id. If a record has an Id member it must be partial, or the build fails with KE0001.
<!-- DataContext is the generated BindableSessionsModel, not SessionsModel --><!-- Scalar state --><TextBlockText="{Binding TimeRange}" /><!-- Two-way scalar state --><ToggleSwitchIsOn="{Binding Use24HourFormat, Mode=TwoWay}" /><!-- List feed/state binds straight to ItemsSource --><ListViewItemsSource="{Binding Sessions}" /><!-- Method-as-command --><ButtonCommand="{Binding Refresh}" /><!-- Item-click as command (Uno Toolkit) --><ListViewIsItemClickEnabled="True"utu:CommandExtensions.Command="{Binding OpenSession}" /><!-- Reach the parent model from an item template --><ButtonCommand="{Binding ElementName=SessionsList, Path=DataContext.ToggleFavorite}"CommandParameter="{Binding}" /><!-- Empty-state via the IListFeed.None projection (auto-bindable) --><TextBlockText="No items yet"Visibility="{Binding Sessions.None}" />
Anti-Patterns
❌ Using a class instead of a record
// Won't get a Bindable wrapper. Bindings will silently fail or bind to the raw object.publicclassSessionsModel { ... }
✅ public partial record SessionsModel { ... }
❌ Forgetting partial
// KE0001 if record has an Id; otherwise the source generator can't emit the bindable wrapper.publicrecordSessionsModel { ... }
❌ Mutating state with assignment
Items = newList; // doesn't compile (init-only) and wouldn't notify if it did
Title.Value = "X"; // not the API
✅ await Items.UpdateAsync(_ => newList, ct);
❌ INotifyPropertyChanged / ObservableObject / [ObservableProperty] in an MVUX model
These are MVVM. In MVUX they add noise, hide bugs (the bindable wrapper exposes them differently), and signal that the author hasn't internalized the pattern.
❌ Storing the bindable wrapper as the model
// SpeakerDetailPage.xaml.csif (DataContext is SpeakerDetailModel m) { ... } // ❌ silently false
The runtime DataContext is BindableSpeakerDetailModel, not the record. Use the navigator extension instead:
The only acceptable use of async void is an EventHandler callback. Anywhere else it crashes the app on exception and breaks await.
❌ Subscribing to events without try/catch
External events (SettingsChanged, SignalR pushes, timer ticks) end up in async void handlers. An uncaught exception there is process-fatal. Always wrap.
❌ Long-running work in the constructor
Use Feed.Async(...) / ListState.Async(this, ...) to defer work until the View binds. A blocking LoadAsync().Result in a ctor will deadlock or stall navigation.
❌ Calling ToggleFavorite() from the page's code-behind
// ❌ Bypasses the bindable wrapper, can't be unit-tested, hides the dependency.privatevoidStar_Click(object sender, RoutedEventArgs e)
=> ((SessionDetailModel)DataContext).ToggleFavorite(CancellationToken.None);
✅ Bind the button's Command to {Binding ToggleFavorite}.
❌ Sharing a State.Value(...) without the model instance
C# Markup global usings shadow Path. When <UnoFeatures> includes CSharpMarkup, Path resolves to Microsoft.UI.Xaml.Shapes.Path. Always write System.IO.Path.Combine(...) in services.
Region navigator with Visibility caches both the page AND the bound MVUX model. This is the single biggest gotcha in this codebase. Detail pages declared as siblings under <Grid uen:Region.Attached="True" uen:Region.Navigator="Visibility"> are pre-instantiated, and on NavigateViewModelAsync<T> / NavigateRouteAsync the framework only toggles Visibility on the existing page. The model's constructor runs once, on the first navigation that needs it. Any data: argument passed via DataViewMap<TPage, TModel, TData> is honoured only on that first navigation — every subsequent navigation re-uses the cached model with the original entity. Symptom: "every session detail shows the first session I clicked".
Workaround (used in DwxCompanion): push the selection onto a singleton service (ISessionService.SelectedSession + SelectedSessionChanged event + SelectSessionAsync setter). Master models call SelectXAsync(payload, ct)thenNavigateRouteAsync(this, "RouteName", cancellation: ct). Detail models inject the service, expose IStates, and re-project from SelectedX in a RefreshAsync(ct) handler subscribed to the change event. Replace DataViewMap<…, …, T>() with ViewMap<…, …>() on the affected routes.
C# Markup sub-exception: if the detail page is C# Markup, its strongly-typed binding lambdas (() => vm.Speaker.Initials) require Speaker on the model to be of type Speaker, not IState<Speaker?>. In that case make the detail model a plain sealed class : INotifyPropertyChanged with a computed Speaker => _sessions.SelectedSpeaker getter and fire PropertyChanged(nameof(Speaker)) on SelectedSpeakerChanged. Document this as an MVUX exception with a comment block — it is the only place in the codebase where INPC is acceptable. Threading caveat: unlike pure MVUX models (whose IState<T>.UpdateAsync is dispatched onto the UI thread by the source-generated Bindable* proxy before any binding is notified), an INPC class talks to WinUI bindings directly. SelectXAsync is invoked from MVUX item-click commands on the threadpool, so a naive PropertyChanged?.Invoke(...) will raise on a background thread and WinUI will throw System.Runtime.InteropServices.COMException ("''" / RPC_E_WRONG_THREAD) the next time it tries to update the bound TextBlock. Fix: capture App.MainWindow.DispatcherQueue at host startup (e.g. App.UIDispatcher) and have the handler do if (!_dispatcher.HasThreadAccess) _dispatcher.TryEnqueue(RaisePropertyChanged); else RaisePropertyChanged();. Don't rely on DispatcherQueue.GetForCurrentThread() inside the model's ctor — the ctor itself can run off-thread.
MVUX FeedsGenerator chokes on C# reserved-word property names. The generator emits a lambda parameter that is char.ToLower(propertyName[0]) + propertyName[1..]. If a record member is named Abstract, the generated lambda is (session, abstract) => …, which is a parser error and cascades into a wall of CS9348 / CS0102 / CS0246 errors all pointing into the generated …BindableModelGenerator…/<Type>.g.cs. The bindable wrapper is only generated when the type appears as IState<T> / IListState<T> somewhere — so the failure can appear suddenly when you wrap an existing record in a state. Rename the offending member to a non-keyword equivalent (e.g. Abstract → Summary, Event → EventInfo, Class → Category). Set <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles> on the csproj temporarily and look under obj/Debug/<TFM>/generated/Uno.Extensions.Reactive.Generator/ to confirm the offending lambda parameter.
State<TRefType?> produces nullability warnings (CS8714, CS8621) because the MVUX State<T> API declares T : notnull. The runtime tolerates nullable reference types fine, so suppress at file scope with #pragma warning disable CS8714, CS8621 rather than rewriting the model. (Value types use Nullable<T> and don't trip this.)
State<IImmutableList<T>>.Value(this, () => ImmutableList<T>.Empty) is ambiguous between the Func<T> and Func<Option<T>> overloads because ImmutableList<T> implements IImmutableList<T> exactly. Cast the lambda result: () => (IImmutableList<T>)ImmutableList<T>.Empty.
WASM LocalApplicationData is emulated against IndexedDB. JSON file I/O works, but a stale bin/ or obj/ after asset changes can cause the splash to hang. Delete and rebuild.
ApplicationData.Current.LocalSettings throws InvalidOperationException on Windows unpackaged. For settings persistence prefer JSON file under Environment.GetFolderPath(SpecialFolder.LocalApplicationData).
Quick Decision Flow
Need to expose data to the View?
├── Read-only async value → IFeed<T> via Feed.Async(ct => …)
├── Read-only async list → IListFeed<T> via ListFeed.Async(ct => …)
├── Mutable scalar → IState<T> via State.Value(this, () => …)
└── Mutable list → IListState<T> via ListState.Async(this, LoadAsync)
Need the user to trigger something?
└── Public ValueTask method on the model. Bind {Binding MethodName} in XAML.
Need state to react to a service event?
└── Subscribe in ctor → async void handler with try/catch → State.UpdateAsync(_ => newValue, default)
Need to navigate with data?
├── First navigation only / no re-navigation → DataViewMap<TPage, TModel, TData>() + ctor takes TData first
│ + caller does _navigator.NavigateViewModelAsync<TModel>(this, data: payload, cancellation: ct)
└── Re-navigable detail page under Visibility → ViewMap<TPage, TModel>() (NO data) — the navigator caches the model.
region (THE COMMON CASE for masters/details) Push selection onto a singleton service, then NavigateRouteAsync.
See "Pitfalls Specific to Uno" → "Region navigator with Visibility caches…".
Review Checklist
When reviewing or generating an MVUX model, verify all of:
public partial record (not class, not sealed)
Records with Id are partial
Constructor receives services via DI; no service locator
Properties are IFeed/IListFeed/IState/IListState — never ObservableCollection, never [ObservableProperty]
State initialized via Feed.Async, ListFeed.Async, State.Value(this, …), or ListState.Async(this, …)
State.Value and ListState.Async pass this so state is per-instance
Methods exposed as commands return ValueTask and accept CancellationToken last
Updates use UpdateAsync(current => next, ct) — no direct assignment
async void only in event handlers, always wrapped in try/catch
Navigation between models uses INavigator.NavigateViewModelAsync<TModel>(this, …), not page types
Typed nav data is registered with DataViewMap<TPage, TModel, TData> and received as the first ctor param
Code-behind contains only navigation glue (or nothing)
No leftover MVVM scaffolding (ObservableObject, RelayCommand, Set(ref ...))