Table of Contents

PropertyState<TParent, TProperty> Class

Provides two-way data binding to individual properties of a record or class stored in a State<T>.

Namespace: DevBitsLab.Feeds
Assembly: DevBitsLab.Feeds.dll

public sealed class PropertyState<TParent, TProperty> : INotifyPropertyChanged, IDisposable
    where TParent : class

Overview

PropertyState<TParent, TProperty> solves a common MVVM challenge: two-way binding to individual properties of an immutable record.

When using State<Person> with a record type, you cannot directly two-way bind to State.Value.Name because records are immutable. PropertyState bridges this gap by:

  • Providing a Value property that can be two-way bound
  • Automatically creating a new record instance using with expressions when the value changes
  • Propagating changes back to the parent State<T>
  • Forwarding IsDirty and validation state from the parent

Use Cases

1. Form Editing with Records

Problem: You have a Person record and want to edit individual fields in a form.

public record Person(string FirstName, string LastName, string Email, int Age);

public partial class PersonEditorViewModel : ObservableObject, IDisposable
{
    private readonly State<Person> _personState;
    
    public PropertyState<Person, string> FirstName { get; }
    public PropertyState<Person, string> LastName { get; }
    public PropertyState<Person, string> Email { get; }
    public PropertyState<Person, int> Age { get; }
    
    public PersonEditorViewModel(Person person)
    {
        _personState = State<Person>.FromValue(person, SavePersonAsync);
        
        FirstName = _personState.Property(p => p.FirstName, (p, v) => p with { FirstName = v });
        LastName = _personState.Property(p => p.LastName, (p, v) => p with { LastName = v });
        Email = _personState.Property(p => p.Email, (p, v) => p with { Email = v });
        Age = _personState.Property(p => p.Age, (p, v) => p with { Age = v });
    }
    
    public bool IsDirty => _personState.IsDirty;
    
    public async Task SaveAsync() => await _personState.SaveAsync();
    
    public void Dispose()
    {
        FirstName.Dispose();
        LastName.Dispose();
        Email.Dispose();
        Age.Dispose();
    }
    
    private Task SavePersonAsync(Person person, CancellationToken ct) =>
        personService.UpdateAsync(person, ct);
}

XAML:

<StackPanel>
    <TextBox Header="First Name" 
             Text="{x:Bind ViewModel.FirstName.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
    <TextBox Header="Last Name" 
             Text="{x:Bind ViewModel.LastName.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
    <TextBox Header="Email" 
             Text="{x:Bind ViewModel.Email.Value, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
    <NumberBox Header="Age" 
               Value="{x:Bind ViewModel.Age.Value, Mode=TwoWay}" />
    
    <Button Content="Save" 
            Command="{x:Bind ViewModel.SaveCommand}"
            IsEnabled="{x:Bind ViewModel.IsDirty, Mode=OneWay}" />
</StackPanel>

2. Settings Editor with Nested Records

Problem: You have nested configuration records and need to edit properties at different levels.

public record AppSettings(
    ThemeSettings Theme,
    NotificationSettings Notifications,
    string Language);

public record ThemeSettings(string Name, bool IsDarkMode, int FontSize);
public record NotificationSettings(bool Enabled, bool Sound, bool Vibration);

public partial class SettingsViewModel : ObservableObject, IDisposable
{
    private readonly State<AppSettings> _settingsState;
    
    // Direct properties
    public PropertyState<AppSettings, string> Language { get; }
    
    // Nested theme properties - use lambda composition
    public PropertyState<AppSettings, string> ThemeName { get; }
    public PropertyState<AppSettings, bool> IsDarkMode { get; }
    public PropertyState<AppSettings, int> FontSize { get; }
    
    // Nested notification properties
    public PropertyState<AppSettings, bool> NotificationsEnabled { get; }
    public PropertyState<AppSettings, bool> SoundEnabled { get; }
    
    public SettingsViewModel()
    {
        _settingsState = State<AppSettings>.Create(
            LoadSettingsAsync,
            SaveSettingsAsync);
        
        Language = _settingsState.Property(
            s => s.Language,
            (s, v) => s with { Language = v });
        
        // Nested properties require composing the with expressions
        ThemeName = _settingsState.Property(
            s => s.Theme.Name,
            (s, v) => s with { Theme = s.Theme with { Name = v } });
        
        IsDarkMode = _settingsState.Property(
            s => s.Theme.IsDarkMode,
            (s, v) => s with { Theme = s.Theme with { IsDarkMode = v } });
        
        FontSize = _settingsState.Property(
            s => s.Theme.FontSize,
            (s, v) => s with { Theme = s.Theme with { FontSize = v } });
        
        NotificationsEnabled = _settingsState.Property(
            s => s.Notifications.Enabled,
            (s, v) => s with { Notifications = s.Notifications with { Enabled = v } });
        
        SoundEnabled = _settingsState.Property(
            s => s.Notifications.Sound,
            (s, v) => s with { Notifications = s.Notifications with { Sound = v } });
    }
    
    // ... Dispose implementation
}

3. Master-Detail with Shared Dirty Tracking

Problem: You have a list of items and a detail editor, and you want to track if any edits have been made.

public partial class OrderViewModel : ObservableObject
{
    private readonly State<Order> _orderState;
    
    public PropertyState<Order, string> CustomerName { get; }
    public PropertyState<Order, string> ShippingAddress { get; }
    public PropertyState<Order, DateTime> DeliveryDate { get; }
    
    // All properties share the same dirty state
    public bool HasUnsavedChanges => _orderState.IsDirty;
    
    public OrderViewModel(Order order)
    {
        _orderState = State<Order>.FromValue(order, SaveOrderAsync);
        
        CustomerName = _orderState.Property(o => o.CustomerName, (o, v) => o with { CustomerName = v });
        ShippingAddress = _orderState.Property(o => o.ShippingAddress, (o, v) => o with { ShippingAddress = v });
        DeliveryDate = _orderState.Property(o => o.DeliveryDate, (o, v) => o with { DeliveryDate = v });
        
        // Subscribe to dirty changes
        _orderState.DirtyChanged += (s, isDirty) => OnPropertyChanged(nameof(HasUnsavedChanges));
    }
}

4. Wizard with Multi-Step Form

Problem: A multi-step wizard where each step edits different properties of the same record.

public record RegistrationData(
    string Email,
    string Password,
    string FirstName,
    string LastName,
    string Phone,
    Address Address);

// Step 1: Account details
public class AccountStepViewModel
{
    public PropertyState<RegistrationData, string> Email { get; }
    public PropertyState<RegistrationData, string> Password { get; }
    
    public AccountStepViewModel(State<RegistrationData> state)
    {
        Email = state.Property(r => r.Email, (r, v) => r with { Email = v });
        Password = state.Property(r => r.Password, (r, v) => r with { Password = v });
    }
}

// Step 2: Personal info
public class PersonalStepViewModel
{
    public PropertyState<RegistrationData, string> FirstName { get; }
    public PropertyState<RegistrationData, string> LastName { get; }
    public PropertyState<RegistrationData, string> Phone { get; }
    
    public PersonalStepViewModel(State<RegistrationData> state)
    {
        FirstName = state.Property(r => r.FirstName, (r, v) => r with { FirstName = v });
        LastName = state.Property(r => r.LastName, (r, v) => r with { LastName = v });
        Phone = state.Property(r => r.Phone, (r, v) => r with { Phone = v });
    }
}

// Main wizard - all steps share the same State<RegistrationData>
public class RegistrationWizardViewModel
{
    private readonly State<RegistrationData> _registrationState;
    
    public AccountStepViewModel AccountStep { get; }
    public PersonalStepViewModel PersonalStep { get; }
    
    public RegistrationWizardViewModel()
    {
        _registrationState = State<RegistrationData>.FromValue(
            new RegistrationData("", "", "", "", "", new Address()),
            SubmitRegistrationAsync);
        
        AccountStep = new AccountStepViewModel(_registrationState);
        PersonalStep = new PersonalStepViewModel(_registrationState);
    }
}

Creating PropertyState

Use the Property extension method on State<T>:

var personState = State<Person>.FromValue(new Person("John", 30));

var nameProperty = personState.Property(
    getter: p => p.Name,           // How to get the property
    setter: (p, v) => p with { Name = v }  // How to create new parent with updated property
);

Properties

Property Type Description
Value TProperty? Gets or sets the property value. Setting updates the parent.
HasValue bool Whether the parent state has a value.
IsDirty bool Whether the parent state has unsaved changes.
Errors IReadOnlyList<string> Validation errors from the parent.

Events

Event Description
PropertyChanged Raised when Value, IsDirty, or HasValue changes.

Thread Safety

PropertyState<TParent, TProperty> inherits thread safety from its parent State<T>. All operations are thread-safe.


Best Practices

1. Always Dispose PropertyStates

public class MyViewModel : IDisposable
{
    public PropertyState<Person, string> Name { get; }
    
    public void Dispose()
    {
        Name.Dispose();
    }
}

2. Use Records for Immutable Updates

// Good: Records with `with` expressions
public record Person(string Name, int Age);
var name = state.Property(p => p.Name, (p, v) => p with { Name = v });

// Works but verbose: Classes require manual cloning
public class Person { public string Name { get; set; } }
var name = state.Property(
    p => p.Name,
    (p, v) => new Person { Name = v, Age = p.Age, /* ... */ });

3. Share State Across ViewModels

// Parent ViewModel owns the State
public class ParentViewModel
{
    public State<AppData> AppState { get; }
}

// Child ViewModels create PropertyStates from shared State
public class ChildViewModel
{
    public PropertyState<AppData, string> Setting { get; }
    
    public ChildViewModel(State<AppData> sharedState)
    {
        Setting = sharedState.Property(s => s.Setting, (s, v) => s with { Setting = v });
    }
}

See Also