WPF

构建容错复合应用程序

Ivan Krivyakov

下载代码示例

目前对复合应用程序的需求十分广泛,但容错要求不同。 在某些情况下,为了一个故障插件而中断整个应用程序可能不要紧。 但在其他情况下,这是不可接受的。 在本文中,我将描述容错复合桌面应用程序的体系结构。 这种建议的体系结构将通过运行其自身 Windows 进程中的每个插件提供高级隔离。 我本着以下设计目标生成它:

  • 宿主与插件之间的强隔离
  • 将插件控件完全可视化集成到宿主窗口中
  • 轻松开发新插件
  • 合理方便地将现有应用程序转换为插件
  • 使插件能够使用宿主提供的服务,反之亦然
  • 合理方便地添加新服务和接口

附带的源代码 (msdn.microsoft.com/magazine/msdnmag0114) 包含两个 Visual Studio 2012 解决方案:WpfHost.sln 和 Plugins.sln。 首先编译宿主,然后编译插件。 主可执行文件为 WpfHost.exe。 插件程序集按序加载。 图 1 显示了已完成的应用程序。

The Host Window Seamlessly Integrates with the Out-of-Process Plug-Ins
图 1 宿主窗口与进程外插件无缝集成

结构概述

宿主在左上角中显示一个选项卡控件和一个可显示可用插件列表的“+”按钮。 此插件列表从名为 plugins.xml 的 XML 文件读取,但其他目录实现也是可能的。 每个插件均在自身进程中执行,宿主中不加载任何插件程序集。 图 2 显示了此体系结构的高级视图。

A High-Level View of the Application Architecture
图 2 应用程序体系结构的高级视图

在内部,插件宿主是一个遵循“模型-视图-视图模型”(MVVM) 模式的常规 Windows Presentation Foundation (WPF) 应用程序。 模型部分由包含已加载插件集合的 PluginController 类表示。 每个已加载的插件均由 Plugin 类的实例表示,该实例包含一个插件控件,与一个插件进程通信。

宿主系统由四个程序集组成,它们的组织方式如图 3 所示。

The Assemblies of the Hosting System
图 3 宿主系统的程序集

WpfHost.exe 是宿主应用程序。 PluginProcess.exe 是插件进程。 此进程的一个实例加载一个插件。 Wpf­Host.Interfaces.dll 包含宿主、插件进程和插件使用的公共接口。 PluginHosting.dll 包含宿主使用的类型,以及针对插件宿主的插件进程。

插件的加载涉及必须在 UI 线程上执行的一些调用,以及可在任何线程上执行的一些调用。 为使应用程序更快地响应,我只在确实有必要时阻塞 UI 线程。 因此,用于 Plugin 类的编程接口细分为 Load 和 CreateView 两个方法:

class Plugin
{
  public FrameworkElement View { get; private set; }
  public void Load(PluginInfo info); // Can be executed on any thread
  public void CreateView();          // Must execute on UI thread
}

Plugin.Load 方法启动插件进程,创建插件进程端的基础结构。 它在工作线程上执行。 Plugin.CreateView 方法将本地视图连接到远程 FrameworkElement。 您需要在 UI 线程上执行此方法,以避免 InvalidOperationException 这类异常。

Plugin 类最后调用插件进程中由用户定义的插件类。 此用户类的唯一要求是,它从 WpfHost.Interfaces 程序集实现 IPlugin 接口:

public interface IPlugin : IServiceProvider, IDisposable
{
  FrameworkElement CreateControl();
}

从插件返回的框架元素可能具有任意复杂性。 它可能是单个文本框,也可能是实现某个业务线 (LOB) 应用程序的完善用户控件。

对复合应用程序的需求

过去几年来,我的许多客户表达了相同的业务需求:能够加载外部插件从而在一个位置组合多个 LOB 应用程序的桌面应用程序。产生这一要求的基本原因可能不同。 多个团队可能按不同的时间表开发应用程序的不同部分。 不同的业务用户可能需要不同的功能集。 或者,这些客户可能需要确保“核心”应用程序的稳定性,同时保持灵活性。 不管怎样,不同的组织都多次表达过对承载第三方插件的要求。

对于此问题有几种传统解决方案:经典的复合应用程序块 (CAB)、托管加载项框架 (MAF)、托管可扩展性框架 (MEF) 和 Prism。 另一种解决方案是由我的前同事 Gennady Slobodsky 和 Levi Haskell 在 2013 年 8 月期的 MSDN 中发布的(请参阅文章“用于承载第三方 .NET 插件的体系结构”msdn.microsoft.com/magazine/dn342875)。 这些解决方案都有很高的价值,许多有用的应用程序都是使用它们创建的。 我也是这些框架的积极用户,但有个问题困扰了我很长一段时间,那就是稳定性。

应用程序崩溃。 这是不争的事实。 空引用、未处理的异常、锁定的文件和损坏的数据库不会很快消失。 好的宿主应用程序必须能够在插件崩溃时不受影响,继续运行。 故障插件不得使宿主或其他插件停止运行。 这种保护不必是无懈可击的;我不是要尝试防止恶意黑客攻击。 但是,简单错误(如工作线程中的未处理异常)不应使宿主停止运行。

隔离级别

Microsoft .NET Framework 应用程序至少可通过三种不同方式处理第三方插件:

  • 无隔离:在具有一个 AppDomain 的单个进程中运行宿主和所有插件。
  • 中等隔离:将每个插件加载到其自身 AppDomain 中。
  • 强隔离:将每个插件加载到其自身进程中。

无隔离需要的保护和控制最少。 所有数据均可全局访问,没有故障保护,无法卸载违规代码。 应用程序崩溃的最典型原因是插件创建的工作线程中出现未处理异常。

您可以尝试使用 try/catch 代码块保护宿主线程,但当涉及到插件创建的线程时,一切都有可能发生。 从 .NET Framework 2.0 开始,任何线程中的未处理异常都会终止进程,您无法防范。 这种看似残酷的现象有一个充足的理由:未处理异常意味着应用程序可能已变得不稳定,不加以遏制是非常危险的。

中等隔离加大了对插件安全性和配置的控制力度。 您至少还可在正常时卸载插件,没有线程忙于执行非托管代码。 不过,宿主进程仍然未受到保护以防止插件崩溃,如我的文章“AppDomains 不会保护宿主不受故障插件影响”(bit.ly/1fO7spO) 中所演示的。 即便有可能,要设计可靠的错误处理策略也很困难,并且无法保证能够卸载故障 AppDomain。

创造 AppDomain 的目的是作为进程的轻型替代方案,用来承载 ASP.NET 应用程序。 请参阅 Chris Brumme 2003 年的博客文章“AppDomains(‘应用程序域’)”bit.ly/PoIX1r。 ASP.NET 应用相对无需干预的容错方法。 崩溃的 Web 应用程序很容易使包含多个应用程序的整个工作进程停止运行。 在这种情况下,ASP.NET 只是重新启动工作进程,重新发出任何待定的 Web 请求。 对于自身没有面向用户的窗口的服务器进程而言,这是合理的设计决策,但对于桌面应用程序而言,可能并非同样有效。

强隔离提供最高级别的故障防护。 因为每个插件都在自身进程中运行,所以插件不会使宿主崩溃,可随意终止它们的运行。 同时,该解决方案需要相当复杂的设计。 应用程序必须处理大量进程间的通信和同步。 还必须跨进程边界封送 WPF 控件,这个任务并不轻松。

像软件开发中的其他工作一样,选择隔离级别也需要进行权衡。 更强的隔离可提供更大的控制度和更高的灵活性,但代价是应用程序复杂性增加,并且性能降低。

有些框架选择忽视容错,在“无隔离”级别工作。 MEF 和 Prism 就是这种方法的很好示例。 在容错和微调插件配置不成问题的情况下,这是行之有效的最简单解决方案,因此是可使用的正确解决方案。

许多插件体系结构(包括 Slobodsky 和 Haskell 所建议的)使用中等隔离。 它们通过 AppDomain 实现隔离。 AppDomain 使宿主开发人员能够极大地控制插件配置和安全性。 在过去几年中,我自己构建了大量基于 AppDomain 的解决方案。 如果应用程序需要卸载代码、沙盒和配置工具(如果容错不成问题),则 AppDomain 必定是行之有效的方式。

MAF 在加载项框架中脱颖而出,因为它可使宿主开发人员选择这三种隔离级别中的任何一种。 它可以使用 AddInProcess 类在自己的进程中运行加载项。 遗憾的是,AddInProcess 对现成的可视化组件没有作用。 可以扩展 MAF,以便在进程间封送可视化组件,但这意味着向已经复杂的框架再添加一个层。 创建 MAF 加载项并非轻而易举,如果在 MAF 之上再加一层,则复杂性可能变得无法管理。

我建议的体系结构旨在填补这个空白,提供可靠的宿主解决方案,这种解决方案可将插件加载到自身进程中,实现插件与宿主之间的可视化集成。

可视化组件的强隔离

在请求插件加载时,宿主进程产生一个新的子进程。 然后该子进程加载一个用户插件类,该类创建宿主中显示的 FrameworkElement(参见图 4)。

Marshaling a FrameworkElement Between the Plug-In Process and the Host Process
图 4 在插件进程与宿主进程之间封送 FrameworkElement

无法直接在两个进程之间封送 FrameworkElement。 它不从 MarshalByRefObject 继承,也没有标记为 [Serializable],因此 .NET 远程处理不封送它。 它未使用 [ServiceContract] 属性加以标记,因此 Windows Communication Foundation (WCF) 也不封送它。 为解决此问题,我使用 System.Windows.Presentation 程序集(MAF 的一部分)的 System.Addin.FrameworkElementAdapters 类。 该类定义两个方法:

  • ViewToContractAdapter 方法将 FrameworkElement 转换成 INativeHandleContract 接口,该接口可以通过 .NET 远程处理进行封送。 此方法是在插件进程内部调用的。
  • ContractToViewAdapter 方法将 INativeHandleContract 实例转换回 FrameworkElement。 此方法是在宿主进程内部调用的。

遗憾的是,简单地组合这两个方法不能马上解决问题。 显然,MAF 旨在 AppDomain 之间而不是在进程之间封送 WPF 组件。 ContractToViewAdapter 方法在客户端失败,出现以下错误:

System.Runtime.Remoting.RemotingException:
Permission denied: cannot call non-public or static methods remotely

其根源是 ContractToViewAdapter 方法调用 MS.Inter­nal.Controls.AddInHost 类的构造函数,该类尝试将 INativeHandleContract 远程处理代理转换为类型 AddInHwndSourceWrapper。 如果转换成功,则它在远程处理代理上调用内部方法 RegisterKeyboardInputSite。 不允许在跨进程代理上调用内部方法。 以下是 AddInHost 类构造函数内部发生的情况:

// From Reflector
_addInHwndSourceWrapper = contract as AddInHwndSourceWrapper;
if (_addInHwndSourceWrapper != null)
{
  _addInHwndSourceWrapper.RegisterKeyboardInputSite(
    new AddInHostSite(this)); // Internal method call!
}

为消除此错误,我创建了 NativeContractInsulator 类。 该类位于服务器(插件)端。 它通过将所有调用均转发到从 View­To­ContractAdapter 方法返回的原始 INativeHandleContract 来实现 INativeHandleContract 接口。 但与原始实现不同,它不能转换为 AddInHwndSourceWrapper。 因此在客户端(宿主)上的转换不成功,不会出现禁用的内部方法调用。

更详细地检查插件体系结构

Plugin.Load 和 Plugin.CreateView 方法为插件集成创建所有必要的移动部件。

图 5 显示生成的对象图。 这有些复杂,但每个部件均负责一个特定角色。 它们共同确保宿主插件系统无缝可靠地运行。

Object Diagram of a Loaded Plug-In
图 5 已加载插件的对象图

Plugin 类表示宿主中的单个插件实例。 它有 View 属性,这是插件在宿主进程中的可视化表示形式。 Plugin 类创建 PluginProcessProxy 的实例,从中检索 IRemotePlugin。 IRemotePlugin 包含 INativeHandleContract 形式的远程插件控件。 然后 Plugin 类接受此协定,将其转换为 FrameworkElement,如下所示(为了简洁,省略了一些代码):

public interface IRemotePlugin : IServiceProvider, IDisposable
{
  INativeHandleContract Contract { get; }
}
class Plugin
{
  public void CreateView()
  {
    View = FrameworkElementAdapters.ContractToViewAdapter(
      _remoteProcess.RemotePlugin.Contract);
  }}

PluginProcessProxy 类在宿主中控制插件进程生命周期。 它负责启动插件进程,创建远程处理通道,监视插件进程运行状况。 它还使用 PluginLoader 服务,从中检索 IRemotePlugin。

PluginLoader 类在插件进程内运行,实现插件进程生命周期。 它建立远程处理通道,启动 WPF 消息调度程序,加载用户插件,创建 RemotePlugin 实例,将该实例传递给宿主端的 PluginProcessProxy。

RemotePlugin 类使用户插件控件可跨进程边界封送。 它将用户的 FrameworkElement 转换成 INativeHandleContract,然后将该协定与 NativeHandleContractInsulator 包装在一起,以规避前述非法方法调用问题。

最后,用户的插件类实现 IPlugin 接口。 它的主要工作是在插件进程中创建插件控件。 通常,是 WPF UserControl,但它可以是任何 FrameworkElement。

在请求插件加载时,PluginProcessProxy 类产生一个新的子进程。 根据插件是 32 位还是 64 位,该子进程可执行文件为 PluginProcess.exe 或 PluginProcess64.exe。 每个插件进程均在命令行中收到一个唯一 GUID,以及插件基目录:

PluginProcess.exe
  PluginProcess.0DAA530F-DCE4-4351-8D0F-36B0E334FF18
  c:\plug-in\assembly.dll

插件进程设置类型 IPluginLoader 的远程处理服务,引发名为“ready”的事件,在本示例中,为 PluginProcess.0DAA530F-DCE4-4351-8D0F-36B0E334FF18.Ready。 然后宿主可使用 IPluginLoader 方法加载插件。

另一个解决方案是在宿主准备就绪后使插件进程调用到宿主中。 这将消除对就绪事件的需求,但这会使错误处理变得更加复杂。 如果“加载插件”操作源自插件进程,则错误信息也保留在插件进程中。 如果出了什么错,宿主可能发现不了错误。 因此,我选择使用就绪事件的设计。

另一个设计问题是,是否容纳未部署在 WPF 宿主目录下的插件。 一方面在 .NET Framework 中,加载不位于应用程序目录内的程序集会产生某些困难。 另一方面,我认识到插件可能有自己的部署问题,可能无法总是将插件部署在 WPF 宿主目录下。 此外,一些复杂应用程序在没有从它们的基目录中运行时可能出现错误行为。

由于这些问题,WPF 宿主允许从本地文件系统上的任何位置加载插件。 为此,插件进程在应用程序基目录设置为插件基目录的辅助 AppDomain 中执行几乎所有操作。 这会产生将 WPF 宿主程序集加载到该 AppDomain 中的问题。 这至少可通过四种方式实现:

  • 将 WPF 宿主程序集放置在全局程序集缓存 (GAC) 中。
  • 使用插件进程的 app.config 文件中的程序集重定向。
  • 使用 LoadFrom/CreateInstanceFrom 重写之一加载 WPF 宿主程序集。
  • 使用非托管宿主 API 在采用所需配置的插件进程中启动 CLR。

其中每种解决方案各有优缺点。 将 WPF 宿主程序集放置在 GAC 中需要管理权限。 尽管 GAC 是一种完全解决方案,但是需要管理权限才能安装在公司环境中,这是很令人头疼的问题,因此我尽量避免。 程序集重定向也有吸引力,但之后配置文件将取决于 WPF 宿主的位置。 这会使 xcopy 安装变得不可能。 创建一个非托管宿主项目有较大维护风险。

因此我使用 LoadFrom 方法。 此方法的一个很大弊端是 WPF 宿主程序集最终在 LoadFrom 上下文中(请参阅 Suzanne Cook 的博客文章“选择绑定上下文”bit.ly/cZmVuz)。 为避免任何绑定问题,我需要重写插件 AppDomain 中的 AssemblyResolve 事件,以便插件的代码能够更轻松地查找 WPF 宿主程序集。

开发插件

您可以将插件实现为类库 (DLL) 或可执行文件 (EXE)。 在 DLL 方案中,步骤如下:

  1. 创建一个新类库项目。
  2. 引用 WPF 程序集 PresentationCore、PresentationFramework、System.Xaml 和 WindowsBase。
  3. 添加到 WpfHost.Interfaces 程序集的引用。 确保将“copy local”设置为 False。
  4. 创建一个新的 WPF 用户控件,如 MainUserControl。
  5. 创建一个从 IKriv.WpfHost.Interfaces.PluginBase 派生、名为 Plugin 的类。
  6. 将插件的条目添加到宿主的 plugins.xml 文件中。
  7. 编译插件,然后运行宿主。

最小的插件类如下所示:

public class Plugin : PluginBase
{
  public override FrameworkElement CreateControl()
  {
    return new MainUserControl();
  }
}

另外,也可以将插件实现为可执行文件。 在这种情况下,步骤为:

  1. 创建一个 WPF 应用程序。
  2. 创建一个 WPF 用户控件,例如 MainUserControl。
  3. 将 MainUserControl 添加到应用程序的主窗口。
  4. 添加到 WpfHost.Interfaces 程序集的引用。 确保将“copy local”设置为 False。
  5. 创建一个从 IKriv.WpfHost.Interfaces.PluginBase 派生、名为 Plugin 的类。
  6. 将插件的条目添加到宿主的 plugins.xml 文件中。

插件类非常像上一个示例,主窗口 XAML 应只包含对 MainUserControl 的引用:

<Window x:Class="MyPlugin.MainWindow"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="clr-namespace:MyProject"
  Title="My Plugin" Height="600" Width="766" >
  <Grid>
    <local:MainUserControl />
  </Grid>
</Window>

这样实现的插件可作为独立应用程序运行,也可在宿主中运行。 这简化了调试与宿主集成无关的插件代码。 图 6 显示了这种“双头”插件的类图。

The Class Diagram for a Dual-Head Plug-In
图 6 双头插件的类图

这种方法还是将现有应用程序快速转换为插件的途径。 您只需要将应用程序的主窗口转换成用户控件。 然后如前所述,在插件类中对用户控件进行实例化。 附带代码下载中的 Solar System 插件是这种转换的示例。 整个转换过程需要不到一小时。

因为该插件不是独立应用程序,而是由宿主启动的,因此可能不直接进行调试。 您可以开始调试宿主,但 Visual Studio 尚不能自动附加到子进程。 您可以在插件进程运行后手动将调试程序附加到插件进程,也可以通过将 PluginProcess app.config 的第 4 行更改为以下内容,使插件进程在启动时进入调试程序中。

<add key="BreakIntoDebugger" value="True" />

另一种方法是如前所述,将插件创建为独立应用程序。 然后您可以将插件作为独立应用程序对其大部分进行调试,只需定期检查与 WPF 宿主的集成是否正常工作。

如果插件进程在启动时进入调试程序中,您需要通过更改 WpfHost app.config 文件的第 4 行增加就绪事件的超时时间,如下所示:

<add key="PluginProcess.ReadyTimeoutMs" value="500000" />

图 7 显示了附带代码下载中可用示例插件的列表及其功能的说明。

图 7 附带代码下载中的可用示例插件

插件项目 功能
BitnessCheck 演示插件如何以 32 位或 64 位运行
SolarSystem 演示转换成插件的旧 WPF 演示应用程序
TestExceptions 演示对用户线程和工作线程异常的异常处理
UseLogServices 演示宿主服务和插件服务的使用

宿主服务和插件服务

在实际情况中,插件通常需要使用宿主提供的服务。 我在代码下载中的 UseLogService 插件中演示了这种情况。 插件类可能具有默认构建函数,或者采用 IWpfHost 类型参数的构建函数。 在后一个示例中,插件加载程序将 WPF 宿主的实例传递给插件。 接口 IWpfHost 的定义如下:

public interface IWpfHost : IServiceProvider
{
  void ReportFatalError(string userMessage,
     string fullExceptionText);
  int HostProcessId { get; }
}

我在插件中使用 IServerProvider 部件。 IServiceProvider 是在 mscorlib.dll 中定义的标准 .NET Framework 接口:

public interface IServiceProvider
{
  object GetService(Type serviceType);
}

我在插件中使用它从宿主获得 ILog 服务:

class Plugin : PluginBase
{
  private readonly ILog _log;
  private MainUserControl _control;
  public Plugin(IWpfHost host)
  {
    _log = host.GetService<ILog>();
  }
  public override FrameworkElement CreateControl()
  {
    return new MainUserControl { Log = _log };
  }
}

然后该控件可使用 ILog 宿主服务写入到宿主的日志文件。

宿主也能够使用插件提供的服务。 我定义了一个称为 IUnsavedData 的此类服务,这证明在实际情况中非常有用。 通过实现此接口,插件可定义未保存工作项的列表。 如果关闭插件或整个宿主应用程序,则宿主将询问用户是否要放弃未保存的数据,如图 8 所示。

Using the IUnsavedData Service
图 8 使用 IUnsavedData 服务

IUnsavedData 接口的定义如下:

public interface IUnsavedData
{
  string[] GetNamesOfUnsavedItems();
}

插件作者无需显式实现 IServiceProvider 接口。 在插件中足以实现 IUnsavedData 接口。 PluginBase.GetService 方法将它返回到宿主。 代码下载中的 UseLogService 项目提供了一个 IUnsavedData 实现示例,相关代码如下:

class Plugin : PluginBase, IUnsavedData
{
  private MainUserControl _control;
  public string[] GetNamesOfUnsavedItems()
  {
    if (_control == null) return null;
    return _control.GetNamesOfUnsavedItems();
  }
}

日志记录和错误处理

WPF 宿主和插件进程在 %TMP%\WpfHost 目录中创建日志。 WPF 宿主写入到 WpfHost.log,每个插件宿主进程写入到 PluginProcess.Guid.log(“Guid”不是文本名称的一部分,但扩展为实际 Guid 值)。 日志服务是自定义生成的。 我没有使用 log4net 或 NLog 等常有日志记录服务,以便示例是独立的。

插件进程还将结果写入到它的控制台窗口,您可以通过将 WpfHost app.config 的第 3 行更改为以下内容来显示该窗口:

<add key="PluginProcess.ShowConsole" value="True" />

我细致地向宿主报告所有错误,妥善处理它们。 宿主监视插件进程,在插件进程停止运行时关闭插件窗口。 同样,插件进程监视其宿主,在宿主停止运行时关闭。 所有错误都记录下来,因此检查日志文件非常有助于排除故障。

请务必注意,在宿主与插件之间传递的任何内容都必须是 [Serializable],或者是从 MarshalByRefObject 派生的类型。 否则,.NET 远程处理将无法在这两方之间封送对象。 双方必须知道这些类型和接口,因此一般只有内置类型和来自 WpfHost.Interfaces 或 PluginHosting 程序集的类型可安全封送。

版本控制

WpfHost.exe、PluginProcess.exe 和 PluginHosting.dll 紧密耦合,应同时发布。 令人高兴的是,插件代码不依靠这三个程序集中的任何一个,因此几乎可通过任何方式加以修改。 例如,您可以轻松更改就绪事件的同步机制或名称,而不会影响这些插件。

在对 WpfHost.Interfaces.dll 组件进行版本控制时应格外小心。 应对它进行引用,但不应将其包含在插件代码中 (CopyLocal=false),因此该程序集的二进制文件始终仅来自宿主。 我没有给这个程序集提供强名称,因为我特别不希望进行并行执行。 整个系统中应仅存在 WpfHost.Interfaces.dll 的一个版本。

一般,应将插件视为不受宿主作者控制的第三方代码。 一次性修改甚至重新编辑所有插件会比较困难或者不可能。 因此,接口程序集的新版本必须与先前版本实现二进制兼容,并且将重要更改的数目保持在绝对最低值。

向程序集添加新类型和接口通常是安全的。 其他任何修改,包括将新方法添加到接口,或者将新值添加到枚举,都可能破坏二进制兼容性,应加以避免。

尽管这些宿主程序集没有强名称,但在任何更改(无论多小)之后递增版本号非常重要,这样不会出现版本号相同的两个程序集具有不同代码的情况。

良好的起点

我在这里提供的参考体系结构不是用于插件宿主集成的生产质量框架,但非常接近,可作为应用程序的极好起点。

此体系结构关注样板,以及较为困难的考虑因素,例如插件进程生命周期,在进程间封送插件控件,交换机制,以及宿主与插件之间的服务发现等。 大多数设计解决方案和解决方法不是随意的。 它们基于在生成 WPF 复合应用程序方面的实际经验。

您很可能需要修改宿主的可视化外观,将日志记录机制替换为企业所用的标准机制,添加新服务,可能更改发现插件的方式。 可进行很多其他修改和改进。

即使您没有创建 WPF 复合应用程序,也可能愿意了解此体系结构,从而了解 .NET Framework 可变得多么强大和灵活,以及您能够如何通过意想不到的有趣高效方式组合熟悉的组件。

Ivan Krivyakov 是 Thomson Reuters 的技术负责人。他是一位有实践经验的开发人员和架构师,擅长构建和改进复杂的业务线 (LOB) Windows Presentation Foundation 应用程序。

衷心感谢以下 Microsoft 技术专家对本文的审阅:Scripto James McCaffrey、Daniel Plaisted 和 Kevin Ransom
Kevin Ransom 已在 Microsoft 工作了 14 年,参与了众多项目,包括:公共语言运行时、Microsoft Business Framework、Windows Vista 与 Windows 7、托管可扩展性框架以及基类库。他目前从事 Visual FSharp 托管语言。

Scripto James McCaffrey 供职于华盛顿地区雷蒙德市沃什湾 Microsoft 总部园区。 他参与过多项 Microsoft 产品的研发工作,其中包括 Internet Explorer 和 MSN Search。 他是《.NET Test Automation Recipes》(Apress, 2006) 的作者,您可以通过以下电子邮箱地址与他联系:jammc@microsoft.com

自 2008 年加入 Microsoft 以来,Daniel Plaisted 参加了用于 Windows 商店应用程序的托管可扩展性框架 (MEF)、可移植类库 (PCL) 和 Microsoft .NET Framework 方面的工作。 他曾出席 MS TechEd、BUILD 及各种本地组、代码活动和会议。 业余时间,他喜欢计算机游戏、阅读、徒步旅行、杂耍和花式沙包。他的博客是 blogs.msdn.com/b/dsplaisted/,您可以通过电子邮件 daplaist@microsoft.com 与他联系。