I’ve been doing a lot of work with the MvvmCross framework lately, and so far have been really enjoying it. As I was building out my application, I found myself wanting some functionality along the lines of Android’s StartActivityForResult method, where the application would navigate to some screen that would return a result back to the calling screen. In iOS this kind of thing would normally be done with a modal view controller. Since this is not built into MvvmCross, I tried to come up with a pattern for allowing this sort of behavior. In this post I’ll outline what I came up with.
In order to allow for communication between different pieces of the system, I am using an event aggregator to help keep everything decoupled by using the publish/subscribe pattern. In this example I am using TinyMessenger, but you can substitute any similar service if there’s one you like more. To easily give all view models access to the aggregator, first we’ll create a common base class for them to inherit from:
public abstract class ViewModelBase
: MvxViewModel,
IMvxServiceConsumer<ITinyMessengerHub>
{
protected ViewModelBase()
{
MessengerHub = this.GetService<ITinyMessengerHub>();
}
protected ITinyMessengerHub MessengerHub { get; private set; }
}
This assumes that you’ve already registered a singleton in your IoC container for ITinyMessenger. Now that the aggregator is exposed, we can define the message to be used to communicate that a a result is being returned from a child view model:
public class SubNavigationResultMessage<TResult> : TinyMessageBase
{
public TResult Result { get; private set; }
public string MessageId { get; set; }
public SubNavigationResultMessage(object sender, string messageId, TResult result)
: base(sender)
{
Result = result;
MessageId = messageId;
}
}
There’s not much going on in the message here. It’s simply a generic transport for the result of of a child view action in our application. Next, we can define a subclass for view models that are to be used for fetching results:
public abstract class SubViewModelBase<TResult> : ViewModelBase
{
protected string MessageId { get; private set; }
protected SubViewModelBase(string messageId)
{
MessageId = messageId;
}
protected void Cancel()
{
ReturnResult(default(TResult));
}
protected void ReturnResult(TResult result)
{
var message = new SubNavigationResultMessage<TResult>(this, MessageId, result);
MessengerHub.Publish(message);
Close();
}
}
The constructor takes in a message ID parameter, which is expected to be a unique value in the system that identifies this particular request. You will see this in use in the next section. The real functionality here comes in the ReturnResult() and Cancel() methods. When implementing the actual view models, these would be exposed in commands that can be triggered from the view. I didn’t want to impose any limitations on how they are exposed to start off with, so I left them as simple method calls. When returning a result it creates and publishes a new instance of the message defined in the last section. After that it closes the view model, which will return flow to the previous screen.
Now we have the message defined and the sub-view that creates it, so all that’s left is giving view models a way to create the request. In order to do this, we’ll add a new method to the ViewModelBase class defined earlier called RequestSubNavigate(). I debated a bit on the name, but decided to stay consistent with the MvvmCross method naming here.
protected bool RequestSubNavigate<TViewModel, TResult>(IDictionary<string, string> parameterValues, Action<TResult> onResult)
where TViewModel : SubViewModelBase<TResult>
{
parameterValues = parameterValues ?? new Dictionary<string, string>();
if (parameterValues.ContainsKey("messageId"))
throw new ArgumentException("parameterValues cannot contain an item with the key 'messageId'");
string messageId = Guid.NewGuid().ToString();
parameterValues["messageId"] = messageId;
TinyMessageSubscriptionToken token = null;
token = MessengerHub.Subscribe<SubNavigationResultMessage<TResult>>(msg =>
{
if (token != null)
MessengerHub.Unsubscribe<SubNavigationResultMessage<TResult>>(token);
onResult(msg.Result);
},
msg => msg.MessageId == messageId);
return RequestNavigate<TViewModel>(parameterValues);
}
This method creates a message ID by simply creating a new GUID, but you could also substitute in your own method here if you had some other mechanism for maintaining unique identifiers. The reason for message IDs is wanting to be able to distinguish between different messages in the event you have nested child views and multiple parents are listening for results. When the result is received, the view model unsubscribes from the message to help clear the reference, andn invokes the callback method provided.
That’s all you need to facilitate communication between view models. In order to test things out, let’s set up some tests. First, let’s define a base class for view model tests that creates the IoC container, some mocks, and exposes the event aggregator. This example uses the mocking classes defined in Stuart’s example on his blog.
[TestFixture]
public abstract class ViewModelTestsBase
{
protected ITinyMessengerHub MessengerHub { get; private set; }
protected MockMvxViewDispatcher Dispatcher { get; private set; }
[SetUp]
public void SetUp()
{
MvxOpenNetCfContainer.ClearAllSingletons();
MvxOpenNetCfServiceProviderSetup.Initialize();
MessageHub = new TinyMessengerHub();
this.RegisterServiceInstance<ITinyMessengerHub>(MessageHub);
Dispatcher = new MockMvxViewDispatcher();
var mockNavigationProvider = new MockMvxViewDispatcherProvider();
mockNavigationProvider.Dispatcher = Dispatcher;
MvxOpenNetCfContainer.Instance.RegisterServiceInstance<IMvxViewDispatcherProvider>(mockNavigationProvider);
}
}
With that defined, we can write some tests to make sure things are working properly:
[TestFixture]
public class SubViewModelTests
{
[Test]
public void RequestSubNavigate_NavigatesToChildViewModel_PassesInMessageId()
{
var parentViewModel = new ParentViewModel();
parentViewModel.GoToChildViewCommand.Execute();
Assert.AreEqual(1, Dispatcher.NavigateRequests.Count);
var request = Dispatcher.NavigateRequests.First();
Assert.That(request.ViewModelType == typeof(ChildViewModel));
Assert.That(request.ParameterValues.ContainsKey("messageId"));
}
[Test]
public void RequestSubNavigate_ResultMessageReceived_ParentIsNotifiedAndUnsubscribes()
{
var parentViewModel = new ParentViewModel();
parentViewModel.GoToChildViewCommand.Execute();
var messageId = Dispatcher.NavigateRequests.First().ParameterValues["messageId"];
MessengerHub.Publish(new SubNavigationResultMessage<string>(this, messageId, "Result 1"));
Assert.AreEqual("Result 1", parentViewModel.LastResult);
MessengerHub.Publish(new SubNavigationResultMessage<string>(this, messageId, "Result 2"));
Assert.AreEqual("Result 1", parentViewModel.LastResult);
}
[Test]
public void ChildView_ReturnsResult_ParentIsNotifiedAndUnsubscribes()
{
var parentViewModel = new ParentViewModel();
parentViewModel.GoToChildViewCommand.Execute();
var messageId = Dispatcher.NavigateRequests.First().ParameterValues["messageId"];
var childViewModel = new ChildViewModel(messageId);
childViewModel.ReturnResult("Result 1");
Assert.AreEqual("Result 1", parentViewModel.LastResult);
var secondChildViewModel = new ChildViewModel(messageId);
secondChildViewModel.ReturnResult("Result 2");
Assert.AreEqual("Result 1", parentViewModel.LastResult);
}
public class ParentViewModel : ViewModelBase
{
public string LastResult { get; private set; }
public IMvxCommand GoToChildViewCommand
{
get { return new MvxRelayCommand(() => RequestSubNavigate<ChildViewModel, string>(null, onResult)); }
}
private void onResult(string result)
{
LastResult = result;
}
}
public class ChildViewModel : SubViewModelBase<string>
{
public ChildViewModel(string messageId)
: base(messageId)
{
}
public new void ReturnResult(string result)
{
base.ReturnResult(result);
}
public new void Cancel()
{
base.Cancel();
}
}
}
If all went smoothly, your tests should be green! I’m using NUnit here, but you can substitute in your favorite unit testing framework. Specifically, I’m using the NUnitLite subset which allows the unit tests to be used in MonoTouch and Mono for Android unit test projects as well.
This Isn’t Perfect
There is one obvious problem with this pattern that I want to mention. If the user switched away from your application and your app got cycled out of memory, the event handlers wouldn’t be in the right state when the application got resumed later on. For now I decided to leave this unsolved since I’m still working out how my application should behave in this scenario, and also didn’t want to impose limitations right from the start. One option could be to persist message subscriptions, such as to a database or file, and then reload them when the application restarts itself. As always, this is a tricky rabbit hole that many apps already don’t handle well, so it remains a problem right now with this pattern as well.
Hopefully you find this pattern useful in your applications as well. If you have any other patterns you’re using to handle this kind of scenario, also please let me know!