User Tools

Site Tools


notes:csharp:mvvm_light

MVVM Light

MVVM pattern in XAML

View

  • Describes the user interface (layout and styling) using a declarative markup such as XAML.
  • Presents information to the user (data-binding).
  • Responds to the user input (commands and keyboard shortcuts).
  • References the ViewModel through DataContext and/or code-behind.
  • Composed of the following elements:
    • XAML elements
    • value converters
    • data templates
    • visual state groups
    • storyboards
    • animations
    • visual state transitions
    • behaviours - pieces of packaged code that is used to add interactivity to the app
    • code - additional UI-related logic

ViewModel

  • Responsible for interaction between the View and the Model.
  • Provides presentation logic for one or multiple Views.
  • Does not know about the view’s design, layout, style, etc.
  • Has three ways of communicating with the View:
    • data-binding
    • visual states
    • commands and method calls
  • Implements the property change notification interface.
  • Implements synchronous and asynchronous validation through the INotifyDataErrorInfo interface.
  • May transform data so that it can be presented in the View.
  • May combine multiple fields from models in a property that is then bound to a visual element in the View.

Model

  • Represents a data object e.g. Customer; it could be an aggregation of data objects.
  • Describes business entities (e.g. Publisher, Book, Order) usually implemented as classes.
  • Describes business logic operating on the entities without involving UI (View) and presentation logic (ViewModel).
  • Describes relations between the entities.
  • Contains state of the entities.
  • Accesses entity data from local or remote sources.
  • Responsible for persisting changes from the View in the data source
  • Includes any data caching mechanism if needed.
  • MS Exam: Implements business logic (I don't agree with that): “the business logic as well as data validation rules are often implemented in the model, although a separate repository for data access, caching, and storage can be used.”
  • MS Exam: Responsible for notifying the ViewModel whenever data changes in the source

ObservableObject and ViewModelBase

Classes deriving from ObservableObject or ViewModelBase can use the following methods:

RaisePropertyChanged("MyProperty");
RaisePropertyChanged(() => MyProperty);       // very small performance impact
Set("MyProperty", ref myProperty, value);     // the "Set" methods return a bool value indicating if the property
                                              // value has changed
Set(() => MyProperty, ref myProperty, value);

Overloads suitable to use with the Messenger:

RaisePropertyChanged("MyProperty", oldValue, value, true);
RaisePropertyChanged(() => MyProperty, oldValue, value, true);
Set("MyProperty", ref myProperty, value, true);
Set(() => MyProperty, ref myProperty, value, true);

The parameters in the above methods are:

  • property name or a lambda
  • old value
  • new value
  • a flag determining if the event needs to be broadcast using Messenger

MVVM Light has a PropertyChanging event. It provides access to the old value:

RaisePropertyChanging("MyProperty");
RaisePropertyChanging(() => MyProperty);

ObservableObject is lighter than ViewModelBase, for example it does not have the design mode detection.

  • Use IsInDesignMode within a class that derives from ViewModelBase
  • Use ViewModelBase.IsInDesignModeStatic within a class that does not derive from ViewModelBase
  • isInDesignMode = DesignMode.DesignModeEnabled;

Simple IoC

  • When a class is registered, no instances are created.
  • Object creation is on demand when the GetInstance method is called the first time.
  • Created objects are cached.
  • When an object is not needed anymore, it has to be removed from the cache explicitly.
  • You may want to register a factory class and then rely on this class to create object instances.

Registration

Example: Register a class to SimpleIoC:

// Class registration.
SimpleIoc.Default.Register<MainViewModel>();
 
// Registration with an interface.
SimpleIoc.Default.Register<IDataService, DataService>();
 
// Conditional registration.
if (condition)
    SimpleIoc.Default.Register<IDataService, DataService>();
else
    SimpleIoc.Default.Register<IDataService, TestDataService>();

Example: Register a class to SimpleIoC using a factory:

  • The factory is expressed as a lambda.
  • The factory creates an object once, on demand; the result is cached.
// Use a pre-created object 'service'.
var service = new DataService();
SimpleIoc.Default.Register<IDataService>(() => service);
 
// Create an instance and pass params to a ctor.
var param1 = true;
var param2 = "Hello world";
SimpleIoc.Default.Register<MainViewModel>(() => new MainViewModel(param1, param2));

Example: Force the immediate creation of an object at registration. It may be useful, for example, for a Settings object that needs to load settings as soon as the app starts:

// example #1
SimpleIoc.Default.Register<IDataService, DataService>(true);
 
// example #2
SimpleIoc.Default.Register<MainViewModel>(true);
 
// example #3
var service = new DataService();
SimpleIoc.Default.Register<IDataService>(() => service, true);
 
// example #4
SimpleIoc.Default.Register<MainViewModel>(() => new MainViewModel(param1, param2), true);

Example: Register a class or an interface with a key. It allows for having multiple instances of the same class or interface inside the IoC container. The corresponding instance needs to be retrieved with the same key:

SimpleIoc.Default.Register<MainViewModel>(() => new MainViewModel(), "UniqueKey");
SimpleIoc.Default.Register<MainViewModel>(() => new MainViewModel(), "AnotherUniqueKey");

Getting an instance

Example: Get a default instance for the MainViewModel class and the IDataService interface:

var instance = SimpleIoc.Default.GetInstance<MainViewModel>();
var service = SimpleIoc.Default.GetInstance<IDataService>();

Example: Get a keyed instance. Keyed instances may be useful if you need multiple instances of the same type. Keep in mind that all instances are cached. If you want to remove the instance from the cache, you need to unregister it using the same key.

// Use the same key for registration and for getting an instance.
SimpleIoc.Default.Register<IDataService>(() => new DataService, "key1");
var specificInstance = SimpleIoc.Default.GetInstance<IDataService>("key1"); // get the instance with key1
// Do not pass any key to registration. A new instance is created for the key1.
SimpleIoc.Default.Register<IDataService, DataService>();
var keyedInstance = SimpleIoc.Default.GetInstance<IDataService>("key1"); // create a new instance

Composing dependencies

SimpleIoc takes care of creating the necessary dependencies and composing them.

// MainViewModel has two dependecies.
public MainViewModel(IDataService dataService, INavigationService navigationService)
{
    this.dataService = dataService;
    this.navigationService = navigationService;
}
...
static ViewModelLocator()
{
    // Register the services (our dependencies).
    SimpleIoc.Default.Register<IDataService, DataService>();
    SimpleIoc.Default.Register<INavigationService, () => new NavigationService()>();
 
    // Register the MainViewModel.
    SimpleIoc.Default.Register<MainViewModel>();
}
...
public MainViewModel Main
{
    // Create an instance of MainViewModel (if it's not already in cache). Before creating the instance though,
    // SimpleIoc creates the dependecies of MainViewModel: DataService and NavigationService.
    get { return SimpleIoc.Default.GetInstance<MainViewModel>(); }
}

Constructor injection and property injection

Constructor injection:

  • Used when a service is created in an IoC container and never changes.

Property injection:

  • Useful when the instance of a service may change during the application lifetime.

Example: DialogService property injection:

public class MainViewModel : ViewModelBase
{
    private readonly IDataService dataService;
 
    public IDialogService DialogService
    {
        get
        {
            return SimpleIoc.Default.GetInstance<IDialogService>();
        }
    }
 
    public MainViewModel(IDataService dataService)
    {
        this.dataService = dataService;
    }
}

Unregistering an instance

  • When a class or an interface is unregistered, all the instances (not interfaces) are removed from the cache.
  • Even if you try to unregister a class, an interface, a key, or an instance that does not exist in SimpleIoc, there are no side-effects.
  • It is a good practice to unregister your objects, once you don't need them anymore, in order to optimize memory usage.

The Unregister method:

  • Calling the Unregister method without a parameter causes the class/interface to be removed from SimpleIoc. Any subsequent calls to GetInstance will cause an exception.
  • Calling the Unregister method with an instance parameter causes the instance to be removed from the cache.
  • Calling the Unregister method with a key parameter causes the instance identified by the key to be removed from the cache.

Example: Unregister the MainViewModel class and the IDialogService interface:

SimpleIoc.Default.Unregister<MainViewModel>();
SimpleIoc.Default.Unregister<IDialogService>();

Example: Unregister an instance identified by the key1:

SimpleIoc.Default.Unregister<IDataService>("key1"); 

Example: Unregister the 'this' instance:

SimpleIoc.Default.Unregister<IDialogService>(this);

Example: Register and unregister the current page as a dialog service during navigation events:

public sealed partial class MainPage : IDialogService
{
    ...
    protected override void OnNavigatedTo(NavigationEventArgs args)
    {
        SimpleIoc.Default.Register<IDialogService>(() => this);
        base.OnNavigatedTo(args);
    }
 
    protected override void OnNavigatedFrom(NavigationEventArgs args)
    {
        SimpleIoc.Default.Unregister(this);
        base.OnNavigatedFrom(args);
    }
}

Utility methods

Example: Check if a class or an interface is registered using the IsRegistered method. Also, check if a class has at least one instance created using the ContainsCreated method:

SimpleIoc.Default.Register<IDataService, DataService>();
 
var test1 = SimpleIoC.Default.IsRegistered<IDataService>(); // true
var test2 = SimpleIoC.Default.ContainsCreated<IDataService>(); // false
 
var instance = SimpleIoC.Default.GetInstance<IDataService>();
 
var test3 = SimpleIoC.Default.ContainsCreated<IDataService>(); // true

Example: Return existing instances using the GetAllCreatedInstances method:

SimpleIoc.Default.Register<IDataService, DataService>();
 
var allInstances = SimpleIoc.Default.GetAllCreatedInstances<IDataService>(); // no instances returned
 
var instance1 = SimpleIoc.Default.GetInstance<IDataService>("key1");
var instance2 = SimpleIoc.Default.GetInstance<IDataService>("key2");
 
allInstances = SimpleIoc.Default.GetAllCreatedInstances<IDataService>(); // two instances returned

Keep in mind that the GetAllInstances method does force the creation of one default instance per registered class/interface.

SimpleIoc.Default.Register<IDataService, DataService>();
 
var allInstances = SimpleIoc.Default.GetAllInstances<IDataService>(); // one instance returned

The attribute PreferredConstructor:

  • Specifies which ctor SimpleIoc should use. By default SimpleIoc uses the default ctor.
  • If multiple ctors are found, ActivationException is thrown.
public class DataService
{
    public DataService()
    {
    }
 
    [PreferredConstructor]
    public DataService(IAnotherService another)
    {
    }
}

Microsoft.Practices.ServiceLocation

  • Contains the ServiceLocator class.
  • Standarizes usage of IoC containers.
  • Allows users to specify which IoC to use by default.
// Define the ServiceLocator.
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
 
// Use the ServiceLocator.
ServiceLocator.Current.GetInstance<IDataService>();
 
// The above line of code is an equivalent to the following one:
SimpleIoc.Default.GetInstance<IDataService>();

Messenger (Message Bus)

  • Messenger sends messages to a list of recipients.
  • Typically we use a default instance of the Messenger: Messenger.Default
  • You can create as many Messenger instances as needed.
  • Pass the Messenger object to ViewModels explicitly. This makes the depenencies clear.
  • Send messages of a certain type rather than generic messages. For example, InvoiceViewModel may send messages of type InvoiceMessage. Then, recipients register to listen for messages of type InvoiceMessage.
  • Do not overuse the Messenger.
  • Use event handlers whenever possible and the Messenger as the last resort.
  • Sometimes the Messenger can be replaced by a service (a DialogService, a NavigationService, etc.)
  • Test your code for memory leaks.
  • If you are in doubt, unregister the messenger handling methods.

Example: Register to listen for messages:

public MessageRecipient()
{
    // Listen for messages of type String.
    Messenger.Default.Register<string>(this, HandleStringMessage); 
 
    // Listen for messages of type CustomMessage.
    Messenger.Default.Register<CustomMessage>(this, HandleCustomMessage); 
 
    // Listen for messages of type NotificationMessage<Invoice>.
    Messenger.Default.Register<NotificationMessage<Invoice>>(this, HandleInvoiceMessage); 
 
    private void HandleStringMessage(string message) { /*...*/ }
    private void HandleCustomMessage(CustomMessage message) { /*...*/ }
    private void HandleInvoiceMessage(NotificationMessage<Invoice> message) { /*...*/ }
 
    // Listen for messages of type NotificationMessage<Invoice> and handle them using a lambda.
    Messenger.Default.Register<NotificationMessage<Invoice>>(this, 
        message => { /*...*/ });
}

Example: Send messages:

// Send a message of a custom type.
var msg = new CustomMessage();
Messenger.Default.Send(msg);
 
// Send a message of a built-in type NotificationMessage.
// - selectedInvoice is a payload.
// - InvoiceSelectedNotification is a string containing an instruction for the receiver.
var msg = new NotificationMessage<Invoice>(
    selectedInvoice, 
    InvoiceSelectedNotification);
Messenger.Default.Send(msg);
 
// An overload that passes the message sender.
// !!! Remember not to store the sender reference as it will create a memory leak.
var msg = new NotificationMessage<Invoice>(
    this,
    selectedInvoice, 
    InvoiceSelectedNotification);
Messenger.Default.Send(msg);

Example: Unregister the message handling method(s):

// Unregister all methods of the 'this' recipient.
Messenger.Default.Unregister(this);
 
// Unregister a specific method. HandleMessage is a delegate of the method.
Messenger.Default.Unregister<IMessage>(this, HandleMessage);

Example: Use a token to send and receive messages:

// Register and listen for messages.
class MessageRecipient
{
    // The token has to be unique. It can be anything (a string, an instance of a custom class, etc.)
    public static readonly Guid Token = Guid.NewGuid();
    ...
    Messenger.Default.Register<NotificationMessage>(this, Token, HandleNotification);
    ...
    private void HandleNotification(NotificationMessage message) { /*...*/ }
}
 
// Send a message. Only the recipients who registered to listen for messages with the given token
// will receive this message.
Messenger.Default.Send(new NotificationMessage("DoSomething"), MessageRecipient.Token);
 
// Unregister.
Messenger.Default.Unregister<IMessage>(this, Token); // all handlers
Messenger.Default.Unregister<IMessage>(this, Token, HandleMessage); // a specific handler

ViewModelLocator

There are two strategies of binding Views and ViewModels:

  • using a ViewModelLocator
  • in code-behind

Example: Bind a View to MainViewModel using the ViewModelLocator:

1. Instantiate the ViewModelLocator as a global resource in App.xaml:

<Application.Resources>
    <vm:ViewModelLocator x:Key="Locator" />
</Application.Resources>

-or-

<Application.Resources>
    <ResourceDictionary>
        <vm:ViewModelLocator x:Key="Locator" />
    </ResourceDictionary>        
</Application.Resources>

2. Expose the most important ViewModels as ViewModelLocator's public properties:

public class ViewModelLocator
{
    public MainViewModel Main
    {
        get;
        private set;
    }
    // ...
}

3. Bind to the ViewModelLocator's Main property in the View:

<... DataContext="{Binding Main, Source={Locator}}">

Not all ViewModels should be declared in the ViewModelLocator. It depends on how long they need to be alive, when they need to be created, and so on.

4. Register services conditionally:

public ViewModelLocator()
{
    if (ViewModelBase.IsInDesignModeStatic)
    {
        SimpleIoc.Default.Register<IDataService, DesignDataService>();
    }
    else
    {
        SimpleIoc.Default.Register<IDataService, DataService>();
    }
 
    SimpleIoc.Default.Register<IDialogService, DialogService>();
    SimpleIoc.Default.Register<INavigationService, NavigationService>();
    SimpleIoc.Default.Register<MainViewModel>();
}

VS code snippets

Snippets are grouped by function:

  • inpc: adds an observable property INotifyPropertyChanged
  • locatorproperty: adds a ViewModel property to ViewModelLocator
  • prop: adds an attached or dependency property (WPF style)
  • relay: adds a RelayCommand
  • slprop: adds an attached or dependency property
  • vm: adds access to a ViewModel inside a View (DataContext)

Examples:

  • mvvminpcset: adds an observable property and uses the Set method in the setter
  • mvvminpclambda: adds an observable property and calls RaisePropertyChanged (and RaisePropertyChanging) in the setter using lambda
  • mvvminpcsetlambdamsg: adds an observable property and uses the overloaded Set method in the setter that calls Messenger (note the “msg” suffix in the snippet name)
  • mvvmrelay: adds a RelayCommand without any parameters
  • mvvmrelaygeneric: adds a RelayCommand with generic parameter
  • mvvmpropdp - adds a dependency property (WPF); it differs from the standard propdb by including the declaration of the property name as a const
  • mvvmpropdpa - adds an attached property (WPF)
  • mvvmslpropdp - adds a dependency property (SL, WP, Win8)
  • mvvmlocatorproperty - adds a ViewModelLocator as a static property; it provides useful registration snippets

VS built-in code snippets:

  • propdp - adds a dependency property

MVVM Light versions

There are three versions of MVVM Light in NuGet:

  • “MVVM Light libraries only” - installs only necessary DLLs and dependencies.
  • “MVVM Light” - installs additional files on top of those installed by “MVVM Light libraries only” such as ViewModelLocator and MainViewModel.
  • “MVVM Light PCL” - Portable Class Library version of MVVM Light
notes/csharp/mvvm_light.txt · Last modified: 2017/04/19 by leszek