Skip to content

nventive/Chinook.DynamicMvvm

Repository files navigation

Chinook.DynamicMvvm

License Version Downloads

The Chinook.DynamicMvvm packages assists in .Net MVVM (Model - View - ViewModel) development.

Cornerstones

  • Highly Extensible
    • Everything is interface-based to easily allow more implementations.
    • A single framework can't cover everything. Our architecture is designed in a way that allows you to integrate your favorites tools easily.
  • Declarative Syntax
    • We aim to understand the behavior of a property by glancing at its declaration.

More like this

The Chinook namespace has other recipes for .Net MVVM applications.

Getting Started

  1. Add the Chinook.DynamicMvvm nuget package to your project.

  2. Create your first ViewModel. Here's one that covers the basics.

    using Chinook.DynamicMvvm;
    // (...)
    public class MainPageViewModel : ViewModelBase
    {
      public string Content
      {
        get => this.Get(initialValue: string.Empty);
        set => this.Set(value);
      }
    
      public IDynamicCommand Submit => this.GetCommand(() =>
      {
        Result = Content;
      });
    
      public string Result
      {
        get => this.Get(initialValue: string.Empty);
        private set => this.Set(value);
      }
    }

    πŸ’‘ Want to go fast? We recommend installing the Chinook Snippets Visual Studio Extension to benefit from code snippets. All our snippets start with "ck" (for "Chinook") and they will help you write those properties and commands extra fast.

  3. Set this MainPageViewModel as the DataContext of your MainPage in MainPage.xaml.cs.

    public MainPage()
    {
      this.InitializeComponent();
      DataContext = new MainPageViewModel();
    }

    Here is some xaml for that MainPage.xaml that demonstrates the basics.

    <Page x:Class="ChinookSample.MainPage"
       xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
      <StackPanel>
         <TextBox Text="{Binding Content, Mode=TwoWay}" />
    
         <Button Content="Submit"
                 Command="{Binding Submit}" />
    
         <TextBlock Text="{Binding Result}" />
      </StackPanel>
    </Page>
  4. Configure a System.IServiceProvider containing the following:

    • IDynamicCommandBuilderFactory
    • IDynamicPropertyFactory

    Here is a simple code sample that does that using Microsoft.Extensions.DependencyInjection and Microsoft.Extensions.Hosting.

    var serviceProvider = new HostBuilder()
      .ConfigureServices(serviceCollection => serviceCollection
        .AddSingleton<IDynamicCommandBuilderFactory, DynamicCommandBuilderFactory>()
        .AddSingleton<IDynamicPropertyFactory, DynamicPropertyFactory>()
      )
      .Build()
      .Services;
  5. Set the IServiceProvider into ViewModelBase.DefaultServiceProvider in the startup of your application.

    ViewModelBase.DefaultServiceProvider = serviceProvider;

    πŸ’‘ It's also possible to avoid using this public static provider and pass it via the constructor of ViewModelBase.

  6. You're all set. You can start your app!

Features

The previous setup is pretty basic. Let's see what else we can do!

Dispatcher

Set an IDispatcher to allow setting properties from any thread. The IDispatcher ensures the INotifyPropertyChanged.PropertyChanged event is raised on the main thread. This is optional, but you'll likely need it.

For WinUI or Uno.WinUI apps, install the Chinook.DynamicMvvm.Uno.WinUI nuget package. You can then use DispatcherQueueDispatcher or BatchingDispatcherQueueDispatcher.

public MainPage()
{
    this.InitializeComponent();
    DataContext = new MainPageViewModel()
    {
        Dispatcher = new DispatcherQueueDispatcher(this)
    };
}

Legacy

For UWP or Uno.UI apps, install the Chinook.DynamicMvvm.Uno nuget package. You can then use CoreDispatcherDispatcher or BatchingCoreDispatcherDispatcher.

public MainPage()
{
    this.InitializeComponent();
    DataContext = new MainPageViewModel()
    {
        Dispatcher = new CoreDispatcherDispatcher(this)
    };
}

Create simple properties

Using IViewModel.Get, you can declare ViewModel properties that will raise the INotifyPropertyChanged.PropertyChanged event of the ViewModel when set. Under the hood, an IDynamicProperty is lazy-initialized.

πŸ”¬ IDynamicProperty simply represents a property of a ViewModel. It has a name, a value, and an event to notify that the property's value changed. Having this interface is great because it allows the creation of custom implementations with various behaviors. You'll see that with the next sections of this document.

public string Content
{
  get => this.Get(initialValue: string.Empty);
  set => this.Set(value);
}

πŸ’‘ If you use Chinook Snippets, you can quickly generate a property from value using the snippets "ckpropv" (chinook property from value) or "ckpropvg" (chinook property from value get-only).

πŸŽ“ We like to call "dynamic properties" the properties of a ViewModel that are backed with a IDynamicProperty. You can still used regular properties in your ViewModels, but they will not raise the PropertyChanged event automatically when they change.

public string Title => "Hello"; // Regular property
public string Subtitle { get; } // Regular property

public long Counter => this.GetFromObservable(ObserveTimer()); // Dynamic Property
public bool IsFavorite // Dynamic property
{
  get => this.Get(initialValue: false);
  set => this.Set(value);
}

You should prefer regular properties over dynamic properties for data that never changes, simply because dynamic properties allocate more memory.

Create properties from IObservable<T>

If you're familiar with System.Reactive, you'll probably like this. Using IViewModel.GetFromObservable, you can declare a ViewModel property from an IObservable<T>. The property automatically updates itself when the observable pushes a new value.

using System.Reactive.Linq;
// (...)
public long Counter => this.GetFromObservable(Observable.Timer(
  dueTime: TimeSpan.Zero,
  period: TimeSpan.FromSeconds(1))
);		

πŸ’‘ If you use Chinook Snippets, you can quickly generate a property from observable using the snippets "ckpropo" (chinook property from observable) or "ckpropog" (chinook property from observable get-only).

πŸ’‘ Consider using the overload that takes a Func<IObservable<T>> to avoid evaluating the observable every time the property is read and potentially save some memory allocations.

Create properties from Task<T>

Using IViewModel.GetFromTask, you can create a property that updates itself based on a Task<T> result.

// This property is initialized with the value 10, but changes to 100 after 1 second.
public int Number => this.GetFromTask(async ct =>
{
  await Task.Delay(1000, ct);
  return 100;
}, initialValue: 10);

πŸ’‘ If you use Chinook Snippets, you can quickly generate a property from task using the snippets "ckpropt" (chinook property from task) or "ckproptg" (chinook property from task get-only).

Decide whether you want a property setter

This could seem obvious, but any IDynamicProperty-backed property can be readonly by simply omitting the property setter.

public long Counter1 => this.GetFromObservable(ObserveTimer());

public long Counter2
{
  get => this.GetFromObservable(ObserveTimer());
  set => this.Set(value);
}

public long Counter3
{
  get => this.GetFromObservable(ObserveTimer());
  private set => this.Set(value);
}

private IObservable<long> ObserveTimer() => Observable.Timer(
  dueTime: TimeSpan.Zero,
  period: TimeSpan.FromSeconds(1));

private void SomeMethod()
{
  Counter1 = 0; // Doesn't build
  Counter2 = 0; // Builds
  Counter3 = 0; // Builds
}

In this code, all properties are updated from an observable. However,

  • Counter1 can't be set manually.
  • Counter2 can be set manually from anywhere (including TwoWay bindings).
  • Counter3 can be set manually only from the ViewModel (which excludes TwoWay bindings).

πŸ’‘ If you use Chinook Snippets, you can quickly generate a get-only property using the "ckprop" snippets ending with "g" (for get-only).

Access the underlying IDynamicProperty instance

You can access the backing IDynamicProperty<T> instance of any property from any ViewModel by using IViewModel.GetProperty().

public MainPageViewModel()
{
  IDynamicProperty<string> contentProperty;
  contentProperty = this.GetProperty(vm => vm.Content);
  // or
  contentProperty = this.GetProperty<string>(nameof(Content));
}

public string Content
{
  get => this.Get(initialValue: string.Empty);
  set => this.Set(value);
}

You can then interact with the property object itself.

contentProperty.Value = "Hello";
// This sets the property value. It also raises the PropertyChanged event on the ViewModel.

contentProperty.ValueChanged += prop => Console.WriteLine($"Property {prop.Name} changed to {prop.Value}.");
// This allows you to easily observe changes to a property value. Note that prop.Value is strongly type as a string here. 

πŸ’‘ With the IDynamicProperty instance, even a get-only property can be set manually. Use this knowledge responsively.

Observe a property value using IObservable

If you are used to System.Reactive, you can install the Chinook.DynamicMvvm.Reactive package to benefit from some more extensions methods.

IObservable<string> observable;
observable = contentProperty.Observe(); // Gets an IObservable that yields when the value changes.
observable = contentProperty.GetAndObserve(); // Gets an IObservable that yields when the value changes and starts with the current value.

Create commands from Action or Action<T>

Using IViewModel.GetCommand, you can create a command using an Action, or Action<T> when you want a command parameter.

public IDynamicCommand SayHi => this.GetCommand(() =>
{
  Console.WriteLine("Hi");
});

public IDynamicCommand SaySomething => this.GetCommand<string>(parameter =>
{
  Console.WriteLine(parameter);
});

πŸ’‘ If you use Chinook Snippets, you can quickly generate a command from Action using the snippets "ckcmda" (chinook command from action) or "ckcmdap" (chinook command from action with parameter).

Create commands from an async method

Using IViewModel.GetCommand, you can create a async command using a Func<Task>, or Func<T, Task> when you want a command parameter.

public IDynamicCommand WaitASecond => this.GetCommandFromTask(async ct =>
{
  await Task.Delay(1000, ct);
});

public IDynamicCommand Wait => this.GetCommandFromTask<int>(async (ct, parameter) =>
{
  await Task.Delay(TimeSpan.FromSeconds(parameter), ct);
});

πŸ’‘ If you use Chinook Snippets, you can quickly generate a command from Task using the snippets "ckcmdt" (chinook command from task) or "ckcmdtp" (chinook command from task with parameter).

The provided CancellationToken ct is cancelled when the IDynamicCommand is disposed. The command itself is disposed when the ViewModel is disposed. You decide when the ViewModel gets disposed.

πŸ’‘ Check out Chinook.Navigation if you want a ViewModel-based navigation system that automatically deals with disposing ViewModels.

Customize command behavior

The IDynamicCommand declarations come with an optional builder. You can use this builder to customize the behavior of any command.

πŸ”¬The command implementation is done using a strategy pattern (very similarly to the HTTP Message Handlers). The builder simply accumulates strategies and chains them together when the command is built.

You can create a base configuration for all your commands at the factory level.

.AddSingleton<IDynamicCommandBuilderFactory>(s =>
    new DynamicCommandBuilderFactory(builder => builder
        .WithLogs(s.GetRequiredService<ILogger<IDynamicCommand>>())
        .OnBackgroundThread()
))

You can add more configuration at the command declaration level. The builders are additive, meaning that the configuration at the command declaration level is applied after the one at the factory level.

public IDynamicCommand Submit => this.GetCommand(() =>
{
  Result = Content;
}, builder => builder
  .SkipWhileExecuting()
);

πŸ”¬ For the previous code the strategy chain looks like this:

- LoggerCommandStrategy (factory level)
 - BackgroundCommandStrategy (factory level)
  - SkipWhileExecutingCommandStrategy (command declaration level)
   - ActionCommandStrategy, which actually executes the method (command declaration level)

The strategy execution starts from the top and goes down the chain and then comes back up the chain for any subsequent processing. This means that strategies allow adding behavior both before and after the actual command execution.

Supported strategies

Observe whether a command is executing

IDynamicCommand adds an IsExecuting property and an IsExecutingChanged event to the classic System.Windows.Input.ICommand. IDynamicCommand and also implements INotifyPropertyChanged, meaning that you can do a XAML binding on IsExecuting.

πŸ’‘ This can be usefull if you want to add a loading indicator in your button's ControlTemplate.

Add child ViewModels

Using IViewModel.GetChild, you can declare an inner ViewModel in an existing ViewModel. This is great to extract repeating code or simply to separate concerns.

public SettingsViewModel Settings => this.GetChild<SettingsViewModel>();

⚠ When creating child ViewModels, it's important to use the GetChild, AttachChild, or AttachOrReplaceChild methods to ensure linking the IDispatcher of the child to its parent.

Consider the following code.

public SettingsViewModel Settings { get; } = new SettingsViewModel();

This might seem to work at first, but know that the IDispatcher of Settings is not set. Therefore, PropertyChanged events might not be raised on the correct thread, which could result in errors.

ItemViewModels

Child ViewModels are also very useful when using what we like to call ItemViewModels, meaning an item from a list. This recipe is quite powerful when you want to change a property on a list item without updating the whole list itself.

Here's an example where ItemViewModel.IsFavorite can be manipulated directly and any XAML binding to it will update as expected.

public class ItemViewModel : ViewModelBase
{
  public string Title { get; init; }
	
  public bool IsFavorite
  {
    get => this.Get(initialValue: false);
    set => this.Set(value);
  }
}

// (...)

public MainPageViewModel()
{
  IEnumerable<string> someSource = Enumerable
    .Range(0, 10)
    .Select(i => i.ToString());
  Items = someSource
    .Select(title => this.AttachChild(new ItemViewModel { Title = title }, name: title))
    .ToArray();
}

public ItemViewModel[] Items { get; }

πŸ’‘ ItemViewModels are great when individual list items change overtime. However, when your list items don't update themselves, you should probably avoid creating ItemViewModels.

Resolve services from a ViewModel

Using IViewModel.GetService, you can easily get a service from the service provider that you set. Note that the IServiceProvider is also directly exposed via IViewModel.ServiceProvider.

var logger = this.GetService<ILogger<MainPageViewModel>>();

Add disposables to a ViewModel

Using IViewModel.AddDisposable, you can add any IDisposable object to a IViewModel. When the ViewModel is disposed, all added disposables are disposed as well. You can also get or remove previously added disposables using IViewModel.TryGetDisposable and IViewModel.RemoveDisposable.

πŸ’‘ Adding disposables can be useful when subscribing to observables or events. You can easily setup the unsubscription to happen when the ViewModel is disposed.

Check out Chinook.Navigation if you want a ViewModel-based navigation system that automatically deals with disposing ViewModels.

πŸ”¬ IViewModel can be seen as a dictionary of IDisposable objects. IDynamicProperty and IDynamicCommand both implement IDisposable, so that's how they're actually stored. It's the same thing for child ViewModels. This architecture contributes to the great extensibility of this library. You can see Chinook.DataLoader as a demonstration of extensibility.

Add errors

IViewModel implements INotifyDataErrorInfo. You can use IViewModel.SetErrors and IViewModel.ClearErrors to manipulate the error info.

Add validation using FluentValidations

You can install Chinook.DynamicMvvm.FluentValidation package to gain access to helpful extension methods. The first step to add validation is to declare a validator on your ViewModel.

public class MainPageValidator : AbstractValidator<MainPageViewModel>
{
  public MainPageValidator()
  {
    RuleFor(vm => vm.Content).NotEmpty();
  }
}

You can add all validators of your app to the service provider by using this line in your configuration.

serviceCollection.AddValidatorsFromAssemblyContaining(typeof(App), ServiceLifetime.Singleton)

For any dynamic property, you can use IViewModel.AddValidation to automatically run the validation rules when a property's value changes.

public MainPageViewModel()
{
  this.AddValidation(this.GetProperty(vm => vm.Content));
}

You can also run the validation manually.

public IDynamicCommand Submit => this.GetCommandFromTask(async ct =>
{
  var result = await this.Validate(ct);
  if (result.IsValid)
  {
    Result = Content;
  }
});

Breaking Changes

Please consult BREAKING_CHANGES.md for more information about breaking changes and version history.

License

This project is licensed under the Apache 2.0 license - see the LICENSE file for details.

Contributing

Please read CONTRIBUTING.md for details on the process for contributing to this project.

Be mindful of our Code of Conduct.