Worry-free MVVM with Xamarin Forms
TL;DR - Super-clean MVVM example with automatic Command status updates here
Xamarin Forms is great for writing decent looking applications on all three major mobile platforms - iOS, Android and Windows Mobile. The Xaml support and databindings are great help to keep things clean and simple.
So I am working on a Xamarin Forms project where we were in the process of choosing some framework or library to help us with implementing the MVVM pattern and more specifically the view models where it is necessary to send notifications as properties get changed. We looked at MvvmLight as well as a preview version of Prism. Both of them provide a base class which has helper methods for implementing properties with sending notifications. Here is how a simple Login view model looks with Prism
using System; | |
using Prism.Mvvm; | |
using Xamarin.Forms; | |
using System.Threading.Tasks; | |
namespace MVVMEasy.Pages | |
{ | |
public class LoginPageViewModel_Prism : BindableBase | |
{ | |
private string _errorMessage; | |
private bool _loginInProgress; | |
private string _password = ""; | |
private string _username = ""; | |
public LoginPageViewModel_Prism () | |
{ | |
ErrorMessage = ""; | |
LoginInProgress = false; | |
Login = new Command (async _ => { | |
ErrorMessage = ""; | |
LoginInProgress = true; | |
Login.ChangeCanExecute (); | |
try { | |
// perform login | |
await Task.Delay (5000); | |
} catch (Exception e) { | |
ErrorMessage = e.Message; | |
} | |
LoginInProgress = false; | |
Login.ChangeCanExecute (); | |
}, _ => !LoginInProgress && (Username.Length > 0 && Password.Length > 0)); | |
} | |
public Command Login { get; set; } | |
public string ErrorMessage { | |
get { return _errorMessage; } | |
set { | |
if (SetProperty (ref _errorMessage, value)) | |
OnPropertyChanged (() => IsError); | |
} | |
} | |
public bool IsError { | |
get { return ErrorMessage != ""; } | |
} | |
public bool LoginInProgress { | |
get { return _loginInProgress; } | |
set { | |
if (SetProperty (ref _loginInProgress, value)) { | |
OnPropertyChanged (() => LoginNotInProgress); | |
} | |
} | |
} | |
public bool LoginNotInProgress { get { return !LoginInProgress; } } | |
public string Username { | |
get { return _username; } | |
set { | |
if (SetProperty (ref _username, value)) { | |
Login.ChangeCanExecute (); | |
} | |
} | |
} | |
public string Password { | |
get { return _password; } | |
set { | |
if (SetProperty (ref _password, value)) { | |
Login.ChangeCanExecute (); | |
} | |
} | |
} | |
} | |
} |
A couple of things to notice here:
- Lot's of repetitive code in the property getters and setters
- Extra complexity in both the property setters and the command itself due to keeping the enabled/disabled state of the Login command in sync (lines 22, 30, 63 and 72)
- We need extra (and may I say not particularly pretty) testing of whether the property change notifications were sent and the update of the Login command status was triggered
In order to clean things up let's use Fody.PropertyChanged and get rid of Prism's BindableBase. If you have not looked at this MVVM specialized weaver, please take your time now to get introduced. I think you might like it. And here is how the new model looks like:
using System; | |
using Prism.Mvvm; | |
using Xamarin.Forms; | |
using System.ComponentModel; | |
using System.Threading.Tasks; | |
namespace MVVMEasy.Pages | |
{ | |
public class LoginPageViewModel_FodyPropertyChanged : INotifyPropertyChanged | |
{ | |
public event PropertyChangedEventHandler PropertyChanged; | |
public LoginPageViewModel_FodyPropertyChanged () | |
{ | |
ErrorMessage = ""; | |
LoginInProgress = false; | |
Username = ""; | |
Password = ""; | |
Login = new Command (async _ => { | |
ErrorMessage = ""; | |
LoginInProgress = true; | |
Login.ChangeCanExecute (); | |
try { | |
// perform login | |
await Task.Delay (5000); | |
} catch (Exception e) { | |
ErrorMessage = e.Message; | |
} | |
LoginInProgress = false; | |
Login.ChangeCanExecute (); | |
}, _ => !LoginInProgress && (Username.Length > 0 && Password.Length > 0)); | |
} | |
public Command Login { get; set; } | |
public string ErrorMessage { get; set; } | |
public bool IsError { | |
get { return ErrorMessage != ""; } | |
} | |
public bool LoginInProgress { get; set; } | |
public bool LoginNotInProgress { get { return !LoginInProgress; } } | |
public string Username { get; set; } | |
public string Password { get; set; } | |
} | |
} |
I think Fody.PropertyChanged is great, it helps us get rid of those super-boring, error-prone property getters and setters but unfortunately does nothing about updating the Login command state. In fact this version of the model does not work correctly since the Login command never gets enabled. One way to fix this problem is by making the Login command state be more MVVM-y. To do that let's use MVVMCommand which is exactly like a normal Xamarin Forms Command except the second parameter has to be a lambda that returns the value of a property. Let's see how the fixed model looks:
using System; | |
using Prism.Mvvm; | |
using Xamarin.Forms; | |
using System.ComponentModel; | |
using System.Threading.Tasks; | |
namespace MVVMEasy.Pages | |
{ | |
public class LoginPageViewModel_FodyPropertyChanged_And_MVVMCommand : INotifyPropertyChanged | |
{ | |
public event PropertyChangedEventHandler PropertyChanged; | |
public LoginPageViewModel_FodyPropertyChanged_And_MVVMCommand () | |
{ | |
ErrorMessage = ""; | |
LoginInProgress = false; | |
Username = ""; | |
Password = ""; | |
Login = new MVVMCommand (async _ => { | |
ErrorMessage = ""; | |
LoginInProgress = true; | |
try { | |
await Task.Delay (5000); | |
} catch (Exception e) { | |
ErrorMessage = e.Message; | |
} | |
LoginInProgress = false; | |
}, _ => IsLoginEnabled); | |
} | |
public Command Login { get; set; } | |
public bool IsLoginEnabled { | |
get { return !LoginInProgress && (Username.Length > 0 && Password.Length > 0); } | |
} | |
public string ErrorMessage { get; set; } | |
public bool IsError { | |
get { return ErrorMessage != ""; } | |
} | |
public bool LoginInProgress { get; set; } | |
public bool LoginNotInProgress { get { return !LoginInProgress; } } | |
public string Username { get; set; } | |
public string Password { get; set; } | |
} | |
} |
Notice how in line 28 the state of the command becomes extracted to a property - IsLoginEnabled. The reason that is good is because it plays on the strength of Fody.PropertyChanged. Meaning that Fody.PropertyChanged sends out ProprtyChanged notifications about IsLoginEnabled whenever one of the properties it's value is based on changes. Now all MVVMCommand needs to do is listen for those notifications and do the updating of the enabled/disabled command state. It's actually pretty simple:
using System; | |
using Xamarin.Forms; | |
using System.Linq.Expressions; | |
using System.ComponentModel; | |
using System.Reflection; | |
namespace MVVMEasy | |
{ | |
class MVVMCommand : Command { | |
public MVVMCommand (Action<Object> action, Expression<Func<Object, bool>> propExpression) | |
: base(action, propExpression.Compile()) | |
{ | |
var member = propExpression.Body as MemberExpression; | |
var expression = member.Expression as ConstantExpression; | |
if (member == null) | |
throw new ArgumentException(string.Format( | |
"Expression '{0}' should be a property.", | |
propExpression.ToString())); | |
if (expression == null) | |
throw new ArgumentException(string.Format( | |
"Expression '{0}' should be a constant expression", | |
propExpression.ToString())); | |
var viewModel = (INotifyPropertyChanged)expression.Value; | |
PropertyInfo propInfo = member.Member as PropertyInfo; | |
if (propInfo == null) | |
throw new ArgumentException(string.Format( | |
"Expression '{0}' refers to a field, not a property.", | |
propExpression.ToString())); | |
var propertyName = propInfo.Name; | |
viewModel.PropertyChanged += (sender, e) => { | |
if (e.PropertyName == propertyName) { | |
this.ChangeCanExecute(); | |
}; | |
}; | |
} | |
}} |
Testing this model now becomes really a rather simple and elegant exercise of changing properties and asserting on the value of other properties and no need to worry about bookkeeping regarding who update who.
Hope this makes sense, the source for a complete example is on GitHub.
BTW: Sorry for not providing a way to do comments, now actively working on it, I promise. Meanwhile please create an issue on the example and let's have the conversation over there ;)
Update: As promised I have a way to post comments, but it's somewhat I-am-too-smart-for-my-own-good so I guess I will have to write a post about that ;)