Mvvm.Core

  • Commands: DelegateCommand, AsyncCommand and CompositeCommand
  • Task Extensions: for Async with void method
  • Mvvm: BindableBase, Validatable and ModelWrapper base classes
  • EventAggregator : allows to subscribe, publish and filter messages

Commands

DelegateCommand

Example

private bool _canSave;
public bool CanSave
{
    get { return _canSave; }
    set
    {
        if (SetProperty(ref _canSave, value))
            SaveCommand.RaiseCanExecuteChanged();
    }
}

private DelegateCommand _saveCommand;
public DelegateCommand SaveCommand =>
    _saveCommand ?? (_saveCommand = new DelegateCommand(ExecuteSaveCommand, CanExecuteSaveCommand));

private void ExecuteSaveCommand()
{
    // do something
}

AsyncCommand

Example

public class ShellViewModel : BindableBase
{
    private bool isBusy;
    public bool IsBusy
    {
        get { return isBusy; }
        set { SetProperty(ref isBusy, value); }
    }

    private IAsyncCommand myCommand;
    public IAsyncCommand MyCommand
    {
        get
        {
            if (myCommand == null)
                myCommand = new AsyncCommand(ExecuteMyCommand);
            return myCommand;
        }
    }

    private async Task ExecuteMyCommand()
    {
        IsBusy = true;

        await Task.Delay(3000); // do some work

        IsBusy = false;
    }
}

Supports cancellation:

myCommand.Cancel(); 
// other sample 
myCommand.CancellationTokenSource.CancelAfter(500);

Checks cancellation

if (myCommand.IsCancellationRequested)
    return;

Handle exception

myCommand = new AsyncCommand(ExecuteMyCommand, ex => { });

CompositeCommand

Example

public interface IApplicationCommands
{
    CompositeCommand SaveAllCommand { get; }
}
public class ApplicationCommands : IApplicationCommands
{
    public CompositeCommand SaveAllCommand { get; } = new CompositeCommand();
}

Inject the application commands in ShellViewModel (and bind the SaveAllCommand in the View)

public class ShellViewModel
{
    public ShellViewModel(IApplicationCommands applicationCommands)
    {
        ApplicationCommands = applicationCommands;
    }

    public IApplicationCommands ApplicationCommands { get; }
}

... Inject the application commands in other ViewModels and add commands to the composite command

public class TabViewModel : BindableBase
{
    private bool _canSave;
    private string _updatedText;
    private DelegateCommand _saveCommand;
    private readonly IApplicationCommands _applicationCommands;

    public TabViewModel(IApplicationCommands applicationCommands)
    {
        _applicationCommands = applicationCommands;
        _applicationCommands.SaveAllCommand.Register(SaveCommand);
    }

    public string UpdatedText
    {
        get { return _updatedText; }
        set { SetProperty(ref _updatedText, value); }
    }

    public bool CanSave
    {
        get { return _canSave; }
        set
        {
            if (SetProperty(ref _canSave, value))
                SaveCommand.RaiseCanExecuteChanged();
        }
    }

    public DelegateCommand SaveCommand =>
        _saveCommand ?? (_saveCommand = new DelegateCommand(ExecuteSaveCommand, CanExecuteSaveCommand));

    private void ExecuteSaveCommand() =>  UpdatedText = $"Save {DateTime.Now}";
}

The composite command can execute (all commands) only when each registered command can be executed.

Task Extensions

// call a task from void method
public void VoidMethod()
{
    DelayedTask().Await(completedCallback: () =>
    {
      
    });
}

public async Task DelayedTask()
{
    await Task.Delay(1000);
}

It's possible to intercept exception with errorCallback and configure await to true (for the UIThread) with continueOnCapturedContext (false by default)

Mvvm

BindableBase

Implements INotifyPropertyChanged interface.

Allows to observe a property and notify the view that a value has changed.

SetProperty

public class UserViewModel : BindableBase
{
    private string firstName;
    public string FirstName
    {
        get { return firstName; }
        set { SetProperty(ref firstName, value); }
    }
}

OnPropertyChanged

public class UserViewModel : BindableBase
{
    private string firstName;
    public string FirstName
    {
        get { return firstName; }
        set
        {
            SetProperty(ref firstName, value);
            OnPropertyChanged(nameof(FullName));
        }
    }

    private string lastName;
    public string LastName
    {
        get { return lastName; }
        set
        {
            SetProperty(ref lastName, value);
            OnPropertyChanged(nameof(FullName));
        }
    }

    public string FullName
    {
        get { return $"{firstName} {LastName}"; }
    }
}

Or with Linq Expression

public class User : BindableBase
{

    private string firstName;
    public string FirstName
    {
        get { return firstName; }
        set
        {
            firstName = value;
            OnPropertyChanged(() => FirstName);
        }
    }
}

Validation

When use :

  • ModelWrapper => Model (or entity) not editable without Data Annotations and/or INotifyPropertyChanged
  • ValidatableBindableBase => Model (or ViewModel) editable with SetProperty

ValidatableBindableBase

Sample validation with Data Annotations (default validator)

public class User : ValidatableBindableBase
{
    private string _firstName;
    private string _lastName;

    public int Id { get; set; }

    [Required]
    [StringLength(8)]
    [NotAllowedUser("Marie")]
    public string FirstName
    {
        get { return _firstName; }
        set { SetProperty(ref _firstName, value); }
    }

    [StringLength(3)]
    public string LastName
    {
        get { return _lastName; }
        set { SetProperty(ref _lastName, value); }
    }
}
// A custom Validation Attribute
public class NotAllowedUserAttribute : ValidationAttribute
{
    public NotAllowedUserAttribute(string name)
    {
        Name = name ?? throw new ArgumentNullException(nameof(name));
    }

    public string Name { get; }

    public override string FormatErrorMessage(string name) => Name + " is not allowed.";

    public override bool IsValid(object value) => !string.Equals(value.ToString(), Name, StringComparison.OrdinalIgnoreCase);
}

ViewModel Sample

public class SampleViewModel
{
    public SampleViewModel()
    {
        User = new User();
        User.ValidateOnPropertyChanged = true; // false by default
        User.ErrorsChanged += User_ErrorsChanged;
    }

    private void User_ErrorsChanged(object sender, DataErrorsChangedEventArgs e)
    {
        SaveCommand.RaiseCanExecuteChanged(); // Save button is disabled with errors
    }

    public User User { get; }

    private DelegateCommand _saveCommand;
    public DelegateCommand SaveCommand =>
        _saveCommand ?? (_saveCommand = new DelegateCommand(ExecuteSaveCommand, CanExecuteSaveCommand));

    private void ExecuteSaveCommand()
    {
        User.Validate();
    }

    private bool CanExecuteSaveCommand() => !User.HasErrors;
}

ModelWrapper

Sample with a custom validator: using FluentValidation

public class User
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class UserWrapper : ModelWrapper<User>
{
    public UserWrapper(User model) : base(model)
    {
    }

    public int Id { get { return Model.Id; } }

    public string FirstName
    {
        get { return GetValue<string>(); }
        set { SetValue(value); }
    }

    public string LastName
    {
        get { return GetValue<string>(); }
        set { SetValue(value); }
    }
}

Fluent Validator Adapter

public class UserValidator : AbstractValidator<User>
{
    public UserValidator()
    {
        RuleFor(x => x.FirstName).NotEmpty().MaximumLength(8)
            .MustAsync(async (firstName, cancellation) =>
        {
            await Task.Delay(200);
            return !string.Equals(firstName, "Marie", StringComparison.OrdinalIgnoreCase);
        }).WithMessage("Marie is not allowed"); ;
        RuleFor(x => x.LastName).MaximumLength(3);
    }
}

public class FluentValidatorAdapter<T> : MvvmLib.Mvvm.IValidator
{
    private readonly IValidator<T> _validator;

    public FluentValidatorAdapter(IValidator<T> validator)
    {
        _validator = validator ?? throw new ArgumentNullException(nameof(validator));
    }

    public IDictionary<string, string[]> Validate(object instance)
    {
        var validationResult = _validator.Validate((T)instance);
        return validationResult.ToDictionary();
    }

    public async Task<IDictionary<string, string[]>> ValidateAsync(object instance)
    {
        var validationResult = await _validator.ValidateAsync((T)instance);
        return validationResult.ToDictionary();
    }

    public IEnumerable<string> ValidateProperty(object instance, string propertyName)
    {
        var validationResult = _validator.Validate((T)instance, options => options.IncludeProperties(propertyName));
        return GetErrors(propertyName, validationResult);
    }

    public async Task<IEnumerable<string>> ValidatePropertyAsync(object instance, string propertyName)
    {
        var validationResult = await _validator.ValidateAsync((T)instance, options => options.IncludeProperties(propertyName));
        return GetErrors(propertyName, validationResult);
    }

    protected IEnumerable<string> GetErrors(string propertyName, ValidationResult validationResult)
    {
        if (!validationResult.IsValid)
        {
            var dictionary = validationResult.ToDictionary();
            if (dictionary.TryGetValue(propertyName, out var errors))
                return errors;
        }
        return new List<string>();
    }
}

ViewModel Sample

public class SampleViewModel
{
    public SampleViewModel()
    {
        User = new UserWrapper(new User());
        // configure
        User.ValidateOnPropertyChanged = true;
        User.Validator = new FluentValidatorAdapter<User>(new UserValidator());
        User.AsyncPropertyValidation = true; // Async validation support with Fluent Validation

        User.ErrorsChanged += User_ErrorsChanged;
    }

    private void User_ErrorsChanged(object sender, DataErrorsChangedEventArgs e)
    {
        SaveCommand.RaiseCanExecuteChanged();
    }

    public UserWrapper User { get; }

    private DelegateCommand _saveCommand;
    public DelegateCommand SaveCommand =>
        _saveCommand ?? (_saveCommand = new DelegateCommand(ExecuteSaveCommand, CanExecuteSaveCommand));

    private void ExecuteSaveCommand()
    {
        // User.Validate();
        // Async
        User.ValidateAsync().Await();
    }

    private bool CanExecuteSaveCommand() => !User.HasErrors;
}

Wpf

Binding

<!-- the default value of UpdateSourceTrigger is LostFocus -->
<TextBox Text="{Binding User.FirstName, UpdateSourceTrigger=PropertyChanged}" />

Create a Style that displays errors

<Style TargetType="TextBox">
    <Setter Property="Validation.ErrorTemplate">
        <Setter.Value>
            <ControlTemplate>
                <StackPanel>
                    <AdornedElementPlaceholder x:Name="placeholder"/>
                    <!--TextBlock with error -->
                    <TextBlock FontSize="12" Foreground="Red" 
                               Text="{Binding ElementName=placeholder,Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"/>
                </StackPanel>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
    <Style.Triggers>
        <Trigger Property="Validation.HasError" Value="True">
            <Setter Property="Background" Value="Red"/>
            <!--Tooltip with error -->
            <Setter Property="ToolTip" 
                    Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
        </Trigger>
    </Style.Triggers>
</Style>

Uwp

<TextBox Text="{Binding User.FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="{Binding User.Errors[FirstName][0]}" Foreground="Red"></TextBlock>

Or Create a Converter that displays the first error of the list

public sealed class FirstErrorConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        var errors = value as IList<string>;
        return errors != null && errors.Count > 0 ? errors.ElementAt(0) : null;
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        throw new NotImplementedException();
    }
}

And use it

<Page.Resources>
    <converters:FirstErrorConverter x:Name="FirstErrorConverter"></converters:FirstErrorConverter>
</Page.Resources>
 <TextBox Text="{Binding User.FirstName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
<!-- with converter -->
<TextBlock Text="{Binding User.Errors[FirstName], Converter={StaticResource FirstErrorConverter}}" Foreground="Red"></TextBlock>

EventAggregator

allows to subscribe, publish and filter messages

Register the eventAggregator as singleton with an ioc container.

public class ShellViewModel
{
    private IEventAggregator _eventAggregator;

    public ShellViewModel(IEventAggregator eventAggregator)
    { 
        _eventAggregator = eventAggregator;
    }
}

Subscribe and publish with empty event (no parameter)

Create the event class

public class MyEvent : EventBase
{ }
// subscriber
eventAggregator.GetEvent<MyEvent>().Subscribe(() => { /* do something with args.Message */ });
// publisher
eventAggregator.GetEvent<MyEvent>().Publish();

Subscribe and publish with parameter

public class DataSavedEvent : EventBase<DataSavedEventArgs>
{ }

public class DataSavedEventArgs
{
    public string Message { get; set; }
}
// subscriber
eventAggregator.GetEvent<DataSavedEvent>().Subscribe(args => { /* do something with args.Message */ });
// publisher
eventAggregator.GetEvent<DataSavedEvent>().Publish(new DataSavedEventArgs { Message = "Data saved." })

Filter

Example: Filter on "user id"

eventAggregator.GetEvent<MyUserEvent>().Subscribe(user => { /* do something */ }).WithFilter(user => user.Id == 1); // <= not notified

messenger.GetEvent<MyUserEvent>().Publish(new User { Id = 2, UserName = "Marie" });  // <=

The event class:

public class MyUserEvent : EventBase<User>
{ }

Execution strategy:

  • PublisherThread (default)
  • UIThread
  • BackgroundThread
eventAggregator.GetEvent<DataSavedEvent>().Subscribe(_ => { }).WithExecutionStrategy(ExecutionStrategyType.UIThread);

Unsubscribe with the token received on subscription.

var subscriberOptions = eventAggregator.GetEvent<DataSavedEvent>().Subscribe(_ => { });

var success = eventAggregator.GetEvent<DataSavedEvent>().Unsubscribe(subscriberOptions.Token);
// or
subscriberOptions.Unsubscribe();