Magellan:WPF 的 MVC 驱动导航框架






4.92/5 (70投票s)
Magellan 简介,一个开源的 WPF 导航框架。
引言
当您在 Visual Studio 中创建一个新的 WPF 项目时,通常会看到一个空白画布;一个最小化的MainWindow.xaml和App.xaml,以及一些空的后台代码文件。作为应用程序开发人员,您需要决定如何构建您的项目,将 UI 代码放在哪里,将业务代码放在哪里,以及如何从一个视图导航到下一个视图。您需要自己找到一种健壮、可维护、可扩展且可测试的方法来做到这一点。简而言之,WPF 没有开箱即用的 “成功陷阱”供您陷入。
Magellan 的目标是为 **WPF 应用程序创建“成功陷阱”**。本文旨在描述这个陷阱是什么样子的,它是如何工作的,以及您为什么会想“掉进去”。
必备组件
要学习本教程,您需要拥有 **Visual Studio 2010**,因为 Magellan 的当前版本仅支持 .NET 4.0。核心 Magellan 代码应该可以在 .NET 3.5 下编译,如果您对 3.5 版本感兴趣,请在 Magellan-friends 讨论组上留下反馈。
什么是 Magellan?
Magellan 是一个 WPF 的开源导航框架。它深受 ASP.NET MVC 的启发,对于任何使用过 ASP.NET MVC 的人来说,它的外观和感觉应该都很熟悉。Magellan 的主要功能是:
- 一个 **URI 路由引擎**,允许您将视图映射到 URI
- 一个 **Model-View-Controller** 和 **Model-View-ViewModel** 框架
- **表单**和**母版页**控件,使创建一致的用户体验更加容易
创建示例应用程序
本文将探讨 Magellan 项目模板自带的示例应用程序。在开始之前,您需要安装该模板。
- 启动一个新实例的 Visual Studio 2010
- 点击 **工具 -> 扩展管理器...**
- 转到 **在线库** 页面,搜索 **“magellan”**
- 点击 **下载** 按钮安装模板
模板安装完成后,您就可以创建示例应用程序了。
- 点击 **文件 -> 新建 -> 项目...**
- 在 **已安装模板**下,展开 **Visual C# -> Magellan**
- 创建一个新的 **Magellan MVC 项目**
为了保持您的系统清洁,Visual Studio 2010 扩展仅部署项目模板,但不部署 Magellan 运行时二进制文件。您需要手动下载它并添加引用。
- 访问 Magellan 的 **下载页面** 并下载最新的二进制文件 ZIP
- 将 ZIP 文件解压到已知的位置 - 我喜欢将其放在源代码控制树中的“lib”文件夹下
- 在 Visual Studio 的解决方案资源管理器中右键单击项目,然后点击 **添加引用...**
- 转到 **浏览** 选项卡
- 浏览到您解压的 *Magellan.dll* 文件并添加引用
此时,您应该能够按 F5 运行应用程序了。花几分钟时间试用一下。我等着...
探索示例应用程序
现在您已经有机会运行应用程序了,让我们一起探索一下。请注意,这个示例只是我如何构建 Magellan 项目的一个例子,但对我来说效果很好。
项目结构
在 ASP.NET MVC 中,您通常会有顶级的文件夹,如 *Models*、*Views* 和 *Controllers*。您可以在 Magellan 中做同样的事情,但我个人认为这样不太容易扩展 - 一旦我有了 10 个控制器,我就会不断地在解决方案资源管理器中上下滚动。我在我的博客上 更详细地解释了这一点。
示例应用程序围绕“功能”的概念组织,每个控制器一个文件夹。在控制器所在的文件夹内,我将为视图、模型、服务代理和其他类型创建子文件夹。我认为这效果更好,因为很可能,如果我正在编写 TaxController
中的代码,我更有可能编辑与税务相关的视图或视图模型,而不是突然想编辑 HomeController
。
您会注意到这种结构的一个特点——它清楚地表明每个 **控制器**都有多个视图,每个 **视图**都有一个对应的 **视图模型**。
控制器
本示例使用 Magellan 的 MVC 框架,其设计与 ASP.NET MVC 非常相似。它有两个控制器 - 一个 HomeController
,用于通用屏幕(主页和关于对话框),以及一个 TaxController
,用于引导用户完成税务估算过程。让我们先看看 HomeController
。
public class HomeController : Controller
{
public ActionResult Index()
{
return Page("Index", new IndexViewModel());
}
public ActionResult About()
{
return Dialog("About", new AboutViewModel());
}
}
编写控制器有几个规则:
- 一个控制器有一个或多个公共方法,称为 **操作**
- 操作返回
ActionResult
,它 封装并延迟最终的 UI 执行
Page
和 Dialog
是来自 Controller
基类的两个方法,它们创建的 ActionResult 知道如何定位和渲染页面或对话框。
传递给每个方法的字符串由 Magellan 用作查找视图的名称。技术上讲,与 ASP.NET MVC 一样,您可以省略它,Magellan 会默认使用操作的名称。Magellan 会尝试查找视图的几种不同变体 - 例如,而不是 IndexView.xaml,您可以称之为 Index.xaml 或 IndexPage.xaml。这些只是 约定,您可以覆盖它们。
传递给每个 ActionResult 的第二个(可选)参数是视图模型。在 WPF 中,当视图被渲染时,它将被设置为视图的 DataContext
。通常,这些可能具有您基于控制器获取的某些信息(例如来自搜索 Web 服务的搜索结果)设置的属性。
TaxController
更有趣一些。它显示两个页面 - 一个是用户输入其税务详细信息,另一个是显示税务估算。
public class TaxController : Controller
{
private readonly ITaxEstimatorSelector _estimatorSelector;
public TaxController(ITaxEstimatorSelector estimatorSelector)
{
_estimatorSelector = estimatorSelector;
}
public ActionResult EnterDetails()
{
return Page("EnterDetails", new EnterDetailsViewModel());
}
public ActionResult Submit(TaxPeriod period, decimal grossIncome)
{
var situation = new Situation(grossIncome);
var estimator = _estimatorSelector.Select(period);
var estimate = estimator.Estimate(situation);
return Page("Submit", new SubmitViewModel(estimate));
}
}
EnterDetails
操作只是显示页面,但 Submit
操作要复杂一些。它接受在 EnterDetails
页面上收集的两个输入参数,并将它们委托给 ITaxEstimatorSelector
来生成估算,然后将估算结果呈现给用户。
控制器的作用是协调外部服务和用户界面。它不知道如何渲染税务估算,也不知道如何精确计算。它只知道如何弥合差距。
ITaxEstimatorSelector
是一个 **服务** 的例子,控制器通常会利用不同类型的服务。
- 与 SQL/NoSQL 数据库交互的类
- 执行计算或复杂处理的类
- 与文件系统交互的类
- 调用远程 WCF 服务的类
我们将在 **“连接起来”** 部分以及 **“依赖注入”** 部分中介绍这些服务如何提供给控制器。
视图和视图模型
现在我们已经了解了控制器是如何工作的,您可能可以猜到视图和视图模型是如何工作的。我将挑选几个例子来研究。
您的控制器返回的模型不必派生自任何特殊内容,但如果您这样做,会获得一些特殊行为。
IViewAware
:如果您的视图模型实现了此接口,当 **视图** 绑定到您的 **视图模型** 时,您将收到通知。这对于实现 Model-View-Presenter 或 VM-first 模式非常有用。INavigationAware
:实现此接口可让您访问INavigator
,您可以使用它进行导航。稍后我们将更详细地讨论这一点。
实现这些接口的最简单方法是简单地派生自 Magellan 的 ViewModel
基类,正如 EnterDetailsViewModel
所做的那样。
public class EnterDetailsViewModel : ViewModel
{
public EnterDetailsViewModel()
{
Submit = new RelayCommand(SubmitExecuted);
}
public ICommand Submit { get; private set; }
[Display(Name = "Gross income")]
public decimal GrossIncome { get; set; }
public TaxPeriod Period { get; set; }
private void SubmitExecuted()
{
Navigator.Navigate<TaxController>(x => x.Submit(Period, GrossIncome));
}
}
这个视图模型是 VMs 的典型代表。
- 它公开属性(
GrossIncome
)以使状态可供 UI 使用。 - 为了响应用户界面事件(单击 **提交** 按钮),它使用
ICommand
。
这两个属性是实现 **Model-View-ViewModel** 模式所需的全部。
ICommand
通常需要为每个 UI 命令编写一个自定义类,但 Magellan 借鉴了几乎所有 MVVM 框架中使用的技巧——它提供了一个 RelayCommand
,该命令接受一个委托(在此示例中为 SubmitExecuted
),并为您实现 ICommand
。正如我们稍后将看到的,UI 包含一个 <Button />
,它绑定到 Submit
属性,当单击时,将调用 SubmitExecuted
方法。
在 SubmitExecuted
中,我们通过执行导航请求来提交用户输入的数据。我将在稍后详细讨论这一点,但目前,我们只需说这就是我们从 EnterDetails
导航到 Submit
的方式。大多数 MVVM 框架对这种导航管理的支持不佳,这也是 Magellan 不仅仅是另一个 MVC/MVVM 框架的一个很好的例子。
EnterDetailsView.xaml 是我们的实际视图。它很短。
<Page
x:Class="MyMagellanApp.Features.Tax.Views.EnterDetails.EnterDetailsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="391" d:DesignWidth="728"
Title="Home"
Style="{DynamicResource Page.Normal}"
>
<Layout>
<Zone ZonePlaceHolderName="ContentZone">
<StackPanel Margin="7">
<TextBlock Margin="7" Text="Enter your tax details"
Style="{DynamicResource Text.Heading}" />
<Form>
<Field Margin="7" For="{Binding Path=GrossIncome}" />
<Field Margin="7" For="{Binding Path=Period}"
Description="Select the financial year in which the income was earned." />
<Field Margin="7">
<Button Command="{Binding Path=Submit}"
Content="Submit" HorizontalAlignment="Left" Width="100" />
</Field>
</Form>
</StackPanel>
</Zone>
</Layout>
</Page>
如果您之前进行过 WPF 工作,您可能会认出 Page
和 StackPanel
元素,但 Layout
、Zone
、Form
和 Field
元素将是新的。这些是自定义 Magellan 控件,旨在帮助您用更少的 XAML 创建更一致的用户体验。
Layouts 是 Magellan 相当于 ASP.NET 母版页。它们非常简单,但对于那些经常重复出现的 UI 需求很有用。您可以在项目文档的 Layouts 部分中了解更多关于它们的信息。
表单和字段更高级一些。它们负责创建数据录入字段所需的标准 Label
、TextBox
等输入控件,并将它们以标准方式定位。字段可以使用 For
绑定来反射目标属性并“弄清楚”要渲染什么。例如,如果您绑定到 string
,它将呈现一个 TextBox
。如果您绑定到 enum
,它将生成一个 ComboBox
。您会记得 EnterDetailsViewModel
有一个 GrossIncome
属性,该属性带有 [Display(Name = "Gross income")]
属性 - Field
用它作为 Label
。这些只是约定,项目文档 描述了如何覆盖它们。
总而言之,Magellan 中的视图看起来与任何标准 WPF 项目中的视图完全一样,除了几个额外的控件。没有需要派生的基类,也没有需要使用的附加属性或行为。
连接起来
现在我们有了控制器和视图,解开谜题的最后一块就是将它们连接起来。
Magellan 是一个导航框架,因此它需要某种 **导航器** 对象来接受导航请求。这些导航请求需要映射到某种处理程序,这是 **路由引擎** 的工作。该处理程序抽象了某种 **演示模式** - 在本例中是 MVC - 所以我们也需要配置它。
Magellan 的设置代码通常位于 App.xaml.cs 中。我喜欢重写 OnStartup
方法。
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var controllerFactory = new ControllerFactory();
controllerFactory.Register("Home", () => new HomeController());
controllerFactory.Register(
"Tax", () => new TaxController(TaxSelectorFactory.CreateSelector()));
var routes = new ControllerRouteCatalog(controllerFactory);
routes.MapRoute("{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" });
var factory = new NavigatorFactory("tax", routes);
var mainWindow = new MainWindow(factory);
mainWindow.MainNavigator.Navigate<HomeController>(x => x.Index());
mainWindow.Show();
}
从下往上工作,我们有一个 NavigatorFactory
,它生成知道如何执行导航请求的 Navigator
。NavigatorFactory
需要路由来匹配导航请求,因此它接受任何派生自 IRouteResolver
的对象,而 ControllerRouteCatalog
就是如此。ControllerRouteCatalog
需要一个 IControllerFactory
来查找和实例化控制器,所以我们也配置了它。
默认的控制器工厂使用 lambda 来实例化控制器。我个人倾向于抛弃这个,转而使用 **控制反转** 容器,我们将在依赖注入部分讨论它,但它对于演示很有用。每个控制器都与一个名称注册,按约定,通常是类名减去“Controller”后缀。编写您自己的控制器工厂是 一个相当常见的扩展点。
Magellan 的一个关键概念是 **每个视图都应该可以通过格式良好的 URI 访问**。这种从 URI 到执行控制器或替代处理程序的映射是通过路由引擎完成的。
等等,我们为什么需要 URI?WPF 不是桌面技术吗?
即使 URI 不向用户显示,它们在很多情况下对我都有用。最主要的原因是它们提供了一种松耦合的导航机制 - 页面 A 可以通过 URI 链接到页面 B,而无需任何直接的代码依赖。当然,您还可以将 URI 提供给用户。与其说“点击这里,去那里,点击那个”,不如只给他们发一个 URI,比如 hr://training/forms/courses/DEV123/request,对他们来说只需单击一次。
ControllerRouteCatalog
允许通过使用传递给它的 IControllerFactory
将路由映射到 MVC 控制器。在此示例中,它使用一种约定,即 URL 的第一部分假定为控制器名称,第二部分为操作名称,第三部分为 ID 参数的值。
使用此路由规范,以下 URI 将映射到以下控制器操作方法:
tax:// | HomeController.Index() |
tax://Home | HomeController.Index() |
tax://Customers | CustomersController.Index() |
tax://Customers/List | CustomersController.List() |
tax://Customers/Show/123 | CustomersController.Show(id = 123) |
tax://Customers/Show/123?revision=3 | CustomersController.Show(id = 123, revision = 3) |
那个 tax:// 前缀是从哪里来的?那是传递给 NavigatorFactory
构造函数的 URI 方案参数。如果您从另一个视图进行导航,Magellan 将使用您的路由规范自动生成 URI。
扩展
好了,这就是原样的示例应用程序。它是如何构建简单应用程序的一个很好的例子,但 Magellan 是否仅限于简单应用程序?
在本节中,我们将讨论对更大、更复杂的 WPF 应用程序很重要的几个问题。
依赖注入
当您的应用程序不断增长,并且您遵循良好的面向对象实践时,您最终会发现实例化一个像 MVC 控制器这样的对象需要实例化或定位一个由对象组成的深层图,这些对象协同工作以实现您的目标。为了应对这种复杂性,我们使用 依赖注入或控制反转容器,这是一个非常重要的话题,我无法在这篇文章中充分阐述。
虽然 Magellan 并不*要求* IOC 容器才能工作,但它假设您可能想使用一个,并且它有很多扩展点来使其成为可能。
正如我们在 **“连接起来”** 部分讨论的那样,ControllerRouteCatalog
(它将路由发送给 MVC 控制器处理)需要一个 IControllerFactory
。它是一个简单的接口,看起来与 ASP.NET MVC 中的同名版本相似。
public interface IControllerFactory
{
IController CreateController(ResolvedNavigationRequest request, string controllerName);
}
使用像 Autofac 这样的 IOC 容器,我们可以实现这个接口。最简单的方法可能是:
public class AutofacControllerFactory : IControllerFactory
{
private readonly IContainer _container;
public AutofacControllerFactory(IContainer container)
{
_container = container;
}
public IController CreateController(ResolvedNavigationRequest request,
string controllerName)
{
var controller = _container.ResolveNamed<IController>(controllerName);
return new ControllerFactoryResult(controller);
}
}
在这种情况下,我们委托 Autofac IContainer
实现来解析我们的控制器。然后,我们的连接代码可以是:
var builder = new ContainerBuilder();
builder.RegisterType<HomeController>().Named<IController>("Home");
builder.RegisterType<TaxController>().Named<IController>("Tax");
// Register other types...
var container = builder.Build();
var routes = new ControllerRouteCatalog(new AutofacControllerFactory(container));
现在,当 Magellan 收到对 **Tax** 控制器的导航请求时,它将委托 Autofac 来解析它,使用名称作为容器中的键。Autofac 将自动查看 TaxController
的依赖项,并确定如何解析它们,最终为您构建一个良好的依赖关系图。
这是 IOC 容器的一个相对简单的用法,但它可能涵盖了您在 Magellan 应用程序中使用 IOC 的 90% 的场景。Magellan 文档列出了其他 可以扩展以支持 IOC 的区域。我稍后会回来讨论组件的处置和作用域管理。
操作过滤器
让我们暂时回到 TaxController
的 EnterDetails
操作。
public ActionResult EnterDetails()
{
return Page("EnterDetails", new EnterDetailsViewModel());
}
假设我们有一个新需求:如果用户不是高级用户,他们应该被重定向到另一个页面。我们可以修改该操作,使其读取:
public ActionResult EnterDetails()
{
if (!Security.HasPermission("Power User"))
{
return Redirect(new { controller = "Security", action = "NoPermission" });
}
return Page("EnterDetails", new EnterDetailsViewModel());
}
这种代码是 **横切关注点** 的一个很好的例子。几乎每个操作都可能有不同的安全限制集,将相同的代码复制粘贴到每个操作中会很繁琐。
Magellan 通过实现 **操作过滤器** 的概念来借鉴 ASP.NET MVC 处理此问题的方法。操作过滤器以声明方式应用于您的操作,类似于这样:
[RequirePermission("Power User")]
public ActionResult EnterDetails()
{
return Page("EnterDetails", new EnterDetailsViewModel());
}
我们不仅减少了每个操作中的代码量,而且还转向了更具声明性的模型。RequirePermission
属性的实现将是:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class RequirePermissionAttribute : Attribute, IActionFilter
{
private string _requiredPermission;
public RequirePermissionAttribute(string requiredPermission)
{
_requiredPermission = requiredPermission;
}
public void OnActionExecuting(ActionExecutingContext context)
{
if (!Security.HasPermission(_requiredPermission))
{
context.OverrideResult = new RedirectResult(
new { controller = "Security", action = "NoPermission" });
}
}
}
与 ASP.NET MVC 一样,操作过滤器是处理这些横切关注点的一种非常强大的方法。以下是您可能决定使用操作过滤器执行的其他一些操作:
- 确保实现
IDataErrorInfo
的任何操作参数都有效;如果无效,则取消导航请求。 - 日志记录和跟踪。
- 将操作包装在数据库事务或 Raven DB/NHibernate 会话中。
- 异常处理策略。
- 缓存和重用页面。
后台线程
MVC 中的控制器通常负责导航和协调外部服务。有时,调用这些服务可能会很慢,而我们不想在发生时冻结 UI。在其他 WPF 框架中,在后台和前台线程之间切换会变得棘手,并且会使测试变得困难。
Magellan 提供了 **自动在后台线程上执行控制器操作** 的能力。当操作完成并且 UI 已准备好渲染视图时,Magellan 会自动将其调度回 UI 线程。这意味着控制器完全不知道它正在后台线程上执行。
例如,假设我们的税务计算花费了很长时间。
public ActionResult Submit(TaxPeriod period, decimal grossIncome)
{
Thread.Sleep(10000);
var situation = new Situation(grossIncome);
var estimator = _estimatorSelector.Select(period);
var estimate = estimator.Estimate(situation);
return Page("Submit", new SubmitViewModel(estimate));
}
我们可以通过 几种不同的方式在 App.xaml.cs 中配置 Magellan 对异步控制器的支持。如果您使用的是开箱即用的 ControllerFactory
,您可以将其切换到同样开箱即用的 AsyncControllerFactory
。
var controllerFactory = new AsyncControllerFactory();
controllerFactory.Register("Home", () => new HomeController());
controllerFactory.Register("Tax",
() => new TaxController(TaxSelectorFactory.CreateSelector()));
如果您正在使用 IControllerFactory
的自定义实现,您只需要设置您解析的任何控制器的 ActionInvoker
属性(这就是 AsyncControllerFactory
所做的)。
public IController CreateController(ResolvedNavigationRequest request,
string controllerName)
{
var controller = _container.ResolveNamed<IController>(controllerName);
if (controller is ControllerBase)
{
((ControllerBase)controller).ActionInvoker = new AsyncActionInvoker();
}
return new ControllerFactoryResult(controller);
}
如果现在运行该应用程序,您会发现我们在 Submit
操作中设置的 10 秒延迟确实需要 10 秒,但 UI 仍然响应。但是,没有任何用户反馈表明有事情正在发生。
为了通知用户我们正在忙,我们可以实现 INavigationProgressListener
。在 MainWindow.xaml.cs 中,我们可以将其更改为:
public partial class MainWindow : Window, INavigationProgressListener
{
public MainWindow(INavigatorFactory navigation)
{
InitializeComponent();
navigation.ProgressListeners.Add(this);
MainNavigator = navigation.CreateNavigator(MainFrame);
}
public INavigator MainNavigator { get; set; }
public void UpdateProgress(NavigationEvent navigationEvent)
{
Dispatcher.Invoke(new Action(
delegate
{
if (navigationEvent is BeginRequestNavigationEvent)
{
Cursor = Cursors.Wait;
}
if (navigationEvent is CompleteNavigationEvent)
{
Cursor = Cursors.Arrow;
}
}));
}
}
现在,当您单击 **提交** 时,鼠标光标将在操作完成前的 10 秒内切换为忙碌光标。您也可以不更改光标,而是显示一个动画图标,禁用屏幕,或以其他方式提供反馈,但扩展点应该是相同的。
摘要
Magellan 借鉴了成功的 ASP.NET MVC 框架的许多思想,并结合了 MVVM 和其他 UI 模式的丰富组合,创建了一个帮助您 **“掉入成功陷阱”** 的框架。Magellan 以导航为中心,并鼓励使用 Model-View-Controller 模式来管理外部服务,以及 MVVM 模式来管理视图的状态。它文档齐全且经过测试,并已在生产应用程序中使用。
本文广泛介绍了 Magellan 的多项功能,以帮助您判断它是否对您有用。接下来的步骤是查看 项目文档,并探索随 源代码下载一起提供的示例应用程序。如果您对 Magellan 有任何疑问,请发送电子邮件至 magellan-friends Google Group。