ObservableModel
ObservableModel is the INPC / INPChanging base. SetProperty compares with EqualityComparer<T>.Default and exits without raising events when the value is unchanged. PropertyChangedEventArgs are cached by property name so a hot bind path does not allocate a new args object on every raise.
NotifyDependsOn raises a named set of dependents. Prefer that over PropertyChanged(null), which forces every binding to refresh. 1.0 is hand-written fields and properties — there is no [Notify] generator in this family yet.
public sealed class CounterViewModel : ViewModel
{
private int _count;
public int Count
{
get => _count;
set
{
if (SetProperty(ref _count, value))
{
NotifyDependsOn(nameof(Count), nameof(Label));
}
}
}
public string Label => $"Count {Count}";
}Core subsystems
ObservableModel implements INotifyPropertyChanged and INotifyPropertyChanging. SetProperty skips notify when the value is unchanged. Event args are cached by property name. NotifyDependsOn raises dependent properties without a PropertyChanged(null) blast. Hand-written SetProperty is the 1.0 path — there is no [Notify] generator in this family yet.
ViewModel adds Status, IsBusy, ViewModelCancellationToken, InitializeAsync / OnAppearingAsync / OnDisappearingAsync, and ExecuteAsync. Dispose cancels the token. The token stays readable after dispose (IsCancellationRequested is true). PageViewModel adds INavigable and optional INavigator / IDialogs. Typed args are applied via IAcceptNavArgs<T>.Accept / IAcceptNavQuery.Accept before initialize.
Construct (DI)
→ Accept(args) / Accept(query) when IAcceptNavArgs / IAcceptNavQuery
→ InitializeAsync(token) once
→ OnNavigatedToAsync(token)
→ OnAppearingAsync(token)
→ OnDisappearingAsync(token)
→ OnNavigatedFromAsync(token)
→ Dispose (cancels ViewModelCancellationToken)ModelCommand / AsyncModelCommand (and generic variants) sit on IOperationExecutor: CanExecute → concurrency gate → timeout → retry → execute → IsRunning → error sink → Outcome. ConcurrencyMode values are Prevent, CancelPrevious, Queue, Allow, and Replace. Timeout, retry, Debounce, and Throttle ship on AsyncCommandOptions. CanExecuteChanged, IsRunning, and State raise on IMainThread. CanExecuteChanged is a weak event so a Button on a popped page does not pin the command. ICommand.Execute never throws — failures go to IErrorSink / IDialogs. ExecuteAsync still rethrows.
AsyncState<T> is the bindable UI status object: Status, Data, Error, Exception, Timestamp, plus IsLoading / IsRefreshing / IsEmpty / HasError / IsSuccess. ViewModelStatus values are Idle, Loading, Refreshing, Saving, Success, Empty, Error, Offline, Unauthorized, and Cancelled. LoadAsync and RefreshAsync return Outcome<T>. Outcome / Outcome<T> is the library result type — named Outcome so it does not fight FluentResults, LanguageExt, or app-level Result<T> types.
IMessageHub defaults to weak subscribe. The handler signature is Action<TRecipient, TMessage> so the delegate does not capture the ViewModel. Strong subscribe is explicit. ObservableRangeCollection.AddRange / ReplaceRange raise one CollectionChanged Reset — required for mid and large lists.