65.9K
CodeProject 正在变化。 阅读更多。
Home

Magellan:WPF 的 MVC 驱动导航框架

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (70投票s)

2010 年 9 月 7 日

CPOL

15分钟阅读

viewsIcon

126628

downloadIcon

1363

Magellan 简介,一个开源的 WPF 导航框架。

引言

当您在 Visual Studio 中创建一个新的 WPF 项目时,通常会看到一个空白画布;一个最小化的MainWindow.xamlApp.xaml,以及一些空的后台代码文件。作为应用程序开发人员,您需要决定如何构建您的项目,将 UI 代码放在哪里,将业务代码放在哪里,以及如何从一个视图导航到下一个视图。您需要自己找到一种健壮、可维护、可扩展且可测试的方法来做到这一点。简而言之,WPF 没有开箱即用的 “成功陷阱”供您陷入。

Magellan 的目标是为 **WPF 应用程序创建“成功陷阱”**。本文旨在描述这个陷阱是什么样子的,它是如何工作的,以及您为什么会想“掉进去”。

Magellan: falling into the pit of success

必备组件

要学习本教程,您需要拥有 **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 项目模板自带的示例应用程序。在开始之前,您需要安装该模板。

  1. 启动一个新实例的 Visual Studio 2010
  2. 点击 **工具 -> 扩展管理器...**
  3. 转到 **在线库** 页面,搜索 **“magellan”**
  4. 点击 **下载** 按钮安装模板

模板安装完成后,您就可以创建示例应用程序了。

  1. 点击 **文件 -> 新建 -> 项目...**
  2. 在 **已安装模板**下,展开 **Visual C# -> Magellan**
  3. 创建一个新的 **Magellan MVC 项目**

为了保持您的系统清洁,Visual Studio 2010 扩展仅部署项目模板,但不部署 Magellan 运行时二进制文件。您需要手动下载它并添加引用。

  1. 访问 Magellan 的 **下载页面** 并下载最新的二进制文件 ZIP
  2. 将 ZIP 文件解压到已知的位置 - 我喜欢将其放在源代码控制树中的“lib”文件夹下
  3. 在 Visual Studio 的解决方案资源管理器中右键单击项目,然后点击 **添加引用...**
  4. 转到 **浏览** 选项卡
  5. 浏览到您解压的 *Magellan.dll* 文件并添加引用

此时,您应该能够按 F5 运行应用程序了。花几分钟时间试用一下。我等着...

探索示例应用程序

现在您已经有机会运行应用程序了,让我们一起探索一下。请注意,这个示例只是我如何构建 Magellan 项目的一个例子,但对我来说效果很好。

项目结构

在 ASP.NET MVC 中,您通常会有顶级的文件夹,如 *Models*、*Views* 和 *Controllers*。您可以在 Magellan 中做同样的事情,但我个人认为这样不太容易扩展 - 一旦我有了 10 个控制器,我就会不断地在解决方案资源管理器中上下滚动。我在我的博客上 更详细地解释了这一点。

示例应用程序围绕“功能”的概念组织,每个控制器一个文件夹。在控制器所在的文件夹内,我将为视图、模型、服务代理和其他类型创建子文件夹。我认为这效果更好,因为很可能,如果我正在编写 TaxController 中的代码,我更有可能编辑与税务相关的视图或视图模型,而不是突然想编辑 HomeController

A screenshot of the Visual Studio solution explorer

您会注意到这种结构的一个特点——它清楚地表明每个 **控制器**都有多个视图,每个 **视图**都有一个对应的 **视图模型**。

控制器

本示例使用 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());
    }
}

编写控制器有几个规则:

PageDialog 是来自 Controller 基类的两个方法,它们创建的 ActionResult 知道如何定位和渲染页面或对话框。

传递给每个方法的字符串由 Magellan 用作查找视图的名称。技术上讲,与 ASP.NET MVC 一样,您可以省略它,Magellan 会默认使用操作的名称。Magellan 会尝试查找视图的几种不同变体 - 例如,而不是 IndexView.xaml,您可以称之为 Index.xamlIndexPage.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 工作,您可能会认出 PageStackPanel 元素,但 LayoutZoneFormField 元素将是新的。这些是自定义 Magellan 控件,旨在帮助您用更少的 XAML 创建更一致的用户体验。

Layouts 是 Magellan 相当于 ASP.NET 母版页。它们非常简单,但对于那些经常重复出现的 UI 需求很有用。您可以在项目文档的 Layouts 部分中了解更多关于它们的信息。

表单和字段更高级一些。它们负责创建数据录入字段所需的标准 LabelTextBox 等输入控件,并将它们以标准方式定位。字段可以使用 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,它生成知道如何执行导航请求的 NavigatorNavigatorFactory 需要路由来匹配导航请求,因此它接受任何派生自 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 的区域。我稍后会回来讨论组件的处置和作用域管理。

操作过滤器

让我们暂时回到 TaxControllerEnterDetails 操作。

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

© . All rights reserved.