wake-up-neo.com

Seitennavigation mit MVVM in Store App

Ich habe ernsthafte Kopfschmerzen mit diesem Problem. Ich mag keine Laden-Apps, muss sie aber in diesem Fall verwenden. Ich habe nur ein paar Wochen mit XAML gearbeitet.

Meine Frage ist: Wie kann ich eine RelayCommand in meiner ViewModel (aus meiner Sicht natürlich) aufrufen, die die Seite in meiner Ansicht ändert? Und noch besser, ändern Sie es mit URI, damit ich einen Befehlsparameter an file übergeben kann.

Ich bin total verloren. Derzeit verwende ich this.Frame.Navigate(type type) im View Code hinter, um durch die Seiten zu navigieren.

Ich würde wirklich und ich meine wirklich eine Beschreibung von A bis Z schätzen, was in diesem Fall zu tun ist.

Ich vermute, ich könnte so etwas wie einen Framecontainer in meiner View erstellen und an mein ViewModel senden und von dort aus den aktuellen Frame zu einem anderen navigieren. Ich bin mir jedoch nicht sicher, wie das in Store-Apps funktioniert.

Es tut mir wirklich leid für den Mangel an guten Fragen, aber ich bin in einer Frist und ich muss meine View mit meinem ViewModel richtig verbinden .. Ich mag es nicht, sowohl View Codebehind als auch ViewModel Code zu haben.

12
Mathias

Es gibt zwei Möglichkeiten, dies zu tun. Eine einfache Möglichkeit besteht darin, eine Relaisbefehlsaktion von der Ansicht an das Ansichtsmodell zu übergeben. 

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>

Eine andere Möglichkeit besteht darin, einen IocContainer und DependencyInjection zu verwenden. Dieser Ansatz ist weniger aneinander gekoppelt. 

Wir benötigen eine Benutzeroberfläche für die Navigationsseite, damit wir nicht über PageX oder über jedes Benutzeroberflächenelement etwas verweisen oder wissen müssen, vorausgesetzt, Ihr Viewmodel befindet sich in einem separaten Projekt, das nichts über die Benutzeroberfläche weiß. 

ViewModel-Projekt:

  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-Projekt: 

  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

Wie Scott sagt, könnten Sie einen NavigationsService verwenden. Ich würde zuerst eine Schnittstelle erstellen, die in diesem Beispiel nicht benötigt wird, aber nützlich ist, wenn Sie in Zukunft Abhängigkeitsinjektion (gute Lösung mit Viewmodels und Services) verwenden :)

INavigationService:

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

NavigationService.cs erbt INavigationService . Sie benötigen die folgenden 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();
    }
}

Einfaches ViewModel zum Anzeigen des RelayCommand-Beispiels. NB Ich navigiere mit dem DoSomething RelayCommand zu einer anderen Seite (Page2.xaml).

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 einem einfachen Beispiel habe ich das Viewmodel in MainPage.cs erstellt und den NavigationsService Hinzugefügt. Sie können dies jedoch an anderer Stelle tun, je nachdem, wie Ihr MVVM-Setup ist.

MainPage.cs

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

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

MainPage.xaml (bindet an den Befehl DoSomething)

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

Hoffentlich hilft das.

12
SWilko

Ich mag es nicht wirklich, wenn ein ViewModel auf Views verweist, zu denen navigiert wird. Also bevorzuge ich einen ViewModel-first-Ansatz. Durch die Verwendung von ContentControls, DataTemplates für ViewModel-Typen und einer Art Navigationsmuster in meinen ViewModels.

Meine Navigation sieht so aus:

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

Mein NavigatableViewModel sieht folgendermaßen aus:

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

    public NavigatableViewModel PreviousViewModel { get; set; }

    public NavigatableViewModel NextViewModel { get; set; }

}

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

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

Der Nachteil ist, dass Sie über mehr ViewModel-Code verfügen, der den Status Ihrer Ansicht verwaltet. Dies ist natürlich auch ein großer Vorteil in Bezug auf die Testbarkeit. Und natürlich müssen Ihre ViewModels nicht von Ihren Ansichten abhängen. 

Außerdem verwende ich Fody/PropertyChanged, darum geht es bei [ImplementPropertyChanged]. Hält mich vom Schreiben von OnPropertyChanged-Code ab. 

3
Wouter Schut

Im Folgenden finden Sie eine weitere Möglichkeit, den NavigationService zu implementieren, ohne eine abstrakte Klasse zu verwenden und auf Ansichtstypen in Ihrem Ansichtsmodell zu verweisen. 

Angenommen, das Ansichtsmodell der Zielseite sieht etwa so aus:

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

Dann kann Ihr NavigationService einfach die folgende Schnittstelle implementieren:

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

In Ihrem Hauptfenster ViewModel müssen Sie den Navigator und das Ansichtsmodell der Zielseite einfügen:

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

    public ICommand GoToPageCommand { get; }
}

Die Implementierung des NavigationService enthält den View-Typ (Seite2) und die Referenz auf den Frame, der durch den Konstruktor eingefügt wird:

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

Um den Frame zu erhalten, benennen Sie ihn einfach in Ihrer MainPage xaml:

<Frame x:Name="RootFrame"/>

Initialisieren Sie Ihren Bootstrapper im Code hinter der MainPage, indem Sie den Stammframe übergeben:

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

Schließlich ist hier die Bootstrapper-Implementierung der Vollständigkeit halber;)

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