In this article by Mark Reynolds, author of the book Xamarin Essentials, we will take the next step and look at how the use of design patterns and frameworks can increase the amount of code that can be reused. We will cover the following topics:
It’s more than a little ambitious to try to cover MvvmCross along with a working example in a single article. Our approach will be to introduce the core concepts at a high level and then dive in and create the national parks sample app using MvvmCross. This will give you a basic understanding of how to use the framework and the value associated with its use. With that in mind, let’s get started.
(For more resources related to this topic, see here.)
MvvmCross is an open source framework that was created by Stuart Lodge. It is based on the Model-View-ViewModel (MVVM) design pattern and is designed to enhance code reuse across numerous platforms, including Xamarin.Android, Xamarin.iOS, Windows Phone, Windows Store, WPF, and Mac OSX. The MvvmCross project is hosted on GitHub and can be accessed at https://github.com/MvvmCross/MvvmCross.
MVVM is a variation of the Model-View-Controller pattern. It separates logic traditionally placed in a View object into two distinct objects, one called View and the other called ViewModel. The View is responsible for providing the user interface and the ViewModel is responsible for the presentation logic. The presentation logic includes transforming data from the Model into a form that is suitable for the user interface to work with and mapping user interaction with the View into requests sent back to the Model. The following diagram depicts how the various objects in MVVM communicate:
While MVVM presents a more complex implementation model, there are significant benefits of it, which are as follows:
These factors make the MVVM approach both flexible and powerful.
Views in an MvvmCross app are implemented using platform-specific constructs. For iOS apps, Views are generally implemented as ViewControllers and XIB files. MvvmCross provides a set of base classes, such as MvxViewContoller, that iOS ViewControllers inherit from. Storyboards can also be used in conjunction with a custom presenter to create Views; we will briefly discuss this option in the section titled Implementing the iOS user interface later in this article.
For Android apps, Views are generally implemented as MvxActivity or MvxFragment along with their associated layout files.
ViewModels are classes that provide data and presentation logic to views in an app. Data is exposed to a View as properties on a ViewModel, and logic that can be invoked from a View is exposed as commands. ViewModels inherit from the MvxViewModel base class.
Commands are used in ViewModels to expose logic that can be invoked from the View in response to user interactions. The command architecture is based on the ICommand interface used in a number of Microsoft frameworks such as Windows Presentation Foundation (WPF) and Silverlight. MvvmCross provides IMvxCommand, which is an extension of ICommand, along with an implementation named MvxCommand.
The commands are generally defined as properties on a ViewModel. For example:
public IMvxCommand ParkSelected { get; protected set; }
Each command has an action method defined, which implements the logic to be invoked:
protected void ParkSelectedExec(NationalPark park)
{
. . .// logic goes here
}
The commands must be initialized and the corresponding action method should be assigned:
ParkSelected =
new MvxCommand<NationalPark> (ParkSelectedExec);
Data binding facilitates communication between the View and the ViewModel by establishing a two-way link that allows data to be exchanged. The data binding capabilities provided by MvvmCross are based on capabilities found in a number of Microsoft XAML-based UI frameworks such as WPF and Silverlight. The basic idea is that you would like to bind a property in a UI control, such as the Text property of an EditText control in an Android app to a property of a data object such as the Description property of NationalPark. The following diagram depicts this scenario:
There are four different binding modes that can be used for data binding:
The INotifyPropertyChanged interface is an integral part of making data binding work effectively; it acts as a contract between the source object and the target object. As the name implies, it defines a contract that allows the source object to notify the target object when data has changed, thus allowing the target to take any necessary actions such as refreshing its display.
The interface consists of a single event—the PropertyChanged event—that the target object can subscribe to and that is triggered by the source if a property changes. The following sample demonstrates how to implement INotifyPropertyChanged:
public class NationalPark : INotifyPropertyChanged
{
public event PropertyChangedEventHandler
PropertyChanged;
// rather than use "… code" it is safer to use
// the comment form
string _name;
public string Name
{
get { return _name; }
set
{
if (value.Equals (_name,
StringComparison.Ordinal))
{
// Nothing to do - the value hasn't changed;
return;
}
_name = value;
OnPropertyChanged();
}
}
. . .
void OnPropertyChanged(
[CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this,
new PropertyChangedEventArgs(propertyName));
}
}
}
Bindings can be specified in a couple of ways. For Android apps, bindings can be specified in layout files. The following example demonstrates how to bind the Text property of a TextView instance to the Description property in a NationalPark instance:
<TextView
android_layout_width="match_parent"
android_layout_height="wrap_content"
android_id="@+id/descrTextView"
local:MvxBind="Text Park.Description" />
For iOS, binding must be accomplished using the binding API. CreateBinding() is a method than can be found on MvxViewController. The following example demonstrates how to bind the Description property to a UILabel instance:
this.CreateBinding (this.descriptionLabel).
To ((DetailViewModel vm) => vm.Park.Description).
Apply ();
Navigating between various screens within an app is an important capability. Within a MvvmCross app, this is implemented at the ViewModel level so that navigation logic can be reused. MvvmCross supports navigation between ViewModels through use of the ShowViewModel<T>() method inherited from MvxNavigatingObject, which is the base class for MvxViewModel. The following example demonstrates how to navigate to DetailViewModel:
ShowViewModel<DetailViewModel>();
In many situations, there is a need to pass information to the destination ViewModel. MvvmCross provides a number of ways to accomplish this. The primary method is to create a class that contains simple public properties and passes an instance of the class into ShowViewModel<T>(). The following example demonstrates how to define and use a parameters class during navigation:
public class DetailParams
{
public int ParkId { get; set; }
}
// using the parameters class
ShowViewModel<DetailViewModel>(
new DetailViewParam() { ParkId = 0 });
To receive and use parameters, the destination ViewModel implements an Init() method that accepts an instance of the parameters class:
public class DetailViewModel : MvxViewModel
{
. . .
public void Init(DetailViewParams parameters)
{
// use the parameters here . . .
}
}
Each MvvmCross solution will have a single core PCL project that houses the reusable code and a series of platform-specific projects that contain the various apps. The following diagram depicts the general structure:
MvvmCross apps generally follow a standard startup sequence that is initiated by platform-specific code within each app. There are several classes that collaborate to accomplish the startup; some of these classes reside in the core project and some of them reside in the platform-specific projects. The following sections describe the responsibilities of each of the classes involved.
The core project has an App class that inherits from MvxApplication. The App class contains an override to the Initialize() method so that at a minimum, it can register the first ViewModel that should be presented when the app starts:
RegisterAppStart<ViewModels.MasterViewModel>();
Android and iOS projects have a Setup class that is responsible for creating the App object from the core project during the startup. This is accomplished by overriding the CreateApp() method:
protected override IMvxApplication CreateApp()
{
return new Core.App();
}
For Android apps, Setup inherits from MvxAndroidSetup. For iOS apps, Setup inherits from MvxTouchSetup.
Android apps are kicked off using a special Activity splash screen that calls the Setup class and initiates the MvvmCross startup process. This is all done automatically for you; all you need to do is include the splash screen definition and make sure it is marked as the launch activity. The definition is as follows:
[Activity(
Label="NationalParks.Droid", MainLauncher = true,
Icon="@drawable/icon", Theme="@style/Theme.Splash",
NoHistory=true,
ScreenOrientation = ScreenOrientation.Portrait)]
public class SplashScreen : MvxSplashScreenActivity
{
public SplashScreen():base(Resource.Layout.SplashScreen)
{
}
The iOS app startup is slightly less automated and is initiated from within the FinishedLaunching() method of AppDelegate:
public override bool FinishedLaunching (
UIApplication app, NSDictionary options)
{
_window = new UIWindow (UIScreen.MainScreen.Bounds);
var setup = new Setup(this, _window);
setup.Initialize();
var startup = Mvx.Resolve<IMvxAppStart>();
startup.Start();
_window.MakeKeyAndVisible ();
return true;
}
Now that we have basic knowledge of the MvvmCross framework, let’s put that knowledge to work and convert the NationalParks app to leverage the capabilities we just learned.
We will start by creating the core project. This project will contain all the code that will be shared between the iOS and Android app primarily in the form of ViewModels. The core project will be built as a Portable Class Library.
To create NationalParks.Core, perform the following steps:
The next step is to create an Android app project in the same solution.
To create NationalParks.Droid, complete the following steps:
Before we start creating the Views and ViewModels for our app, we first need to bring in some code from our previous efforts that can be used to maintain parks. For this, we will simply reuse the NationalParksData singleton and the FileHandler classes that were created previously.
To reuse the NationalParksData singleton and FileHandler classes, complete the following steps:
We will be using data binding to bind UI controls to the NationalPark object and thus, we need to implement the INotifyPropertyChange interface. This ensures that changes made to properties of a park are reported to the appropriate UI controls.
To implement INotifyPropertyChange, complete the following steps:
public class NationalPark : INotifyPropertyChanged
{
public event PropertyChangedEventHandler
PropertyChanged;
. . .
}
void OnPropertyChanged(
CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this,
new PropertyChangedEventArgs(propertyName));
}
}
string _name;
public string Name
{
get { return _name; }
set
{
if (value.Equals (_name, StringComparison.Ordinal))
{
// Nothing to do - the value hasn't changed;
return;
}
_name = value;
OnPropertyChanged();
}
}
Now, we are ready to create the Views and ViewModels required for our app. The app we are creating will follow the following flow:
The process for creating views and ViewModels in an Android app generally consists of three different steps:
In our case, this process will be slightly different because we will reuse some of our previous work, specifically, the layout files and the menu definitions.
To reuse layout files and menu definitions, perform the following steps:
We are now ready to implement the first of our View/ViewModel combinations, which is the master list view.
The first step is to create a ViewModel and add a property that will provide data to the list view that displays national parks along with some initialization code.
To create MasterViewModel, complete the following steps:
. . .
using Cirrious.CrossCore.Platform;
using Cirrious.MvvmCross.ViewModels;
. . .
namespace NationalParks.Core.ViewModels
{
public class MasterViewModel : MvxViewModel
{
. . .
}
}
private List<NationalPark> _parks;
public List<NationalPark> Parks
{
get { return _parks; }
set { _parks = value;
RaisePropertyChanged(() => Parks);
}
}
. . .
using NationalParks.PortableData;
. . .
public async override void Start ()
{
base.Start ();
await NationalParksData.Instance.Load ();
Parks = new List<NationalPark> (
NationalParksData.Instance.Parks);
}
RegisterAppStart<ViewModels.MasterViewModel>();
Update Master.axml so that it can leverage the data binding capabilities provided by MvvmCross.
To update Master.axml, complete the following steps:
This namespace definition is required in order to allow Android to resolve the MvvmCross-specific elements that will be specified.
<Mvx.MvxListView
android_layout_width="match_parent"
android_layout_height="match_parent"
android_id="@+id/parksListView" />
. . .
android_id="@+id/parksListView"
local_MvxBind="ItemsSource Parks" />
local:MvxItemTemplate="@layout/nationalparkitem"
<LinearLayout
android_orientation="vertical"
android_layout_width="fill_parent"
android_layout_height="wrap_content">
<TextView
android_layout_width="match_parent"
android_layout_height="wrap_content"
android_textSize="40sp"/>
<TextView
android_layout_width="match_parent"
android_layout_height="wrap_content"
android_textSize="20sp"/>
</LinearLayout>
. . .
local_MvxBind="Text Name" />
. . .
local_MvxBind="Text Description" />
. . .
Note that in this case, the context for data binding is an instance of an item in the collection that was bound to MvxListView, for this example, an instance of NationalPark.
Next, create MasterView, which is an MvxActivity instance that corresponds with MasterViewModel.
To create MasterView, complete the following steps:
using Cirrious.MvvmCross.Droid.Views;
using NationalParks.Core.ViewModels;
. . .
namespace NationalParks.Droid.Views
{
[Activity(Label = "Parks")]
public class MasterView : MvxActivity
{
. . .
}
}
protected override IMvxApplication CreateApp()
{
NationalParksData.Instance.FileHandler =
new FileHandler ();
NationalParksData.Instance.DataDir =
System.Environment.GetFolderPath(
System.Environment.SpecialFolder.MyDocuments);
return new Core.App();
}
Now that we have the master list view displaying national parks, we can focus on creating the detail view. We will follow the same steps for the detail view as the ones we just completed for the master view.
We start creating DetailViewModel by using the following steps:
protected NationalPark _park;
public NationalPark Park
{
get { return _park; }
set { _park = value;
RaisePropertyChanged(() => Park);
}
}
public class DetailViewModel : MvxViewModel
{
public class Parameters
{
public string ParkId { get; set; }
}
. . .
public void Init(Parameters parameters)
{
Park = NationalParksData.Instance.Parks.
FirstOrDefault(x => x.Id == parameters.ParkId);
}
Next, we will update the layout file. The main changes that need to be made are to add data binding specifications to the layout file.
To update the Detail.axml layout, perform the following steps:
<TextView
android_layout_width="match_parent"
android_layout_height="wrap_content"
android_id="@+id/nameTextView"
local_MvxBind="Text Park.Name" />
Now, create the MvxActivity instance that will work with DetailViewModel.
To create DetailView, perform the following steps:
The last step is to add navigation so that when an item is clicked on in MvxListView on MasterView, the park is displayed in the detail view. We will accomplish this using a command property and data binding.
To add navigation, perform the following steps:
protected IMvxCommand ParkSelected { get; protected set; }
protected void ParkSelectedExec(NationalPark park)
{
ShowViewModel<DetailViewModel> (
new DetailViewModel.Parameters ()
{ ParkId = park.Id });
}
ParkClicked =
new MvxCommand<NationalPark> (ParkSelectedExec);
local:MvxBind="ItemsSource Parks; ItemClick ParkClicked"
We are now almost experts at implementing new Views and ViewModels. One last View to go is the edit view.
Like we did previously, we start with the ViewModel.
To create EditViewModel, complete the following steps:
public void Init(Parameters parameters)
{
if (string.IsNullOrEmpty (parameters.ParkId))
Park = new NationalPark ();
else
Park =
NationalParksData.Instance.
Parks.FirstOrDefault(
x => x.Id == parameters.ParkId);
}
Update Edit.axml to provide data binding specifications.
To update the Edit.axml layout, you first need to open Edit.axml and add the project namespace to the XML file. Then, add the data binding specifications to each of the EditView elements that correspond to a national park property.
Create a new MvxActivity instance named EditView to will work with EditViewModel.
To create EditView, perform the following steps:
Add navigation to two places: when New (+) is clicked from MasterView and when Edit is clicked in DetailView. Let’s start with MasterView.
To add navigation from MasterViewModel, complete the following steps:
protected IMvxCommand NewParkClicked { get; set; }
protected void NewParkClickedExec()
{
ShowViewModel<EditViewModel> ();
}
Note that we do not pass in a parameter class into ShowViewModel(). This will cause a default instance to be created and passed in, which means that ParkId will be null. We will use this as a way to determine whether a new park should be created.
case Resource.Id.actionNew:
((MasterViewModel)ViewModel).
NewParkClicked.Execute ();
return true;
To add navigation from DetailViewModel, complete the following steps:
protected IMvxCommand EditPark { get; protected set;}
protected void EditParkHandler()
{
ShowViewModel<EditViewModel> (
new EditViewModel.Parameters ()
{ ParkId = _park.Id });
}
Note that an instance of the Parameters class is created, initialized, and passed into the ShowViewModel() method. This instance will in turn be passed into the Init() method on EditViewModel.
EditPark =
new MvxCommand<NationalPark> (EditParkHandler);
case Resource.Id.actionEdit:
((DetailViewModel)ViewModel).EditPark.Execute ();
return true;
The process of creating the Android app with MvvmCross provides a solid understanding of how the overall architecture works. Creating the iOS solution should be much easier for two reasons: first, we understand how to interact with MvvmCross and second, all the logic we have placed in NationalParks.Core is reusable, so that we just need to create the View portion of the app and the startup code.
To create NationalParks.iOS, complete the following steps:
We are now ready to create the user interface for the iOS app. The good news is that we already have all the ViewModels implemented, so we can simply reuse them. The bad news is that we cannot easily reuse the storyboards from our previous work; MvvmCross apps generally use XIB files. One of the reasons for this is that storyboards are intended to provide navigation capabilities and an MvvmCross app delegates that responsibility to ViewModel and presenter. It is possible to use storyboards in combination with a custom presenter, but the remainder of this article will focus on using XIB files, as this is the more common use. The screen layouts can be used as depicted in the following screenshot:
We are now ready to get started.
The first view we will work on is the master view.
To implement the master view, complete the following steps:
// ios7 layout
if (RespondsToSelector(new
Selector("edgesForExtendedLayout")))
EdgesForExtendedLayout = UIRectEdge.None;
MvxStandardTableViewSource _source;
. . .
_source = new MvxStandardTableViewSource(
parksTableView,
UITableViewCellStyle.Subtitle,
new NSString("cell"),
"TitleText Name; DetailText Description",
0);
parksTableView.Source = _source;
Note that the example uses the Subtitle cell style and binds the national park name and description to the title and subtitle.
var set = this.CreateBindingSet<MasterView,
MasterViewModel>();
set.Bind (_source).To (vm => vm.Parks);
set.Apply();
Now, implement the detail view using the following steps:
this.CreateBinding (this.nameLabel).
To ((DetailViewModel vm) => vm.Park.Name).Apply ();
this.CreateBinding (this.descriptionLabel).
To ((DetailViewModel vm) => vm.Park.Description).
Apply ();
this.CreateBinding (this.stateLabel).
To ((DetailViewModel vm) => vm.Park.State).Apply ();
this.CreateBinding (this.countryLabel).
To ((DetailViewModel vm) => vm.Park.Country).
Apply ();
this.CreateBinding (this.latLabel).
To ((DetailViewModel vm) => vm.Park.Latitude).
Apply ();
this.CreateBinding (this.lonLabel).
To ((DetailViewModel vm) => vm.Park.Longitude).
Apply ();
Add navigation from the master view so that when a park is selected, the detail view is displayed, showing the park.
To add navigation, complete the following steps:
. . .
_source.SelectedItemChanged += ParkSelected;
. . .
protected void ParkSelected(object sender, EventArgs e)
{
. . .
}
((MasterViewModel)ViewModel).ParkSelected.Execute (
(NationalPark)_source.SelectedItem);
We now need to implement the last of the Views for the iOS app, which is the edit view.
To implement the edit view, complete the following steps:
protected void DoneClicked (object sender, EventArgs e)
{
((EditViewModel)ViewModel).Done.Execute();
}
NavigationItem.SetRightBarButtonItem(
new UIBarButtonItem(UIBarButtonSystemItem.Done,
DoneClicked), true);
Add navigation to two places: when New (+) is clicked from the master view and when Edit is clicked on in the detail view. Let’s start with the master view.
To add navigation to the master view, perform the following steps:
protected void NewParkClicked(object sender,
EventArgs e)
{
((MasterViewModel)ViewModel).
NewParkClicked.Execute ();
}
NavigationItem.SetRightBarButtonItem(
new UIBarButtonItem(UIBarButtonSystemItem.Add,
NewParkClicked), true);
To add navigation to the details view, perform the following steps:
protected void EditParkClicked (object sender,
EventArgs e)
{
((DetailViewModel)ViewModel).EditPark.Execute ();
}
NavigationItem.SetRightBarButtonItem(
new UIBarButtonItem(UIBarButtonSystemItem.Edit,
EditParkClicked), true);
One last detail that needs to be taken care of is to refresh the UITableView control on MasterView when items have been changed on EditView.
To refresh the master view list, perform the following steps:
public override void ViewDidAppear (bool animated)
{
base.ViewDidAppear (animated);
parksTableView.ReloadData();
}
After completing our work, we now have the basis to make some fundamental observations. Let’s start with the pros:
All of these positives are not necessarily free; let’s look at some cons:
With these things in mind, I would definitely consider using MvvmCross for a cross-platform mobile project. Yes, you need to learn an addition framework and yes, you will likely have to align the way some of the apps are laid out, but I think MvvmCross provides enough value and flexibility to make these issues workable. I’m a big fan of reuse and MvvmCross definitely pushes reuse to the next level.
In this article, we reviewed the high-level concepts of MvvmCross and worked through a practical exercise in order to convert the national parks apps to use the MvvmCross framework and the increase code reuse. In the next article, we will follow a similar approach to exploring the Xamarin.Forms framework in order to evaluate how its use can affect code reuse.
Further resources on this subject:
At Packt, we are always on the lookout for innovative startups that are not only…
I remember deciding to pursue my first IT certification, the CompTIA A+. I had signed…
Key takeaways The transformer architecture has proved to be revolutionary in outperforming the classical RNN…
Once we learn how to deploy an Ubuntu server, how to manage users, and how…
Key-takeaways: Clean code isn’t just a nice thing to have or a luxury in software projects; it's a necessity. If we…
While developing a web application, or setting dynamic pages and meta tags we need to deal with…