Wednesday, September 3, 2014

Navigation from ViewModel using Xamarin.Forms

I'm probably going to get shot for this post by MVVM-purists but I'll write it anyway. :) Worth noting is that the guys over at Xamarin Forms Labs extends stuff to let you navigate easier. This is the vanilla approach.

I described the basic of MVVM using Xamarin.Forms in a previous post and I'd like to extend it a little bit to allow for navigation from the ViewModel.

The example app was dead simple and allowed for a name to be updated in a fake repository. The command defined in the ViewModel looked like this:

      public ICommand Save
        {
            get {
                return new Command (() => {

                    // Perform some logic
                    Updated = DateTime.Now.ToString();

                    // Store data to back end
                    var person = _repository.GetPerson(42);
                    person.Name = _name;
                    person.Updated = _updated;
                    _repository.Update(person);
                });
            }
        }


I would now like to extend this so that I can navigate to a thank you page. The problem is that the Navigation stuff is part of the base classes that our pages inherit from. After all, the ViewModel is just a POCO that implements INotifyPropertyChanged and nothing else.

Step 1 - We need some references

We need a reference to an object implementing the INavigation interface. This is where some people get nervous that we pollute the ViewModel with stuff from the View. Well, so be it. This could also be moved to a ViewModel base class if you'd like.

   public class MainPageViewModel : INotifyPropertyChanged
    {
        private PersonRepository _repository;
        private INavigation _navigation; // HERE

        public MainPageViewModel (INavigation navigation) // HERE
        {
            _navigation = navigation; // AND HERE

            // This should be injected
            _repository = new PersonRepository();

            // Populate the ViewModel
            var person = _repository.GetPerson(42);
            Name = person.Name;
            Updated = person.Updated;
        }
    }

We created a private field and assigned by using a reference passed to us in the constructor.

Step 2 - Pass the reference

We need to pass the reference to the Navigation object from the page. 

    public partial class MainPage : ContentPage
    {    
        public MainPage ()
        {
            InitializeComponent ();
            BindingContext = new MainPageViewModel (this.Navigation); // HERE
        }
    }

Step 3 - Modify the app startup to allow for navigation

To be able to navigate, we must wrap out MainPage in a NavigationPage as such:

    public class App
    {
        public static Page GetMainPage ()
        {    
            return new NavigationPage (new MainPage());
        }
    }

Step 4 - Navigate on Command

Update our Save command to navigate to the ThanksPage!

        public ICommand Save
        {
            get {
                return new Command (async () => { // HERE

                    // Perform some logic
                    Updated = DateTime.Now.ToString();

                    // Store data to back end
                    var person = _repository.GetPerson(42);
                    person.Name = _name;
                    person.Updated = _updated;
                    _repository.Update(person);

                    await _navigation.PushAsync(new ThanksPage()); // HERE
                });
            }
        }

Conclusion

Why do we do this? The reason is to avoid writing code in the views code behind and define all logic and navigation in the ViewModel. Since the navigation is exposed as an interface, I'd say this is a valid way to do it.

It also seems like the Navigation object is a new instance on each page, so don't save a global reference since that might break something later on.

23 comments:

  1. Great post!

    I noticed, that if you for instance wish to navigate from, for instance, a Login Page to a Main Page, then you could use a "PushModalAsync" like so:

    await _navigation.PushModalAsync(new ThanksPage());

    In doing so hides the "Back" button once the user is logged in which is a regular use case.

    In this regard it should be noted, that when using a modal navigation pattern, the "Title" of the next page is no longer implicitly included.

    If this is an issue, simply return a navigation page like so:

    await _navigation.PushModalAsync(new NavigationPage (new ThanksPage() ) );

    // David

    ReplyDelete
    Replies
    1. A bit late for my reply but thanks for the info!

      Delete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Johan,
    I used your approach to load a new page from a listview on the current page. I loaded the page from my current page's viewmodel. When I navigate page to the original page, the listview.selecteditem is still selected. How do I set listview.selecteditem = null from either XAML or my modelview? Thanks.

    ReplyDelete
    Replies
    1. Hi! Good question! I usually override ListView and use something like this

      public class ListView : Xamarin.Forms.ListView
      {
      public static BindableProperty ItemClickCommandProperty = BindableProperty.Create(x => x.ItemClickCommand, null);

      public ListView()
      {
      this.ItemTapped += this.OnItemTapped;
      }


      public ICommand ItemClickCommand
      {
      get { return (ICommand)this.GetValue(ItemClickCommandProperty); }
      set { this.SetValue(ItemClickCommandProperty, value); }
      }


      private void OnItemTapped(object sender, ItemTappedEventArgs e)
      {
      if (e.Item != null && this.ItemClickCommand != null && this.ItemClickCommand.CanExecute(e))
      {
      this.ItemClickCommand.Execute(e.Item);

      }

      this.SelectedItem = null;
      }
      }

      I'll write a post about this tomorrow explaining it in a bit more detail if you'd like. With the Xaml and all in place.

      Delete
    2. Here's the blog post about your specific issue. http://www.johankarlsson.net/2015/08/extending-listview-to-make-it-clickable.html

      Delete
  4. There's no need for strong references to View properties in the View Model because there is a bindable version of the Navigation property built-in to the ContentPage class. In this case, all you have to do is add this to your ContentPage XAML element:
    Navigation="{Binding Navigation}"
    That will bind to this in your View Model:
    public INavigation Navigation { get; set; }

    You can then use that View Model Navigation property from a View Model method like this:
    async Task NavigateNext()
    {
    await MyNavigation.PushAsync(new SecondView());
    }

    Then you just need a Command:
    public ICommand NavigateNextCommand { get; }

    You can instantiate that from the constructor:
    NavigateNextCommand = new Command(async () => await NavigateNext());

    ReplyDelete
    Replies
    1. I actually missed this comment about a year ago! That is a great alternative! Thanks!

      Delete
    2. Hi Good idea! I'd like to evolve the ideas and solutions here:
      If a try Navigation.InsertPageBefore, I'd need the "current page"
      The call would be something like(new WhateverView, CurrentPage);
      The point is: in a VM, how would I have the current page? Perhaps passing from previous View?

      Thanks a lot!

      Delete
  5. I do not really like the need for instanciating the next view directly in the viewModel:
    await _navigation.PushAsync(new ThanksPage())

    Did you think about a DI aproach for decoupling? The ViewModel from the next view? I'm searching for a solutio to be able to use specific View implementations in Xamarin Forms cross-platform development.

    ReplyDelete
    Replies
    1. Thanks for the comment. A DI approach is always the way to go. Didn't want to take focus from the issue of navigation.

      Delete
    2. The only place you could inject the next view into the view-model would be from the View's code-behind. Wouldn't that defeat the whole purpose of decoupling if the logic of the next view is decided in the code-behind that binds to the view model?

      Delete
    3. This comment has been removed by the author.

      Delete
    4. Sorry for late reply.

      I usually do something like this in all code behinds.

      MyPage(MyPageViewModel vm)
      {
      InitializeComponent();
      vm.Navigation = Navigation;
      BindingContext = vm;
      }

      I see you point that it would defeat the purpose a bit, but you can always pass in an interface instead and do some magic in the IoC container. There are so many choices so pick the one that fits the project. The amount complexity you pour into it depends on what kind of product it is. Is it a framework then you should spend a lot of time on architecture. If is a promotion app for baby powder, you might get away with some hacks.

      Delete
  6. Yes, I know that it breaks the rule of independence but it's at least through an interface. Also as stated above, you can actually bind to the navigation service, but I personally find that not to practical. I can accept this minor bad practice since I gain more than I lose. It's a matter of opinion and I would love to see alternatives. Navigation certainly don't belong in the view so but the navigation service does. We cannot have a global reference since iOS can have multiple navigation controllers. So how would you solve it?

    ReplyDelete
  7. where is the _navigation initializes? it is always NULL. throws exception. can you please post full code?

    ReplyDelete
    Replies
    1. It's initialized when you create the page and passed to the view model in the page constructor. I only have a mobile device to write on now but I'll add a full example in a few days if you'd like me to.

      Delete
  8. i.e here...

    _navigation = Xamarin.Forms.Application.Current.MainPage.Navigation;

    ReplyDelete
  9. Xamarin.Forms.Application.Current.MainPage.Navigation(new page())

    ReplyDelete
  10. Hi,

    ICommand button click event I wrote above code for Navigation.It's working fine.

    ReplyDelete
  11. Blog posts like this perpetuate bad code.

    This violates everything about MVVM. Just implement your own navigation service to keep from violating MVVM.

    Also, don't use Xamarin.Forms for Command. Just rip the source for that file from the Forms source in order to keep from having a dependency on Forms. Then you can actually use the ViewModels like they're meant to be used.

    ReplyDelete
    Replies
    1. I think you should have a healthy balance between being purist and being pragmatic. Don't over architecture things.

      Delete