WPF

裝載協力廠商 .NET 外掛程式的架構

根納季 · Slobodsky
Levi 哈斯克爾

下載代碼示例

去年 11 月彭博卡 發佈應用程式門戶、 應用程式平臺,允許獨立協力廠商軟體發展商出售其 Microsoft.NET 框架Windows Presentation Foundation(WPF)-基於彭博專業服務的應用程式,向超過 30 萬使用者。

在這篇文章中我們會提出類似雇用的彭博 App 入口網站主辦的協力廠商"不受信任"的.NET 應用程式的通用體系結構。 附帶的原始程式碼 (archive.msdn.microsoft.com/mag201308Plugins) 包含.NET 外掛程式主機和演示外掛程式到歷史定價為給定的安全資訊的圖表使用彭博 API 參考執行。

外掛程式主機體系結構

該體系結構中提出圖1包括一個主應用程式進程和外掛程式的宿主進程。

Architecture for Hosting .NET Plug-Ins
圖 1 為承載.NET 外掛程式的體系結構

實現外掛程式的宿主基礎結構的開發人員應仔細考慮執行外掛程式的主機作為一個單獨的進程的利弊。 在入口網站的應用程式方案中,我們認為這種做法的優點大大利多於弊,但我們會列出你自己考慮的最重要因素。

執行外掛程式的主機作為一個單獨的進程的優點包括:

  • 它將分離的外掛程式從主應用程式進程和結果,減少的可能性產生任何負面影響的外掛程式可以有的性能或應用程式的可用性。 它可以降低風險的主要應用程式的 UI 執行緒阻塞的外掛程式。 此外,它是不太可能導致記憶體或任何其他關鍵資源洩漏主要過程中。 這種方法還可以減少可能性為編寫拙劣外掛程式來降低主應用程式進程造成"託管"或"非託管"未經處理的異常。
  • 它可以潛在地提高整個解決方案的安全,通過應用沙箱技術西米­lar 給那些使用由鉻專案 (見 bit.ly/k4V3wq 的詳細資訊)。
  • 它留下了更多可用 (這是更重要的是 32 位的過程,受到 2 GB 的進程使用者模式代碼可用的虛擬記憶體空間的限制) 的主應用程式進程的虛擬記憶體空間。
  • 它允許您擴展非.NET 應用程式使用.NET 外掛程式的功能。

全面實施複雜性的增加大多涉及的利弊:

  • 您需要執行一個單獨進程間通信 (IPC) 機制 (特別注意應予 IPC 介面的版本控制外掛程式的主機和主應用程式進程都有不同的釋放或部署週期時)。
  • 你必須管理外掛程式的宿主進程的存留期。

設計不受信任的協力廠商外掛程式的宿主進程時解決使用者的安全是主要關切問題之一。 定義一個適當的安全體系結構是值得單獨談話和超出了本文的範圍。

.NET 應用程式域 (System.AppDomain 類) 主辦.NET 外掛程式提供了一個全面和可靠的解決方案。

AppDomain 具有以下強大功能:

  • 一個應用程式域中的型別安全物件不能直接存取在另一個的 AppDomain,允許主機強制隔離的一個外掛程式從另一物件。
  • 可以單獨配置 AppDomain,允許主機為不同類型的外掛程式 AppDomain 微調通過提供不同的配置設置。
  • AppDomain 可以卸載,作為非特定于域允許主機卸載的外掛程式和所有相關程式集,但程式集載入 (使用載入程式優化選項 LoaderOptimization.Multi­域或 LoaderOptimization.MultiDomainHost)。 此功能允許主機卸載外掛程式,在託管代碼中失敗,從而使宿主進程更強健。

主應用程式和外掛程式的宿主進程可以介面使用的可用,例如 COM、 具名管道、 Windows 通信基礎 (WCF) 等各種 IPC 機制之一。 我們建議的體系結構,在主應用程式進程的作用是管理的複合 UI 創建和提供各種應用程式服務的外掛程式。 圖2顯示的彭博據點視圖,代表這種複合的 UI。 "隱身分析"的一個組成部分是創建並呈現由基於 WPF 的外掛程式主辦由彭博 App 門戶,而所有其他元件創建和呈現由彭博終端基於 Win32 的應用程式。 主要的應用程式進程將命令發送到外掛程式的宿主進程,通過外掛程式控制器代理。

An Example Composite UI
圖 2 示例複合 UI

外掛程式控制器預設外掛程式的宿主進程的 AppDomain 中運行,並負責處理命令從主應用程式進程,接收到專用的 Appdomain 載入外掛程式和管理他們的一生。

示例原始程式碼提供參考我們的體系結構的實現,由宿主基礎結構和薩普和演示外掛程式組成。

應用程式目錄結構

作為圖 3 所示,應用程式基目錄包含三個程式集:

  • Main.exe 表示主應用程式的流程,並提供使用者介面啟動外掛程式。
  • PluginHost.exe 表示該外掛程式的宿主進程。
  • Hosting.dll 包含 PluginController,具現化外掛程式和管理他們的一生負責。

The Base Application Directory Structure
圖 3 基本應用程式的目錄結構

使用提供的外掛程式的 API 程式集部署到一個單獨的子目錄中,稱為的 PAC,站立為私營組件快取中,概念類似于.NET 全域組件快取 (GAC) 但是,正如其名稱所示,保存專案的應用程式的私有。

每個外掛程式被部署到其自己的外掛程式資料夾下的子目錄。 資料夾的名稱對應于四個字母助記符插頭中的用來從 UI 命令列啟動它。 參考實現包含兩個外掛程式。 第一,關聯助記薩普,是空的 WPF 使用者控制項只列印其名稱。 第二個,與助記符的演示中,關聯顯示為給定的安全使用彭博桌面 API (DAPI) 價格歷史圖表。

每個外掛程式的子目錄裡面有一個 Metadata.xml 檔,以及一個或多個.NET 程式集。 薩普的 Metadata.xml 包含外掛程式中的標題 (用作視窗標題為外掛程式) 和外掛程式 MainAssembly 和 MainClass,執行外掛程式中的進入點的名稱:

<?xml version="1.0" encoding="utf-8" ?>
<Plugin>
  <Title>Simple App</Titlte>
  <MainAssembly>SimpleApp</MainAssembly>
  <MainClass>SimpleApp.Main</MainClass>
</Plugin>

發射用的外掛程式

外掛程式的宿主進程在啟動時預設應用程式域中創建 PluginController 的單個的實例。 應用程式的主進程使用.NET 遠端處理調用 PluginController.Launch (字串 [] args) 方法來啟動外掛程式關聯的助憶鍵輸入的使用者名 (薩普或演示示例參考執行)。 PluginController 實例必須重寫繼承從 System.MarshalByRefObject,以延長其使用壽命的 InitializeLifetimeService 方法,否則將銷毀物件後五分鐘 (預設的存留期內 MarshalByRefObject):

public override object InitializeLifetimeService()
{
  return null;
}
Public class PluginController : MarshalByRefObject
{
  // ...
public void Launch(string commandLine)
  {
    // ...
}
}

PluginController 的基目錄設置為新的應用程式­中所示的目錄結構按功能變數名稱圖 3

var appPath = Path.Combine(_appsRoot, mnemonic);
var setup = new AppDomainSetup {ApplicationBase = appPath};

為應用程式使用不同的基目錄­域有下列重要結果:

  • 它提高了外掛程式從彼此的隔離。
  • 通過使用外掛程式中的主要程式集的位置作為基目錄中獨立的.NET 應用程式相同的方式,它簡化了開發過程。
  • 它需要特殊的承載基礎結構邏輯來定位和載入的基礎設施和 PAC 位於外部插頭-的基目錄中的程式集。

當啟動一個外掛程式,我們首先創建新的外掛程式的宿主應用程式域:

var domain =
    AppDomain.CreateDomain(
    mnemonic, null, setup);

下一步,我們將新創建的應用程式域載入 Hosting.dll、 創建 PluginContainer 類的一個實例和調用 instan 的啟動方法­兵工廠的外掛程式。

表面上,最簡單的方法來完成這些任務將使用 AppDomain.CreateInstanceFromAndUnwrap 方法,因為這種方法允許您直接指定程式集的位置。 但是,使用這種方法會導致 Hosting.dll 要載入到負載從上下文中而不是預設的上下文。 使用負載從上下文會有一些微妙的副作用,如不能使用本機映射,或作為非特定于域的形式載入程式集。 增加外掛程式啟動時將使用的負載從上下文的最明顯的負面結果。 有關程式集載入上下文的詳細資訊可以在發現 MSDN 庫頁上,"最佳做法的程式集載入," bit.ly/2Kwz8u

更好的方法是使用 AppDomain.CreateInstanceAndUnwrap 和指定的 Hosting.dll 的位置和依賴程式集的 AppDomain 使用 XML 配置資訊中 <codeBase> 元素。 在參考實現,我們動態生成 XML 配置並將其分配給新的 AppDomain,使用 AppDomainSetup.SetConfigurationBytes 方法。 生成的 XML 的示例所示圖 4

圖 4 生成的 AppDomain 配置的一個示例

<configuration>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity
          name="PluginHost.Hosting"
          publicKeyToken="537053e4e27e3679" culture="neutral"/>
        <codeBase version="1.0.0.0" href="Hosting.dll"/>
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="Bloomberglp.Blpapi"
          publicKeyToken="ec3efa8c033c2bc5" culture="neutral"/>
        <codeBase version="3.6.1.0" href="PAC/Bloomberglp.Blpapi.dll"/>
      </dependentAssembly>
      <dependentAssembly>
        <assemblyIdentity name="WPFToolkit"
          publicKeyToken="51f5d93763bdb58e" culture="neutral"/>
        <codeBase version="3.5.40128.4" href="PAC/WPFToolkit.dll"/>
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

PluginContainer 類派生從 System.MarshalBy­RefObject 不需要重寫預設存留期管理的 PluginController 類,因為它在創建後立即處理只有一個遠端調用 (發射方法):

var host = (PluginContainer) domain.CreateInstanceAndUnwrap(
  pluginContType.Assembly.FullName, pluginContType.FullName);
host.Launch(args);

發射的 PluginContainer 類的方法創建一個 UI 執行緒的外掛程式和 COM 公寓狀態設置為單線程單元 (STA),作為 WPF 要求:

[SecurityCritical]
public void Launch(string[] args)
{
  _args = args;
  var thread = new Thread(Run);
  thread.TrySetApartmentState(ApartmentState.STA);
  thread.Start();
}

PluginContainer 類的 Run 方法 (見圖 5) 是插頭中的 UI 執行緒的啟動方法。 它提取插頭中的主要程式集的名稱,並指定由 MainAssembly 和 MainClass 的 Metadata.xml 檔中,元素的主要類載入到主程式集,並使用反射來的主類中找到進入點。

圖 5 PluginContainer 類的運行的方法

private void Run()
{
  var metadata = new XPathDocument(
    Path.Combine(AppDomain.CurrentDomain.BaseDirectory, 
      "Metadata.xml"))
    .CreateNavigator().SelectSingleNode("/Plugin");
  Debug.Assert(metadata != null);
  var mainAssembly = (string) metadata.Evaluate("string(MainAssembly)");
  var mainClass = (string) metadata.Evaluate("string(MainClass)");
  var title = (string) metadata.Evaluate("string(Title)");
  Debug.Assert(!string.IsNullOrEmpty(mainAssembly));
  Debug.Assert(!string.IsNullOrEmpty(mainClass));
  Debug.Assert(!string.IsNullOrEmpty(title));
  var rootElement = ((Func<string[], UIElement>) 
    Delegate.CreateDelegate(
    typeof (Func<string[], UIElement>),
    Assembly.Load(mainAssembly).GetType(mainClass),
    "CreateRootElement"))(_args);
  var window =
    new Window
    {
      SizeToContent = SizeToContent.WidthAndHeight,
      Title = title,
      Content = rootElement
    };
  new Application().Run(window);
  AppDomain.Unload(AppDomain.CurrentDomain);
}

在參考實現,作為命名為 CreateRootElement,接受一個字串陣列,作為啟動參數和返回的 System.Windows.UIElement 實例的主類的公共靜態方法定義的進入點。

後調用進入點方法,我們在 WPF 視窗物件中包裝它的傳回值,並啟動外掛程式。 Run 方法中所示的 System.Windows.Application 類的圖 5、 進入消息迴圈和插頭的主視窗中關閉之前,不會返回。 之後,我們計畫卸載的外掛程式中的 AppDomain 和清理它被使用的所有資源。

外掛程式演示

可以使用命令演示 IBM 股票啟動演示外掛程式應用程式作為參考實現的一部分提供。 它演示來開始創建引人注目的有針對性的專業金融人士使用我們建議的體系結構,彭博 API 和 WPF 應用程式是多麼容易。

歷史價格資訊對於給定的安全,是一種功能演示外掛程式顯示任何財務應用程式中找到 (見圖 6)。 外掛程式演示使用彭博 DAPI,並要求有效的訂閱到彭博專業服務。 彭博 API 的詳細資訊可以在發現 openbloomberg.com/開放的 api

The DEMO Plug-In
圖 6 演示外掛程式

中所示的 XAML 圖 7 定義的使用者介面的外掛程式的演示。 興趣點是具現化的圖表、 LinearAxis、 DateTimeAxis 和 LineSeries 的類,並且綁定 LineSeries DependentValuePath 和 IndependentValuePath 的安裝程式。 我們決定使用 WpfToolkit 資料視覺化,因為它在部分信任的環境中工作,提供所需的功能和授權下微軟公共許可 (MS-PL)。

圖 7 外掛程式 XAML 演示

<UserControl x:Class="DapiSample.MainView"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:c=
    "clr-namespace:System.Windows.Controls.DataVisualization.Charting;
    assembly=System.Windows.Controls.DataVisualization.Toolkit"
  xmlns:v=
    "clr-namespace:System.Windows.Controls.DataVisualization;
    assembly=System.Windows.Controls.DataVisualization.Toolkit"
  Height="800" Width="1000">
    <Grid>
      <Grid.RowDefinitions>
        <RowDefinition Height="30"/>
        <RowDefinition/>
      </Grid.RowDefinitions>
      <c:Chart x:Name=
        "_chart" Background="White" Grid.Row="1" Visibility="Hidden">
        <c:Chart.Axes>
          <c:LinearAxis x:Name=
            "_linearAxis" Orientation="Y" ShowGridLines="True"/>
          <c:DateTimeAxis x:Name=
            "_DateAxis" Orientation="X" ShowGridLines=
            "True" Interval="1" IntervalType="Months" />
        </c:Chart.Axes>
        <c:LineSeries x:Name=
          "_lineSeries" DependentValuePath="Value"
          IndependentValuePath="Date" ItemsSource="{Binding}"/>
        <c:Chart.LegendStyle>
          <Style TargetType="{x:Type v:Legend}">
            <Setter Property="Width" Value="0"></Setter>
            <Setter Property="Height" Value="0"></Setter>
          </Style>
        </c:Chart.LegendStyle>
      </c:Chart>
    <TextBox Grid.Row="0" x:Name=
      "_security" IsReadOnly="True" TextAlignment="Center"/>
  </Grid>
</UserControl>

訪問彭博 API,對 Bloomberglp.Blpapi 程式集的引用應添加到專案的引用清單中,與下面的代碼已添加到清單中的使用語句:

using Bloomberglp.Blpapi;

在應用程式啟動建立一個新的 API 會話並獲取參考資料服務 (RDS) 物件,用於靜態定價、 歷史資料和盤中的刻度線和酒吧請求,如圖中所示的圖 8

圖 8 中獲取參考資料服務的物件

private Session _session;
private Service _refDataService;
var sessionOptions = new SessionOptions
  {
    ServerHost = "localhost",
    ServerPort = 8194,
    ClientMode = SessionOptions.ClientModeType.DAPI
  };
_session = new Session(sessionOptions, ProcessEventCallBack);
if (_session.Start())
{
  // Open service
  if (_session.OpenService("//blp/refdata"))
  {
    _refDataService = _session.GetService("//blp/refdata");
  }
}

下一步是要求給定的市場安全的歷史定價資訊。

創建請求類型的物件的 HistoricalDataRequest 和構造請求通過指定安全,外地 (PX_LAST-最後價格),週期,並開始和結束日期的 YYYYMMDD 格式 (見圖 9)。

圖 9 請求歷史價格資訊

public void RequestReferenceData(
  string security, DateTime start, DateTime end, string periodicity)
{
  Request request = _refDataService.CreateRequest("HistoricalDataRequest");
  Element securities = request.GetElement("securities");
  securities.AppendValue(security);
  Element fields = request.GetElement("fields");
  fields.AppendValue("PX_LAST");
  request.Set("periodicityAdjustment", "ACTUAL");
  request.Set("periodicitySelection", periodicity);
  request.Set("startDate", string.Format(
    "{0}{1:D2}{2:D2}", start.Year, start.Month, start.Day));
  request.Set("endDate", string.Format(
    "{0}{1:D2}{2:D2}", end.Year, end.Month, end.Day));
  _session.SendRequest(request, null);
}

中顯示的最後步驟圖 10、 非同步處理 RDS 回應訊息,構築一個時間序列,通過設置 _chart 視覺化資料。DataCoNtext 屬性。

圖 10 加工的參考資料服務的回應訊息

private void ProcessEventCallBack(Event eventObject, 
    Session session)
{
  if (eventObject.Type == Event.EventType.RESPONSE)
  {
    List<DataPoint> series = new List<DataPoint>();
    foreach (Message msg in eventObject)
    {
      var element = msg.AsElement;
      var sd = element.GetElement("securityData");
      var fd = sd.GetElement("fieldData");
      for (int i = 0; i < fd.NumValues; i++)
      {
        Element val = (Element)fd.GetValue(i);
        var price = (double)val.GetElement("PX_LAST").GetValue();
        var dt = (Datetime)val.GetElement("date").GetValue();
        series.Add(new DataPoint(
          new DateTime(dt.Year, dt.Month, dt.DayOfMonth),
          price));
      }
      if (MarketDataEventHandler != null)
        MarketDataEventHandler(series);
    }
  }
}
private void OnMarketDataHandler(List<DataPoint> series)
{
  Dispatcher.BeginInvoke((Action)delegate
  {
    _chart.DataContext = series;
  });
}

自己動手操作

我們提出主辦信任基於.NET WPF 的外掛程式,被成功地用於執行彭博 App 門戶平臺的通用體系結構。 本文附帶的下載代碼可以説明您構建您自己的外掛程式宿主解決方案,或者它可能會激發你的彭博 App 門戶使用彭博 API 生成應用程式。

根納季 · Slobodsky 是一個研發經理和建築師在彭博資訊 他主要從事建築利用 Microsoft 和開源技術的產品。高譚市的寄居者,他喜歡漫步在中央公園和沿博物館英哩。

Levi Haskell 是一個研發團隊領導者和建築師在彭博資訊 他喜歡夾層.NET 內件和建築企業系統。

感謝以下技術專家對本文的審閱:裡德 Borsuk (Microsoft) 和DavidWrighton (微軟)