Skip to content

Properties backed by observables

Run the complete page example.

A view model often needs a read-only property whose value comes from a stream: a full name built from two other properties, a count of unfinished items, a label that depends on several fields. ToProperty turns a stream into that kind of property. It returns an ObservableAsPropertyHelper<T>, a helper that keeps the latest value and raises your type's own change notification each time a new one arrives.

Back a property with a stream

1. Get a stream. Observing covers WhenChanged and the other ways to get one.

2. Call ToProperty. Pass the object that declares the property and a lambda that names it, of the form x => x.Property. Keep the returned helper in a field.

3. Read the helper's Value. The property getter returns it.

_titleHelper = item.WhenChanged(static x => x.Title).ToProperty(this, static x => x.Title);
public string Title => _titleHelper.Value;

When to use a backed property

Use a backed property for a read-only value that a stream computes: a value derived from other properties, a running count, or a status label. Give the object a settable property instead when a view or a command writes to it directly; a backed property only ever changes because its source produced a new value.

A backed property composes with the rest of the library the same way any other stream does. Chain WhenChanged with Where and Select before ToProperty to filter or transform the values, and combine two properties with WhenChanged(x => x.A, x => x.B, (a, b) => ...) before backing a third property with the result.

_fullNameHelper = this.WhenChanged(static x => x.FirstName, static x => x.LastName, static (first, last) => $"{first} {last}")
    .ToProperty(this, static x => x.FullName);

A source that only produces a value when something asks for it, such as a hot connection that starts on first subscribe, pairs well with deferSubscription: true: nothing runs until a view first reads the property.

The helper delivers the source's current value as soon as it subscribes, then a new value each time the source produces one. A value equal to the one already stored is skipped.

How the generator raises your notification

A type can only raise its own change notifications, so ToProperty needs a way in. The generator uses the first of these your type offers.

Your typeHow the generator raises the notification
A public or internal RaisePropertyChanged, OnPropertyChanged, NotifyPropertyChanged or NotifyOfPropertyChange methodCalls that method directly. The type does not have to be partial.
A partial typeAdds one small member to your type. The member invokes your own PropertyChanged event, or calls a protected raise method your base class declares.

Both routes prefer an overload that takes PropertyChangedEventArgs over one that takes a name, because the generator can then pass one cached instance per property: raising a notification allocates nothing.

The example project shows all three shapes side by side.

A type with its own event

PartialOwnEventViewModel declares PropertyChanged itself. Only code inside the type can invoke a field-like event, so the type has to stay partial: the generator adds the member that invokes it on the type's behalf.

public sealed partial class PartialOwnEventViewModel : INotifyPropertyChanged
{
    private readonly ObservableAsPropertyHelper<string> _notesHelper;
    private readonly ObservableAsPropertyHelper<string> _titleHelper;

    public PartialOwnEventViewModel(TodoItem item)
    {
        _titleHelper = item.WhenChanged(static x => x.Title).ToProperty(this, static x => x.Title);
        _ = item.WhenChanged(static x => x.Notes).ToProperty(this, static x => x.Notes, out _notesHelper);
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    public string Title => _titleHelper.Value;

    public string Notes => _notesHelper.Value;
}

Notes uses the overload that returns the helper through an out parameter instead of the method's own return value, so you can assign a readonly field from an expression body or a field initializer that never runs the constructor body.

var item = new TodoItem { Title = "Renew car registration", Notes = "Bring the insurance certificate" };
var viewModel = new PartialOwnEventViewModel(item);

Console.WriteLine(viewModel.Title);
Console.WriteLine(viewModel.Notes);

item.Title = "Renew car registration online";

Console.WriteLine(viewModel.Title);
Renew car registration
Bring the insurance certificate
Renew car registration online

A type with a public raise method

RaiseMethodViewModel inherits RaisePropertyChanged from a base class, of the kind CommunityToolkit.Mvvm and Prism declare. The generator calls it directly, so the type itself does not need to be partial.

public sealed class RaiseMethodViewModel : RaiseMethodBase
{
    public static readonly string LoadingLabel = "Loading…";

    private readonly ObservableAsPropertyHelper<string> _remainingLabelHelper;
    private readonly ObservableAsPropertyHelper<TodoPriority> _priorityHelper;

    public RaiseMethodViewModel(TodoItem item)
    {
        _remainingLabelHelper = item.WhenChanged(static x => x.IsDone)
            .Select(static done => done ? "Done" : "Open")
            .ToProperty(this, static x => x.RemainingLabel, static () => LoadingLabel);
        _priorityHelper = item.WhenChanged(static x => x.Priority).ToProperty(this, nameof(Priority), TodoPriority.Normal);
    }

    public string RemainingLabel => _remainingLabelHelper.Value;

    public TodoPriority Priority => _priorityHelper.Value;
}

RemainingLabel names its property with a selector and an initial-value factory: the factory runs only if the helper needs a value before the stream produces one. Priority instead names its property with a constant, nameof(Priority), and a plain initial value. Name a property this way when you build the property name from a setting, or simply prefer not to repeat the selector.

var item = new TodoItem { IsDone = false, Priority = TodoPriority.High };
var viewModel = new RaiseMethodViewModel(item);

Console.WriteLine(viewModel.RemainingLabel);
Console.WriteLine(viewModel.Priority);

item.IsDone = true;

Console.WriteLine(viewModel.RemainingLabel);
Open
High
Done

A type with a protected base raise method

PartialProtectedBaseViewModel inherits ObservableObject, an example base class whose RaisePropertyChanged is protected. Code outside the type cannot call a protected member, so the generator falls back to the partial route: it adds a member to the type that calls the base method on the type's own behalf.

public sealed partial class PartialProtectedBaseViewModel : ObservableObject
{
    public static readonly string NoDueDateLabel = "No due date";

    private readonly ObservableAsPropertyHelper<bool> _isDoneHelper;
    private readonly ObservableAsPropertyHelper<string> _dueDateLabelHelper;

    public PartialProtectedBaseViewModel(TodoItem item, ISequencer scheduler)
    {
        _isDoneHelper = item.WhenChanged(static x => x.IsDone).ToProperty(this, static x => x.IsDone, deferSubscription: true);
        _dueDateLabelHelper = item.WhenChanged(static x => x.DueDate)
            .Select(static due => due is { } d ? d.ToString("yyyy-MM-dd") : NoDueDateLabel)
            .ToProperty(this, static x => x.DueDateLabel, scheduler);
    }

    public bool IsDone => _isDoneHelper.Value;

    public string DueDateLabel => _dueDateLabelHelper.Value;
}

IsDone passes deferSubscription: true, so the helper does not follow item until something first reads IsDone. DueDateLabel passes a scheduler, so its notifications wait for that scheduler to run; here the scheduler is a VirtualClock, a sequencer you step by hand, which the Threading page covers.

var item = new TodoItem { IsDone = false, DueDate = RegistrationDue };
VirtualClock scheduler = new();
var viewModel = new PartialProtectedBaseViewModel(item, scheduler);

item.IsDone = true;

// The first read subscribes. WhenChanged delivers the item's current value on subscribe, so it is not missed.
Console.WriteLine(viewModel.IsDone);

item.DueDate = null;

// The scheduler has not run yet, so DueDateLabel still holds its default value.
Console.WriteLine(viewModel.DueDateLabel);

scheduler.Start();

Console.WriteLine(viewModel.DueDateLabel);
True

No due date

Deferred subscription only delays when the helper starts following the source. Reading IsDone for the first time subscribes it, and WhenChanged delivers the item's current value as soon as anything subscribes, so the read still sees the up-to-date True. DueDateLabel, on the other hand, subscribed in the constructor but raises its notifications through the scheduler, so its value stays at its default (an empty line, since no initial value was given) until scheduler.Start() runs the queued work.

Name the property directly

Name the property with a lambda of the form x => x.Property, or with a constant such as nameof(Property). The overloads that take an initial value, an initial-value factory, deferSubscription, a scheduler, or the out parameter for the helper all work the same way; combine as many of them as you need. RXUIBIND012 warns you when the generator has no way to raise your type's notifications. RXUIBIND013 warns you when it cannot read the property's name. Either way the call generates nothing and throws when it runs. For those properties, call ToPropertyUnsafe, which finds the name and the raise member by reflection.

Below C# 13, name the initial value of a string property when you name the property with a lambda: ToProperty(this, x => x.Title, initialValue: "(untitled)"). Without the name the compiler cannot choose an overload, and RXUIBIND014 reports the argument to name. From C# 13, and with a nameof name at any version, the plain argument works.

Declare the property with an attribute

[ObservableAsProperty] writes the property's body and its backing field for you. Declare the property as partial and get-only, so every generator in the build can see it. The generator writes a field named _{name}Helper, which you assign in the constructor with ToProperty.

public sealed partial class SummaryViewModel : INotifyPropertyChanged
{
    public SummaryViewModel(TodoItem item)
    {
        _titleHelper = item.WhenChanged(static x => x.Title).ToProperty(this, static x => x.Title);
        _isDoneHelper = item.WhenChanged(static x => x.IsDone).ToProperty(this, static x => x.IsDone);
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    [ObservableAsProperty]
    public partial string Title { get; }

    [ObservableAsProperty]
    public partial bool IsDone { get; }
}
var item = new TodoItem { Title = "Renew car registration" };
var summary = new SummaryViewModel(item);

Console.WriteLine(summary.Title);
Console.WriteLine(summary.IsDone);

item.IsDone = true;

Console.WriteLine(summary.IsDone);
Renew car registration
False
True

[ObservableAsProperty] needs C# 13 or newer, because partial properties are a C# 13 feature.

Construct the helper directly

Most of the time ToProperty is all you need. Construct ObservableAsPropertyHelper<T> yourself when you already have callbacks that raise a change notification, or when a property has no source at all.

var item = new TodoItem { Title = "Renew car registration" };
var titles = item.WhenChanged(x => x.Title);

using var helper = new ObservableAsPropertyHelper<string>(
    titles,
    onChanged: static value => Console.WriteLine($"Now: {value}"),
    onChanging: static value => Console.WriteLine($"Was: {value}"));

item.Title = "Renew car registration online";
Was: 
Now: 
Was: Renew car registration
Now: Renew car registration
Was: Renew car registration online
Now: Renew car registration online

Starting the helper delivers its default value as a first notification (the blank Was:/Now: pair), then follows the source, whose own current value arrives as a second notification. onChanging runs just before the new value is stored, and onChanged runs just after; pass null for onChanging when you only need the value after it changes.

With no scheduler, or with the immediate one, a helper delivers a value on the thread that produced it. A value produced while an earlier one is still being delivered waits its turn, so notifications never overlap and always arrive in order.

Read errors instead of losing them

A source that fails sends its error to ThrownExceptions instead of ending the helper. Nothing observes that stream by default, so the error is rethrown on the thread that produced it. Subscribe to see it instead.

var item = new TodoItem { Title = "Renew car registration" };
var titles = item.WhenChanged(x => x.Title);
var failure = new InvalidOperationException("The title source failed.");
var withFailure = Signal.Concat(titles, Signal.Fail<string>(failure));

var helper = new ObservableAsPropertyHelper<string>(withFailure, Console.WriteLine);

using var subscription = helper.ThrownExceptions.Subscribe(static ex => Console.WriteLine(ex.Message));

item.Title = "Renew car registration online";

helper.Dispose();
Renew car registration
Renew car registration online
Not connected

A helper that never changes

ObservableAsPropertyHelper<T>.Default() returns a helper that holds a fixed value and never subscribes to anything. Use it for a property that has no source yet, such as a design-time or disconnected view model.

using var helper = ObservableAsPropertyHelper<string>.Default("Not connected");

Console.WriteLine(helper.Value);
Console.WriteLine(helper.IsSubscribed);
Not connected
False

Where this differs from ReactiveUI

ReactiveUI's ToProperty only accepts an IReactiveObject. Here it accepts any class the generator can raise notifications for, including a plain INotifyPropertyChanged type. A ReactiveUI ReactiveObject still raises through ReactiveUI, so its Changed stream and suppressed notifications behave as before.

ReactiveUI reads the property name from an expression tree while your code runs, and builds two delegates and a closure for every helper. Here the selector is a plain lambda of the form x => x.Property: the generator reads it when you build, so creating the property builds no expression tree, uses no reflection, and the helper takes one cached delegate per notification.

ReactiveUI's helper raises its notifications through the current-thread scheduler. Here a helper with no scheduler raises them on the thread that produced the value, in order, one at a time. ReactiveUI sends a helper's error to its default exception handler, which throws on the main thread; here the error goes to ThrownExceptions, and is rethrown where it happened only when nothing observes that stream.

The helper type has ReactiveUI's name, ObservableAsPropertyHelper<T>, in the ReactiveUI.Binding namespace. A file that imports both ReactiveUI and ReactiveUI.Binding has to name one of them in full, or give it an alias.

API reference

MemberWhat it doesType and valuesNotes
ToPropertyBacks a read-only property with an observable's latest value.Extension method on IObservable<TRet>. Takes the declaring object and a selector Func<TObj, TRet> of the form x => x.Property, or the property's name as a constant string. Optional: an initial value or a Func<TRet> factory for one, deferSubscription, an ISequencer? scheduler, and an out ObservableAsPropertyHelper<TRet> that also returns the helper. Returns ObservableAsPropertyHelper<TRet>.The selector has to be written inline at the call. A call the generator cannot read throws InvalidOperationException when it runs. RXUIBIND012 and RXUIBIND013 report why.
ObservableAsPropertyAttributeMarks a partial, get-only property as backed by an ObservableAsPropertyHelper<T>.Attribute, usable once per property.Needs C# 13 or newer. The generator writes the property body and a field named _{name}Helper, which you assign with ToProperty.

ObservableAsPropertyHelper<T>

DeclarationDescriptionParametersReturns
sealed class ObservableAsPropertyHelper<T> : IDisposableBacks a read-only "output property" with an observable.T is the type of the property value.—

Constructors

DeclarationDescriptionParametersNotes
ObservableAsPropertyHelper(IObservable<T?> observable, Action<T?> onChanged)Follows an observable and calls onChanged after each new value is stored.observable, onChanged.The property holds default(T) before the first value arrives.
ObservableAsPropertyHelper(IObservable<T?> observable, Action<T?> onChanged, T? initialValue)As above, with an initial value.Adds initialValue.—
ObservableAsPropertyHelper(IObservable<T?> observable, Action<T?> onChanged, T? initialValue, ISequencer? scheduler)As above, with a scheduler for the callback.Adds scheduler.null raises the callback on the producing thread.
ObservableAsPropertyHelper(IObservable<T?> observable, Action<T?> onChanged, T? initialValue, bool deferSubscription)As above, with deferred subscription.Adds deferSubscription.Subscribes the first time Value is read.
ObservableAsPropertyHelper(IObservable<T?> observable, Action<T?> onChanged, T? initialValue, bool deferSubscription, ISequencer? scheduler)Combines an initial value, deferred subscription and a scheduler.All four.—
ObservableAsPropertyHelper(IObservable<T?> observable, Action<T?> onChanged, Action<T?>? onChanging)Adds a callback run just before each new value is stored.onChanging may be null.—
ObservableAsPropertyHelper(IObservable<T?> observable, Action<T?> onChanged, Action<T?>? onChanging, T? initialValue)As above, with an initial value.——
ObservableAsPropertyHelper(IObservable<T?> observable, Action<T?> onChanged, Action<T?>? onChanging, T? initialValue, bool deferSubscription)As above, with deferred subscription.——
ObservableAsPropertyHelper(IObservable<T?> observable, Action<T?> onChanged, Action<T?>? onChanging, T? initialValue, bool deferSubscription, ISequencer? scheduler)As above, with a scheduler.——
ObservableAsPropertyHelper(IObservable<T?> observable, Action<T?> onChanged, Action<T?>? onChanging, Func<T?>? getInitialValue)Reads the initial value from a factory instead of a constant.getInitialValue may be null, which reads default(T).—
ObservableAsPropertyHelper(IObservable<T?> observable, Action<T?> onChanged, Action<T?>? onChanging, Func<T?>? getInitialValue, bool deferSubscription)As above, with deferred subscription.——
ObservableAsPropertyHelper(IObservable<T?> observable, Action<T?> onChanged, Func<T?> getInitialValue, bool deferSubscription)As above, with no onChanging callback.——
ObservableAsPropertyHelper(IObservable<T?> observable, Action<T?> onChanged, Action<T?>? onChanging, Func<T?>? getInitialValue, bool deferSubscription, ISequencer? scheduler)Combines a factory, deferred subscription and a scheduler.——

Properties

DeclarationDescriptionNotes
T Value { get; }The current value of the property.Subscribes first when subscription was deferred.
bool IsSubscribed { get; }Whether the helper has subscribed to its observable.Stays false with deferred subscription until Value is first read.
IObservable<Exception> ThrownExceptions { get; }An observable that reports each error the source produces.An error that arrives while nothing observes this stream is rethrown on the thread that produced it.

Methods

DeclarationDescriptionParametersReturns
static ObservableAsPropertyHelper<T> Default()Creates a helper that holds the default value and never changes.None.The helper.
static ObservableAsPropertyHelper<T> Default(T? initialValue)Creates a helper that holds one value and never changes.initialValue.The helper.
static ObservableAsPropertyHelper<T> Default(T? initialValue, ISequencer? scheduler)As above, with a scheduler.initialValue, scheduler.The helper.
void Dispose()Stops following the observable.None.The property keeps its last value.

Create is [EditorBrowsable(EditorBrowsableState.Never)]: generated ToProperty code calls it, but it is not meant to be called by hand.