Wednesday, January 02, 2013

Navigating between ViewModels by more than just strings...

One of the questions that has come up a few times on StackOverflow is about why MvvmCross insists on using Strings for ViewModel navigation parameters.

The answer to this has always been that these are used in order to ensure that the navigation can be serialised down into a Xaml Uri or into an Android Intent. e.g. see the question and answer in http://stackoverflow.com/questions/10192505/passing-on-variables-from-viewmodel-to-another-view-mvvmcross

The good news is that from a checkin on New Year's Eve, strings are now not the only option - you can now use:
  • string
  • long
  • int
  • double
  • your own custom enums

To see the changes that enabled this, see the commit at: https://github.com/slodge/MvvmCross/commit/bbe73cfcc0e290533656ab6d862c787c7fbae04a

For an example of integer use, see the way I modified the Conference sample: https://github.com/slodge/MvvmCross/commit/c5e0be6728f855810f8f6d19d6b92dd84cb3d26c

--------------

These changes, however, still ask the user to use lists of parameters.... and these parameters lists can get quite long...

One request on Jabbr was:

  • gshackles:

    one of the things I don't like is that i'm just passing these anonymous objects around with magic properties

    would prefer to have a class for the parameters to a view model and just pass that in

    would also make it much more resistant to refactoring too

This seemed like a very reasonable request.... and the good news is that you can add this yourselves by:
  • overriding the way navigations are requested 
  • and overriding the way ViewModels are located.

Here's one way to achieve this:

First, create the base type for all your navigation parameter objects

namespace Casino.Core.ViewModels
{
public abstract class NavigationParametersBase
{
public const string Key = "Nav";
}
}

Then modify your BaseViewModel (or ViewModelBase) in order to allow it to support this parameter object navigation

using System.Collections.Generic;
using Cirrious.MvvmCross.ExtensionMethods;
using Cirrious.MvvmCross.Interfaces.ViewModels;
using Cirrious.MvvmCross.Plugins.Json;
using Cirrious.MvvmCross.ViewModels;
namespace Casino.Core.ViewModels
{
public abstract class BaseViewModel : MvxViewModel
{
protected void RequestNavigate<TViewModel>(NavigationParametersBase parameters)
where TViewModel : IMvxViewModel
{
var json = this.GetService<IMvxJsonConverter>();
var text = json.SerializeObject(parameters);
this.RequestNavigate<TViewModel>(
new Dictionary<string, object>()
{
{ NavigationParametersBase.Key, text}
});
}
}
}

Now modify each of your view models so that they have a new NavigateTo method - which either take no argument or which take a class derived from NavigationParametersBase:

using System.Windows.Input;
using Cirrious.MvvmCross.Commands;
using Cirrious.MvvmCross.Platform.Diagnostics;
namespace Casino.Core.ViewModels
{
public class HomeViewModel : BaseViewModel
{
public void Init()
{
MvxTrace.Trace("Initialising HomeViewModel ... with no parameters");
}
public ICommand GoCommand
{
get
{
return new MvxRelayCommand(() => this.RequestNavigate<SubViewModel>(new SubViewModel.NavigationParameters() { Test1 = "Testing 1", Test2 = 99 }));
}
}
}
}
using Cirrious.MvvmCross.Platform.Diagnostics;
namespace Casino.Core.ViewModels
{
public class SubViewModel : BaseViewModel
{
public class NavigationParameters : NavigationParametersBase
{
public string Test1 { get; set; }
public int Test2 { get; set; }
}
public SubViewModel()
{
MvxTrace.Trace("Constructing...");
}
public void Init(NavigationParameters parameters)
{
MvxTrace.Trace("Initialising SubViewModel ... with parameters {0} and {1}", parameters.Test1, parameters.Test2);
}
}
}
view raw SubViewModel.cs hosted with ❤ by GitHub

Now, create a default ViewModelLocator which can deserialise the incoming NavigationParameters and can initialise the ViewModel:

using System;
using System.Collections.Generic;
using Cirrious.MvvmCross.Application;
using Cirrious.MvvmCross.ExtensionMethods;
using Cirrious.MvvmCross.Interfaces.Platform.Diagnostics;
using Cirrious.MvvmCross.Interfaces.ViewModels;
using Cirrious.MvvmCross.Platform.Diagnostics;
using Cirrious.MvvmCross.Plugins.Json;
namespace Casino.Core.ViewModels
{
public class CustomViewModelLocator
: MvxBaseViewModelLocator
{
public override bool TryLoad(Type viewModelType, IDictionary<string, string> parameterValueLookup, out IMvxViewModel model)
{
model = null;
// create the new ViewModel
// here we use Activator.CreateInstance but this could also be done using an IoC container
var newViewModel = Activator.CreateInstance(viewModelType);
// find the NavgiateTo method
var initMethod = viewModelType.GetMethod("Init");
if (initMethod == null)
{
MvxTrace.Trace(MvxTraceLevel.Error, "Missing Init method in ViewModel");
return false;
}
var navigateToParameters = initMethod.GetParameters();
if (navigateToParameters.Length > 1)
{
MvxTrace.Trace(MvxTraceLevel.Error, "Missing Init method has too many parameters {0} - expecting zero or one", navigateToParameters.Length);
return false;
}
if (navigateToParameters.Length == 0)
{
initMethod.Invoke(newViewModel, new object[0]);
}
else
{
var navigationParameter = navigateToParameters[0];
if (!typeof(NavigationParametersBase).IsAssignableFrom(navigationParameter.ParameterType))
{
MvxTrace.Trace(MvxTraceLevel.Error, "The parameter for NavigateTo must inherit from NavigationParametersBase");
return false;
}
if (!parameterValueLookup.ContainsKey(NavigationParametersBase.Key))
{
MvxTrace.Trace(MvxTraceLevel.Error, "The Navigaton was missing the parameter for {0}", NavigationParametersBase.Key);
return false;
}
var text = parameterValueLookup[NavigationParametersBase.Key];
var json = this.GetService<IMvxJsonConverter>();
var navigationParameterObject = json.DeserializeObject(navigationParameter.ParameterType, text);
initMethod.Invoke(newViewModel, new object[] {navigationParameterObject});
}
model = (IMvxViewModel)newViewModel;
return true;
}
}
}

Note that this method uses a new deserialise method - only just added: https://github.com/slodge/MvvmCross/commit/f329a2ee6b68809060a52674b924549156d8c9b1

Finally, to make sure that the locator is used, override CreateDefaultViewModelLocator in your MvxApplication:

using Casino.Core.ViewModels;
using Cirrious.MvvmCross.Application;
using Cirrious.MvvmCross.ExtensionMethods;
using Cirrious.MvvmCross.Interfaces.ServiceProvider;
using Cirrious.MvvmCross.Interfaces.ViewModels;
namespace Casino.Core
{
public class App
: MvxApplication
, IMvxServiceProducer
{
public App()
{
// set the start object
var startApplicationObject = new StartApplicationObject();
this.RegisterServiceInstance<IMvxStartNavigation>(startApplicationObject);
}
protected override IMvxViewModelLocator CreateDefaultViewModelLocator()
{
return new CustomViewModelLocator();
}
}
}
view raw App.cs hosted with ❤ by GitHub

That's it.

Obviously, there's more that could be done - and different ways in which the ViewModel could be initialised - but this is a basic start which others can build on if they want to.

No comments:

Post a Comment