How to pass parameter to navigated view model with WinRT Caliburn.Micro?
Asked Answered
J

3

6

I am developing a Windows Store apps game using WinRT Caliburn.Micro, and I am relying on the navigation framework.

I have view models for the game setup (define players) and the actual game. When navigating from the setup to the game, I want to pass the collection of players to the game view model. How can I do this?

Schematically, my view models currently look like this:

public class SetupGameViewModel : NavigationViewModelBase
{
    public SetupGameViewModel(INavigationService ns) : base(ns) { }

    public IObservableCollection<Player> Players { get; set; }

    public void StartGame()
    {
        // This is as far as I've got...
        base.NavigationService.NavigateToViewModel<GameViewModel>();

        // How can I pass the Players collection from here to the GameViewModel?
    }
}

public class GameViewModel : NavigationViewModelBase
{
    public GameViewModel(INavigationService ns) : base(ns) { }

    public ScoreBoardViewModel ScoreBoard { get; private set; }

    public void InitializeScoreBoard(IEnumerable<Player> players)
    {
        ScoreBoard = new ScoreBoardViewModel(players);
    }
}

Ideally, I would like to call InitializeScoreBoard from within the GameViewModel constructor, but as far as I have been able to tell it is not possible to pass the SetupGameViewModel.Players collection to the GameViewModel constructor.

The INavigationService.NavigateToViewModel<T> (extension) method optionally takes an [object] parameter argument, but this parameter does not seem to reach the view model constructor navigated to. And I cannot figure out how to explicitly call the GameViewModel.InitializeScoreBoard method from the SetupGameViewModel.StartGame method either, since the GameViewModel has not been initialized at this stage.

Juxtaposition answered 6/3, 2013 at 15:49 Comment(0)
J
4

In the end, I solved this by implementing a temporary event handler. It turned out that I could use the NavigateToViewModel<T>(object) overload to pass the player collection.

From the Caliburn Micro discussion forum and MSDN documentation I get the impression that this approach is only guaranteed to work for "primitive" types, although in my scenario I have so far not detected any problems with it.

My SetupGameViewModel.StartGame method is now implemented as follows:

public void StartGame()
{
    base.NavigationService.Navigated += NavigationServiceOnNavigated;
    base.NavigationService.NavigateToViewModel<GameViewModel>(Players);
    base.NavigationService.Navigated -= NavigationServiceOnNavigated;
}

And the very temporarily attached NavigationServiceOnNavigated event handler is implemented as follows:

private static void NavigationServiceOnNavigated(object sender, NavigationEventArgs args)
{
    FrameworkElement view;
    GameViewModel gameViewModel;
    if ((view = args.Content as FrameworkElement) == null || 
        (gameViewModel = view.DataContext as GameViewModel) == null) return;

    gameViewModel.InitializeScoreBoard(args.Parameter as IEnumerable<Player>);
}

Not really the clean solution I had striven for, but at least it seems to work.

Juxtaposition answered 10/4, 2013 at 6:57 Comment(1)
In this post from Rob himself he explains that it's because of the navigation based system in Windows Phone that's working based on Uri's which don't support the complex deserialization. caliburnmicro.codeplex.com/discussions/267562Krefetz
H
6

OK, just putting it out there, Caliburn.Micro has unified navigation for WP8 and WinRT:

NavigationService.UriFor<TargetViewModel>().WithParam(x => x.TargetProperty, ValueToPass).Navigate();

And you can chain WithParam for multiple parameters. Now there are some constraints, not all types go through, I'm not quite sure what the exact reason for that is, but it has something to do how the navigation works in WinRT. There was a mention of it somewhere in Caliburn.Micro discussion section.

Anyway, you can navigate this way. Don't rely on constructor though, It will call OnInitialize and OnActivate. So, just to cut it into the example:

NavigationService.UriFor<DetailsViewModel>().WithParam(x => x.Id, SelectedDetailsId).Navigate();

then in the DetailsViewModel:

protected override void OnInitialize()
{
    //Here you'll have Id property initialized to 'SelectedDetailsId' from the previous screen.
}

So, in pure theory, you could do:

NavigationService.UriFor<GameViewModel>().WithParam(x => x.Players, Players).Navigate();

in the setup and then:

public class GameViewModel
{
    public GameViewModel(INavigationService ns) : base(ns) 
    { 
       //It would probably be good to initialize Players here to avoid null
    }

    public ScoreBoardViewModel ScoreBoard { get; private set; }

    public IObservableCollection<Player> Players {get;set;}

    protected void OnInitialize()
    {
        //If everything goes as expected, Players should be populated now.
        ScoreBoard = new ScoreBoard(Players);
    }
}

In practice though, I don't think that passing a complex construct like that (collection of classes etc) is going to work.

More primitive types work just fine (int, string, DateTime etc., but e.g. URI didn't work for me, was always null), so worst-case scenario/workaround is, for example, to serialize the Players list to a temp file before the navigation and pass the file path as string to deserialize in the GameViewModel.

There are people more involved in the framework roaming the SO, they might give you more valuable insight.

Hierolatry answered 6/3, 2013 at 16:9 Comment(3)
Many thanks, this was extremely useful information, I learned a lot from this response. As you suspect however, the Players collection does not seem to be passable via WithParam. I currently have implemented a very awkward solution where I define a local handler for the Navigated event. The event arguments give the navigation content, and I implicitly assume that the content's DataContext is my view model. Still, it would be good if someone has a more reliable solution available for this problem.Juxtaposition
BTW, I think I found the discussion you were referring to, here.Juxtaposition
@AndersGustafsson Yep, that's exactly this discussion. As I said, I don't really know better solution at the moment with Caliburn.Micro apart from saving the data somewhere and then re-creating it from some kind of key; from the said discussion - even Microsoft discourages it, but it is generally supported...Periodontal
J
4

In the end, I solved this by implementing a temporary event handler. It turned out that I could use the NavigateToViewModel<T>(object) overload to pass the player collection.

From the Caliburn Micro discussion forum and MSDN documentation I get the impression that this approach is only guaranteed to work for "primitive" types, although in my scenario I have so far not detected any problems with it.

My SetupGameViewModel.StartGame method is now implemented as follows:

public void StartGame()
{
    base.NavigationService.Navigated += NavigationServiceOnNavigated;
    base.NavigationService.NavigateToViewModel<GameViewModel>(Players);
    base.NavigationService.Navigated -= NavigationServiceOnNavigated;
}

And the very temporarily attached NavigationServiceOnNavigated event handler is implemented as follows:

private static void NavigationServiceOnNavigated(object sender, NavigationEventArgs args)
{
    FrameworkElement view;
    GameViewModel gameViewModel;
    if ((view = args.Content as FrameworkElement) == null || 
        (gameViewModel = view.DataContext as GameViewModel) == null) return;

    gameViewModel.InitializeScoreBoard(args.Parameter as IEnumerable<Player>);
}

Not really the clean solution I had striven for, but at least it seems to work.

Juxtaposition answered 10/4, 2013 at 6:57 Comment(1)
In this post from Rob himself he explains that it's because of the navigation based system in Windows Phone that's working based on Uri's which don't support the complex deserialization. caliburnmicro.codeplex.com/discussions/267562Krefetz
L
2

In Win Store Apps you can hand over complex objects between ViewModels with the help of the NavigationService. Only in Silverlight Apps you are restricted to objects which must be serializable to a string. This restriction does not exist in Win Store Apps.

In your case something like the following should work. In StartGame() the NavigationService is used to call the GameViewModel. The player list is handed over as a simple parameter. By convention this parameter will be assigned to a property Parameter of the destination ViewModel.

public class SetupGameViewModel : Screen 
{
    private readonly INavigationService _navigationService;

    public MainPageViewModel(INavigationService navigationService)
    {
        _navigationService = navigationService;
    }

    public IObservableCollection<Player> Players { get; set; }

    public void StartGame() 
    {
        _navigationService.NavigateToViewModel<GameViewModel>(Players);
    }

    ...
}


public class GameViewModel : Screen
{   
    private IObservableCollection<Player> _parameter;

    public IObservableCollection<Player> Parameter
    {
        get { return _parameter; }
        set
        {
            if (value.Equals(_parameter)) return;
            _parameter = value;
            NotifyOfPropertyChange(() => Parameter);
        }
    }

    protected override void OnActivate()
    {
        // do something with the player list
        // ...
    }

    ...
}

More detailed information on this subject can be found here: http://wp.qmatteoq.com/using-caliburn-micro-with-universal-windows-app-navigation/

Lew answered 20/3, 2015 at 21:3 Comment(1)
Thanks, that's the answer for my Windows 10 UWP app. And yes, the property in the destination view model (GameViewModel in the sample) must be called Parameter - seems to be the convention.Rawhide

© 2022 - 2024 — McMap. All rights reserved.