使用 MVVMC 导航的 WPF 中的向导应用程序






3.86/5 (6投票s)
演示了如何使用 Model-View-ViewModel-Controller 导航框架创建两个向导式应用程序
引言
我知道你在想什么——2018 年底,世界需要另一个 WPF 向导应用程序的实现。好吧,本文中提出的解决方案将展示一种构建多屏应用程序的新方法,它具有一些独特的优势。
所解释的实现使用了 MVVMC 模式。这与 MVC 有些相似,并为您的应用程序添加了控制器。我们将看到如何使用 Wpf.MVVMC 库来构建两个向导应用程序。第一个是简单的 4 步向导。第二个具有一些高级功能,如果使用简单的 MVVM 或其他导航框架,这将是一项繁重的工作。您将看到 MVVMC 开发速度快,简单且灵活多变。
完全公开:我是 Wpf.MVVMC
的创建者。
基本向导
高级向导
基本向导教程
首先创建一个常规的 WPF 项目,然后将 Wpf.MVVMC NuGet 包添加到您的项目中。无需进行任何初始化或引导。
我们将遍历所有文件,但为了让您对它的工作原理有一个大致的了解,解决方案资源管理器将如下所示
如您所见,向导的每个步骤都有一个“View
”和一个“ViewModel
”,除了第一个。还有一个“WizardController
”文件,我们很快就会讲到。首先,让我们从 MainWindow.xaml 开始
MainWindow.xaml
<Window x:Class="WizardAppMvvmc.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:mvvmc="clr-namespace:MVVMC;assembly=MVVMC"
Title="Wizard with MVVMC" Height="450" Width="800"
Background="#FFDDDDDD">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="180"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="50"/>
</Grid.RowDefinitions>
<Border Background="LightSkyBlue"></Border>
<mvvmc:Region Grid.Column="1" ControllerID="Wizard"></mvvmc:Region>
<Border Grid.Row="1" Grid.ColumnSpan="2" Background="LightGray">
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal">
<Button Command="{mvvmc:GoBackCommand ControllerID=Wizard}">Back</Button>
<Button Command="{mvvmc:NavigateCommand Action=Next, ControllerID=Wizard}">
Next</Button>
</StackPanel>
</Border>
</Grid>
</Window>
解释
mvvmc:Region
是屏幕上的一个区域,由Controller
控制。它本质上是一个普通的ContentControl
,其内容会根据您的导航而变化。ControllerID=Wizard
属性决定了它与哪个Controller
相关联。它是基于约定的,因此您必须有一个名为WizardController
的类,它派生自MVVMC.Controller
(否则将抛出异常)。- 第一个
Button
的Command
是mvvmc:GoBackCommand
。使用Wpf.MVVMC
,每次导航都会保存历史记录,您可以自动返回上一步(或前进)。 - 第二个按钮的
Command
是mvvmc:NavigateCommand
,其属性为Action=Next
。这意味着在按钮点击时,WizardController
中的Next
方法将被调用。在该方法中,我们可以(但不必须)执行导航。
WizardController.cs
public class Model
{
public string Position { get; set; }
public int YearsOfExperience { get; set; }
public string Notes { get; set; }
}
public class WizardController : Controller
{
private Model _model;
public override void Initial()
{
FirstStep();
}
private void FirstStep()
{
_model = new Model();
ExecuteNavigation();
}
private void SecondStep()
{
ExecuteNavigation();
}
private void ThirdStep()
{
ExecuteNavigation();
}
private void FourthStep()
{
ExecuteNavigation(null, new Dictionary<string, object>()
{
{ "Position",_model.Position},
{ "YearsOfExperience",_model.YearsOfExperience.ToString()},
{ "Notes",_model.Notes},
});
}
public void Next()
{
if (this.GetCurrentPageName() + "View" == nameof(FirstStepView))
{
SecondStep();
}
else if (this.GetCurrentViewModel() is SecondStepViewModel secondStepViewModel)
{
_model.Position = GetPosition(secondStepViewModel);
ThirdStep();
}
else if (this.GetCurrentViewModel() is ThirdStepViewModel thirdStepViewModel)
{
_model.YearsOfExperience = thirdStepViewModel.YearsOfExperience;
_model.Notes = thirdStepViewModel.Notes;
FourthStep();
}
else // From fourth step
{
ClearHistory();
FirstStep();
}
}
private string GetPosition(SecondStepViewModel secondStepViewModel)
{
if (secondStepViewModel.IsQAEngineer)
return "QA Engineer";
else if (secondStepViewModel.IsSoftwareEngineer)
return "Software Engineer";
else
return "Team Leader";
}
}
如前所述,Wpf.MVVMC
是基于约定的。所以当我们在 XAML 中写入 <mvvmc:Region ControllerID="Wizard"/>
时,我们必须有一个名为 WizardController
的类,它派生自 MVVMC.Controller
。除此之外,Region
和 Controller
之间的配对是自动的。
解释
Model
是一个用于在步骤之间保存数据的小类。- 在每个
Controller
中,我们必须重写Initial()
,它决定了Region
的初始Content
。您可以在该方法中不做任何事情,在这种情况下Region
将保持为空。我们正在调用FirstStep()
方法 - 请参阅下文。 - 控制器中的每个步骤方法
FirstStep
、SecondStep
、ThirdStep
和FourthStep
都调用了protected
方法ExecuteNavigation
。ExecuteNavigation()
取决于调用方法名。例如,当从“FirstStep()
”调用时,它将导航到“FirstStep
”页面。这意味着它将创建FirstStepView
和FirstStepViewModel
实例,并连接它们进行绑定。您不必拥有ViewModel
。 - 在
FourthStep()
中,我们正在向ExecuteNavigation
传递参数。这就是ViewBag
,您可能从 MVC 中了解它。它的作用方式类似——它允许从 XAML 轻松绑定,您将在后面看到。 Next
方法是从 XAML 自动调用的,使用Command="{mvvmc:NavigateCommand Action=Next, ControllerID=Wizard}"
。它检查我们当前在哪个步骤,然后调用下一个步骤。如您所见,这是在代码中实现的,并且非常灵活。您可以选择跳过步骤、添加步骤、回退一些步骤或不执行任何操作。- 您的
View
、ViewModel
和Controller
应位于同一namespace
中。这样,您可以在同一项目中拥有同名步骤。
至此,代码中最难的部分就结束了。所有 Views
都是一个简单的 UserControl
。所有 ViewModel
都应该派生自 MVVMC.MVVMCViewModel
。在 MVVMC 术语中,一对 View
和 ViewModel
被称为 Page
。让我们看看其中一个页面的代码。
页面示例:SecondStep
View
是一个普通的 UserControl
。SecondStepView.xaml
<UserControl x:Class="WizardAppMvvmc.Wizard.SecondStepView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006">
<StackPanel Margin="50">
<TextBlock FontSize="25" >Second Step - Start recruitment</TextBlock>
<TextBlock FontSize="18" >Who do you want to recruit to your team?</TextBlock>
<RadioButton IsChecked="{Binding IsQAEngineer}">QA Engineer</RadioButton>
<RadioButton IsChecked="{Binding IsSoftwareEngineer}">Software Engineer</RadioButton>
<RadioButton IsChecked="{Binding IsTeamLeader}">Team Leader</RadioButton>
</StackPanel>
</UserControl>
ViewModel
应派生自 MVVMC.MVVMCViewModel
public class SecondStepViewModel : MVVMCViewModel
{
private bool _isQAEngineer;
public bool IsQAEngineer
{
get { return _isQAEngineer; }
set
{
_isQAEngineer = value;
OnPropertyChanged();
}
}
private bool _isSoftwareEngineer;
public bool IsSoftwareEngineer
{
get { return _isSoftwareEngineer; }
set
{
_isSoftwareEngineer = value;
OnPropertyChanged();
}
}
private bool _isTeamLeader;
public bool IsTeamLeader
{
get { return _isTeamLeader; }
set
{
_isTeamLeader = value;
OnPropertyChanged();
}
}
}
如您所见,View
和 ViewModel
与您的常规 View
和 ViewModel
完全相同。这里唯一的例外是 FourthStep
,我们在其中使用了 ViewBag
。代码如下
使用 ViewBag - FourthStep
FourthStepView.xaml:
<UserControl x:Class="WizardAppMvvmc.Wizard.FourthStepView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:mvvmc="clr-namespace:MVVMC;assembly=MVVMC">
<StackPanel >
<TextBlock FontSize="25" >Finished, Recruitment is on the way</TextBlock>
<UniformGrid Columns="2">
<TextBlock>Position:</TextBlock>
<TextBlock Text="{mvvmc:ViewBagBinding Path=Position}"></TextBlock>
<TextBlock>Years of Experience:</TextBlock>
<TextBlock Text="{mvvmc:ViewBagBinding Path=YearsOfExperience}"></TextBlock>
<TextBlock>Notes:</TextBlock>
<TextBlock Text="{mvvmc:ViewBagBinding Path=Notes}"></TextBlock>
</UniformGrid>
</StackPanel>
</UserControl>
FourthStepViewModel.cs:
public class FourthStepViewModel : MVVMCViewModel
{
}
您可以使用 mvvmc:ViewBagBinding
自动绑定到您在 ExecuteNavigation
中的 ViewBag
中传递的值。使用 ViewBag
时,即使它是一个空 ViewModel
,您也必须拥有一个 ViewModel
。
基本向导总结
我想您可以看到 MVVMC 和 MVC 之间的相似之处。Navigation
“请求”发送到 Controller
。Controller
具有内部逻辑,决定要导航到的“Page
”。这实现了 View
/ViewModel
与导航逻辑之间的关注点分离。基于约定的方法也受 MVC 的启发,在我看来,它节省了您否则必须编写的大量样板代码。
所有 Navigation
“请求”都在 View
中,带有 mvvmc:NavigateCommand
。这只是一种方式。ViewModel
也可以通过获取 Controller
对象并调用其方法来启动 navigation
(本文末尾附近有一个示例)。
高级向导教程
“高级
”向导(其实并没有那么高级)展示了 Wpf.MVVMC
的更多功能。以下是一些它简单实现但否则需要大量工作的功能
- 您可以拥有嵌套导航。就像主向导中的子向导一样。
- 导航按钮可以位于“
动态
”内容内部或外部。 - 向导不必有线性步骤“1、2、3...”。相反,它可以根据用户所做的选择,拥有您选择的任何步骤逻辑。
向导的源代码可供下载。我将向您展示其中一些更有趣的部分。让我们从嵌套导航开始。
嵌套区域
在视频中,当点击“软件工程师”时,您将被引导至一个子向导,您可以在其中选择技术(前端)和框架(Angular、React)。这是 SoftwareEngineerView
的代码
<UserControl x:Class="AdvancedWizard.Wizard.SoftwareEngineerView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mvvmc="clr-namespace:MVVMC;assembly=MVVMC">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="100"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Border Background="WhiteSmoke">
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="18">
Software Engineer Recruitment</TextBlock>
</Border>
<mvvmc:Region ControllerID="SoftwareEngineer" Grid.Row="1"></mvvmc:Region>
</Grid>
</UserControl>
SoftwareEngineerView
是“主
”向导中的一个步骤。在其中,您可以拥有更多由另一个 Controller
控制的 Region
。在我们的例子中,是 SoftwareEngineerController
。
您可以从任何 Controller
请求任何导航。在我们的例子中,SoftwareEngineerController
对自身和“main
”Controller 都进行导航。这是 FrontEndView.xaml
<UserControl x:Class="AdvancedWizard.Wizard.SoftwareEngineer.FrontEndView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mvvmc="clr-namespace:MVVMC;assembly=MVVMC">
<StackPanel>
<TextBlock Margin="5">What framework do you like best?</TextBlock>
<Button Margin="5" Command="{mvvmc:NavigateCommand ControllerID=SoftwareEngineer,
Action=Angular1}">Angular 1</Button>
<Button Margin="5" Command="{mvvmc:NavigateCommand ControllerID=Wizard,
Action=Finish}">Angular 2+</Button>
<Button Margin="5" Command="{mvvmc:NavigateCommand ControllerID=Wizard,
Action=Finish}">React</Button>
<Button Margin="5" Command="{mvvmc:NavigateCommand ControllerID=Wizard,
Action=Finish}">Vue.js</Button>
</StackPanel>
</UserControl>
这是 Angular1View.xaml
<UserControl x:Class="AdvancedWizard.Wizard.SoftwareEngineer.Angular1View"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mvvmc="clr-namespace:MVVMC;assembly=MVVMC">
<StackPanel>
<TextBlock Margin="5" FontSize="18">Angular 1, Really?</TextBlock>
<Button Margin="5" Command="{mvvmc:NavigateCommand ControllerID=Wizard,
Action=Finish}">Yes</Button>
<Button Margin="5" Command="{mvvmc:GoBackCommand ControllerID=SoftwareEngineer}">No</Button>
</StackPanel>
</UserControl>
我想展示的另一个例子是从 ViewModel
进行导航。以下是 StartViewModel.cs
using MVVMC;
namespace AdvancedWizard.Wizard
{
public class StartViewModel : MVVMCViewModel<WizardController>
{
private bool _isQA;
public bool IsQA
{
get { return _isQA; }
set
{
_isQA = value;
OnPropertyChanged();
NextCommand.RaiseCanExecuteChanged();
}
}
private bool _isSoftwareEngineer;
public bool IsSoftwareEngineer
{
get { return _isSoftwareEngineer; }
set
{
_isSoftwareEngineer= value;
OnPropertyChanged();
NextCommand.RaiseCanExecuteChanged();
}
}
private bool _isTeamLeader;
public bool IsTeamLeader
{
get { return _isTeamLeader; }
set
{
_isTeamLeader = value;
OnPropertyChanged();
NextCommand.RaiseCanExecuteChanged();
}
}
public ICommand _nextCommand;
public ICommand NextCommand
{
get
{
if (_nextCommand == null)
{
_nextCommand = new DelegateCommand(
() =>
{
var controller = GetExactController();
if (IsQA)
controller.QA();
else if (IsSoftwareEngineer)
controller.SoftwareEngineer();
else
controller.TeamLeader();
},
// can execute
() => IsQA || IsSoftwareEngineer || IsTeamLeader);
}
return _nextCommand;
}
}
}
}
解释
ViewModel
派生自 MVVMCViewModel<WizardController>
。这允许使用 GetExactController()
方法,该方法返回 Controller
实例。从那里,导航就像调用方法一样简单。例如,controller.SoftwareEngineer()
。
摘要
我希望我能让您相信 MVVMC 的强大之处。每当您有一个多屏应用程序时,这都是一个不错的选择。它无疑是一个有主见的库,但通常是最好的,因为它为您提供了结构和更多时间来处理应用程序的功能,而不是导航。