[WinRT] Navigate to VM with parameters

Topics: Framework Services
Aug 27, 2012 at 6:03 PM

Is there currently any way to navigate to a viewmodel while providing input parameters, either through the constructor or by filling properties?

Aug 28, 2012 at 10:16 PM

Because the Frame control only takes a single parameter as part of it's navigation you'll need to condense parameters to a single object. Currently the WinRT version looks for a property named "Parameter" on the view model and attempts to populate that.

Sep 7, 2012 at 2:21 AM

I discovered the "Parameter" property trick, and it worked while I was using the built-in WinRTContainer. We have a "FooViewModel" where we call:

_navigationService.NavigateToViewModel<BarViewModel>(someObject);

As long as we have a property on BarViewModel called "Parameter" of the same type as the "someObject" variable, it works. However, we switched to using a NinjectContainer and for some reason that doesn't work now. I get a NullReferenceException when the GetInstance override in App.xaml.cs is called (on the line where it gets the service from the Ninject kernel). I don't know why. Ninject is otherwise working, and I don't think anything else was changed other than swapping out the built-in container for Ninject.

So I'd love to figure out why that's not working, but ultimately I'm not real happy with having a property on my ViewModel called "Parameter" anyway. It's really a required parameter to the "BarViewModel".

So I found this thread from a while back: http://caliburnmicro.codeplex.com/discussions/244042. I kinda liked Rob's suggestion, based on some of the stuff in the HelloScreens sample project, to inject a Func<BarViewModel> into FooViewModel, and then call an initialization method on BarViewModel. Then call ActivateItem instead of using the NavigationService. So it would look like this in FooViewModel:

var vm = _barViewModelFactory().WithData(someObject);
ActivateItem(vm);

But there is no ActivateItem available on FooViewModel (which inherits from Screen). Is ActivateItem only a Conductor function? Thinking about it now, I guess that doesn't make sense for our case. We want to use the navigationService because we want to be able to go back to FooViewModel from BarViewModel.

So is there any other way to pass some data from FooViewModel when navigating to BarViewModel, other than the using the "Parameter" property technique? The factory method trick would work if we could navigate to an instance of a view model, instead of just to a view model type (so I could keep the factory method above but replace ActivateItem with NavigateToViewModel). Would it make sense to allow that?

Thanks!

Kevin

Sep 7, 2012 at 9:16 AM

All the work around handling Frame navigation, injecting parameters etc is handled by a class named FrameAdapter. It simply wraps a Frame control, implements INavgationService, listens for new page navigation's, creates and populates view models etc. 

There's a method on WinRT container that wires this up but it's simply a helper method, you should be able to duplicate a lot of that in your own Ninject Container.

Regarding the null reference I can't comment much without the code if you'd like to post it.

Sep 8, 2012 at 12:24 AM

Hi Nigel,

Thanks for your reply. We debugged this a bit more and determined that there's most likely a bug with the WinRT version of Ninject. Ninject is trying to do something with the Parameter property because we created it as a Set only property, but it was assuming there was also a Get. So by adding a Get to the property, we can use the Parameter while also using our Ninject container.

Ultimately, I don't really like having a property named "Parameter" on my view model to handle this scenario. We kicked around ideas for doing it differently, but haven't come up with a good solution yet. I wanted to be able to pass another value to the NavigateToViewModel method - either a string or an expression for the name of the property that we want the parameter to be set on. But we discovered that's not really possible, since the navigation is dependent on Frame.Navigate (which isn't virtual) and the NavigationEventArgs class (which is sealed). Go Microsoft!

So for now we're just going to use the "Parameter" property. If anyone has any other ideas though, we'd love to explore them. We thought about modifying IActivate to accept the parameter, or maybe introducing something like an IAcceptParameter interface to pass it in through a method. But we're not sure if that's really any better than the Parameter property. Opinions?

Kevin

Sep 9, 2012 at 3:35 AM
Edited Sep 9, 2012 at 3:35 AM

After thinking about this some more, I came up with what I think is a pretty good solution. I've submitted a pull request.

Basically, the idea is that I added another optional parameter called "propertyName" to all of the Navigate and NavigateToViewModel extension methods. The value defaults to "Parameter" in order to maintain existing behavior for users who were relying on that. Anyway, the actual parameter along with the property name are wrapped in another class which is passed as the parameter object to the actual INavigationService.Navigate method.

Then, in the TryInjectParameter method of the FrameAdapter, I unwrap the parameter and use the property name to set the value on the user supplied property of the view model. I've tested it out in my project, and it works quite well. I like this quite a bit better than adding a "Parameter" property to my view model which only exists to accept the parameter value passed to the navigate methods.

Let me know what you think and if you'd like to see any changes to this. Thanks!

Oct 10, 2012 at 7:56 PM

WinRT and WP7 navigation is a little different:
http://mikaelkoskinen.net/post/winrt-metro-navigate-between-pages-passing-parameters.aspx

  • WP7 uses a QueryString (containing the other view and query parameters)
  • WinRT uses typeof(view) and a single parameter

 

For consistency, Caliburn.Micro should be similar on all supported platforms, I think.
This would mean that the navigation parameter on WinRT should be a string that contains the query part of the Uri.
So we can handle Windows Phone and WinRT the same way.

Coordinator
Oct 10, 2012 at 8:05 PM

I agree. We should try to harmonize the api so that they are the same across both platforms.

Oct 11, 2012 at 4:16 AM

I'm happy to look into this.

Oct 11, 2012 at 5:04 AM

Ok. I have a quick spike complete that probably warrants some discussion. The UriBuilder works fine with a couple of small modifications, allowing code such as:

public void Navigate()
{
    navigationService.UriFor<NavigationTargetViewModel>()
         .WithParam(m => m.Name, "Nigel Sampson")
         .WithParam(m => m.Age, 31)
         .WithParam(m => m.IsMarried, true)
         .Navigate();
}

Right now I have UriBuilder passing through the uri it would normally try to navigate to as a parameter. Then the FrameAdapter checks to see if the passed parameter is a Uri and then breaks it down an injects the query string values, otherwise it falls back to the behaviour that's currently available (looking for a Parameter property).

The point that probably needs discussion is whether using the type of the parameter (in this case Uri) as the indicator we should use query string parse behaviour.

Thoughts?

Oct 11, 2012 at 2:06 PM

I guess the only thing I don't like about this is it means we can only pass parameters that can be easily serialized to a string. With the method I created in the fork, you can pass any arbitrary object. In the case of the app I'm building I actually need that ability as I'm passing a complex object that I can't really put on the query string.

I could probably change my design to pass an identifier for the object, and have the navigated-to view model retrieve it again, but I'd rather not do that when we have a good option for just passing the object to the new view model.

Oct 11, 2012 at 2:42 PM

According to MSDN you should not pass complex types to Navigate method because of serialization.

@Nigel: I think using Uri will work.

Oct 11, 2012 at 2:50 PM

Ah, interesting. I didn't realize that. That's not obvious from the API, since it just accepts any object as the parameter. I think they're going to trip up a lot of developers with that.

Anyway, I guess I will need to change my design a bit. Thanks for pointing that out. I guess my original pull request isn't going to prove very useful then. :-)

That being said, I like the API shown in Nigel's sample code above.

Oct 11, 2012 at 6:52 PM

I'll double check that the Frame control can correctly serialise Uri in GetNavigationState (which is where the prohibition on complex types comes from). 

If that doesn't work it'll have to be a string, I'll try and make it very unique ("caliburn://view?name=.... etc). May main concern is that I don't want the behavior of breaking it up into multiple parameters to be accidentally triggered.

Oct 11, 2012 at 7:54 PM

Ok so Frame can't correctly serialise Uri in GetNavigationState which would play havoc with any PLM scenarios. So it would need to be a string.

Coordinator
Oct 11, 2012 at 11:13 PM

One thought I have about complex objects...not for this initial release, but for the future. We can detect that the parmater value is not a primitive type, generate an id and store it somewhere. Then we can fetch it later by id and push in. So, the whole process becomes transparent. However, let's hold off on that for now and just make sure we get the basic scenario with primitives working.

Oct 12, 2012 at 3:40 AM

Ok, I've committed the change that builds a custom uri as the single parameter, the FrameAdapter picks up on this and unpacks the query string. Otherwise it falls back to previous behaviour.

Oct 17, 2012 at 12:21 AM
EisenbergEffect wrote:

One thought I have about complex objects...not for this initial release, but for the future. We can detect that the parmater value is not a primitive type, generate an id and store it somewhere. Then we can fetch it later by id and push in. So, the whole process becomes transparent. However, let's hold off on that for now and just make sure we get the basic scenario with primitives working.

A few things to consider:

- Storing away navigation state (thus parameters) for all active navigation stacks (e.g. because of pending suspension/termination)
- Hierarchical types (possibly lots of redundant serialization, loss of the object graph between views on serializing back) 
- The IDs would need to remain meaningful across sessions so there will probably need to be a deep tie-in to some suspension manager 

- (Binary data)

Seeing how easy it is to stringify data, though, it is probably best to build it into the framework since people will do it anyway or come up with schemes of their own.

Maybe it would be nice to encourage use of the URL/query string scheme. Or alternatively, to encourage splitting between pure navigation state and page state. This would also make it easier to manage (conceptually) the respective restoration in a general way.

I'll have to implement something like that myself for an application, so maybe I can come back with more experience.

Coordinator
Oct 17, 2012 at 12:29 AM

I'd say...see what works for your own application first. Then, we can examine it and see if there is anything generalizable about it. That prevents us from getting into an ivory tower situation...where we just invent features with no connection to real application developer needs. I'm pretty stubborn on that point. Pretty much everything in Caliburn.Micro at this point has come out of real application development. So, I'd favor keeping that trend. Keep us posted on your progress!

Oct 18, 2012 at 11:57 AM
Edited Oct 18, 2012 at 11:58 AM

I think the 'natural' way is to have the parameter passed to either the constructor of a view model i.e

MyViewModel(INavigationService myNavigationService, object parameter) or to add a NavigatedTo method to the IScreen interface to replicate that provided to the view by WinRT

In WinRT the parameter is passed to the event NavigatedTo of the view but as we are using Caliburn it would be nice to have that in the ViewModel

Oct 23, 2012 at 3:20 AM

I agree that having it passed to the constructor of the view model would be great. But in the meantime, I converted our project from the custom fork I created over to use the new UriFor method that Nigel added and it works great.

I like the api of the UriFor<T>().WithParam().Navigate(). It's a nice fluent api that's easy to read and understand and works well for our situation. Thanks!