Skip to content
Jan M edited this page Feb 18, 2020 · 41 revisions

Overview

In this package you will find the default MVVM implementations like PropertyChanged and some "convenience implementations" to make your life a little easier.

BindingBase

The BindingBase is our default INotifyPropertyChanged implementation.

  • implementation of INofityPropertyChanged
  • convenient methods GetValue and SetValue to easily declare a property and use INotifyPropertyChanged
  • INotifyPropertyChanged for nested classes
  • INotifyPropertyChanged for dependent properties (e.g. update message for Age property when DateOfBirth has changed)
  • controlling the INotifyPropertyChanged events via attributes
  • automatically updating ICommand.CanExecute if there is any ICommand in the class

How to create a property

public class Model :
    BindingBase
{
    // simple property with PropertyChanged support
    public string Name
    {
        get => GetValue<string>();
        set => SetValue(value);
    }

    // add custom PropertyChanged handler
    public DateTime DateOfBirth
    {
        get => GetValue<DateTime>();
        set => SetValue(value, OnDateOfBirthChanged);
    }
    private void OnDateOfBirthChanged (object sender, PropertyChangedEventArgs eventArgs) { ... }

    // add custom PropertyChanged handler
    // and custom PropertyChanging handler
    public Guid Id
    {
        get => GetValue<Guid>();
        set => SetValue(value, OnIdChanged, OnIdChanging);
    }
    private void OnIdChanged (object sender, PropertyChangedEventArgs eventArgs) { ... }
    private void OnIdChanging (object sender, PropertyChangingEventArgs eventArgs) { ... }

    // add custom PropertyChanged action
    public string Url
    {
        get => GetValue<string>();
        set => SetValue(value, (url) => 
            {
                if (!url.EndsWith('/'))
                    url += "/";
            });
    }
}

Working with ViewModels

Overview of the different base classes

1. The basic one: ViewModelBase

This is a very basic implementation and just provides a property public bool IsInitialized to state wether this instance has been set up using the InitializeAsync method. This method is automatically invoked when creating instances using the ViewModelFactory.

Example usage:

public class CalendarViewModel :
    ViewModelBase
{
    public override async Task InitializeAsync()
    {
        // some async logic like database access here...
    }
}

2. The model-dependent one: ViewModelBase<TModel>

The generic implementation extends the ViewModelBase by providing the InitializeAsync<TModel>(TModel model) method. This way it is possible to initialise your ViewModel instance directly with a model that is necessary for the instance anyways.

Example usage:

public class DetailsViewModel :
    ViewModelBase<Item>,
    IDetailsViewModel
{
    public Item Item
    {
        get => GetValue<Item>();
        private set => SetValue(value);
    }
    public override async Task InitializeAsync(Item model)
    {
        Item = model;
    }
}

// Navigation example:
navigationService.ShowAsync<IDetailsViewModel, Item>(selectedItem);

3. Simplified navigation: Navigation.ViewModelBase

The implementations in the namespace CodeMonkeys.MVVM.ViewModels.Navigation are making navigating between ViewModels much easier since there is no need to store and call the IViewModelNavigationService instance every time. Instead, these ViewModel implementations are resolving an instance of the service itself and encapsulating the most common methods:

public async Task ShowAsync<TDestinationViewModel>
public async Task CloseAsync()
public async Task CloseAsync<TListenerViewModel>()
public async Task CloseAsync<TListenerViewModel, TResult>(TResult result)

In addition, these ViewModels are called when the page is closed (e.g. because the user is navigating back or forward).

Please note that the CloseAsync method only works when registering specific ViewModel type to specific View type at navigation service bootstrap (e.g. NavigationService.Register<MainViewModel, MainView>();).
If you want to use interfaces for registration, you have to use the Navigation.ViewModelBase<TInterface>.

// CodeMonkeys.MVVM.ViewModels.ViewModelBase:
public class ItemListViewModel :
    CodeMonkeys.MVVM.ViewModels.ViewModelBase 
{
    private readonly IViewModelNavigationService _navigationService;

    public ICommand ItemSelected => new AsyncCommand<Item>(HandleItemSelected);

    public ItemListViewModel(IViewModelNavigationService navigationService)
    {
        _navigationService = navigationService;
    }

    private async Task HandleItemSelected (Item selectedItem)
    {
        _navigationService.ShowAsync<IItemDetailsViewModel, Item>(selectedItem);
    }
}

// CodeMonkeys.MVVM.ViewModels.Navigation.ViewModelBase:
public class ItemListViewModel : 
    CodeMonkeys.MVVM.ViewModels.Navigation.ViewModelBase
{
    public ICommand ItemSelected => new AsyncCommand<Item>(HandleItemSelected);

    private async Task HandleItemSelected (Item selectedItem)
    {
        ShowAsync<IItemDetailsViewModel, Item>(selectedItem);
    }
}

Note that there is also a "navigation implementation" of the ViewModelBase<TModel>. It is also located in the CodeMonkeys.MVVM.ViewModels.Navigation namespace and called ViewModelBase<TInterface, TModel>

You can find the ViewModelFactory in the namespace CodeMonkeys.MVVM.Factories. In order to use the factory, you need to configure it calling the ViewModelFactory.Configure method and passing an IDependencyResolver instance and optionally an ILogService instance, too.

After setting up the factory, you are able to create ViewModel instances with it, using one of the following methods:

public static TViewModelInterface Resolve<TViewModelInterface>(bool initialize = true)
public static async Task<TViewModelInterface> ResolveAsync<TViewModelInterface>(bool initialize = true)
public static async Task<TViewModelInterface> ResolveAsync<TViewModelInterface, TModel>(TModel model)

The bool initialize = true parameter indicates wether the Initialize method of your IViewModel should be called.

Using our ICommand implementations

The namespace of our Commands is CodeMonkeys.MVVM.Commands, so your using directive would look like this:

using CodeMonkeys.MVVM.Commands;

1. Keeping it simple: Command

public class LoginViewModel
{
    public string UserName
    {
        get => return GetValue<string>();
        set => SetValue(value);
    }

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

    public ICommand ClearUserNameCommand => new Command(() => UserName = string.Empty);
    public ICommand ClearPasswordCommand { get; private set; }    

    public LoginViewModel()
    {
        ClearPasswordCommand = new Command(ClearPasswordField);
    }

    private void ClearPasswordField() { ... }
}

2. Defining the parameter type: Command<TParameter>

public class ItemsListViewModel
{
    public IList<Item> Items
    {
        get => return GetValue<IList<Item>>();
        set => SetValue(value);
    }

    public ICommand SelectItemCommand => new Command<Item>((item) => SelectItem(item));   

    private void SelectItem (Item selectedItem) { ... }
}

3. Want to do some asynchronous stuff? AsyncCommand

public class LoginViewModel
{
    public ICommand LoginCommand => new AsyncCommand(Login);   

    private async Task Login () { ... }
}

4. Async and defined: AsyncCommand<TParameter>

public class ItemsListViewModel
{
    public IList<Item> Items
    {
        get => return GetValue<IList<Item>>();
        set => SetValue(value);
    }

    public ICommand ShowItemDetailsCommand => new AsyncCommand<Item>((item) => ShowItemDetails(item));   

    private async Task ShowItemDetails (Item selectedItem) { ... }
}

Working with the ModelBase

The ModelBase is an extension of the BindingBase that adds the following functionalities:

  • IsDirty property indicating wether another property of the model has changed recently
  • LastPropertySet holding the name of the property changed the most recent (and changed IsDirty)
  • CommitChanges() and ResetChanges() methods that allow you to always keep a consistent state

I will go through the features and give some examples on how to use them.

Understanding the IsDirty flag

This flag indicates wether one of your model's properties has been changed after the last commit.
It is set to true whenever SetValue() is called and on the other hand set to false when calling
CommitChanges() or ResetChanges(). In addition, there is a ClearIsDirtyFlag() for cases when you need to reset IsDirty to false but can't use CommitChanges() or ResetChanges().

var model = new Model(); // IsDirty is false
model.Name = "Joe Example"; // IsDirty is true here

model.CommitChanges(); // IsDirty is false again

model.Phone = "1234567890"; // IsDirty is true
model.ResetChanges(); // IsDirty is true and Phone is string.Empty again

model.Id = 1234; // IsDirty is true
model.ClearIsDirtyFlag(); // IsDirty is false

LastPropertySet

This can be used to get the name of the property that was last changed.

var model = new Model(); // LastPropertySet is string.Empty
model.Name = "Joe Example"; // LastPropertySet is "Name"

model.CommitChanges(); // LastPropertySet is string.Empty

model.Phone = "123456789"; // LastPropertySet is "Phone"
model.ResetChanges(); // LastPropertySet is string.Empty

CommitChanges()

This method will copy all of your model's current property values into a private ConcurrentDictionary.
IsDirty and LastPropertySet are reset to its initial values.
A boolean indicating wether commiting was successful is returned from the method.

You can override this method to adapt it to your needs and extend its functionality.

ResetChanges()

This method is used for the exact opposite of CommitChanges().
It reads the values that are stored inside the private ConcurrentDictionary and writes them back at the according properties. If there are no values stored yet (no CommitChanges() call has been made), initial values are used.

You can override this method to adapt it to your needs and extend its functionality (e.g. reset fields aswell).

Attributes

In the namespace CodeMonkeys.MVVM.Attributes we provide you some functionality to adapt the INotifyPropertyChanged process to your needs:

Target: Property
Can use multiple times: yes
Is inherited: yes

Indicates that this property's value relies on some other property (or properties).
This is the case for calculated properties like age.

In the following example, whenever DateOfBirth changes, the PropertyChanged event is also raised for Age.

public class Person :
    ModelBase
{
    public DateTime DateOfBirth
    {
        get => return GetValue<DateTime>();
        set => SetValue(value);
    }

    // we want a PropertyChanged event for Age whenever the DateOfBirth changes
    [DependsOn(nameof(DateOfBirth)]
    public int Age
    {
        get => return (DateTime.Now - DateOfBirth).Years;
    }
}

Target: Property
Can use multiple times: no
Is inherited: yes

Use this attribute when you want PropertyChanged support for a property, but dont want it to affect your model's IsDirty flag.

public class Item
{
    // IsDirty should not be set to true when IsSelected changes
    [DontAffectIsDirty]
    public bool IsSelected
    {
        get => return GetValue<bool>();
        set => SetValue(value);
    }
}

Target: Class, Property
Can use multiple times: no
Is inherited: yes

This attribute prevents PropertyChanged events from being raised. You can use it on both classes and properties and can even disable the event for a whole class but exclude one property.

public class Model
{
    // we dont want to raise a PropertyChanged event when Name changes
    [SuppressNotifyPropertyChanged]
    public string Name
    {
        get => return GetValue<string>();
        set => SetValue(value);
    }
}
-----------------------------------------------------------------------------
// we dont want to raise a PropertyChanged event for any of Model's properties
[SuppressNotifyPropertyChanged]
public class Model
{
    ...
}
-----------------------------------------------------------------------------
// we dont want to raise a PropertyChanged event for any of Model's properties...
[SuppressNotifyPropertyChanged]
public class Model
{
    // ... but we want one for Name
    [SuppressNotifyPropertyChanged(false)]
    public string Name
    {
        get => return GetValue<string>();
        set => SetValue(value);
    }
}

Clone this wiki locally