13

I'm having a serious headache with this problem. I really dislike store apps but am forced to use it in this case. I've only worked with XAML for a few weeks.

My question is: How can I call a RelayCommand in my ViewModel (from my View of course) that will change the page on my view? And even better, change it using URI, so that I can pass a command parameter to file.

I'm totally lost on this. Currently I'm using this.Frame.Navigate(type type) in the View Code behind to navigate through pages.

I would really and I mean REALLY appreciate a description from a to z on what to do in this case.

I presume i could do something like building a framecontainer on my View and send it to my ViewModel and from there navigate the current frame to another. But I'm not sure how that works in Store apps.

I am really sorry for the lack of good questions, but I'm on a deadline and i need to get my View connected to my ViewModel in a proper way.. I don't like having both view codebehind as well as ViewModel code.

darcyy
  • 5,236
  • 5
  • 28
  • 41

5 Answers5

11

As Scott says you could use a NavigationService. I would firstly create an interface this is not needed in this example but will be useful if you use Dependency Injection (good solution with viewmodels and services) in the future :)

INavigationService:

public interface INavigationService
{
    void Navigate(Type sourcePage);
    void Navigate(Type sourcePage, object parameter);
    void GoBack();
}

NavigationService.cs will inherit INavigationService you will need the following namespaces

using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;


public sealed class NavigationService : INavigationService
{
    public void Navigate(Type sourcePage)
    {
        var frame = (Frame)Window.Current.Content;
        frame.Navigate(sourcePage);
    }

    public void Navigate(Type sourcePage, object parameter)
    {
        var frame = (Frame)Window.Current.Content;
        frame.Navigate(sourcePage, parameter);
    }

    public void GoBack()
    {
        var frame = (Frame)Window.Current.Content;
        frame.GoBack();
    }
}

Simple ViewModel to show RelayCommand example. NB I Navigate to another Page (Page2.xaml) using the DoSomething RelayCommand.

MyViewModel.cs

public class MyViewModel : INotifyPropertyChanged
{
    private INavigationService _navigationService;

    public event PropertyChangedEventHandler PropertyChanged;

    public void OnPropertyChanged(string propertyName)
    {
        var handler = PropertyChanged;
        if (handler != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

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

    private ICommand _doSomething;

    public ICommand DoSomething
    {
        get
        {
            return _doSomething ??
                new RelayCommand(() =>
                    {
                        _navigationService.Navigate(typeof(Page2));
                    });
        }
    }}

In simple example Ive created the viewmodel in MainPage.cs and added the NavigationService but you can do this elsewhere depending on what your MVVM setup is like.

MainPage.cs

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();

        var vm = new MyViewModel(new NavigationService());
        this.DataContext = vm;
    }
}

MainPage.xaml (binds to the command DoSomething)

 <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Button Width="200" Height="50" Content="Go to Page 2"
             Command="{Binding DoSomething}"/>
</Grid>

Hope that helps.

Manushin Igor
  • 3,398
  • 1
  • 26
  • 40
SWilko
  • 3,542
  • 1
  • 16
  • 26
  • What's the relevance of vm in MainPage.cs @dellywheel – Sajeev C Dec 18 '14 at 09:02
  • @WD - I create a new instance of MyViewModel and then add it as MainPage's DataContext to enable databinding eg so that we can bind to the DoSomething command from the xaml. – SWilko Dec 22 '14 at 19:12
  • 1
    @SWilko - This would mean that the VM will have an idea about Page2, I think it's ok but not recommended. I would rather pass something that's more abstract than passing the type page2 itself. – Lance Aug 28 '15 at 13:11
  • 2
    VIewModels should not know about views. – bleepzter Feb 10 '17 at 03:57
11

There are 2 ways to do this, a simple way is to pass a relay command action from the view to the view model.

public MainPage()
{
  var vm = new MyViewModel();
  vm.GotoPage2Command = new RelayCommand(()=>{ Frame.Navigate(typeof(Page2)) });
  this.DataContext = vm;
}

<Button Command={Binding GoToPage2Command}>Go to Page 2</Button>

Another way is by using an IocContainer and DependencyInjection. This one is a more losely coupled approach.

We will need an interface for navigation page so that we don't need to reference or Know anything about PageX or any UI element assuming that your viewmodel is in a separate project that doesn't know anything about the UI.

ViewModel Project:

  public interface INavigationPage
  {
    Type PageType { get; set; }
  }

  public interface INavigationService
  {
    void Navigate(INavigationPage page) { get; set; }
  }



public class MyViewModel : ViewModelBase
  {
    public MyViewModel(INavigationService navigationService, INavigationPage page)
    {
      GotoPage2Command = new RelayCommand(() => { navigationService.Navigate(page.PageType); })
    }

    private ICommand GotoPage2Command { get; private set; }
  }

UI Project:

  public class NavigationService : INavigationService
    {
       //Assuming that you only navigate in the root frame
       Frame navigationFrame = Window.Current.Content as Frame;
       public void Navigate(INavigationPage page)
       {
          navigationFrame.Navigate(page.PageType);
       }
    }

public abstract class NavigationPage<T> : INavigationPage
{
   public NavigationPage()
   {
      this.PageType = typeof(T);
   }
}

public class NavigationPage1 : NavigationPage<Page1> { }


public class MainPage : Page
{
   public MainPage()
   {
      //I'll just place the container logic here, but you can place it in a bootstrapper or in app.xaml.cs if you want. 
      var container = new UnityContainer();
      container.RegisterType<INavigationPage, NavigationPage1>();
      container.RegisterType<INavigationService, NavigationService>();
      container.RegisterType<MyViewModel>();

      this.DataContext = container.Resolve<MyViewModel>();       
   }
}
Lance
  • 2,774
  • 4
  • 37
  • 57
  • Nice, but I have question: What if I have one container created in App.xaml.cs. This solution works great, but I have few NavigationPages which implements one interface. I can't create two register with the same interface. I can use RegisterCollection, but in ViewModel constructor I have to use indexes and it's not nice solution. I would like register few NavigationPages and use them in viewModels constructor. Is there any possibility to identity which NavigationPage from IEnumerable collecion i want to use without check the type (becouse these types are in UIProject and I haven't access. – darson1991 Oct 07 '15 at 08:39
  • Ioc has ways to register multiple instances and identify those instance during resolve. In unitycontainer and in ninject, I believe that you can give a name when you register. You can check this post http://stackoverflow.com/questions/16921008/getting-unity-to-resolve-multiple-instances-of-the-same-type – Lance Oct 07 '15 at 13:08
3

I don't really like when a ViewModel references Views to navigate to. So I prefer to a ViewModel-first approach. By using ContentControls, DataTemplates for ViewModel types & some kind of navigation pattern in my ViewModels.

My navigation looks like this:

[ImplementPropertyChanged]
public class MainNavigatableViewModel : NavigatableViewModel
{
    public ICommand LoadProfileCommand { get; private set; }

    public ICommand OpenPostCommand { get; private set; }

    public MainNavigatableViewModel ()
    {
        LoadProfileCommand = new RelayCommand(() => Navigator.Navigate(new ProfileNavigatableViewModel()));
        OpenPostCommand = new RelayCommand(() => Navigator.Navigate(new PostEditViewModel { Post = SelectedPost }), () => SelectedPost != null);
    }
}

My NavigatableViewModel looks like:

[ImplementPropertyChanged]
public class NavigatableViewModel
{
    public NavigatorViewModel Navigator { get; set; }

    public NavigatableViewModel PreviousViewModel { get; set; }

    public NavigatableViewModel NextViewModel { get; set; }

}

And my Navigator:

[ImplementPropertyChanged]
public class NavigatorViewModel
{
    public NavigatableViewModel CurrentViewModel { get; set; }

    public ICommand BackCommand { get; private set; }

    public ICommand ForwardCommand { get; private set; }

    public NavigatorViewModel()
    {
        BackCommand = new RelayCommand(() =>
        {
            // Set current control to previous control
            CurrentViewModel = CurrentViewModel.PreviousViewModel;
        }, () => CurrentViewModel != null && CurrentViewModel.PreviousViewModel != null);

        ForwardCommand = new RelayCommand(() =>
        {
            // Set current control to next control
            CurrentViewModel = CurrentViewModel.NextViewModel;
        }, () => CurrentViewModel != null && CurrentViewModel.NextViewModel != null);
    }

    public void Navigate(NavigatableViewModel newViewModel)
    {
        if (newViewModel.Navigator != null && newViewModel.Navigator != this)
            throw new Exception("Viewmodel can't be added to two different navigators");

        newViewModel.Navigator = this;

        if (CurrentViewModel != null)
        {
            CurrentViewModel.NextViewModel = newViewModel;
        }

        newViewModel.PreviousViewModel = CurrentViewModel;
        CurrentViewModel = newViewModel;
    }
}

My MainWindows.xaml:

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:viewmodels="clr-namespace:MyApp.ViewModels"
        x:Class="MyApp.Windows.MainWindow"
        mc:Ignorable="d"
        Title="MainWindow" Height="389" Width="573" 
        d:DataContext="{d:DesignInstance {x:Type viewmodels:MyAppViewModel}, IsDesignTimeCreatable=True}">
    <Grid>
        <!-- Show data according to data templates as defined in App.xaml -->
        <ContentControl Content="{Binding Navigator.CurrentViewModel}"  Margin="0,32,0,0" />

        <Button Content="Previous" Command="{Binding Navigator.BackCommand}" Style="{DynamicResource ButtonStyle}" HorizontalAlignment="Left" Margin="10,5,0,0" VerticalAlignment="Top" Width="75" />
        <Button Content="Next" Command="{Binding Navigator.ForwardCommand}" Style="{DynamicResource ButtonStyle}" HorizontalAlignment="Left" Margin="90,5,0,0" VerticalAlignment="Top" Width="75" />
    </Grid>
</Window>

App.xaml.cs:

public partial class App
{
    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        new MainWindow {DataContext = new MyAppViewModel()}.Show();
    }
}

MyAppViewModel:

[ImplementPropertyChanged]
public class MyAppViewModel
{
    public NavigatorViewModel Navigator { get; set; }

    public MyAppViewModel()
    {
        Navigator = new NavigatorViewModel();
        Navigator.Navigate(new MainNavigatableViewModel());
    }
}

App.xaml:

        <DataTemplate DataType="{x:Type viewmodels:MainNavigatableViewModel}">
            <controls:MainControl/>
        </DataTemplate>
        <DataTemplate DataType="{x:Type viewmodels:PostEditViewModel}">
            <controls:PostEditControl/>
        </DataTemplate>

The downside is that you have more ViewModel-code which manages the state of what you are looking at. But obviously that is also a huge advantage in terms of Testability. And of course your ViewModels do not need to depend on your Views.

Plus I use Fody/PropertyChanged, that's what the [ImplementPropertyChanged] is about. Keeps me from writing OnPropertyChanged code.

Wouter Schut
  • 907
  • 2
  • 10
  • 22
  • DataType is not supported in UWP templates The data type is computed. Template based navigation where the content control resolves the data template automagically is not supported (easily) in uwp as it is in wpf or silverlight. – bleepzter Feb 10 '17 at 03:58
  • Interesting (and sad). Wouldn't this be a solution? http://stackoverflow.com/questions/33252915/how-to-associate-view-with-viewmodel-or-multiple-datatemplates-for-viewmodel – Wouter Schut Feb 10 '17 at 08:27
1

Here is another way to implement the NavigationService, without using an abstract class and without referencing view types in your view model.

Assuming that the view model of the destination page is something like this:

public interface IDestinationViewModel { /* Interface of destination vm here */ }
class MyDestinationViewModel : IDestinationViewModel { /* Implementation of vm here */ }

Then your NavigationService can simply implement the following interface:

public interface IPageNavigationService
{
    void NavigateToDestinationPage(IDestinationViewModel dataContext);
}

In your main window ViewModel you need to inject the navigator and the view model of the destination page:

class MyViewModel1 : IMyViewModel
{
    public MyViewModel1(IPageNavigationService navigator, IDestinationViewModel destination)
    {
        GoToPageCommand = new RelayCommand(() => 
                navigator.NavigateToDestinationPage(destination));
    }

    public ICommand GoToPageCommand { get; }
}

The implementation of the NavigationService encapsulates the view type (Page2) and the reference to the frame which is injected through the constructor:

class PageNavigationService : IPageNavigationService
{
    private readonly Frame _navigationFrame;

    public PageNavigationService(Frame navigationFrame)
    {
        _navigationFrame = navigationFrame;
    }

    void Navigate(Type type, object dataContext)
    {
        _navigationFrame.Navigate(type);
        _navigationFrame.DataContext = dataContext;
    }

    public void NavigateToDestinationPage(IDestinationViewModel dataContext)
    {
        // Page2 is the corresponding view of the destination view model
        Navigate(typeof(Page2), dataContext);
    }
}

To get the frame simply name it in your MainPage xaml:

<Frame x:Name="RootFrame"/>

In the code behind of the MainPage initialize your bootstrapper by passing the root frame:

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();
        var bootstrapper = new Bootstrapper(RootFrame);
        DataContext = bootstrapper.GetMainScreenViewModel();
    }
}

Finally here is the bootstrapper implementation for completeness ;)

class Bootstrapper
{
    private Container _container = new Container();

    public Bootstrapper(Frame frame)
    {
        _container.RegisterSingleton(frame);
        _container.RegisterSingleton<IPageNavigationService, PageNavigationService>();
        _container.Register<IMyViewModel, MyViewModel1>();
        _container.Register<IDestinationViewModel, IDestinationViewModel>();

#if DEBUG
        _container.Verify();
#endif
    }

    public IMyViewModel GetMainScreenViewModel()
    {
        return _container.GetInstance<IMyViewModel>();
    }
}
meldim
  • 1,116
  • 1
  • 9
  • 9
0

This simply bothers me that no one has solved this at the architectural level. So this is the code for complete decoupling the views, viewmodels and the mapping between them with using the built-in Frame based navigation. The implementation uses Autofact as DI container, but can be easily ported to other IoC solutions.

Core VM logic (these should be in the same assembly):

// I would not get into how the ViewModel or property change notification is implemented
public abstract class PageViewModel : ViewModel
{
    protected internal INavigationService Navigation { get; internal set; }

    internal void NavigationCompleted()
    {
        OnNavigationCompleted();
    }

    protected virtual void OnNavigationCompleted()
    {

    }
}

public interface INavigationService
{
    void Navigate<TModel>() where TModel : PageViewModel;
}

public abstract class NavigationServiceBase : INavigationService
{
    public abstract void Navigate<TModel>() where TModel : PageViewModel;
    protected void CompleteNavigation(PageViewModel model)
    {
        model.Navigation = this;
        model.NavigationCompleted();
    }
}

This code should be in a UWP class library or executable:

public interface INavigationMap<TModel>
    where TModel: PageViewModel
{
    Type ViewType { get; }
}

internal class NavigationMap<TModel, TView> : INavigationMap<TModel>
    where TModel: PageViewModel
    where TView: Page
{
    public Type ViewType => typeof(TView);
}

public class NavigationService : NavigationServiceBase
{
    private readonly Frame NavigationFrame;
    private readonly ILifetimeScope Resolver;

    public NavigationService(ILifetimeScope scope)
    {
        Resolver = scope;
        NavigationFrame = Window.Current.Content as Frame;
        NavigationFrame.Navigated += NavigationFrame_Navigated;
    }

    private void NavigationFrame_Navigated(object sender, Windows.UI.Xaml.Navigation.NavigationEventArgs e)
    {
        if(e.Content is FrameworkElement element)
        {
            element.DataContext = e.Parameter;

            if(e.Parameter is PageViewModel page)
            {
                CompleteNavigation(page);
            }
        }
    }

    public override void Navigate<TModel>()
    {
        var model = Resolver.Resolve<TModel>();
        var map = Resolver.Resolve<INavigationMap<TModel>>();

        NavigationFrame.Navigate(map.ViewType, model);
    }
}

The rest is just convenience code for registering in the DI and usage examples:

public static class NavigationMap
{
    public static void RegisterNavigation<TModel, TView>(this ContainerBuilder builder)
        where TModel : PageViewModel
        where TView : Page
    {
        builder.RegisterInstance(new NavigationMap<TModel, TView>())
            .As<INavigationMap<TModel>>()
            .SingleInstance();
    }
}

     builder.RegisterNavigation<MyViewModel, MyView>();


public class UserAuthenticationModel : PageViewModel
{
    protected override void OnNavigationCompleted()
    {
        // UI is visible and ready
        // navigate to somewhere else
        Navigation.Navigate<MyNextViewModel>();
    }
}
Daniel Leiszen
  • 1,827
  • 20
  • 39