This article outlines the basic principles and mechanisms of the MvvmCross framework, which facilitates the creation of loosely coupled, maintainable, and testable mobile solutions. There are many tested methods of organizing projects to create an individual architecture for each project based on a Shared Project or Portable Class Library.
However, it is more effective to use a ready-made cross-platform frame solution, which will define the structure and basic mechanisms of its operation; will provide a set of libraries, components, and plugins; which speeds up the development process. Among many popular frameworks, such as Xamarin.Forms, ReactiveUI, FreshMvvm, Prism, and MvvmCross deserve special attention. It offers a good compromise between a high-quality UX (User Experience) and the amount of code shared across projects.
The following text is an English translation of an article by Sylwester Wieczorkowski, published in the 06/2016 (49) issue of the Polish IT magazine “Programista” The translation was performed by Leaware.
WHAT IS MVVMCROSS?
As the name suggests, MvvmCross is a framework facilitating the creation of cross-platform applications conforming with the MVVM model (Model-View-ViewModel). It supports many popular types of .NET projects, such as:
-
Xamarin.Android
-
Xamarin.iOS
-
Xamarin.Mac
-
WinRT (Windows 8.1, Windows Phone 8.1)
-
Universal Windows Platform (UWP) (Windows 10)
-
Windows Presentation Foundation (WPF)
-
It also provides mechanisms for data binding for platforms that natively use the MVC model (Model-View-Controller).
MvvmCross applications usually are composed of two fundamental parts: A core project built around a Portable Class Library (PCL), containing all the view models, models, and interfaces of platform-specific services. The core PCL carries the business logic, database handling, and a layer of access to web services. A native project for each platform, containing the user interface and implementation of platform-specific services. It is a good practice to create an additional PCL project to separate the application business logic from the data access layer. The amount of shared code changes depending on the application type. Of course, if our application uses more of the native API, a smaller part of the solution will be used again. In the case of business applications, it is possible to share about 70-80% of the entire solution.
To start your adventure with the MvvmCross framework, you need to create a solution containing all the necessary projects: at least one PCL library and a native project for each platform you plan to support. Next, you need to add a NuGet MvvmCross Starter Pack to each of them and make a few basic configuration steps described in the files contained in the ToDo-MvvmCross directory.
The entire MvvmCross framework, together with documentation and video tutorials, is available on GitHub.
BASIC ELEMENTS OF THE FRAMEWORK
Every MvvmCross application has certain elements: an App class, a Setup class, and view models. This section examines those parts along with typical implementations.
Listing 1 below shows a typical example of the App class. In every MvvmCross application, there is exactly one implementation of the App class, which inherits from the MvxApplication class. The Initialize method registers the entry point: the first view model that will be created after entering the application (in this case ProductsViewModel). Initialize also registers types injected on the common side.
Listing 1. An example implementation of the App class in a PCL project
public class App : MvvmCross.Core.ViewModels.MvxApplication
{
public override void Initialize()
{
CreatableTypes()
.EndingWith("Service")
.AsInterfaces()
.RegisterAsLazySingleton();
RegisterAppStart<ViewModels.ProductsViewModel>;
Mvx.RegisterType<IProductRepository>(
() => new ProductWebRepository("http://webservice/api/product/"));
}
}
MvvmCross provides the static Mvx class, which functions as a container for injecting dependencies and is responsible for managing implementations registered both in the common part in the platform-specific projects in the Setup class.
The Setup class is a kind of bootstrapper for MvvmCross and is present in every platform-specific project. Project Xamarin.Android is an example (Listing 2). The basic task of this class is to create an instance of the App class, as well as to adjust the framework to the specifics of our application.
Listing 2. Implementation of the Setup class in Xamarin.Android project
public class Setup: MvxAndroidSetup
{
public Setup(Context applicationContext) : base(applicationContext)
{
}
protected override IMvxApplication CreateApp()
{
return new Core.App();
}
protected override IMvxTrace CreateDebugTrace()
{
return new DebugTrace();
}
protected override void InitializePlatformServices()
{
base.InitializePlatformServices();
Mvx.RegisterType<ICallerService, DroidCallerService>();
Mvx.RegisterType<IEmailService, DroidEmailService>();
Mvx.RegisterType<IPopupService, DroidPopupService>();
}
}
The MvxAndroidSetup class, from which the Setup class inherits, provides a series of virtual methods that need to be overridden to, among other things, register all the platform services (referring to the native API). The platform-specific services are used in the common part to execute instructions specific for every platform – the Inversion of Control mechanism.
Another important element of the MvvmCross solution is the ViewModel, which functions as a container for properties and commands responsible for changing the state and retaining the view related to it.
Listing 3. A fragment of an example viewmodel
public class ProductsViewModel: MvxViewModel
{
private IProductRepository product repository;
private List<Product> products;
public List<Product> Products
{
get { return products; }
set { SetProperty(ref products, value); }
}
private bool isAddButtonEnabled;
public bool IsAddButtonEnabled
{
get { return isAddButtonEnabled; }
set
{
isAddButtonEnabled = value;
RaisePropertyChanged(() => IsAddButtonEnabled);
}
}
private IMvxCommand adddProductCommand;
public IMvxCommand AddProductCommand
{
get
{
adddProductCommand = adddProductCommand ?? new MvxCommand(
() => ShowViewModel<AddProductViewModel>());
return adddProductCommand;
}
}
public ProductsViewModel(IProductRepository productRepository)
{
this.productRepository = productRepository;
}
...
}
The base class of the presented fragment of a ViewModel (Listing 3) contains an implementation of interfaces INotifyPropertyChanged, INotifyCollectionChanged, and the methods such as SetProperty or RaisePropertyChanged. These interfaces and methods allow the system to refresh elements of a view: when certain properties change they trigger an event that conveys that change through the UI.
Commands implemented using the MvxCommand class manage individual actions performed by the user, e.g. changing a view. MvxViewModel provides many useful methods for functions such as navigation between view models (ShowViewModel) or management of view lifecycle. The job of the Mvx container is to automatically inject dependencies into created view models.
DEFINING THE USER INTERFACE: HOW TO CREATE VIEWS
The data binding mechanism is a natural element of the Windows ecosystem (WPF, WinRT, and UWP), so in Windows, the method for creating views uses the native approach. Therefore, let’s concentrate on the Android and iOS platforms, for which the native model is MVC, where controllers (iOS) and activities/fragments (Android) play a key role.
An exceptional advantage of the MvvmCross framework is the fact that contrary to Xamarin.Forms all the views, layouts, and widgets are defined entirely natively, using native mechanisms and tools.
Xamarin.Android
In the case of Android (Listing 4), we create XML or XML files (or axml) that use only the native API for constructing layouts for individual views. MvvmCross provides the local: MvxBind attribute, which can be used for binding properties of the elements of the view (widgets, layouts) with appropriate properties of a view model according to the above listing.
Listing 4. Definition of a layout for the Android system
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/ res/android"
xmlns:local="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:padding="20dp">
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="20dp">
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.5"
android:text="Add Product"
local:MvxBind="Click AddProductCommand;
Enabled IsAddButtonEnabled" />
<Button
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="0.5"
android:text="Remove All"
local:MvxBind="Click RemoveAllProductsCommand;
Enabled IsRemoveAllButtonEnabled" />
</LinearLayout>
<MvxListView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:divider="@null"
android:scrollbars="none"
android:footerDividersEnabled="false"
android:overScrollFooter="@android:color/transparent"
local:MvxItemTemplate="@layout/view_product_item"
local: MvxBind="ItemsSource Products" />
</LinearLayout>
A framework provides an additional set of UI controls. One of them is a widget (MvxListView) that displays a list of elements. MvxListView specifies a template of a cell of a given list using the local: MvxItemTemplate property. Implementation of an adapter is unnecessary, so we avoid excess code in the platform-specific project.
Listing 5. Implementation of activity in the Xamarin.Android project
[Activity]
public class ProductsActivity: MvxActivity
{
protected override void OnCreate(Bundle bundle)
{
base.OnCreate(bundle);
SetContentView(Resource.Layout.layout_products_activity);
}
}
In MvvmCross, controllers are used only to load the view and bind it with the correct view model (Listing 5). Of course, if the application requires providing certain specific functionalities/actions on a given platform, they can be implemented in the controllers. However, one must remember that this will reduce the amount of code that can be shared, and possible differences or inconsistencies in application behavior make debugging and implementing additional changes more time-consuming. Xamarin.iOS
Creation of a view for the iOS system starts with adding a controller class together with a file with a xib extension that represents the view. Edit xib files with Xamarin Studio (the environment provided by Xamarin) or the Xcode Interface Builder tool built-in in Xcode.
After arranging the view, hold the CTRL key and drag the elements that you want to bind with the view model to the appropriate header file. The changes will be automatically synchronized to the C# language, and more specifically to the class of the created controller (Listing 6). Properties of the controller marked with the attribute Outlet are called outlets. Through them, we get access to individual elements of the user interface.
Listing 6. A section of the controller class containing synchronized outlets
[Register ("ProductsViewController")]
partial class ProductsViewController
{
[Outlet]
UIKit.UIButton AddProductButton { get; set; }
[Outlet]
UIKit.UITableView ProductList { get; set; }
[Outlet]
UIKit.UIButton RemoveAllButton { get; set; }
}
After loading the view we create a set that binds properties of available outlets with the properties of a given viewmodel (Listing 7). The Bind method accepts the object (usually an outlet), which we will consider. The For method specifies the object property which will be bound with the property of the ViewModel specified by the To method. If the For method is skipped, the default property of a given outlet is bound.
Listing 7. The main section of the controller class – binding data
public partial class ProductsViewController: BaseViewController
{
public ProductsViewController() : base("ProductsViewController", null)
{
}
public override void ViewDidLoad()
{
base.ViewDidLoad();
InitializeBinding();
}
private void InitializeBinding()
{
var set = this.CreateBindingSet<ProductsViewController, ProductsViewModel>();
var source = new ProductListDataSource(ProductList);
ProductList.Source = source;
set.Bind(source).For(mn => mn.ItemsSource).To(mn => mn.Products);
set.Bind(AddProductButton).To(mn => mn.AddProductCommand);
set.Bind(AddProductButton).For(mn => mn.Enabled).To(mn => mn.IsAddButtonEnabled);
set.Bind(RemoveAllButton).To(mn => mn.RemoveAllProductsCommand);
set.Bind(RemoveAllButton).For(mn => mn.Enabled).To(mn => mn.IsRemoveAllButtonEnabled);
set.Apply();
}
}
ADVANCED DATA BINDING – CREATING CONVERTERS AND CUSTOM BINDINGS
Frequently, composed views require additional conversion (translation) of bound data, i.e. changing the type or format of viewmodel properties, which gets bound with the property of a given UI control. For this purpose, it is possible to define a converter implementing an abstract class MvxValueConverter (Listing 8).
Listing 8. Converter translating the bool value into appropriate UIColor
public class BoolToTextColorConverter: MvxValueConverter
{
protected override UIColor Convert(bool value, Type targetType, object parameter, CultureInfo culture)
{
return value? UIColor.Red: UIColor.Black;
}
}
How to apply the defined converter? In the case of Xamarin.iOS it comes down to requesting the WithConversion method, which assumes the converter instance (Listing 9).
Listing 9. Data binding with the converter in the Xamarin.iOS project
set.Bind(EmailTextField).For(x => x.TextColor)
.To (x => x.IsErrorVisible)
.WithConversion(new BoolToTextColorConverter());
Xamarin.Android seems to be even more intuitive – it is enough to connect the bound property with the name of our converter (Listing 10).
Listing 10. Data binding with the converter in the Xamarin.Android project
<EditText
style="@style/EmailEditText"
local:MvxBind="TextColor BoolToTextColor(IsErrorVisible)" />
Frequently, a specific property of a ViewModel might deter ViewModel change of a few properties of a widget or requires changing the widget, which can be performed only by requesting calling one or a series of methods for it. In such a case the mechanism allowing for registering custom data bindings becomes necessary.
Listing 11. An example definition of a custom data binding for Xamarin.Android
public class TextViewWithHtmlBinding: MvxConvertingTargetBinding
{
public TextViewWithHtmlBinding(TextView textView)
: base(textView)
{
}
public override Type TargetType
{
get
{
return typeof(TextView);
}
}
protected override void SetValueImpl(object target, object value)
{
var textView = target as TextView;
textView.MovementMethod = LinkMovementMethod.Instance;
textView.SetText(Html.FromHtml((string)value), TextView.BufferType.Spannable);
}
}
The first step is to create a class inheriting from the MvxConvertingTargetBinding class according to the presented example (Listing 11). In the presented example we defined a binding for the text value containing HTML markups. To interpret them correctly, the SetText method needs to be used with appropriate parameters and the property MovementMethod of the TextView widget must be changed – we are not able to do it effectively, based on default bindings.
Listing 12. Registration of a custom binding in the Setup class
protected override void FillTargetFactories(IMvxTargetBindingFactoryRegistry registry)
{
registry.RegisterCustomBindingFactory<TextView>("TextWithHtml", x => new TextViewWithHtmlBinding(x));
base.FillTargetFactories(registry);
}
Next, overload overrides the FillTargetFactories method in the Setup class, registering the created binding under a selected name with an appropriate type of UI control (Listing 12). The entire process is analogous to the Xamarin.iOS system. A registered binding can be successfully used in the entire application in the same way as standard predefined data bindings.
CHANGING THE STANDARD NAVIGATION SCHEME
Each platform-specific MvvmCross package contains a default presenter implementing the IMvxViewPresenter interface. The presenter is responsible for providing a navigation scheme between certain views. By default it uses the mechanism of reflection for associating controllers with view models corresponding to them, therefore the key element is to give names to controllers which correspond to the names of view models used in the PCL project.
If for some reason we want to change the default navigation scheme, it is enough to override the appropriate methods of the default presenter (Listing 13), and after that return it in the CreateViewPresenter method in the Setup class (Listing 14). We deal with this situation, for example, when there is a need to associate a certain group of view models with fragments displayed in the area of main activity – flyout navigation.
Listing 13. Overriding the default presenter MvxAndroidViewPresenter in the Xamarin.Android project
public class DroidPresenter: MvxAndroidViewPresenter
{
public override void Close(IMvxViewModel viewModel)
{
Activity.FinishAffinity();
}
public override void Show(MvxViewModelRequest request)
{
base.Show(request);
}
}
Listing 14. Overriding the default presenter in the Setup class
protected override IMvxAndroidViewPresenter CreateViewPresenter()
{
return new DroidPresenter();
}
UNIT TESTS
Undoubtedly, unit tests are some of the most important elements in the process of ensuring the high quality of the created software. Using the MvvmCross framework requires programmers to create a testable architecture of the entire solution, thanks to which, without too much workload, we can write unit tests for particular elements of our business logic. The NUnit framework recommended by Xamarin executes this function perfectly.
Writing tests must start with adding to our testing project the following set of NuGet packages: MvvmCross, and MvvmCross.Core and MvvmCross.Tests. Naturally, one also needs to ensure that references are attached and added to the project with the business logic of the application, as well as tools allowing for quick imitation of objects – one of the more popular is the Moq framework.
Listing 15. Creating unit tests using NUnit and MvvmCross.Tests
[TestFixture]
public class AddProductViewModelTests : MvxIoCSupportingTest
{
[SetUp]
public new void Setup()
{
base.Setup();
Ioc.RegisterType(() => new Mock<IProductRepository>().Object);
}
[TestCase(" ", "2.5", false)]
[TestCase("Product 1", " ", false)]
[TestCase("", "", false)]
[TestCase(null, null, false)]
[TestCase("Product 1", "2.5", true)]
public void IsAddButtonEnabled_NamePrice(string name, string price, bool expectedValue)
{
var addProductViewModel = Mvx.IocConstruct<AddProductViewModel>();
addProductViewModel.Name = name;
addProductViewModel.Price = price;
var actualValue = addProductViewModel.IsAddButtonEnabled;
Assert.AreEqual(expectedValue, actualValue);
}
}
The MvvmCross.Tests package contains the MvxIoCSupportingTest class, which is a base class for every newly created testing class (Listing 15). Using the Ioc property we register all the types necessary for creating a ViewModel that we are going to test (in the example I mock IProductRepository using the Moq framework).
The Mvx container is responsible for creating a view model and providing all the necessary dependencies. Next, we assign test values to specific properties of the ViewModel and we test the behavior of the remaining ones. In practice, they will determine the changes in the view state associated with the tested view model. The SetUp class and test instances are specified by using standard attributes of the NUnit framework.
PLUGINS AND ADDITIONAL COMPONENTS
The MvvmCross framework provides a large number of plugins and libraries available on GitHub, as well as in the form of NuGet packages. They are, among others, components for simplifying database operation, network availability, connections, locations, operations on files, sending e-mails, integration with social networks, and downloading and storing data in cache memory. All we need to do is add an appropriate package to all the projects in a solution and then use an available API in the common part. There is also a possibility to create your internal plugins or develop existing ones by the instruction available.
SUMMARY
This article presents only selected, most significant mechanisms of the MvvmCross framework. The discussed solution is continuously developed, providing more and more new possibilities. It is undoubtedly the best solution for complex and demanding Xamarin business applications.
Thanks to native methods of building a user interface we can deliver a great UX, and at the same time share a significant part of the entire solution including the testable business logic of the application.
I had the pleasure of participating in complex projects realized with the use of this technology composed of dozens of screens and functionalities, such as mobile banking applications for serious foreign clients, which from the moment of publication have received the highest ratings from hundreds of users, which is the best proof that the presented solution is effective.