web-dev-qa-db-fra.com

Navigation de page à l'aide de MVVM in Store App

J'ai un grave mal de tête avec ce problème. Je n'aime vraiment pas les applications en magasin, mais je suis obligé de l'utiliser dans ce cas. Je ne travaille avec XAML que depuis quelques semaines.

Ma question est la suivante: Comment appeler une RelayCommand dans ma ViewModel (de mon point de vue de cours) qui modifiera la page de mon point de vue? Et encore mieux, changez-le en utilisant l'URI, afin que je puisse passer un paramètre de commande à fichier.

Je suis totalement perdu à ce sujet. Actuellement, j'utilise this.Frame.Navigate(type type) dans le code d'affichage derrière pour naviguer entre les pages.

Je voudrais vraiment et je veux dire vraiment apprécier une description de A à Z sur ce qu'il faut faire dans ce cas.

Je suppose que je pourrais faire quelque chose comme construire un conteneur framec sur ma vue et l'envoyer à mon ViewModel et, de là, naviguer dans le cadre actuel. Mais je ne suis pas sûr de savoir comment cela fonctionne dans les applications Store.

Je suis vraiment désolé pour le manque de bonnes questions, mais je suis sur une date limite et je dois connecter mon View à mon ViewModel de manière appropriée. Je n'aime pas avoir les deux vues codebehind ainsi que le code ViewModel.

12
Mathias

Il existe deux méthodes pour cela. Une méthode simple consiste à transmettre une action de commande de relais du modèle de vue au modèle de vue. 

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>

Une autre méthode consiste à utiliser un IocContainer et un DependencyInjection. Celui-ci est une approche plus faiblement couplée. 

Nous aurons besoin d'une interface pour la page de navigation afin de ne pas avoir besoin de faire référence ou de savoir quelque chose à propos de PageX ou de tout élément d'interface utilisateur en supposant que votre modèle de vue se trouve dans un projet séparé qui ne sait rien de l'interface utilisateur. 

Projet ViewModel:

  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; }
  }

Projet d'interface utilisateur: 

  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>();       
   }
}
11
Lance

Comme Scott dit, vous pouvez utiliser un service de navigation. Je voudrais d’abord créer une interface qui n’est pas nécessaire dans cet exemple, mais qui sera utile si vous utilisez l’injection de dépendance (bonne solution avec viewmodels et services) à l’avenir :)

INavigationService:

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

NavigationService.cs héritera de INavigationService , Vous aurez besoin des espaces de noms suivants

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();
    }
}

ViewModel simple pour montrer l'exemple RelayCommand. NB I Naviguez vers une autre page (Page2.xaml) à l'aide de la commande 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));
                    });
        }
    }}

Dans un exemple simple, j'ai créé le modèle de vue dans MainPage.cs et ajouté le NavigationService , Mais vous pouvez le faire ailleurs en fonction de la configuration de votre MVVM.

MainPage.cs

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

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

MainPage.xaml (se lie à la commande DoSomething)

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

J'espère que cela pourra aider.

12
SWilko

Je n'aime pas vraiment quand ViewModel fait référence à des vues. Donc, je préfère une approche ViewModel-first. En utilisant ContentControls, DataTemplates pour les types ViewModel et une sorte de modèle de navigation dans mes ViewModels.

Ma navigation ressemble à ceci:

[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);
    }
}

Mon NavigatableViewModel ressemble à ceci:

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

    public NavigatableViewModel PreviousViewModel { get; set; }

    public NavigatableViewModel NextViewModel { get; set; }

}

Et mon navigateur:

[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;
    }
}

Mon 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>

L'inconvénient est que vous avez plus de code ViewModel qui gère l'état de ce que vous regardez. Mais évidemment, c'est aussi un énorme avantage en termes de testabilité. Et bien sûr, vos modèles de vue ne doivent pas nécessairement dépendre de vos vues. 

De plus, j'utilise Fody/PropertyChanged, voilà en quoi consiste [ImplementPropertyChanged]. M'empêche d'écrire du code OnPropertyChanged. 

3
Wouter Schut

Voici un autre moyen d'implémenter NavigationService, sans utiliser de classe abstraite ni faire référence à des types de vue dans votre modèle de vue. 

En supposant que le modèle d'affichage de la page de destination ressemble à ceci:

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

Ensuite, votre NavigationService peut simplement implémenter l'interface suivante:

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

Dans votre fenêtre principale ViewModel, vous devez insérer le navigateur et le modèle de vue de la page de destination:

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

    public ICommand GoToPageCommand { get; }
}

L'implémentation de NavigationService encapsule le type de vue (Page2) et la référence à la trame injectée via le constructeur:

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);
    }
}

Pour obtenir le cadre, nommez-le simplement dans votre xaml MainPage:

<Frame x:Name="RootFrame"/>

Dans le code situé derrière MainPage, initialisez votre programme d’amorçage en passant le cadre racine:

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

Enfin, voici l’implémentation du bootstrapper pour la complétude;)

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>();
    }
}
1
meldim