Skip to content

3.2. Getting Started with VMx — C#

This tutorial walks you through building viewmodels with the VMx C# library. You will build a ComponentVM<UserModel>, a RelayCommand with a reactive trigger, and a CompositeVM<TabVM> with tab selection — all in a console or unit-test project.

For the contracts behind each type, see the component family, command families, and composite family.


3.2.1. Install

The source tree currently implements v3.23.0. The NuGet package is not published yet; use the package command after a csharp-v* release publishes it.

dotnet add package VMx

For local development from a checked-out clone, reference the project directly:

<!-- In your .csproj -->
<ItemGroup>
  <ProjectReference Include="path/to/langs/csharp/src/VMx/VMx.csproj" />
</ItemGroup>

3.2.2. Wire up IMessageHub and IDispatcher

Every viewmodel needs two services: a hub that carries messages between viewmodels and a dispatcher that knows about your UI thread.

3.2.2.1. Option A — manual construction (console / tests)

using VMx.Services;

// Both foreground and background = immediate (deterministic for console / tests).
// Mirrors RxDispatcher.immediate() in Python and TypeScript.
IDispatcher dispatcher = RxDispatcher.Immediate();

IMessageHub hub = new MessageHub();

3.2.2.2. Option B — dependency injection (VMx.Extensions.DependencyInjection)

Add the optional DI package:

<ProjectReference Include="path/to/langs/csharp/src/VMx.Extensions.DependencyInjection/
    VMx.Extensions.DependencyInjection.csproj" />

Then register in your host startup (WPF, MAUI, ASP.NET, etc.):

using Microsoft.Extensions.DependencyInjection;
using VMx.Extensions.DependencyInjection;

var services = new ServiceCollection();

// IMessageHub → singleton MessageHub
// IDispatcher → singleton RxDispatcher bound to SynchronizationContext.Current
services.AddVMx();

// Or supply your own dispatcher factory:
services.AddVMx(opts =>
    opts.UseDispatcher(_ => new RxDispatcher(
        foreground: new SynchronizationContextScheduler(SynchronizationContext.Current!),
        background: TaskPoolScheduler.Default)));

var provider = services.BuildServiceProvider();
var hub        = provider.GetRequiredService<IMessageHub>();
var dispatcher = provider.GetRequiredService<IDispatcher>();

3.2.3. Build a ComponentVM<UserModel>

ComponentVM<M> is the primary leaf viewmodel. It holds a typed model, fires IPropertyChangedMessage on the hub when the model changes, and participates in the five-state lifecycle (Destructed, Constructing, Constructed, Destructing) plus terminal Disposed.

using VMx.Components;
using VMx.Messages;
using VMx.Services;

// A simple domain model — use your own real types here.
public record UserModel(string Name, string Email);

// Build the viewmodel — builder is immutable; every setter returns a new instance.
ComponentVM<UserModel> userVM =
    ComponentVM<UserModel>.Builder()
        .Name("user-card")
        .Model(new UserModel("Alice", "alice@example.com"))
        .Services(hub, dispatcher)
        // Optional: derive a display hint from the model.
        .ModeledHinter(m => m.Name)
        // Optional: callback when Model is set to a new value.
        .OnModelChanged(m => Console.WriteLine($"Model updated → {m.Name}"))
        .OnConstruct(() => Console.WriteLine("user-card constructed"))
        .OnDestruct(() => Console.WriteLine("user-card destructed"))
        .Build();

// Subscribe to PropertyChangedMessage BEFORE constructing so you don't miss it.
hub.Messages
   .OfType<IPropertyChangedMessage<IComponentVM>>()
   .Where(msg => msg.Sender == userVM)
   .Subscribe(msg =>
       Console.WriteLine($"Property '{msg.PropertyName}' changed on {msg.Sender.Name}"));

// Construct transitions the VM: Destructed → Constructing → Constructed.
// The VM publishes ConstructionStatusChangedMessage on the hub for each
// transition and fires its OnConstruct callback when entering Constructing.
userVM.Construct();

// Update the model — triggers OnModelChanged and publishes PropertyChangedMessage.
userVM.Model = new UserModel("Alice Smith", "asmith@example.com");

Console.WriteLine(userVM.ModeledHint);  // "Alice Smith"  (ModeledHinter result)

See the component family for the full IComponentVM<M> contract and Services, Messages & Dispatching for the message schema.


3.2.4. Build a RelayCommand

RelayCommand wraps a nullable Action, an optional Func<bool> predicate, and a set of IObservable<Unit> triggers that re-evaluate CanExecute.

using System.Reactive;
using System.Reactive.Subjects;
using System.Windows.Input;
using VMx.Commands;
using VMx.Services;

// A subject you fire whenever the predicate outcome may have changed.
var canSaveTrigger = new Subject<Unit>();

bool isDirty = false;

ICommand saveCommand =
    RelayCommand.Builder()
        .Task(() =>
        {
            Console.WriteLine("Saving…");
            isDirty = false;
            canSaveTrigger.OnNext(Unit.Default);   // re-evaluate CanExecute
        })
        .Predicate(() => isDirty)
        .Triggers(canSaveTrigger)
        .Build();

// CanExecute is false until isDirty is true.
Console.WriteLine(saveCommand.CanExecute(null));   // False

isDirty = true;
canSaveTrigger.OnNext(Unit.Default);               // fires CanExecuteChanged
Console.WriteLine(saveCommand.CanExecute(null));   // True

saveCommand.Execute(null);                         // prints "Saving…"
Console.WriteLine(saveCommand.CanExecute(null));   // False again

// Dispose to unsubscribe all trigger subscriptions.
((IDisposable)saveCommand).Dispose();

See command families for the full command contract including the "predicate-false gates Execute" rule (CMD-003).


3.2.5. Build a CompositeVM<TabVM>

CompositeVM<VM> owns an ordered child collection and a Current selection. Children are built ahead of time and handed in via a factory; the factory runs lazily on the first Construct.

using VMx.Components;
using VMx.Composites;
using VMx.Messages;

// A tab viewmodel — a ComponentVM wrapping a tab model.
public record TabModel(string Title);

// Build two tab children. They share the same hub and dispatcher.
ComponentVM<TabModel> tab1 =
    ComponentVM<TabModel>.Builder()
        .Name("home-tab")
        .Model(new TabModel("Home"))
        .Services(hub, dispatcher)
        .Build();

ComponentVM<TabModel> tab2 =
    ComponentVM<TabModel>.Builder()
        .Name("settings-tab")
        .Model(new TabModel("Settings"))
        .Services(hub, dispatcher)
        .Build();

// Build the composite. The children factory is evaluated on Construct.
CompositeVM<ComponentVM<TabModel>> tabs =
    CompositeVM<ComponentVM<TabModel>>.Builder()
        .Name("tab-bar")
        .Services(hub, dispatcher)
        .Children(() => [tab1, tab2])
        .OnConstruct(() => Console.WriteLine("tab-bar ready"))
        .Build();

// Watch for Current changes on the hub.
hub.Messages
   .OfType<IPropertyChangedMessage<IComponentVM>>()
   .Where(msg => msg.Sender == tabs && msg.PropertyName == nameof(tabs.Current))
   .Subscribe(msg =>
   {
       Console.WriteLine($"Selected tab: {tabs.Current?.Model.Title ?? "(none)"}");
   });

// Construct cascades: the composite constructs itself, then each child.
tabs.Construct();

// Select a tab — publishes PropertyChangedMessage for Current, sets child.IsCurrent.
tabs.Current = tab2;  // prints "Selected tab: Settings"
tabs.Current = tab1;  // prints "Selected tab: Home"

See the composite family for the full ICompositeVM<VM> contract, including the IList<VM> / INotifyCollectionChanged semantics.


3.2.6. Lifecycle and cleanup

Every VM follows a five-state lifecycle: Destructed → Constructing → Constructed → Destructing → Destructed, plus the terminal Disposed.

// States are exposed via IComponentVM.Status (a ConstructionStatus enum).
Console.WriteLine(userVM.Status);  // Constructed (after Construct())

// Reconstruct is Destruct + Construct in one call. It is only valid from
// Constructed (CanReconstruct is true iff Status == Constructed); it
// round-trips through Destructed and back to Constructed.
userVM.Reconstruct();
Console.WriteLine(userVM.Status);  // Constructed

// Destruct transitions back to Destructed and runs OnDestruct.
userVM.Destruct();
Console.WriteLine(userVM.Status);  // Destructed

// Dispose is terminal and idempotent. Calling Construct() or Destruct() on a
// disposed VM raises StatusTransitionException.
userVM.Dispose();
Console.WriteLine(userVM.Status);  // Disposed

// CompositeVM.Dispose() disposes children then itself.
tabs.Dispose();

// MessageHub.Dispose() completes the underlying Rx Subject.
((IDisposable)hub).Dispose();

The C#-specific ConstructAsync(), DestructAsync(), and ReconstructAsync() methods complete after terminal lifecycle publication. If a background hook or deferred child cascade fails, the VM publishes its transactional rollback first and the returned task then faults with the original exception (ADR-0109). If terminal disposal wins the race, the waiter completes at Disposed and the abandoned transition cannot overwrite it.

See Lifecycle & Messaging for the transition table and the lifecycle contract (LIFE-001..015), including StatusTransitionException rules and admitted-hook/disposal coordination.


3.2.7. Threading

IDispatcher pairs two Rx schedulers:

Scheduler Typical mapping
dispatcher.Foreground UI thread (SynchronizationContext, Dispatcher)
dispatcher.Background Task pool / background threads

All hub observations delivered on Foreground are safe to bind directly to UI controls. Use ObserveOn to marshal:

hub.Messages
   .OfType<IPropertyChangedMessage<IComponentVM>>()
   .ObserveOn(dispatcher.Foreground)     // marshal to UI thread
   .Subscribe(msg => UpdateLabel(msg));  // safe to touch UI here

For background work (e.g. loading data before a Construct):

Observable
    .Start(() => LoadFromDatabase(), dispatcher.Background)
    .ObserveOn(dispatcher.Foreground)
    .Subscribe(data =>
    {
        userVM.Model = data;
        userVM.Construct();
    });

See Services, Messages & Dispatching for the THR-001..THR-004 conformance rules.


3.2.8. Where to go next

Resource Documentation page
Specification status Specification & Conformance
Lifecycle contract Lifecycle & Messaging
Messages & threading Services, Messages & Dispatching
Commands Command Families
Component contract Component Family
Composite contract Composite Family
Builders & tree Builders, Collections & Tree Utilities
Architecture Architecture Map
C# status C# Flavor
Examples Smaller Examples