wpf 在商店应用程序中使用 MVVM 进行页面导航

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/26816264/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-13 12:41:34  来源:igfitidea点击:

Page Navigation using MVVM in Store App

c#wpfxamlmvvmwindows-store-apps

提问by Mathias

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.

我对这个问题很头疼。我真的不喜欢商店应用程序,但在这种情况下被迫使用它。我只使用 XAML 几个星期。

My question is: How can I call a RelayCommandin 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.

我的问题是:如何RelayCommand在我的ViewModel(当然是从我的视图中)调用一个会更改我视图中的页面?更好的是,使用 URI 更改它,以便我可以将命令参数传递给文件。

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

我完全迷失了这一点。目前我正在使用this.Frame.Navigate(type type)后面的查看代码来浏览页面。

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

我真的而且我的意思是非常感谢从 a 到 z 的关于在这种情况下该怎么做的描述。

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.

我想我可以做一些事情,比如在我的 View 上构建一个框架容器并将它发送到我的 ViewModel,然后从那里将当前框架导航到另一个框架。但我不确定这在 Store 应用程序中是如何工作的。

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.

对于缺少好的问题,我真的很抱歉,但我在截止日期前,我需要以正确的方式将我的视图连接到我的 ViewModel。我不喜欢同时使用视图代码隐藏和 ViewModel 代码。

采纳答案by Lance

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.

另一种方法是使用 IocContainer 和 DependencyInjection。这是一种更加松散耦合的方法。

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.

我们将需要一个导航页面的界面,这样我们就不需要参考或了解 PageX 或任何 UI 元素,假设您的视图模型位于一个对 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>();       
   }
}

回答by SWilko

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

正如 Scott 所说,您可以使用 NavigationService。我将首先创建一个在本示例中不需要的接口,但如果您将来使用依赖注入(具有视图模型和服务的良好解决方案),这将很有用 :)

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

NavigationService.cs 将继承 INavigationService 您将需要以下命名空间

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.

显示 RelayCommand 示例的简单 ViewModel。注意,我使用 DoSomething RelayCommand 导航到另一个页面 (Page2.xaml)。

MyViewModel.cs

视图模型.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 中创建了视图模型并添加了 NavigationService,但您可以在其他地方执行此操作,具体取决于您的 MVVM 设置。

MainPage.cs

主页.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)

MainPage.xaml(绑定到命令 DoSomething)

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

Hope that helps.

希望有帮助。

回答by Wouter Schut

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.

我真的不喜欢 ViewModel 引用要导航到的视图。所以我更喜欢 ViewModel-first 的方法。通过在我的 ViewModel 中使用 ContentControls、ViewModel 类型的 DataTemplates 和某种导航模式。

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:

我的 NavigatableViewModel 看起来像:

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

我的 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:

应用程序.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:

应用程序.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.

缺点是您有更多的 ViewModel 代码来管理您正在查看的内容的状态。但显然,这在可测试性方面也是一个巨大的优势。当然,您的 ViewModel 不需要依赖于您的视图。

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

另外,我使用 Fody/PropertyChanged,这就是 [ImplementPropertyChanged] 的用途。阻止我编写 OnPropertyChanged 代码。

回答by meldim

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

这是实现 NavigationService 的另一种方法,无需使用抽象类,也无需在视图模型中引用视图类型。

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:

那么你的 NavigationService 可以简单地实现以下接口:

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:

在您的主窗口 ViewModel 中,您需要注入导航器和目标页面的视图模型:

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:

NavigationService 的实现封装了视图类型(Page2)和通过构造函数注入的框架的引用:

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:

要获取框架,只需在 MainPage xaml 中为其命名:

<Frame x:Name="RootFrame"/>

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

在 MainPage 后面的代码中,通过传递根框架来初始化引导程序:

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

回答by Daniel Leiszen

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.

这让我很困扰,因为没有人在架构级别解决过这个问题。所以这是使用内置的基于框架的导航完全解耦视图、视图模型和它们之间的映射的代码。该实现使用 Autofact 作为 DI 容器,但可以轻松移植到其他 IoC 解决方案。

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

核心 VM 逻辑(这些应该在同一个程序集中):

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

此代码应位于 UWP 类库或可执行文件中:

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:

其余的只是用于在 DI 中注册和使用示例的便利代码:

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