去年 11 月,Bloomberg L.P.發布了應用程序門戶,它是一個應用程序平台,獨立的第三 方軟件開發者可借助該平台面向 Bloomberg 專業服務的 300,000 多名用戶銷售其基於 Microsoft .NET Framework Windows Presentation Foundation (WPF) 的應用程序。
在本文中,我們將介紹一個用來承載第三方“不受信任的”.NET 應用程序的通用體系結 構,它與 Bloomberg 應用程序門戶使用的體系結構類似。附帶的源代碼 (archive.msdn.microsoft. com/mag201308Plugins) 包含對 .NET 插件宿主和演示插件的引用實現,該演示插件使 用 Bloomberg API 來繪制給定安全性的歷史定價信息圖表。
插件宿主體系結構
圖1 中顯示的體系結構包含主應用程序進程和插件 宿主進程。
圖 1 用於承載 .NET 插件的體系結構
實現插件宿主基礎結構的開發人員應仔細考慮將插件宿主作為獨立進程來實現的優缺點。 就應用程序門戶而言,我們堅信此方法的利遠大於弊,但我們會列出您需要注意的最重要因 素。
將插件宿主作為獨立進程實現的優點包括:
它將主應用程序進程與插件分離開來,這將減少插件對應用程序的性能或可用性產生任何 負面影響的可能。它減小了插件阻止主應用程序的 UI 線程的風險。此外,它不太可能導致 主進程中出現內存或任何其他關鍵資源洩漏的情況。此方法還降低了因編寫得非常糟糕的插 件導致出現“托管”或“未托管”未處理異常,從而減慢主應用程序進程的可能性。
通過應用與 The Chromium Projects(有關詳細信息,請參閱 bit.ly/k4V3wq)使用的沙盒技術類似的沙盒技術,有可 能提高整個解決方案的安全性。
它為主應用程序進程留出了更多的虛擬內存空間(這對於 32 位進程來說更為重要,此類 進程受到進程用戶模式代碼的 2GB 可用虛擬內存空間的限制)。
它允許您使用 .NET 插件擴展非 .NET 應用程序的功能。
缺點主要與整體實施復雜性的增加相關:
您需要實現單獨的進程間通信 (IPC) 機制(當主應用程序進程和插件宿主具有不同的版 本或部署循環時,應特別注意 IPC 接口的版本控制)。
您必須管理插件宿主進程的生存期。
在為不受信任的第三方插件設計宿主進程時,實現用戶安全性是需要考慮的主要問題之一 。定義適當的安全體系結構這一主題有必要單獨進行討論,不在本文的范圍內。
.NET 應用程序域(System.AppDomain 類)提供了用於承載 .NET 插件的全面而可靠的解 決方案。
AppDomain 具有以下強大功能:
一個 AppDomain 中的類型安全的對象無法直接訪問另一個 AppDomain 中的對象,從而允 許宿主強制將一個插件與另一個插件隔離開。
可以單獨配置 AppDomain,從而允許宿主通過提供不同的配置設置來為不同類型的插件微 調 AppDomain。
可以卸載 AppDomain,從而允許宿主卸載插件和所有關聯的程序集,以非特定於域的形式 加載的程序集除外(使用加載程序優化選項 LoaderOptimization.MultiDomain 或 LoaderOptimization.MultiDomainHost)。此功能通過允許宿主卸載在托管代碼中失敗的插 件來讓宿主進程變得更可靠。
主應用程序和插件宿主進程可使用各種可用的 IPC 機制之一(例如,COM、命名管道、 Windows Communication Foundation (WCF) 等)來進行交互。在建議的體系結構中,主應用 程序進程的角色是管理復合 UI 的創建,並提供針對插件的各種應用程序服務。圖 2 顯示 Bloomberg Launchpad 視圖,該視圖表示此類復合 UI 。“Stealth Analytics”組件由 Bloomberg 應用程序門戶托管的基於 WPF 的插件創建和呈 現,所有其他組件由基於 Win32 的 Bloomberg Terminal 應用程序創建和呈現。主應用程序 進程通過插件控制器代理將命令發送到插件宿主進程。
圖 2 示例復合 UI
插件控制器正在插件宿主進程的默認 AppDomain 中運行,它負責處理從主應用程序進程 接收的命令,從而將插件加載到專用 AppDomains 並管理其生存期。
示例源代碼提供了對體系結構的引用實現,並包含托管基礎結構以及 SAPP 和 DEMO 插件 。
應用程序目錄結構
如圖 3 所示,基本應用程序目錄包含三個程序集:
Main.exe 表示主應用程序進程並提供用於啟動插件的 UI。
PluginHost.exe 表示插件宿主進程。
Hosting.dll 包含 PluginController,後者負責實例化插件並管理其生存期。
圖 3 基本應用程序目錄結構
供插件使用的 API 程序集將部署到一個稱作 PAC 的單獨子目錄中,該子目錄代表專用程 序集緩存,顧名思義,這是一個與 .NET 全局程序集緩存 (GAC) 類似的概念,不過它包含應 用程序專用的項目。
每個插件將部署到 Plugins 文件夾下其自身的子目錄中。文件夾的名稱與用於從 UI 命 令行啟動插件的其四字母助記鍵對應。引用實現包含兩個插件。第一個插件與助記鍵 SAPP 關聯,它是一個只打印其名稱的空 WPF UserControl。第二個插件與助記鍵 DEMO 關聯,它 使用 Bloomberg 桌面 API (DAPI) 顯示給定安全性的價格歷史記錄圖表。
每個插件子目錄中包含一個 Metadata.xml 文件以及一個或多個 .NET 程序集。SAPP 的 Metadata.xml 包含插件的標題(用作插件的窗口標題)以及插件 MainAssembly 和 MainClass 的名稱,以便實現插件的入口點:
<?xml version="1.0" encoding="utf-8" ?> <Plugin> <Title>Simple App</Titlte> <MainAssembly>SimpleApp</MainAssembly> <MainClass>SimpleApp.Main</MainClass> </Plugin>
啟動插件
啟動時,插件宿主進程將在默認的 AppDomain 中創建單個 PluginController 實例。應 用程序的主進程使用 .NET Remoting 調用 PluginController.Launch(string[] args) 方法 來啟動與用戶輸入的助記鍵(示例引用實現中的 SAPP 或 DEMO)關聯的插件。 PluginController 實例必須重寫從 System.MarshalByRefObject 繼承的 InitializeLifetimeService 方法來擴展自己的生存期,否則該對象將在 5 分鐘 (MarshalByRefObject 的默認生存期)後被銷毀:
public override object InitializeLifetimeService() { return null; } Public class PluginController : MarshalByRefObject { // ... public void Launch(string commandLine) { // ... } }
PluginController 將按照圖 3 中顯示的目錄結構為新的 AppDomain 設置基本目錄:
var appPath = Path.Combine(_appsRoot, mnemonic); var setup = new AppDomainSetup {ApplicationBase = appPath};
對 AppDomain 使用不同的基本目錄將產生以下重大影響:
改善各個插件的隔離情況。
通過采用與獨立 .NET 應用程序相同的方式將插件主程序集的位置用作基本目錄來簡化開 發過程。
需要特殊的托管基礎結構邏輯來查找和加載位於插件基本目錄外部的基礎結構和 PAC 程 序集。
啟動插件時,我們會先創建新的插件宿主 AppDomain:
var domain = AppDomain.CreateDomain( mnemonic, null, setup);
接下來,我們將 Hosting.dll 加載到新創建的 AppDomain 中,再創建 PluginContainer 類的實例,然後調用 Launch 方法來實例化插件。
表面上看,完成這些任務的最簡單方式是使用 AppDomain.CreateInstanceFromAndUnwrap 方法,因為可通過此方法直接指定程序集的位置。但是,使用此方法將導致 Hosting.dll 被 加載到 load-from 上下文而非默認上下文中。使用 load-from 上下文會產生各種細微的負 面影響,例如,無法使用本機映像或以非特定於域的形式加載程序集。插件啟動時間增加是 使用 load-from 上下文所導致的最明顯負面影響。有關程序集加載上下文的詳細信息,請參 閱 MSDN 庫頁面上的“程序集加載的最佳方法”,網址為 bit.ly/2Kwz8u。
更好的方法是使用 AppDomain.CreateInstanceAndUnwrap,並使用 <codeBase> 元 素指定 Hosting.dll 和依賴程序集在 AppDomain 的 XML 配置信息中的位置。在引用實現中 ,我們動態生成配置 XML,並使用 AppDomainSetup.SetConfigurationBytes 方法將其分配 給新的 AppDomain。圖 4 顯示生成的 XML 的示例。
圖 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>
從 System.MarshalByRefObject 派生的 PluginContainer 類不需要重寫默認生存期管理(就像 PluginController 類一樣),因為 它在創建後僅會立即處理單個遠程調用(Launch 方法):
var host = (PluginContainer) domain.CreateInstanceAndUnwrap( pluginContType.Assembly.FullName, pluginContType.FullName); host.Launch(args);
PluginContainer 類的 Launch 方法將創建插件的 UI 線程, 並將 COM 單元狀態設置為 WPF 所需的單線程單元 (STA):
[SecurityCritical] public void Launch(string[] args) { _args = args; var thread = new Thread(Run); thread.TrySetApartmentState(ApartmentState.STA); thread.Start(); }
PluginContainer 類的 Run 方法(見圖 5)是插件的 UI 線程的啟動 方法。它提取插件的主程序集、MainAssembly 所指定的主類和 Metadata.xml 文件的 MainClass 元素的名稱,加載主程序集,並使用反射來查找主類中的入口點。
圖 5 PluginContainer 類的 Run 方法
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 窗口對象中並啟動插件。 System.Windows.Application 類的 Run 方法(如圖 5 中所示)進入一 個消息循環,並且在插件的主窗口關閉前不會返回。之後,我們將安排卸載插件的 AppDomain 並清理它正在使用的所有資源。
DEMO 插件
可使用命令 DEMO IBM Equity 啟動作為引用實現的一部分提供的 DEMO 插件應用程序。 它展示了使用我們建議的體系結構(Bloomberg API 和 WPF)來創建適用於金融專業人士的 引人注目的應用程序是多麼的容易。
DEMO 插件顯示了給定安全性的歷史定價信息,任意財務應用程序中都提供了這項功能( 見圖 6)。DEMO 插件使用 Bloomberg DAPI 並要求有效訂閱 Bloomberg 專業服務。有關 Bloomberg API 的詳細信息,請參閱 openbloomberg.com/open-api。
圖 6 DEMO 插件
查看本欄目
圖 7 中顯示的 XAML 定義了 DEMO 插件的 UI。需要注意的地方是 Chart、LinearAxis、DateTimeAxis 和 LineSeries 類的實例化以及 LineSeries DependentValuePath 和 IndependentValuePath 的綁定的設置。我們決定使用 WpfToolkit 進行數據可視化,因為它適用於部分受信任的環境,可提供所需的功能並且在 Microsoft 公 共許可 (MS-PL) 下獲得許可。
圖 7 DEMO 插件 XAML
<UserControl x:Class="DapiSample.MainView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://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>
若要訪問 Bloomberg API,應將對 Bloomberglp.Blpapi 程 序集的引用添加到項目引用列表中,並且必須將以下代碼添加到 using 語句的列表中:
using Bloomberglp.Blpapi;
該應用程序首先會建立新的 API 會話並獲取 Reference Data Service (RDS) 對象開始 ,用於靜態定價、歷史數據和當日刻度線和柱狀圖請求,如圖 8 中所示 。
圖 8 獲取 Reference Data Service 對象
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 的 Request 對象,並通過指定安全性、字段 (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 處理 Reference Data Service 響應消息
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; }); }
量身構建
我們介紹了成功用於 Bloomberg 應用程序門戶平台的實現的不受信任的基於 .NET WPF 的主機插件的通用體系結構。本文附帶的代碼下載可幫助您構建您自己的插件宿主解決方案 ,或者它可能促使您使用 Bloomberg API 來生成面向 Bloomberg 應用程序門戶的應用程序 。
下載代碼示例