The Model-View-ViewModel (MVVM) pattern
The Model-View-ViewModel (MVVM) pattern helps you to cleanly separate the business and presentation logic of your application from its user interface (UI). It can also greatly improve code re-use opportunities. Maintaining a clean separation between application logic and UI helps to address numerous development and design issues and can make your application much easier to test, maintain, and evolve.
Using the MVVM pattern, the UI of the application and the underlying presentation and business logic is separated into three separate classes: the view, which encapsulates the UI and UI logic; the view model, which encapsulates presentation logic and state; and the model, which encapsulates the application's business logic and data.
The MVVM pattern is a close variant of the Presentation Model pattern, optimized to leverage some of the core capabilities of XAML, such as data binding, data templates, commands, and behaviors.
In the MVVM pattern, the view encapsulates the UI and any UI logic, the view model encapsulates presentation logic and state, and the model encapsulates business logic and data. The view interacts with the view model through data binding, commands, and change notification events. The view model queries, observes, and coordinates updates to the model, converting, validating, and aggregating data as necessary for display in the view.
The following illustration shows the three MVVM classes and their interaction.
To summarize, the MVVM has the following key characteristics:
If there's an existing model implementation that encapsulates existing business logic, it can be difficult or risky to change it. In this scenario, the view model acts as an adapter for the model classes and enables you to avoid making any major changes to the model code.
Developers can create unit tests for the view model and the model, without using the view. The unit tests for the view model can exercise exactly the same functionality as used by the view.
The app UI can be redesigned without touching the code, provided that the view is implemented entirely in XAML. Therefore, a new version of the view should work with the existing view model.
Designers and developers can work independently and concurrently on their components during the development process. Designers can focus on the view, while developers can work on the view model and model components.
DEM uses two options for executing code on a view model in response to interactions on a view, such as a button click or item selection. If the control is a command source, the control�s Command property is data-bound to an ICommand property on the view model. When the control�s command is invoked, the code in the view model will be executed. In addition to commands, behaviors can be attached to an object in the view and can listen for an event to be raised. In response, the behavior can then invoke an Action or an ICommand on the view model.
Creating View and ViewModel
The following procedure shows how to create a view and viewmodel
1. Add a folder named as Usecases to the root folder of your project if don't exsisit.
2. Add a new folder named as your business case to the previous created folder.
3. Create a new page in that folder whose name ends with "Page"
4. Modify the page class to derive from the Usecasecontent page class, which provides support for navigation, and state management.
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class SamplePage : UsecaseContentPage
{
5. Add default namespaces in xaml, and change the page root element pages:UsecaseContentPage
<pages:UsecaseContentPage x:Class="AssecoSEE.DEMO.Customized.SamplePage"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:components="clr-namespace:AssecoSEE.DEM.Components.Components;assembly=AssecoSEE.DEM.Components"
xmlns:ctrl="clr-namespace:AssecoSEE.DEM.Components.Controls;assembly=AssecoSEE.DEM.Components"
xmlns:ext="clr-namespace:AssecoSEE.DEM.Components.XamlExtensions;assembly=AssecoSEE.DEM.Components"
xmlns:pages="clr-namespace:AssecoSEE.DEM.Core.Pages;assembly=AssecoSEE.DEM.Core">
<ContentPage.Content>
</ContentPage.Content>
</pages:UsecaseContentPage>
Note
To summarize, the view has the following key characteristics:
- The view is a visual element, such as a page, view, or data template. The view defines the controls contained in the view and their visual layout and styling.
- The view references the view model through its DataContext property. The controls in the view are data bound to the properties and commands exposed by the view model.
- The view may customize the data binding behavior between the view and the view model. For example, the view may use value converters to format the data to be displayed in the UI, or it may use validation rules to provide additional input data validation to the user.
- The view defines and handles UI visual behavior, such as animations or transitions that may be triggered from a state change in the view model or via the user's interaction with the UI.
- The view's code-behind may define UI logic to implement visual behavior that is difficult to express in XAML or that requires direct references to the specific UI controls defined in the view.
6. Create a new class in the same folder whose name corresponds with the name of a view and ends with "ViewModel," in order to use the ViewModelLocator's default convention to instantiate and associate view model classes with view classes.
7. Derive the view model class from the ViewModel base class, provided by the AssecoSEE.DEM.Core.ViewModels library, so that you can use the base class's implementation of the INotifyPropertyChanged interface and gain support for navigation and state management.
public class SampleViewModel : PageViewModel
{
Note
View Model hierarchy Multipage VM is used for product catalog, list page for inbox, orders, transactions �, wizards for payments and step forms, widget for widgets and page for single pages and VM for all others.
We support a view model hierarchy becouse it help to eliminate redundant code in your view model classes. If you find identical functionality in multiple view model classes, such as code to handle navigation, it should be refactored into a base view model class from which all view models classes will derive.
8. Modify the view model constructor so that it accepts the need application services required by the view model, such as an ApiClientService instance
public SampleViewModel(IApiClient apiClient)
{
this.apiClient = Guard.ArgumentNotNull(apiClient, nameof(apiClient));
9. Add some INotifyPropertyChanged propertys if needed
public bool SampleProperty { get => GetValue<bool>(); set => SetValue(value); }
10. Implement core view-model methods for initalizing, parsing navigation paramas and for retrving
public override Task<bool> InitializeAsync(NavigationParameters navigationParameters = null)
{
return Task.FromResult(SampleProperty);
}
public async override Task<bool> StartAsync()
{
var result = await apiClient.GetAsync<List<Contact>, Rest.DialogAPI.DataContracts.UriParams.ContactQueryParams>(
new RequestData<Rest.DialogAPI.DataContracts.UriParams.ContactQueryParams>("v1/dialog/contacts/", new Rest.DialogAPI.DataContracts.UriParams.ContactQueryParams()));
if (result.HasData())
{
var contacts = result.Data;
}
return await base.StartAsync();
}
11. Add some commands
public Command DoCommand { get; protected set; }
protected override void InitiateCommands()
{
base.InitiateCommands();
DoCommand = CreateDelegateCommand<object>(Do, CanDo);
}
protected bool CanDo(object arg)
{
return arg != null;
}
public async Task<bool> Do(object args)
{
return await Task.FromResult( true);
}
12. Bind view to view model
<StackLayout>
<Label Text="Helo World" />
<Button Text="Click me" Command="{Binding DoCommand}" CommandParameter="{Binding SampleProperty}" />
</StackLayout>
Note
To summarize, the view model has the following key characteristics:
- The view model is a non-visual class and does not derive from any XAMARIN base class. It encapsulates the presentation logic required to support a use case or user task in the application. The view model is testable independently of the view and the model.
- The view model typically does not directly reference the view. It implements properties and commands to which the view can data bind. It notifies the view of any state changes via change notification events via the INotifyPropertyChanged and INotifyCollectionChanged interfaces.
- The view model coordinates the view's interaction with the model. It may convert or manipulate data so that it can be easily consumed by the view and may implement additional properties that may not be present on the model.
- The view model may define logical states that the view can represent visually to the user.
13. Register view model in dependency injection container
typeCatalog.Add(
new ViewModelInfo(typeof(SampleViewModel))
{
ViewType = ViewType.Both
}
);
App use service view model locator object to automatically perform the connection. View model dependencies are registered with the Autofac dependency injection container, and resolved when the view model is created. A base view model class implements common functionality such as navigation and suspend/resume support for view model state. View model classes then derive from this base class in order to inherit the common functionality.
14. Associate view and view model in dependency injection container
In the DEM, views are constructed before view models. There is one view class per page of the UI (a page is an instance of the BaseContentPage class or ModalContentPage class for PopUps). Each view model is declaratively connected to a corresponding view with convetion view-model name mast be same as view name without sufix Page or ViewModel.
typeCatalog.AddOrUpdate(
new ViewModelInfo(typeof(Customized.AccountDetailsViewModelOverride))
{
PageTypes = new Type[] { typeof(Usecases.AccountDetails.AccountDetailsPage) }
}
);
With multiple pages view model you can specifay more then one view, that is asocited with view-model
new ViewModelInfo(typeof(Usecases.Transactions.TransactionsViewModel))
{
PageTypes = new Type[]{
typeof(Usecases.Transactions.TransactionsPage),
typeof(Usecases.Transactions.TransactionFilterPage),
typeof(Usecases.Transactions.TransactionDetailsPage)
}
},
For more infromation see Navigation
Dependency injection
Dependency injection enables decoupling of concrete types from the code that depends on these types. It uses a container that holds a list of registrations and mappings between interfaces and abstract types and the concrete types that implement or extend these types. The DEM uses the Autofac dependency injection container to manage the instantiation of the view model and service classes in the app.
Before you can inject dependencies into an object, the types of the dependencies need to be registered with the container. After a type is registered, it can be resolved or injected as a dependency.
In the DEM, the base application class instantiates the Container object and is the only class in the app that
holds a reference to a container object. Types are registered in the OnRegister method in the App class.
For more info see dependency injection.
All of the view models in the DEM share the app�s domain model DemApplication, which is often just called the model. The model consists of classes that the view models use to implement the app�s functionality. View models are connected to the model classes through model properties on the view mode.
Updating a view in response to changes in the underlying view model or model
All view model and model classes that are accessible to the view should implement the INotifyPropertyChanged interface. Implementing the INotifyPropertyChanged interface in view model or model classes allows them to provide change notifications to any data-bound controls in the view when the underlying property value changes. However, this can be repetitive and errorprone. Becouse that DEM library provides the ObservableObject class that implements the INotifyPropertyChanged interface. The following code example shows this class.
public class ObservableObject : INotifyPropertyChanged, IDisposable
{
/// <summary>
/// Sets the property.
/// </summary>
/// <returns><c>true</c>, if property was set, <c>false</c> otherwise.</returns>
/// <param name="backingStore">Backing store.</param>
/// <param name="value">Value.</param>
/// <param name="propertyName">Property name.</param>
/// <param name="onChanged">On changed.</param>
/// <typeparam name="T">The 1st type parameter.</typeparam>
protected bool SetProperty<T>(
ref T backingStore, T value,
[CallerMemberName]string propertyName = "",
Action onChanged = null)
{
if (EqualityComparer<T>.Default.Equals(backingStore, value))
{
return false;
}
backingStore = value;
onChanged?.Invoke();
OnPropertyChanged(propertyName);
return true;
}
/// <summary>
/// Occurs when property changed.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Raises the property changed event.
/// </summary>
/// <param name="propertyName">Property name.</param>
protected void OnPropertyChanged([CallerMemberName]string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected void RefreshProperty(string propertyName)
{
// ReSharper disable once ExplicitCallerInfoArgument, We are updating one property from another method / property
OnPropertyChanged(propertyName);
}
public virtual void Dispose()
{
PropertyChanged = null;
}
}
Each view model class in the DEM derives from the ViewModel base class that in turn derives from the ObservableObject base class. Therefore, each view model class uses the SetProperty method in the ObservableObject class to provide property change notification. The following code example shows how property change notification is implemented in a view model class in the Bank sample.
private int tabPosition = 1;
public int TabPosition
{
get { return tabPosition; }
set { SetProperty(ref tabPosition, value); }
}
Customize exsisting VM
The preferred way to change a VM's behavior is to subclass the implementation of the VM, add members / override virtual members as needed
public class AccountDetailsViewModelOverride : AccountDetailsViewModel
{
public override async Task<bool> StartAsync()
{
var output = await base.StartAsync();
Account["Test"] = "33";
return output;
}
}
Then inject the modified VM through the DI
typeCatalog.AddOrUpdate(
new ViewModelInfo(typeof(Customized.AccountDetailsViewModelOverride))
);
When an implementation is not able to modify the VM for its needs through existing virtual methods and by adding new members, they should create an Issue and the development team will consider upgrading the vie VM as needed.
View customization
To add new View for customized VM, just register PageType with VM like below
typeCatalog.AddOrUpdate(
new ViewModelInfo(typeof(Customized.AccountDetailsViewModelOverride))
{
PageTypes = new Type[] { typeof(Customized.AccountDetailsPage) }
}
);
It's the same thing if you want to change the view for existing VM
typeCatalog.AddOrUpdate(
new ViewModelInfo(typeof(AccountData.Usecases.AccountDetails.AccountDetailsViewModel))
{
PageTypes = new Type[] { typeof(Customized.AccountDetailsPage) }
}
);
View looks like
<pages:UsecaseContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:pages="clr-namespace:AssecoSEE.DEM.Core.Pages"
xmlns:local="clr-namespace:AssecoSEE.DEM.AccountData"
xmlns:specific="clr-namespace:AssecoSEE.DEM.AccountData.Usecases.AccountDetails;assembly=AssecoSEE.DEM.AccountData"
xmlns:templates="clr-namespace:AssecoSEE.DEM.Components.Templates;assembly=AssecoSEE.DEM.Components"
NavigationPage.TitleView="{templates:PageTitleView}"
x:Class="Bank.Sample.Customized.AccountDetailsPage">
<pages:UsecaseContentPage.Content>
<ScrollView Orientation="Vertical" HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
<StackLayout>
<StackLayout Padding="20">
<Label Text="Test" Style="{StaticResource TitleLabel}" />
<Label Text="{Binding Account[Test].Value}" Style="{StaticResource ValueLabel}" />
<BoxView Style="{StaticResource DisplayValueBox}" />
</StackLayout>
<specific:AccountDetailsTemplate />
<specific:AccountDetailsActionsTemplate />
</StackLayout>
</ScrollView>
</pages:UsecaseContentPage.Content>
</pages:UsecaseContentPage>
Note
Be sure to change tick for type configuration cache in Application
Creating a view defined as a data template
A view can be defined as a data template and associated with a view model type. Data templates can be defined as resources, or they can be defined inline within the control that will display the view model. The content of the control is the view model instance, and the data template is used to visually represent it. This technique is an example of a situation in which the view model is instantiated first, followed by the creation of the view.
Data templates are flexible and lightweight. The UI designer can use them to easily define the visual representation of a view model without requiring any complex code. Data templates are restricted to views that do not require any UI logic (code-behind).
The following example shows the AccountWidgetView custom component that is bound to a collection of Acounts. The view for each Account is defined by the ItemTemplate property.
<ctrl:ACarouselViewControl x:Name="carouselAccountWidgetView" Orientation="Horizontal" Position="{Binding Position}" InterPageSpacing="10" ItemsSource="{Binding CollectionData, Converter={StaticResource EnumerableNullConverter}}"
HeightRequest="170" AutomationId="AccountsCarouselTestUI" ItemTemplate="{StaticResource AccountWidgetItemTemplate}" ArrowsBackgroundColor="Transparent"
ShowIndicators="false" ShowArrows="True" VerticalOptions="FillAndExpand" HorizontalOptions="FillAndExpand" >
</ctrl:ACarouselViewControl>
The AccountWidgetItemTemplate specifies that the view for each Account consists of a StackLayout containing multiple child elements, including several Labels.
<DataTemplate x:Key="AccountWidgetItemTemplate">
<StackLayout Padding="10,10,10,10" Margin="10,10,10,10">
<Label TextColor="Black" Text="Override AccountWidgetItemViewTemplate" FontAttributes="Bold"
HorizontalTextAlignment="Center" VerticalTextAlignment="Center" />
<Label TextColor="Black" Text="{Binding Value.Nickname}"
HorizontalTextAlignment="Center" VerticalTextAlignment="Center" />
<Label TextColor="Black" Text="{Binding Value.AccountNumber}"
HorizontalTextAlignment="Center" VerticalTextAlignment="Center" />
</StackLayout>
</DataTemplate>
Additional MVVM considerations
Here are some additional considerations when applying the MVVM pattern to DEM apps in C#.
Centralize data conversions in the view model or a conversion layer
The view model provides data from the model in a form that the view can easily use. To do this the view model sometimes has to perform data conversion. Placing this data conversion in the view model is a good idea because it provides properties in a form that the UI can bind to. It is also possible to have a separate data conversion layer that sits between the view model and the view. This might occur, for example, when data types need special formatting that the view model doesn�t provide, like converters.
Expose operational modes in the view model
The view model may also be responsible for defining logical state changes that affect some aspect of the display in the view, such as an indication that some operation is pending or whether a particular command is available. You don't need code-behind to enable and disable UI elements�you can achieve this by binding to a view model property, or with visual states.
Keep views and view models independent
The binding of views to a particular property in its data source should be a view's principal dependency on its corresponding view model. In particular, do not reference view types or the DemApplication.Current object from view models. If you follow the principles we outlined here, you will have the ability to test view models in isolation, and reduce the likelihood of software defects by limiting scope.
Use asynchronous programming techniques to keep the UI responsive
DEM apps are about a fast and fluid user experience. For that reason the DEM reference implementation keeps the UI thread unblocked. DEM uses asynchronous library methods for I/O operations and raises events to asynchronously notify the view of a property change.
Using the Model-View-ViewModel (MVVM) pattern
MVVM provides a way for developers to cleanly separate the user interface controls from their logic. This separation makes it easy to test the business logic of the app. The MVVM pattern is well documented, and it brings benefits to many categories of apps. However, it is not always suited to every app. For example, using code-behind may be the best choice for small apps that have a limited life span.
When use MVVM, please check this
- Used a dependency injection container to decouple concrete types from the code that depends on those types, if appropriate.
- Used a convention-based approach for view model construction to remove the need for some boilerplate code.
- Used an DI to automatically connect views to view models.
- Promoted the testability of the app by exposing commands from the view models for controls on the views that supports Commad.
- Promoted the testability of the app by exposing behaviors to views for controls that not-supports Commad.
- Supported a view model hierarchy in order to eliminate redundant code in the view model classes.
More info
Other Resources