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

Calcium:利用 PRISM 的模块化应用程序工具集 - 第一部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (68投票s)

2009年5月31日

BSD

17分钟阅读

viewsIcon

267529

Calcium 提供了快速构建多功能、复杂的模块化应用程序所需的大部分功能。它包含一系列现成的模块和服务,以及一个可供您的下一个应用程序直接使用的基础设施。

Caclium Logo

目录

引言

Calcium 是一个利用 Composite Application Library 的 WPF 复合应用程序工具集。它提供了快速构建多功能、复杂的模块化应用程序所需的大部分功能。

Calcium 由客户端应用程序和基于服务器的 WCF 服务组成,允许客户端之间进行交互和通信。Calcium 内置了大量现成的模块和服务,以及一个可供您的下一个应用程序直接使用的基础设施。

我已将 Calcium 部署到一个 演示服务器,以便您能看到它的实际运行效果。我的服务器由于防火墙的原因存在一些连接问题,所以您需要下载项目才能看到它的所有功能。

Calcium Overview

图示:Calcium 概述。

以下是 Calcium 的一些主要功能列表:

  • 用于通过同一 API 与客户端或服务器上的用户进行交互的双工消息服务。可以使用服务器上的消息框与用户进行交互!
  • 模块管理器,用于在运行时启用或禁用模块。
  • 一个用户亲和力模块,有助于与其他应用程序用户进行协作。
  • 一个命令服务,用于将 WPF 的 ICommands 与内容接口关联,这些接口仅在活动视图或视图模型实现该接口时才激活。
  • 用于 ToolBarsMenus 的区域适配器。
  • 开箱即用的客户端-服务器日志记录。
  • 包含 Web 浏览器、文本编辑器、输出窗口等众多模块。
  • 带未保存文件指示符的标签式界面(可在模块间重用)。
  • 还有更多!

Calcium Screenshot

图示:Calcium 桌面环境

我们将要覆盖的内容很多。因此,我决定将本文分为三到四篇文章。

  1. Calcium 和模块管理器简介(本文)。
  2. 消息服务、WebBrowser 模块、Output 模块
  3. 文件服务、视图服务、Calcium 品牌重塑
  4. 待定

在本文中,您将学习如何:

  • 使用 Prism 将控件加载到 UI 区域;
  • 定义区域;
  • 创建自定义 Prism IModuleCatalog 以自定义 Prism 如何定位程序集;
  • 自定义模块初始化以处理异常;
  • 创建自定义模块管理器,允许用户选择性地禁用或启用模块;

本文系列中的部分内容不是高级别的,即使是 Prism 新手也能从中受益。而其他内容,例如消息系统,则适合更高级的读者。希望每个人都能从中有所收获。

本文在某些方面是对 Prism 一些领域的介绍。不过,如果您是 Prism 的新手,可能会觉得有些地方信息量过大,我建议您在深入研究 Calcium 之前,先看看一些初学者 Prism 的文章。

背景

构建大型应用程序是一个挑战。随着应用程序规模的增长,团队规模的增长,管理这种增长的难度呈指数级增长。团队成员可能会发现自己越来越碍手碍脚。Prism 提供了使用离散的、称为模块的组件来构建应用程序的方法,这些组件通过事件、命令和服务进行交互。模块化开发的优势不仅仅局限于团队和开发人员的独立性;即使是一个小型团队也可以从这种方法中受益。模块在运行时被发现,并且可以被选择性地启用和禁用。模块化开发可以提高可维护性和可测试性。此外,模块化允许基于功能的许可,其中特定模块组可以关联到例如标准版本,而更大的集合则可以构成专业版本。

虽然 Prism 提供了一些很棒的示例应用程序,但仍然需要一些时间才能上手并创建可用的外壳。并且除了 Prism 之外,肯定还需要更多的基础设施。这就是 Calcium 的用武之地。它是一个示例应用程序,同时也是一个具有实质性基础设施的入门套件,包括一系列服务、实用程序和 UI 组件。

我将 Calcium 推向世界,以便其他人可以在开发自己的复合应用程序时获得先机。

组件和依赖项

本文首先简要介绍客户端和服务器应用程序的构成。Calcium 由一组专用的客户端组件、专用的服务器端组件和共享组件组成。下面的图示虽然有些复杂,但概述了 Calcium 中使用的一些主要类型。

Calcium Component Diagram

图示:Calcium 组件图

Calcium 的 DesktopClient 项目在项目内包含一些必需的模块,而其余的则位于 DesktopClient 项目主体之外。

应用程序启动

应用程序的入口点是 Client.Launcher.App 类。使用启动器项目是为了适应不同的部署场景。当使用例如 ClickOnce 时,这种方法会非常有用,因为需要为不同环境设置不同的部署设置。我们可以通过为每个 ClickOnce 场景(例如,开发/发布)使用一个启动器项目来提供不同的设置。

我们的启动器入口点通过主客户端项目中的 AppStarter 类进行挂钩。正是 AppStarter 显示启动画面、加载资源、运行 Bootstrapper,并显示 Prism 术语中的外壳或主窗口。

Startup sequence diagram

图示:应用程序初始化和启动

引导程序 (Bootstrapper)

Bootstrapper 的任务是初始化应用程序的环境。这包括注册要由依赖注入容器(Unity)解析的类型,为 Prism 提供其所需的 IModuleCatalog 实例,以便它可以加载我们的模块。它还负责创建应用程序的主窗口或 Prism 术语中的外壳

桌面外壳 (DesktopShell)

DesktopShell 是我们桌面 CLR 版 Calcium 的主窗口。Calcium 中的外壳实现了一个名为 IShell 的接口,该接口位于客户端项目中。

Calcium screenshot with browser

图示:DesktopShell 区域

DesktopShell 有四个可能被填充的区域:

  • 工具(左)
  • 工作区(中)
  • 属性(右)
  • 页脚(底)

StandardMenuStandardToolBarTray 控件也托管了多个区域,这些区域可用于添加特定于模块的 MenusMenuItemsToolBars

内容接口方法

在 WPF 中创建多视图应用程序时的一个挑战是,如何在命令目标可能不是当前获得焦点的视图的情况下,正确激活 RoutedCommands。我采取的方法是允许将 ICommands 任意地与特定的内容接口关联,并注册到外壳。这样,我们就可以使用自定义逻辑来评估何时激活命令。

某些任务应该在所有情况下,由应用程序的所有部分以相同的方式执行。这些任务包括保存文件或打印文档等。因此,我们需要一种机制来由一个中心化设施委托执行这些任务。为此,我们使用命令服务。当我们在系列下一篇文章中查看 WebBrowser 模块时,将对此进行更详细的介绍。

模块化

在 Prism 中,我们定义了脚手架,包括外壳 Bootstrapper 等,我们在其中添加模块,模块是应用程序的构建块。

为了实现模块的运行时发现,Prism 使用 IModuleCatalog 的实现,该实现必须在我们的 Bootstrapper 中定义。Calcium 的 CustomModuleCatalog 基于 Prism 的 DirectoryModuleCatalog。然而,一个区别是,我们允许从已加载到 AppDomain 中的程序集中加载模块,例如我们的核心模块:ModuleManager、Communication、Output 和 WebBrowser 模块。这些模块都位于客户端项目中。

模块

此版本的 Calcium 附带了几个模块。

  • ModuleManager
    为用户提供了在应用程序中启用和禁用模块的功能。
  • WebBrowser
    一个简单的 Web 浏览器模块,包含一个在浏览器窗口显示时激活的地址栏。
  • 输出
    显示通过复合事件分派的输出消息。
  • TextEditor
  • 一个类似记事本的模块,演示了内容命令的可关联性。
  • UserAffinity
    一个模块,它使 Calcium 客户端能够感知到其他使用该应用程序的用户。
  • 沟通
    提供双工消息支持,以便服务器可以在 WCF 调用期间直接与用户进行交互。

依赖注入

  • Prism 通过其 Unity 扩展提供了对依赖注入 (DI) 的支持。然而,它也允许将 Unity 替换为其他 DI 容器(如果我们愿意)。与配置应用程序运行时环境的大多数事物一样,Unity 容器的配置在 Bootstrapper 中完成。下面的示例演示了我们如何同时使用 app.config 文件来配置容器以及一些已知的核心服务。
/// <summary>
/// We configure the unity container using the config file as well as imperatavely.
/// </summary>
protected override void ConfigureContainer()
{
    var section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
    section.Containers.Default.Configure(Container);
    /* For resolving types with delegates. */
    Container.AddNewExtension<StaticFactoryExtension>(); 

    /* Composite logging is sent to Clog. */
    Container.RegisterType<ILoggerFacade, CompositeLogAdapter>();
    /* IChannelManager is used to create simplex and duplex WCF channels. */
    Container.RegisterInstance<IChannelManager>(ChannelManagerSingleton.Instance);
    /* By registering the UI thread dispatcher 
     * we are able to invoke controls from anywhere. */
    Container.RegisterInstance<Dispatcher>(Dispatcher.CurrentDispatcher);
    /* Register the module load error strategy 
     * so that if a module doesn't load properly it will be excluded. */
    Container.RegisterType<IModuleLoadErrorStrategy, ModuleLoadErrorStrategy>();
    /* We use a custom initializer so that we can handle load errors. */
    Container.RegisterType<IModuleInitializer, CustomModuleInitializer>();
    /* IViewService will hide and show visual elements 
     * depending on workspace content. */
    Container.RegisterInstance<IViewService>(new ViewService());
    /* Message Service */
    Container.RegisterInstance<IMessageService>(new MessageService());
    /* File Service */
    Container.RegisterInstance<IFileService>(new FileService());

    /* To avoid parameter injection everywhere we expose unity via a singleton. */
    UnitySingleton.Initialize(Container);

    base.ConfigureContainer();
} 

虽然我提供了开箱即用的配置文件容器配置支持,但我自己通常会尽量避免使用配置文件进行容器配置,只有在有明确需求需要额外的灵活性时才会这样做。我这样做是因为包含类型映射的配置文件会成为维护的负担,而且我更喜欢编译时验证我的类型名称是否正确。我建议,如果读者决定主要使用配置文件来构建您的应用程序,请确保为所有必需的类型映射定义了单元测试。在重构或重命名某项内容时,很容易忘记容器配置。

区域适配

Prism 区域回顾

在 Prism 中,视图仅仅是对象,可以使用开箱即用的或自定义的区域适配器放置在称为区域的容器中。区域适配器实现 IRegionAdapter,并解耦了填充区域的方法。在构建基于 Prism 的应用程序时,通常的惯例是用一个附加属性来装饰外壳或自定义控件,该属性定义外壳内的位置作为区域。在下面的示例中,我们看到 RegionName 附加属性如何将 tabControl_Right 声明为具有唯一名称 Properties 的区域。

<TabControl x:Name="tabControl_Right" MinWidth="0"
        HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
        IsSynchronizedWithCurrentItem="True" BorderThickness="0,0,0,0" 
        cal:RegionManager.RegionName="{x:Static Desktop:RegionNames.Properties}"
…
/> 

一旦我们定义了区域,我们就可以从任何地方用视图对象填充它,但最有用的是从我们的模块中。下面的示例摘自 ModuleManagerModule,展示了如何从 Unity 容器中检索 Region Manager,按名称定位 Properties 区域,然后将 ModuleManagerView 的新实例添加到该区域。

public void Initialize()
{
    var regionManager = UnitySingleton.Container.Resolve<IRegionManager>();
    regionManager.Regions[RegionNames.Properties].Add(new ModuleManagerView());
}

区域是 Prism UI 组合基础设施的核心。它们使模块作者能够将内容与 UI 内的已知区域关联起来,而无需了解外壳内的容器,也无需了解区域在外壳内的实际位置。有了这些,我们就能获得灵活性,从而在布局实现发生变化时避免重新工程化模块。

Calcium 区域适配器

现在我们已经牢固地理解了区域的工作原理,让我们来研究 Prism 如何将 UI 元素正确地放入区域的方法。这里的难点在于,添加到一个容器类型不同的区域必须通过不同的方式进行。例如,我们不能像处理 TabControl 那样向 ToolBar 添加项目。

在 Calcium 中,我们有许多自定义的 IRegionAdapters。其中包括 ToolBarTrayRegionAdapterCustomItemsControlRegionAdapter。第一个 ToolBarTrayRegionAdapter 允许我们在模块中创建一个 ToolBar,然后在运行时将其插入到 ToolBarTray 中。第二个 CustomItemsControlRegionAdapter 可用于大多数 ItemsControls,但专门用于 MenuItemsMenus。通过它,我们可以将 MenuMenuItem 定义为一个区域。当我们向区域添加内容时,会看到子 MenusMenuItems

为了向 Prism 提供我们的自定义 IRegionAdapter 实现,我们可以在 Bootstrapper 中重载 ConfigureRegionAdapterMappings,如下面的示例所示。

protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
{
    var mappings = base.ConfigureRegionAdapterMappings() ?? Container.Resolve<RegionAdapterMappings>();
    mappings.RegisterMapping(typeof(ToolBarTray), Container.Resolve<ToolBarTrayRegionAdapter>());
    mappings.RegisterMapping(typeof(Menu), Container.Resolve<CustomItemsControlRegionAdapter>());
    mappings.RegisterMapping(typeof(MenuItem), Container.Resolve<CustomItemsControlRegionAdapter>());
    return mappings;
}

区域适配器不仅让我们能够控制 UI 元素如何放置在区域的宿主容器中,还为我们提供了机会来连接事件处理程序等,以适应任何目的。

Calcium M-V-VM

Prism 在视图的构成方面非常灵活。事实上,它对我们的视图结构并不在意。另一方面,Calcium 在视图构成方面与 Prism 一样是不可知的,但 Calcium 的 IShell 实现对它的视图有一些期望。虽然它们不是强制性的,但它们有助于外壳执行其任务。其中一个期望是,放置在 Prism 区域中的视图应实现 IView 接口,如下所示。

/// <summary>
/// Represents the visual interface that a user interacts with.
/// </summary>
public interface IView 
{
    /// <summary>
    /// Gets the view model for the view. 
    /// The ViewModel is usually the DataContext of the view.
    /// </summary>
    /// <value>The view model.</value>
    IViewModel ViewModel { get; }
}

每个视图都将自己与特定的 IViewModel 实例关联起来。视图的 IViewModel 通常用作其 DataContext

下面的示例显示了 IViewModel 接口。

public interface IViewModel
{
    /// <summary>The header on the host tab.</summary>
    object TabHeader { get; }
}

设计要求视图了解其视图模型,反之则不然。

IView Class Diagram

图示:IViewIViewModel 及其继承者。

总的来说,视图模型通常不需要直接与视图交互,有些人甚至认为它根本不应该交互。这实际上是一个有争议的问题。然而,在某些场景下,从视图模型了解视图可能会很有用,而且如果视图易于为测试而模拟,并且交互完全通过接口进行,那么它可能也并非那么糟糕。因此,我们在 Calcium 中看到,基类 IView 实现(ViewControl)有一个 IViewModel,同样,基类 IViewModelViewModelBase)有一个 IView

ViewControl Class Diagram

图示:ViewControlViewModelBase 类层次结构。

下面的示例显示了完整的 ViewModelBase 类,并展示了在实例化时如何需要一个 IView 实例。

public abstract class ViewModelBase : DependencyObject, 
    IViewModel, INotifyPropertyChanged
{
    readonly IView view;

    protected ViewModelBase(IView view)
    {
        ArgumentValidator.AssertNotNull(view, "view");
        notifier = new PropertyChangedNotifier(this);
        this.view = view;
    }

    public IView View
    {
        get
        {
            return view;
        }
    }

    #region TabHeader Dependency Property

    public static DependencyProperty TabHeaderProperty 
        = DependencyProperty.Register(
            "TabHeader", typeof(object), typeof(ViewModelBase));

    [Description("The text to display on a tab.")]
    [Browsable(true)]
    [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
    public object TabHeader
    {
        get
        {
            return (object)GetValue(TabHeaderProperty);
        }
        set
        {
            SetValue(TabHeaderProperty, value);
        }
    }

    #endregion

    #region Property Changed Notification
    readonly PropertyChangedNotifier notifier;

    public event PropertyChangedEventHandler PropertyChanged
    {
        add
        {
            notifier.PropertyChanged += value;
        }
        remove
        {
            notifier.PropertyChanged -= value;
        }
    }

    protected void OnPropertyChanged(string propertyName)
    {
        notifier.OnPropertyChanged(propertyName);
    }
    #endregion
}

提供的 TabItemDictionary ResourceDictionary 使用视图 ViewModel 中的 TabHeader 属性在 UI 中显示它。以下显示了 TabHeaderDataTemplate。为了清晰起见,样式已被省略。

<DataTemplate x:Key="TabHeaderDataTemplate">
    <StackPanel Orientation="Horizontal" VerticalAlignment="Stretch">
        <TextBlock x:Name="textBlock" 
            Text="{Binding TabHeader}" 
            TextTrimming="CharacterEllipsis" 
            TextWrapping="NoWrap" />
        <TextBlock Text="*" 
            Visibility="{Binding Path=Content.Dirty, FallbackValue=Collapsed, Converter={StaticResource BooleanToVisibilityConverter}}" />
        <Button x:Name="button"  
            Command="ApplicationCommands.Close" CommandParameter="{Binding Path=View}" 
            Template="{DynamicResource CloseTabButtonControlTemplate}" ToolTip="Close" />
    </StackPanel>
</DataTemplate>

此模板的两个关键点是:

首先,与 TextBlockContent.Dirty 属性进行绑定,如果文件已修改且需要保存,则显示一个星号。

Visibility="{Binding Path=Content.Dirty, FallbackValue=Collapsed, Converter={StaticResource BooleanToVisibilityConverter}}"

请注意 FallbackValue 指定符的使用。如果找不到依赖属性 Content.Dirty,它将用于隐藏 TextBlock。如果我们不使用它,TextBlock 的可见性将在找不到时默认为 Visible

其次,选项卡上的关闭按钮使用 ApplicationCommands.Close 命令,并将视图用作 CommandParameter。这使我们能够在单击按钮时解析要关闭的视图。

引用上述 TabHeaderDataTemplateTabItem 模板将 Header 属性绑定到视图 ViewModel,如下面的示例所示。

<Style TargetType="{x:Type TabItem}">
    <Setter Property="Header" Value="{Binding ViewModel}" />
    <Setter Property="HeaderTemplate" 
        Value="{DynamicResource TabHeaderDataTemplate}" />
    <Setter Property="Template">
    …
</Style>

IShell 实现 DesktopShell 利用了这个使用模式,使模块创建者能够在外视图或视图模型中指定内容接口。

什么是内容接口?Calcium 中的内容接口用于以标准方式提供外壳级别的功能,例如保存文件或打印,并且允许外壳根据这种内容的存在来启用或禁用该功能。例如,如果一个视图实现了 ISavableContent,那么外壳将自动启用/禁用 ApplicationCommands.SaveSaveAs 命令。我们将在本系列的第三篇文章中更详细地介绍这一点。

模块管理器

模块管理器本身就是一个 Prism 模块,它允许用户在 UI 中选择性地禁用和启用模块。此功能 Prism 默认不提供。

为了实现模块的禁用和启用,我们需要对 Prism 进行几项增强。如何在 Prism 中自定义模块加载?答案是提供一个自定义的 Microsoft.Practices.Composite.Modularity.IModuleCatalogIModuleCatalog 的目的是为 Prism 提供应用程序的模块,并确定模块之间的依赖关系。

我们的 IModuleCatalog 实现是 DanielVaughan.Calcium.Client.Modularity.CustomModuleCatalog。它实际上扩展了 Prism Composite.Desktop 项目中的 ModuleCatalog 基类,并基于 Prism 的 DirectoryModuleCatalog,但有一些重要的区别。特别是,我们的 CustomModuleCatalog 类使用了 Calcium 的 ModuleManagerSingleton 暴露的几个列表:FailedModulesNonStartupModules。在启动时,当 Prism 使用自定义 IModuleCatalog 来定位要加载的模块时,自定义模块目录会排除这些列表中的模块及其所有依赖模块。

出于显而易见的原因,ModuleManager 模块本身不能从 UI 中禁用。

Module Manager Screenshot

图示:Calcium 中 ModuleManagerView 选项卡屏幕截图。

处理模块加载异常

模块初始化时可能会失败。Prism 在这方面提供的帮助不大,事实上,如果用户不提供 Microsoft.Practices.Composite.Modularity.IModuleInitializer 的自定义实现,并且模块在初始化期间抛出异常,则可能会导致应用程序崩溃。在 Prism 的早期版本中,对于处理模块初始化错误不存在扩展点。幸运的是,现在我们可以通过提供 IModuleInitializer 的自定义实现来做到这一点。我们的 CustomModuleInitializer 类正是这样做的,并将初始化异常的处理委托给一个 IModuleLoadErrorStrategy,如下面的示例所示。

public override void HandleModuleInitializationError(
    ModuleInfo moduleInfo, string assemblyName, Exception exception)
{
    /* Here we use the configured error strategy to deal with the error. */
    var errorStrategy = UnitySingleton.Container.Resolve<IModuleLoadErrorStrategy>();
    if (errorStrategy != null)
    {
        errorStrategy.HandleModuleLoadError(moduleInfo, assemblyName, exception);
        return;
    }
    base.HandleModuleInitializationError(moduleInfo, assemblyName, exception);
}

IModuleLoadErrorStrategy 会通知 ModuleManagerSingleton 该模块已失败,并额外分派一个 Prism CompositeEvent,如下面的示例所示。

class ModuleLoadErrorStrategy : IModuleLoadErrorStrategy
{
    public void HandleModuleLoadError(
        ModuleInfo moduleInfo, string assemblyName, Exception exception)
    {
        ArgumentValidator.AssertNotNull(moduleInfo, "moduleInfo");

        ModuleManagerSingleton.Instance.AddFailedModuleName(moduleInfo.ModuleName);

        var moduleException = new ModularityException(
        moduleInfo.ModuleName, exception.Message, exception);

        Log.Error(moduleException.Message, moduleException);

        var messageService = UnitySingleton.Container.Resolve<IMessageService>();
        /* TODO: Make localizable resource. */
        string moduleLoadUserMessage = string.Format(
            "Module {0} failed to load and has been disabled.", moduleInfo.ModuleName);
        messageService.ShowError(moduleLoadUserMessage);

        var moduleLoadError = new ModuleLoadError { AssemblyName = assemblyName, 
            Exception = exception, ModuleInfo = moduleInfo };
        var eventAggregator = UnitySingleton.Container.Resolve<IEventAggregator>();
        var moduleLoadErrorEvent = eventAggregator.GetEvent<ModuleLoadErrorEvent>();
        moduleLoadErrorEvent.Publish(moduleLoadError);
    }
}

这会将模块的名称放入失败模块列表中,并防止模块及其依赖模块在下次应用程序启动时加载。

ModuleManager 模块实现

模块管理器由 IModule 实现 ModuleManagerModule 类、视图(ModuleManagerView)和视图模型(ModuleManagerViewModel)组成。

<Gui:ViewControl x:Class="DanielVaughan.Calcium.Client.Modules.ModuleManager.ModuleManagerView"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Gui="clr-namespace:DanielVaughan.Calcium.Client.Gui">
    <DockPanel x:Name="ContentPanel">
        <DockPanel.Resources>
            <DataTemplate x:Key="checkBoxTemplate">
                <Border>
                    <CheckBox IsChecked="{Binding Path=Enabled, Mode=TwoWay}" 
                        IsEnabled="{Binding Path=UserCanEnable, Mode=OneWay}" />
                </Border>
            </DataTemplate>
        </DockPanel.Resources>

        <ListView HorizontalContentAlignment="Stretch" 
            ItemsSource="{Binding ApplicationModules}" FontSize="8">
            <ListView.View>
                <GridView>
                    <GridViewColumn 
                        CellTemplate="{DynamicResource checkBoxTemplate}" Header="Enabled"/>
                    <GridViewColumn Header="Name" 
                        DisplayMemberBinding="{Binding Path=ModuleName}" Width="75"/>
                    <GridViewColumn Header="State" 
                        DisplayMemberBinding="{Binding Path=State}" />
                </GridView>
            </ListView.View>
        </ListView>
    </DockPanel>
</Gui:ViewControl>

关于模块化的说明

本文中我们没有涉及的一个问题是,如何处理那些只实现了部分初始化的模块。例如,在初始化过程中,一个模块可能会成功地向外壳添加一个带有相关命令的菜单项,但它可能在完成初始化之前抛出异常,并且可能处于不一致的状态。由于没有自动机制来移除 UI 菜单项,它会变得孤立,并且模块在被调用时可能无法执行该命令。为了解决这个问题,我们可以增强我们的 IModuleInitializer 实现,在初始化过程中监控常见 UI 组件的状态变化,以便如果一个模块失败,它可以被自动卸载;添加到区域的视图等可以被移除。我还对 Undoable Action 基础设施进行了一些工作,稍后将集成。这也有助于安全地撤销模块的部分初始化。在这些功能到位之前,模块作者负责检测其模块失败的情况并进行适当的清理。

模块管理器的未来增强

目前,禁用和启用模块需要重新启动应用程序。我们可以设想对 Calcium 进行增强,以实现无需重新启动即可动态卸载。我认为这可能通过实现例如 IUnloadableModule 接口在模块级别来实现。每个模块都知道如何卸载自身及其视图等。当然,还需要分析依赖关系,以防止依赖模块突然失败。

结论

在本文中,我们了解了 Calcium 如何用于快速创建多功能模块化应用程序。特别是,我们概述了 Calcium 的 Bootstrapper 和外壳实现,检查了 Calcium 中使用的 M-V-VM 方法,探索了用于命令委托的内容接口方法,并研究了开发用于运行时启用或禁用模块的模块管理器。

在下一篇文章中,我们将研究双工消息系统,该系统提供了客户端或服务器与用户交互的功能,并使用相同的 API。我们可以使用服务器发起的的消息框与用户进行交互!我们还将介绍 Calcium 的其他一些模块,例如 Web 浏览器和输出模块。我们还有很多内容要介绍,希望您能和我一起期待下一期。

我希望您觉得这个项目有用。如果有用,我将不胜感激您能对其进行评分和/或在下方留下反馈。这将帮助我写出更好的下一篇文章。

历史

2009 年 5 月

  • 初始发布。
© . All rights reserved.