如何从MVVM WPF中的一个视图转到另一个视图?

w6lpcovy  于 2023-01-31  发布在  其他
关注(0)|答案(4)|浏览(236)

这里我有一个用MVVM结构制作的WPF应用程序。我对C#WPF相当陌生,对这个概念也不熟悉。我试图通过按下按钮,通过一个视图中的函数切换到另一个视图。
下面是应用程序的外观

按下登录按钮后,将触发一个功能,该功能将验证输入,如果有效,则切换到另一个视图。

文件结构

如何切换视图?下面是一些代码供参考。

    • 主窗口. xaml**
<Window x:Class="QuizAppV2.MainWindow"
        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:local="clr-namespace:QuizAppV2"
        xmlns:viewModel="clr-namespace:QuizAppV2.MVVM.ViewModel"
        mc:Ignorable="d"
        Height="600" Width="920"
        WindowStartupLocation="CenterScreen"
        WindowStyle="None"
        ResizeMode="NoResize"
        Background="Transparent"
        AllowsTransparency="True">
    <Window.DataContext>
        <viewModel:MainViewModel/>
    </Window.DataContext>
    <Border Background="#272537"
            CornerRadius="20">
        <Grid>
            <Grid.RowDefinitions>
                <RowDefinition Height="75"/>
                <RowDefinition/>
                <RowDefinition Height="25"/>
            </Grid.RowDefinitions>

            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                    <ColumnDefinition/>
                </Grid.ColumnDefinitions>

                <TextBlock Text="Online Quiz"
                            Grid.Column="1"
                            FontSize="20"
                            Foreground="White"
                            HorizontalAlignment="Center"
                            VerticalAlignment="Center"/>
                <StackPanel Grid.Column="2"
                            Margin="30,20"
                            Orientation="Horizontal"
                            HorizontalAlignment="Right"
                            VerticalAlignment="Top">

                    <Button Content="–"
                            Background="#00CA4E"
                            Style="{StaticResource UserControls}"
                            Click="Minimise"/>
                    <Button Content="▢"
                            Background="#FFBD44"
                            Style="{StaticResource UserControls}"
                            Click="Restore"/>
                    <Button Content="X"
                            Background="#FF605C"
                            Style="{StaticResource UserControls}"
                            Click="Exit"/>
                </StackPanel>
            </Grid>
            <ContentControl Grid.Column="1"
                            Grid.Row="1"
                            Margin="20,10,20,50"
                            Content="{Binding CurrentView}"/>
        </Grid>
    </Border>
</Window>
    • 主视图模型. cs**
using QuizAppV2.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace QuizAppV2.MVVM.ViewModel
{
    class MainViewModel : ObservableObject
    {

        public RelayCommand LoginViewCommand { get; set; }
        public RelayCommand SubjectSelectionViewCommand { get; set; }
        public RelayCommand QuizViewCommand { get; set; }
        public RelayCommand ResultViewCommand { get; set; }

        public LoginViewModel LoginVM { get; set; }
        public SubjectSelectionViewModel SubjectSelectVM { get; set; }
        public QuizViewModel QuizVM { get; set; }
        public ResultViewModel ResultVM { get; set; }

        private object _currentView;

        public object CurrentView
        {
            get { return _currentView; }
            set
            {
                _currentView = value;
                onPropertyChanged();
            }
        }

        public MainViewModel()
        {
            LoginVM = new LoginViewModel();
            SubjectSelectVM = new SubjectSelectionViewModel();
            QuizVM = new QuizViewModel();
            ResultVM = new ResultViewModel();
            CurrentView = SubjectSelectVM;

            LoginViewCommand = new RelayCommand(o =>
            {
                CurrentView = LoginVM;
            });
            SubjectSelectionViewCommand = new RelayCommand(o =>
            {
                CurrentView = SubjectSelectVM;
            });
        }
    }
}
    • 登录视图. xaml**
using QuizAppV2.MVVM.ViewModel;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace QuizAppV2.MVVM.View
{
    /// <summary>
    /// Interaction logic for LoginView.xaml
    /// </summary>
    public partial class LoginView : UserControl
    {
        public LoginView()
        {
            InitializeComponent();
        }

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            if (UsrId.Text == "" || UsrName.Text == "")
            {
                UsrIDErrMsg.Visibility = Visibility.Visible;
                UsrNameErrMsg.Visibility = Visibility.Visible;
            }
            else
            {
                UsrIDErrMsg.Visibility = Visibility.Hidden;
                UsrNameErrMsg.Visibility = Visibility.Hidden;
                MainWindow.currentUser = new Student(UsrId.Text, UsrName.Text);
                
            }
        }
    }
}

谢谢

r9f1avp5

r9f1avp51#

我建议使用“数据模板”。在主窗口中放置以下资源:

<DataTemplate DataType="{x:Type viewmodel:QuizViewModel}">
            <local:QuizView/>
 </DataTemplate>
<DataTemplate DataType="{x:Type viewmodel:LoginViewModel}">
            <local:LoginView/>
 </DataTemplate>

等等与其他... WPF正在为您做所有的工作,它检查“当前视图”属性,并选择如何查看它根据适当的数据模板。

wnvonmuf

wnvonmuf2#

导航是一个棘手的主题,有几种方法可以做到这一点,但由于您是WPF的新手,我尝试概述一个简单的技术,沿着您提供的示例的路线,要求是必须从一个页面转到另一个页面,一个简单的想法是交换内容。我的意思是,当用户单击**“登录”**时,我们验证用户并将LoginPage与其他一些页面交换。在您的情况下,一个测验页面,当用户选择任何选项,我们交换了视图与下一个视图等等。
我已经编写了一个简单的解决方案与 shell 机制。本质上,我们创建了一个空壳在主窗口(即它没有用户界面),我们加载页面到这个空壳使用导航服务/助手。我已经用了一个单例类在这里只是为了简单,有3个主要的方法,
寄存器 shell :这必须是将发生交换的窗口,理想情况下需要设置一次。
加载视图:用新视图替换旧视图的方法,我使用了用户控件,因为在WPF中大多数子视图都可以是用户控件。
加载带有自定义数据的视图:与上面类似,但具有更大的灵活性,因为它允许您提供额外的数据。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;

namespace Navigation
{
    class NavigationService
{
    /// <summary>
    /// Singleton so we keep on shell which views can use this to navigate to different pages.
    /// </summary>
    public static NavigationService Instance = new NavigationService();
    private MainWindow myShell;

    private NavigationService()
    {
    }

    /// <summary>
    /// Register the main shell so this service know where to swap the data out and in of
    /// </summary>
    /// <param name="theShell"></param>
    public void RegisterShell(MainWindow theShell)
    {
        this.myShell = theShell;
    }

    /// <summary>
    /// Swaps out any view to the shell.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public void LoadView<T>() where T : UserControl, new()
    {
        myShell.TheShell = new T();
    }

    /// <summary>
    /// Swaps out any view to the shell with custom data, here the user responsible to create UserControl with all the reqired data for the view.
    /// We can automate this via reflection if required.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="theNewControl"></param>
    public void LoadViewWithCustomData<T>(UserControl theNewControl) where T : UserControl, new()
    {
        myShell.TheShell = theNewControl;
    }
}

下面是登录页面的外观,其中重要的一行是**NavigationService.Instance.LoadView<_4OptionQuizPage>()**这实际上是将用户发送到_4OptionQuizPage。

public partial class LoginPage : UserControl
        {
            public ICommand LoginClicked { get; }
            public LoginPage()
            {
                InitializeComponent();
                this.DataContext = this;
                LoginClicked = new SimpleCommand(OnLoginClicked);
            }
    
            private void OnLoginClicked()
            {
                // TODO : Authenticate user here.
    
                // Send the user to Quiz Page
                NavigationService.Instance.LoadView<_4OptionQuizPage>();
            }
        }

在_4OptionQuizPage中,我们可以有这样的内容,这是大量业务逻辑可能驻留的地方,这里有4个按钮,其中2个显示消息框,但按钮1将您返回LoginPage,按钮2将使用不同的数据重新加载同一页面(即,将用户发送到下一个问题)

public partial class _4OptionQuizPage : UserControl, INotifyPropertyChanged
    {
        public ICommand Option1Clicked { get; }
        public ICommand Option2Clicked { get; }
        public ICommand Option3Clicked { get; }
        public ICommand Option4Clicked { get; }
        private string myQuestion;
        public event PropertyChangedEventHandler PropertyChanged;
        public string Question
        {
            get { return myQuestion; }
            set 
            {
                myQuestion = value;
                NotifyPropertyChanged();
            }
        }
        public _4OptionQuizPage() : this($"Question Loaded At {DateTime.Now}, this can be anything.")
        {
        }
        public _4OptionQuizPage(string theCustomData)
        {
            InitializeComponent();
            Question = theCustomData; 
            this.DataContext = this;
            this.Option1Clicked = new SimpleCommand(OnOption1Clicked);
            this.Option2Clicked = new SimpleCommand(OnOption2Clicked);
            this.Option3Clicked = new SimpleCommand(OnOption3Clicked);
            this.Option4Clicked = new SimpleCommand(OnOption4Clicked);
        }
        private void OnOption4Clicked()
        {
            MessageBox.Show("Option 4 selected, Store the results");
        }
        private void OnOption3Clicked()
        {
            MessageBox.Show("Option 3 selected, Store the results");
        }
        private void OnOption1Clicked()
        {
            NavigationService.Instance.LoadView<LoginPage>();
        }
        private void OnOption2Clicked()
        {
            NavigationService.Instance.LoadViewWithCustomData<LoginPage>(new _4OptionQuizPage("A custom question to emulate custom data"));
        }
        private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }

最后,您的MainWindow将注册shell并将用户发送到LoginPage,并且它的XAML文件中不应包含任何内容

public partial class MainWindow : Window, INotifyPropertyChanged
{
    private object myShell;

    public object TheShell
    {
        get { return myShell; }

        set 
        { 
            myShell = value;
            this.NotifyPropertyChanged();
        }
    }

    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = this;
        NavigationService.Instance.RegisterShell(this);
        NavigationService.Instance.LoadView<LoginPage>();
    }

    public event PropertyChangedEventHandler PropertyChanged;

    private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

}

MainWindow.xaml应该是空的,本质上是一个用于其他所有内容的shell。

<Window x:Class="Navigation.MainWindow"
        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:local="clr-namespace:Navigation"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800" Content="{Binding TheShell}">
</Window>
bxfogqkk

bxfogqkk3#

这个例子演示了两种导航方法。通常很有用,因为你说要从登录开始,但在用户登录之前不显示任何菜单等。然后,一旦他们登录,你想要某种菜单或视图列表,他们可以导航到这保持静态。
我的主窗口纯粹是一个 shell 来包含一切。
其标记为:

<Window ......
    Title="{Binding Title}" 
    Content="{Binding}"
    />

此示例首先使用视图模型进行所有导航。视图模型被模板化到UI中。
后面的代码中还有更多内容。

public partial class LoginNavigationWindow : Window
{
    public Type ParentViewModel
    {
        get { return (Type)GetValue(ParentViewModelProperty); }
        set { SetValue(ParentViewModelProperty, value); }
    }

    public static readonly DependencyProperty ParentViewModelProperty =
        DependencyProperty.Register(name: "ParentViewModel",
        propertyType: typeof(Type),
        ownerType: typeof(LoginNavigationWindow),
        typeMetadata: new FrameworkPropertyMetadata(
            defaultValue: null,
        propertyChangedCallback: new PropertyChangedCallback(ParentViewModelChanged)
      ));
    private static void ParentViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var vm = Activator.CreateInstance((Type)e.NewValue);
        ((Window)d).DataContext = vm;
        Task.Run(((IInitiatedViewModel)vm).Initiate);
    }

    public LoginNavigationWindow()
    {
        InitializeComponent();
        WeakReferenceMessenger.Default.Register<ParentNavigate>(this, (r, pn) =>
        {
            this.SetValue(LoginNavigationWindow.ParentViewModelProperty, pn.ParentViewModelType);
        });
    }

messenger注册将使用dependency属性切换出窗口的datacontext。消息只是一个类,它有一个属性来传递Type

public class ParentNavigate
{
    public Type ParentViewModelType { get; set; }
}

回调ParentViewModelChanged接受一个类型,示例化它并在窗口上设置datacontext。
通常,您对保留窗口或父级视图的状态不感兴趣。您已经登录。如果您想再次登录,则需要重新开始并输入用户名和密码。
入口点有点不寻常,因为我处理应用程序启动并依赖于依赖属性回调。

private void Application_Startup(object sender, StartupEventArgs e)
    {
        var mw = new LoginNavigationWindow();
        mw.Show();
        mw.SetValue(LoginNavigationWindow.ParentViewModelProperty, typeof(LoginViewModel));
    }

而不是一个主窗口充满菜单等,我当然什么也没有得到。
我有一个LoginUC是你在启动时看到的第一件事。这只是说明性的。
我们将从用户那里获取输入,并在真实的应用中导航之前进行验证。我们只是对这里的导航感兴趣,因此此版本只有一个导航到MainViewModel的按钮:

<Grid>
    <StackPanel>
        <TextBlock Text="Log in"/>
        <Button Content="Go"
                Command="{Binding LoadMainCommand}"/>
    </StackPanel>
</Grid>
</UserControl>

我的LoginViewModel有一个命令、标题和任务。

public partial class LoginViewModel : BaseParentViewModel
{
    [RelayCommand]
    private async Task LoadMain()
    {
        var pn = new ParentNavigate{ ParentViewModelType = typeof(MainViewModel) };
        WeakReferenceMessenger.Default.Send(pn);
    }
    public LoginViewModel()
    {
        Title = "Please Log In first";
    }

    public override async Task Initiate()
    {
        // Get any data for login here
    }
}

基础父视图模型

public partial class BaseParentViewModel : ObservableObject, IInitiatedViewModel
{
    [ObservableProperty]
    private string title = string.Empty;

    virtual public async Task Initiate() { }
}

接口

public interface IInitiatedViewModel
{
    Task Initiate();
}

这个接口的目的是给予我们一个通用的方法让任何视图模型获取它所需要的数据。通过设置datacontext,然后启动一个后台线程来获取数据,视图将快速出现,然后填充它所需要的任何数据。如果获取数据需要一段时间,那么至少视图是“向上”的,并且在任务继续工作的同时快速可见。
在一个更完整的例子中,我们会在一个基础视图模型中设置IsBusy,它会从true开始,然后被更改为false,这将在视图中驱动一个“throbber”或busing指示器。
资源字典使用以下数据类型将视图模型数据模板与用户控件相关联:

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:local="clr-namespace:LoginNavigation"
                    >
    <DataTemplate DataType="{x:Type local:MainViewModel}">
        <local:MainUC/>
    </DataTemplate>
    <DataTemplate DataType="{x:Type local:LoginViewModel}">
        <local:LoginUC/>
    </DataTemplate>
    <DataTemplate DataType="{x:Type local:SubjectsViewModel}">
        <local:SubjectsView/>
    </DataTemplate>
    <DataTemplate DataType="{x:Type local:ResultViewModel}">
        <local:ResultView/>
    </DataTemplate>
</ResourceDictionary>

合并到app.xaml中

<Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                 <ResourceDictionary  Source="/Resources/ViewDataTemplates.xaml"/>
             </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

登录后,窗口的全部内容将被替换。datacontext从LoginViewModel更改为MainViewModel,然后模板化到MainUC:

public partial class MainViewModel : BaseParentViewModel
{

    [ObservableProperty]
    private object currentChildViewModel;

    [ObservableProperty]
    private List<ChildViewModel> childViewModelList;

    [RelayCommand]
    private async Task ChildNavigation(ChildViewModel cvm)
    {
        if (cvm.Instance == null)
        {
            cvm.Instance = Activator.CreateInstance(cvm.ViewModelType);
            if (cvm.Instance is IInitiatedViewModel)
            {
                Task.Run(((IInitiatedViewModel)cvm.Instance).Initiate);
            }
        }
        CurrentChildViewModel = cvm.Instance;
    }

    public override async Task Initiate()
    {
        ChildViewModelList = new List<ChildViewModel>()
        {
            new ChildViewModel{ DisplayName="Subjects", ViewModelType= typeof(SubjectsViewModel) },
            new ChildViewModel{ DisplayName="Results", ViewModelType= typeof(ResultViewModel) }
        };
    }
    public MainViewModel()
    {
        Title = "Quiz";
    }
}

当然,您可能希望拥有更多视图,并选择一个视图进行初始显示,该视图将在Initiate中设置。
主UC:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="100"/>
        <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <ListBox ItemsSource="{Binding ChildViewModelList}"
             HorizontalContentAlignment="Stretch">
        <ListBox.ItemTemplate>
            <DataTemplate>
                <Button Content="{Binding DisplayName}"
                        Command="{Binding DataContext.ChildNavigationCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
                        CommandParameter="{Binding}"/>
            </DataTemplate>
        </ListBox.ItemTemplate>
    </ListBox>
    <ContentPresenter Content="{Binding CurrentChildViewModel}"
                      Grid.Column="1"/>
</Grid>
</UserControl>

在视图中,你会在左列中看到一个按钮列表,它允许在右列中导航。当然,保留MainUC。
它可以是一个菜单或者一个选项卡控件,而不是一个列表框。
单击按钮调用MainViewModel中的命令,并将ChildViewModel的示例作为参数传递。
然后使用它示例化视图模型、设置CurrentChildViewmodel并缓存示例。
当然,CurrentChildViewmodel本身将被模板化到MainUC内的用户控件中。

public partial class ChildViewModel : ObservableObject
{
    public string DisplayName { get; set; }

    public Type ViewModelType { get; set; }

    public object Instance { get; set; }
}

这是一个相当简单的方法,在真实的世界中,你可能需要依赖注入、工厂等等,但这已经是解决堆栈溢出问题的相当多的代码了。
剩下的视图模型和视图只是简单的实现来证明它是可行的

public partial class SubjectsViewModel : ObservableObject, IInitiatedViewModel
{
    public async Task Initiate()
    {
        // Get any data for Subjects here
    }
}

以及

<Grid>
        <TextBlock Text="Subjects"/>
    </Grid>
</UserControl>
nuypyhwy

nuypyhwy4#

允许视图模型参与页面导航的方法有很多种。
通常,参与导航的每个类都必须能够访问您的导航API。
例如,您可以将导航逻辑移动到专用类NavigationService,并提供对每个类的共享引用,这些类应该能够导航到不同的视图。
或者(也是推荐的),您可以使用在MainWindow上处理的路由命令,然后MainWindow将命令委托给MainViewModel
在这个场景中,每个按钮都必须以CommandParameter的形式传递目的地,这个解决方案允许特定的视图模型不直接参与导航,你不需要用导航细节污染你的视图模型类。
以下示例显示如何使用RoutedCommandQuizView导航到ResultView

主视图模型.cs

MainViewModel是唯一知道如何导航和了解相关细节的视图模型类。
这在保持视图模型类的实现简单的同时实现了可扩展性。
通常,要启用数据验证,请让视图模型实现INotifyDataErrorInfo
然后,您可以在允许离开页面之前查询INotifyDataErrorInfo.HasErrors属性。

class MainViewModel : ObservableObject
{
  public object CurrentView { get; set; }
  private Dictionary<Type, INotifyPropertyChanged> ViewModelMap { get; }

  public MainViewModel()
  {
    this.ViewModelMap = new Dictionary<Type, INotifyPropertyChanged>
    {
      { typeof(QuizVm),  new QuizVm() },
      { typeof(ResultVm),  new ResultVm() },
    };
  }

  // Check if destination type is valid.
  // In case the navigation source implements INotifyDataErrorInfo,
  // check if the source is in a valid state (INotifyDataErrorInfo.HasEWrrors returns 'false').
  // This method is called by the view. It will delegate its ICommand.CanExecute to this method
  // If this method returns 'false' the command source e.g. Button will be disabled.
  public bool CanNavigate(Type navigationSourceType, Type navigationDestinationType)
    => CanNavigateAwayFrom(navigationSourceType) 
      && CanNavigateTo(navigationDestinationType);

  private bool CanNavigateAwayFrom(Type navigationSourceType) 
    => this.ViewModelMap.TryGetValue(navigationSourceType, out INotifyPropertyChanged viewModel)
      && viewModel is INotifyDataErrorInfo notifyDataErrorInfo
        ? !notifyDataErrorInfo.HasErrors
        : true;

  private bool CanNavigateTo(Type navigationDestinationType)
    => this.ViewModelMap.ContainsKey(navigationDestinationType);

  // This method is called by the view. It will delegate its ICommand.Execute to this method
  public void NavigateTo(Type destinationType)
  {
    if (this.ViewModelMap.TryGetValue(destinationType, out INotifyPropertyChanged viewModel))
    {
      this.CurrentView = viewModel;
    }
  }
}

主窗口.xaml.cs

partial class MainWindow : Window
{
  public static RoutedCommand NavigateCommand { get; } = new RoutedUICommand(
    "Navigate to view command", 
    nameof(NavigateCommand), 
    typeof(MainWindow));

  private MainViewModel MainViewModel { get; }

  public MainWindow()
  {   
    InitializeComponent();

    this.MainViewModel = new MainViewModel();
    this.DataContext = this.MainViewModel;

    var navigateCommandBinding = new CommandBinding(MainWindow.NavigateCommand, ExecuteNavigateCommand, CanExecuteNavigateCommand);
    this.CommandBindings.Add(navigateCommandBinding);
  }

  private void CanExecuteNavigateCommand(object sender, CanExecuteRoutedEventArgs e)
  {
    if (e.Source is not FrameworkElement commandSource)
    {
      return;
    }

    Type navigationSourceType = commandSource.DataContext.GetType();
    Type navigationDestinationType = (Type)e.Parameter;
    e.CanExecute = this.MainViewModel.CanNavigate(navigationSourceType, navigationDestinationType);
  }

  private void ExecuteNavigateCommand(object sender, ExecutedRoutedEventArgs e)
  {
    var destinationViewModelType = (Type)e.Parameter;
    this.MainViewModel.NavigateTo(destinationViewModelType);
  }
}

主窗口.xaml

要实际呈现视图(例如自定义控件),您需要定义一个隐式DataTemplate(不带x:Key指令),它将关联的视图模型类定义为DataType。然后ContentControl将自动选择与ContentControl.Content属性值类型匹配的正确视图。

<Window>
  <Window.Resources>
    <DataTemplate DataType="{x:Type local:QuizVM}">
      <QuizView />
    </DataTemplate>

    <DataTemplate DataType="{x:Type local:ResultVM}">
      <ResultView />
    </DataTemplate>
  </Window.Resources>

  <ContentControl Content="{Binding CurrentView}" />
</Window>

如果一个视图需要导航,它必须使用静态路由命令(在MainWindow中定义和处理),并将目标视图模型的Type作为CommandParameter传递。
这样,导航就不会污染视图模型,并停留在视图中。

测验视图.xaml

<QuizView>
  <Button Content="Next"
          Command="{x:Static local:MainWindow.NextPageCommand}"
          CommandParameter="{x:Type local:ResultVM}"/>
</QuizView>

结果视图.xaml

<ResultView>
  <Button Content="Back"
          Command="{x:Static local:MainWindow.NextPageCommand}"
          CommandParameter="{x:Type local:QuizVM}"/>
</ResultView>

因为视图模型类通常不直接参与导航,
它们不必实现任何相关命令或依赖于任何导航服务。
导航完全由MainWindow和它的MainViewModel控制。
对于可选的数据验证,让他们实现INotifyDataErrorInfo

虚拟机测验.cs

class QuizVM : INotifyPropertyChnaged, INotifyDataErrorInfo
{
}

结果虚拟机.cs

class ResultVM : INotifyPropertyChnaged, INotifyDataErrorInfo
{
}

相关问题