使用MVVM在Store应用中进行页面导航

13

我对这个问题感到非常头痛。我真的不喜欢商店应用程序,但在这种情况下被迫使用它。我只接触XAML几周。

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

我完全迷失了。目前我正在使用this.Frame.Navigate(type type)在View Code Behind中导航页面。

我会非常非常地感激您从头到尾详细描述该怎么做。

我想我可以在我的View上构建一个framecontainer并将其发送到我的ViewModel,然后从那里导航到另一个当前框架。但是我不确定在商店应用程序中如何运作。

非常抱歉我没有提出好的问题,但我有最后期限,我需要以正确的方式将我的View连接到我的ViewModel..我不喜欢同时拥有View CodeBehind和ViewModel Code。


你查过 NavigationService 吗? - Scott Nimrod
你的应用程序中使用了MVVM Light吗? - SWilko
不,我不是dellywheel。 - user4063668
5个回答

11

正如Scott所说,你可以使用NavigationService。首先我会创建一个接口(在这个例子中不是必需的,但如果你将来使用依赖注入(与ViewModel和Service一起使用的好解决方案)会很有用:)

INavigationService:

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

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

一个简单的ViewModel,用于展示RelayCommand的示例。注意:我使用DoSomething RelayCommand导航到另一个页面(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));
                    });
        }
    }}
在这个简单的示例中,我在MainPage.cs中创建了viewmodel并添加了NavigationService,但是根据您的MVVM设置不同,您也可以在其他地方进行此操作。

MainPage.cs


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

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

MainPage.xaml(绑定到命令DoSomething)

 <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Button Width="200" Height="50" Content="Go to Page 2"
             Command="{Binding DoSomething}"/>
</Grid>
希望那能有所帮助。

MainPage.cs中vm的相关性是什么?@dellywheel - Sajeev C
@WD - 我创建了一个MyViewModel的新实例,然后将其作为MainPage的DataContext添加,以启用数据绑定,例如我们可以从xaml绑定到DoSomething命令。 - SWilko
1
@SWilko - 这意味着虚拟机将了解Page2,我认为这样做没问题,但并不推荐。我宁愿传递比传递page2类型更抽象的东西。 - Lance
2
ViewModels 不应该了解视图。 - bleepzter

11

有两种方法可以实现这个,一种简单的方式是从视图传递继电器命令操作到视图模型。

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>

另一种方法是使用IocContainer和DependencyInjection。这种方法更加松散耦合。
我们需要一个导航页面的接口,这样我们就不需要引用或了解PageX或任何UI元素,假设您的视图模型在一个独立的项目中,该项目不知道任何关于UI的内容。
视图模型项目:
  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项目:

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

不错,但我有问题: 如果我在App.xaml.cs中创建了一个容器怎么办?这个解决方案很好,但是我有几个NavigationPages实现了一个接口。我不能创建两个具有相同接口的注册表。我可以使用RegisterCollection,但在ViewModel构造函数中,我必须使用索引,这不是一个好的解决方案。 我想注册一些NavigationPages并在viewModels构造函数中使用它们。有没有可能识别我想要使用哪个IEnumerable集合中的NavigationPage而不检查类型(因为这些类型在UIProject中,我没有访问权限)? - darson1991
Ioc有多种注册多个实例并在解析期间识别这些实例的方法。在UnityContainer和Ninject中,我相信您可以在注册时命名。您可以查看此帖子https://dev59.com/MWQn5IYBdhLWcg3wVF0w。 - Lance

3

我不太喜欢ViewModel引用View进行导航。因此,我更喜欢采用ViewModel-first方法。通过在我的ViewModel中使用ContentControls、DataTemplates和某种导航模式。

我的导航看起来像这样:

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

我的NavigatableViewModel长这样:

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

    public NavigatableViewModel PreviousViewModel { get; set; }

    public NavigatableViewModel NextViewModel { get; set; }

}

还有我的导航器:

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

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

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

此外,我使用Fody/PropertyChanged,这就是[ImplementPropertyChanged]的含义。让我不必编写OnPropertyChanged代码。


数据类型在UWP模板中不受支持。该数据类型是计算出来的。基于模板的导航,其中内容控件自动解析数据模板,在UWP中不像在WPF或Silverlight中那样容易实现。 - bleepzter
有趣(也令人难过)。这会是一个解决方案吗?https://dev59.com/h1wX5IYBdhLWcg3w6S4h - Wouter Schut

1
这里有另一种实现NavigationService的方式,不使用抽象类,也不在视图模型中引用视图类型。
假设目标页面的视图模型如下:
public interface IDestinationViewModel { /* Interface of destination vm here */ }
class MyDestinationViewModel : IDestinationViewModel { /* Implementation of vm here */ }

那么您的 NavigationService 只需要实现以下接口即可:
public interface IPageNavigationService
{
    void NavigateToDestinationPage(IDestinationViewModel dataContext);
}

在您的主窗口ViewModel中,您需要注入导航器和目标页面的ViewModel:
class MyViewModel1 : IMyViewModel
{
    public MyViewModel1(IPageNavigationService navigator, IDestinationViewModel destination)
    {
        GoToPageCommand = new RelayCommand(() => 
                navigator.NavigateToDestinationPage(destination));
    }

    public ICommand GoToPageCommand { get; }
}

导航服务的实现封装了视图类型(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);
    }
}

要获取框架,只需在您的MainPage xaml中命名即可:

<Frame x:Name="RootFrame"/>

在 MainPage 的代码后端,通过传递根框架来初始化您的引导程序:
public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();
        var bootstrapper = new Bootstrapper(RootFrame);
        DataContext = bootstrapper.GetMainScreenViewModel();
    }
}

最后,为了完整性,这里是引导程序的实现 ;)

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

0

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

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

这段代码应该放在一个 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);
    }
}

剩下的代码只是便于在 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>();
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接