24

I am working on xamarin.form cross-platform application , i want to navigate from one page to another on button click. As i cannot do Navigation.PushAsync(new Page2()); in ViewModel because it only possible in Code-Behid file. please suggest any way to do this?

Here is my View:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="Calculator.Views.SignIn"
             xmlns:ViewModels="clr-namespace:Calculator.ViewModels;assembly=Calculator">
    
    <ContentPage.BindingContext>
        <ViewModels:LocalAccountViewModel/>
    </ContentPage.BindingContext>
    
    <ContentPage.Content>    
        <StackLayout>
            <Button Command="{Binding ContinueBtnClicked}" />    
        </StackLayout>
    </ContentPage.Content>
</ContentPage>

Here is my ViewModel:

public class LocalAccountViewModel : INotifyPropertyChanged
{
    public LocalAccountViewModel()
    {
        this.ContinueBtnClicked = new Command(GotoPage2);
    }
        
    public void GotoPage2()
    {
        /////
    }
    
    public ICommand ContinueBtnClicked
    {
        protected set;
        get;
    }
    
    public event PropertyChangedEventHandler PropertyChanged;
    
    protected virtual void OnPropertyChanges([CallerMemberName] string PropertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(PropertyName));
    }
}
Junaid Pathan
  • 3,850
  • 1
  • 25
  • 47
Waleed Arshad
  • 273
  • 1
  • 2
  • 8

9 Answers9

54

One way is you can pass the Navigation through the VM Constructor. Since pages inherit from VisualElement, they directly inherit the Navigation property.

Code behind file:

public class SignIn : ContentPage
{
    public SignIn(){
       InitializeComponent();
       // Note the VM constructor takes now a INavigation parameter
       BindingContext = new LocalAccountViewModel(Navigation);
    }
}

Then in your VM, add a INavigation property and change the constructor to accept a INavigation. You can then use this property for navigation:

public class LocalAccountViewModel : INotifyPropertyChanged
{
    public INavigation Navigation { get; set;}

    public LocalAccountViewModel(INavigation navigation)
    {
        this.Navigation = navigation;
        this.ContinueBtnClicked = new Command(async () => await GotoPage2());
    }

    public async Task GotoPage2()
    {
        /////
        await Navigation.PushAsync(new Page2());
    }
    ...
}

Note an issue with your code that you should fix: The GoToPage2() method must be set async and return the Task type. In addition, the command will perform an asynchronous action call. This is because you must do page navigation asychronously!

Hope it helps!

Junaid Pathan
  • 3,850
  • 1
  • 25
  • 47
TaiT's
  • 3,138
  • 3
  • 15
  • 26
  • 1
    It works but what if we want to Bind Context in Xaml view file. how can we pass parameter to view model's constructor ? and which is the best way to bind context in Code Behid File or in Xaml ? – Waleed Arshad Apr 07 '17 at 05:35
  • @WaleedArshad: The `DataContext` gets created after the `InitializeComponent()` call. So you can just pass it to the ViewModel afterwards. I usually set the ViewModel in the constructor in case it needs extra parameters (it's the only code I try to put in the code behind). I also find it easier to read. If you don't have parameters, its a matter of choice. – TaiT's Apr 07 '17 at 13:40
  • 14
    I´m sorry to bolt in - but this absolutely breaks all MVVM / MVC / MVWhatever rules.... Your ViewModel should never ever create a view - it´s the other way round. Goal of all these concepts is to separate view from model, so the view is exchangeable, which in your example is not the case. Just imagine doing separate projects (real *.csproj-files) for View, ViewModel and Model and you´ll see the issue. Coming from WPF background I came through here looking how Xamarin guys do this and till now I´m not really pleased on the concepts I found... Will keep looking! – Jessica Oct 27 '17 at 18:20
  • 1
    @JessicaMiceli I see your point but this is how Xamarin works by design. Then you can still implement your own dependency injection service or use a free framework that does it for you – TaiT's Nov 05 '18 at 19:12
35

A simple way is

this.ContinueBtnClicked = new Command(async()=>{

    await Application.Current.MainPage.Navigation.PushAsync(new Page2());
});
Alessandro Caliaro
  • 5,623
  • 7
  • 27
  • 52
  • This also worked. and this is very simple way to navigate to other page. – Waleed Arshad Apr 07 '17 at 05:37
  • Why all sites, documents and blogs don't use this very simple solution? – Daniele Tentoni Jul 19 '19 at 15:37
  • My app starts with a tabbed page which does not include navigation. So I cannot use "MainPage.Navigation" in the view model. How to access the sub page which includes navigation instead? – MatterOfFact Aug 03 '19 at 06:42
  • Which is the problem – Alessandro Caliaro Jan 19 '20 at 07:09
  • @MatterOfFact you should almost always set your MainPage to a NavigationPage or you will not be able to access navigation functions within your app. You should change `MainPage = new TabbedPage();` to `MainPage = new NavigationPage(new TabbedPage());` and then this feature will work. The only downside is that you have to then define the "HasNavigationBar" property for every page if you want the navigation but not the top bar. – Matthew Swallow Aug 18 '22 at 15:12
4

Passing INavigation through VM constructor is a good solution indeed, but it can also be quite code-expensive if you have deep nested VMs architecture.

Wrapping INavigation with a singleton, accesible from any view model, is an alternative:

NavigationDispatcher Singleton:

 public class NavigationDispatcher
    {
        private static NavigationDispatcher _instance;

        private INavigation _navigation;

        public static NavigationDispatcher Instance =>
                      _instance ?? (_instance = new NavigationDispatcher());

        public INavigation Navigation => 
                     _navigation ?? throw new Exception("NavigationDispatcher is not initialized");

        public void Initialize(INavigation navigation)
        {
            _navigation = navigation;
        }
    }

Initializing in App.xaml.cs:

       public App()
       {
          InitializeComponent();
          MainPage = new NavigationPage(new MainPage());
          NavigationDispatcher.Instance.Initialize(MainPage.Navigation);
       }

Using in any ViewModel:

 ...
 private async void OnSomeCommand(object obj)
        {
            var page = new OtherPage();
            await NavigationDispatcher.Instance.Navigation.PushAsync(page);
        }
 ...
Anatoly Nikolaev
  • 520
  • 3
  • 12
3

From your VM

public Command RegisterCommand
        {
            get
            {
                return new Command(async () =>
                {
                    await Application.Current.MainPage.Navigation.PushAsync(new RegisterNewUser());
                });

            }
        }
AG70
  • 874
  • 1
  • 9
  • 23
2

I looked into this, and it really depends on how you want to handle your navigation. Do you want your view models to handle your navigation or do you want your views. I found it easiest to have my views handle my navigation so that I could choose to have a different navigation format for different situations or applications. In this situation, rather than using the command binding model, just use a button clicked event and add the new page to the navigation stack in the code behind.

Change your button to something like:

<StackLayout>
    <Button Clicked="Button_Clicked"></Button>
</StackLayout>

And in your code behind, implement the method and do the navigation there.

public void Button_Clicked(object sender, EventArgs e)
{
    Navigation.PushAsync(new Page2());
}

If you are looking to do viewmodel based navigation, I believe there is a way to do this with MvvmCross, but I am not familiar with that tool.

Rob
  • 144
  • 1
  • 11
2

my approach based on principle every View can navigate to VM context based places of the app only:

In ViewModel i declare INavigationHandler interfeces like that:

public class ItemsViewModel : ViewModelBase
{
    public INavigationHandler NavigationHandler { private get; set; }


    // some VM code here where in some place i'm invoking
    RelayCommand<int> ItemSelectedCommand => 
        new RelayCommand<int>((itemID) => { NavigationHandler.NavigateToItemDetail(itemID); });


    public interface INavigationHandler
    {
        void NavigateToItemDetail(int itemID);
    }
}

And assign code-behind class as INavigationHandler for ViewModel:

public class ItemsPage : ContentPage, ItemsViewModel.INavigationHandler
{
    ItemsViewModel viewModel;

    public ItemsPage()
    {
        viewModel = Container.Default.Get<ItemsViewModel>();
        viewModel.NavigationHandler = this;
    }


    public async void NavigateToItemDetail(int itemID)
    {
        await Navigation.PushAsync(new ItemDetailPage(itemID));
    }
}
1

decided to add two ways to pass Page instance to viewmodel which you can use later for navigation, displaying alerts. closing page and so on.

1. if you can pass it with command parameter

in view model:

public ICommand cmdAddRecord { get; set; }

viewmodel constructor

cmdAddRecord = new Command<ContentPage>(AddRecord);

somewhere in viewmodel

    void AddRecord(ContentPage parent)
    {
        parent.Navigation.Whatever
    }

XAML

header

            x:Name="thisPage"

usage

 <ToolbarItem IconImageSource="{StaticResource icAdd}"  Command="{Binding cmdAddRecord}"  CommandParameter="{Binding ., Source={x:Reference thisPage}}" />

2. started using this in my base class for viewmodels

viewmodel

public class cMyBaseVm : BindableObject

...

public static BindableProperty ParentProperty = BindableProperty.Create("Parent", typeof(ContentPage), typeof(cMyBaseVm), null, BindingMode.OneWay);

...

   public ContentPage Parent
    {
        get => (ContentPage)GetValue(ParentProperty);
        set => SetValue(ParentProperty, value);
    }

XAML

        xmlns:viewModels="clr-namespace:yournamespace.ViewModels"
        x:Name="thisPage"

and here we go

<ContentPage.BindingContext>
    <viewModels:cPetEventsListVm Parent="{Binding ., Source={x:Reference thisPage}}" />
</ContentPage.BindingContext>

child viewmodel

public class cPetEventsListVm : cMyBaseVm

and now, all around child view model we can use Page like Parent.DisplayAlert , or Parent.Navigation.PushAsync etc we may even close page now from view model with Parent.PopAsync ();

ish1313
  • 321
  • 3
  • 6
0

I racked my brains on this a few days hitting the same hurdle when I switched to Xamarin development.

So my answer is to put the Type of the page in the Model but not restricting the View or the ViewModel to work with it also if one so chooses. This keeps the system flexible in that it does not tie up the Navigation via hard-wiring in the view or in the code-behind and therefore it is far more portable. You can reuse your models across the projects and merely set the type of the Page it will navigate to when such circumstance arrive in the other project.

To this end I produce an IValueConverter

    public class PageConverter : IValueConverter
    {
        internal static readonly Type PageType = typeof(Page);

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            Page rv = null;

            var type = (Type)value;

            if (PageConverter.PageType.IsAssignableFrom(type))
            {
                var instance = (Page)Activator.CreateInstance(type);
                rv = instance;
            }

            return rv;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var page = (Page)value;
            return page.GetType();
        }
    }

And an ICommand

public class NavigateCommand : ICommand
{
    private static Lazy<PageConverter> PageConverterInstance = new Lazy<PageConverter>(true);

    public event EventHandler CanExecuteChanged;

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public void Execute(object parameter)
    {
        var page = PageConverterInstance.Value.Convert(parameter, null, null, null) as Page;

        if(page != null)
        {
            Application.Current.MainPage.Navigation.PushAsync(page);
        }
    }
}

Now the Model may have an assignable Type for a page, therefore it can change, and the page can be different between your device types (e.g Phone, Watch, Android, iOS). Example:

        [Bindable(BindableSupport.Yes)]
        public Type HelpPageType
        {
            get
            {
                return _helpPageType;
            }
            set
            {
                SetProperty(ref _helpPageType, value);
            }
        }

And an example of it's use then in Xaml.

<Button x:Name="helpButton" Text="{Binding HelpButtonText}" Command="{StaticResource ApplicationNavigate}" CommandParameter="{Binding HelpPageType}"></Button>

And for the sake of completeness the resource as defined in App.xaml

<Application.Resources>
    <ResourceDictionary>   
        <xcmd:NavigateCommand x:Key="ApplicationNavigate" />             
    </ResourceDictionary>
</Application.Resources>

P.S. While the Command pattern generally should use one instance for one operation, in this case I know it is very safe to reuse the same instance across all controls and since it is for a wearable I want to keep things lighter than normal hence defining a single instance of the NavigationCommand in the App.xaml.

cineam mispelt
  • 393
  • 1
  • 8
0

I believe that navigation should be in View not in ViewModel

So here is my solution

RecordsPage.xaml

  <Button x:Name="btnAddRecord"  Text="Add record" Command="{Binding AddRecordCommand}"  />

RecordsPage.xaml.cs

[XamlCompilation(XamlCompilationOptions.Compile)]
    public partial class RecordViewPage : ContentPage
    {
      public RecordsPage()
      { 
        InitializeComponent();
        var view_model =(RecordsPageViewModel) BindingContext;
        viewModel.RecordAdded+=OnRecordAdded;
      }
      public void OnRecordAdded(object sender, EventArgs e)
      { 
       await Shell.Current.GoToAsync($"{nameof(Page2)}");

  
      }
    }

RecordsPageViewModel.cs

public class RecordsPageViewModel: INotifyPropertyChanged
{   
    //it's better to create own EventHandler
    public event EventHandler RecordAdded;

    public ICommand AddRecordCommand=> new Command(async () =>
        { 
         //some code
         //if it's ok
          RecordAdded(this,new EventArgs())
        }

}
DespeiL
  • 993
  • 9
  • 28