Creating a view model with a model and mappings

During of the use of the MVVM pattern, we noticed that lots and lots of developers have a model, and map the values of the model to all properties of the view model. When the UI closes, the developers map all the properties back to the model. All this redundant code is not necessary when using the view models of Catel.

In Catel, we have created attributes that allow you to define a property as a model. A model is a property that a part of the view model represents to the user. A view model might have multiple models if it is a combination of several models.

To use the mapping features, the following attributes are very important:

In Catel 4.0 a new mapping mechanism is introduced that makes it possible to convert types of properties of the mappings between the Model and ViewModel. It is also possible to map to a collection of properties to a single property as result (like MultiBinding and Converter in WPF).

To use new mechanism you should declare this attributes:

Code snippets

Explanation

Defining a model is very simple, you only have to decorate your property with the ModelAttribute:

/// <summary>
/// Gets or sets the person.
/// </summary>
[Model]
public IPerson Person
{
    get { return GetValue<IPerson>(PersonProperty ); }
    private set { SetValue(PersonProperty , value); }
}

/// <summary>
/// Register the Person property so it is known in the class.
/// </summary>
public static readonly PropertyData PersonProperty = RegisterProperty("Person", typeof(IPerson));

Using the ModelAttribute is very powerful. Basically, this is the extended functionality in the view model. If the model supports IEditableObject, BeginEdit is automatically called in the initialization of the view model. When the view model is canceled, the CancelEdit is called so the changes are undone.

When a model is defined, it is possible to use the ViewModelToModelAttribute, as you can see in the code below:

/// <summary>
/// Gets or sets the FirstName of the person.
/// </summary>
[ViewModelToModel("Person")]
public string FirstName
{
    get { return GetValue<string>(FirstNameProperty); }
    set { SetValue(FirstNameProperty, value); }
}

/// <summary>
/// Register the FirstName property so it is known in the class.
/// </summary>
public static readonly PropertyData FirstNameProperty = RegisterProperty("FirstName", typeof(string));
 
/// <summary>
/// Gets or sets the LastName of the person.
/// </summary>
[ViewModelToModel("Person")]
public string LastName
{
    get { return GetValue<string>(LastNameProperty); }
    set { SetValue(LastNameProperty, value); }
}

/// <summary>
/// Register the LastName property so it is known in the class.
/// </summary>
public static readonly PropertyData LastNameProperty = RegisterProperty("LastName", typeof(string));

If there is a single model on a view model, the name of the model in the ViewModelToModel can be ommitted as shown in the code below:

[ViewModelToModel]
public string FirstName
{
    get { return GetValue<string>(FirstNameProperty); }
    set { SetValue(FirstNameProperty, value); }
}

public static readonly PropertyData FirstNameProperty = RegisterProperty("FirstName", typeof(string));

The ViewModelToModelAttribute in the code example above automatically maps the view model FirstName and LastName properties to the Person.FirstName and Person.LastName properties. This way, you don’t have to manually map the values from and to the model. Another nice effect is that the view model automatically validates all objects defined using the ModelAttribute, and all field and business errors mapped are automatically mapped to the view model.

Sometimes you need the full name of a person, you can easily acquire it by creating a custom converter:

      public class CollapsMappingConverter : DefaultViewModelToModelMappingConverter
    {
        #region Fields
        private readonly char _separator;
        #endregion

        #region Constructors
        public CollapsMappingConverter(string[] propertyNames)
            : this(propertyNames, ' ')
        { }

        public CollapsMappingConverter(string[] propertyNames, char separator = ' ')
            : base(propertyNames)
        {
            _separator = separator;
        }
        #endregion

        #region Properties
        public char Separator
        {
            get { return _separator; }
        }
        #endregion

        #region Methods
        public override bool CanConvert(Type[] types, Type outType, Type viewModelType)
        {
            return types.All(x => x == typeof (string)) && outType == typeof (string); //check that all input and output values are strings
        }

        public override object Convert(object[] values, IViewModel viewModel)
        {
            return string.Join(Separator.ToString(), values.Where(x => !string.IsNullOrWhiteSpace((string) x)));
        }

        public override bool CanConvertBack(Type inType, Type[] outTypes, Type viewModelType)
        {
            return outTypes.All(x => x == typeof (string)) && inType == typeof (string); //check that all input and output values are strings
        }

        public override object[] ConvertBack(object value, IViewModel viewModel)
        {
            return ((string) value).Split(Separator);
        }
        #endregion
    }

Now, when we created the converter we should define it in mapping like this:

/// <summary>
/// Gets or sets the FullName of the person.
/// </summary>
[ViewModelToModel("Person", "FirstName", AdditionalPropertiesToWatch = new[] { "LastName" }, ConverterType = typeof(CollapsMappingConverter))]
public string FullName
{
    get { return GetValue<string>(FullNameProperty); }
    set { SetValue(FullNameProperty, value); }
}

/// <summary>
/// Register the LastName property so it is known in the class.
/// </summary>
public static readonly PropertyData FullNameProperty = RegisterProperty("FullName", typeof(string));

The ViewModelToModelAttribute in the code example above automatically maps the view model FullName property to the Person.FirstName and Person.LastName properties and converts them with CollapsMappingConverter. This way, you don’t have to manually map the values from the model and update FullName property when FirstName or LastName property changed.

Summarized, the Model and ViewModelToModel attributes make sure no duplicate validation and no manual mappings are required.

 


Have a question about Catel? Use StackOverflow with the Catel tag!


Discussion