August 2013

Volume 28 Number 08

WPF - Architektur zum Hosten von .NET -Plug-Ins von Drittanbietern

Gennady Slobodsky | August 2013

Im November letzten Jahres führte Bloomberg L.P. das App-Portal ein, eine Anwendungsplattform, über die unabhängige Entwickler von Drittanbietersoftware ihre auf Microsoft .NET Framework Windows Presentation Foundation (WPF) basierenden Anwendungen über 300.000 Nutzern des Bloomberg Professional Service anbieten können.

In diesem Artikel wird eine Universalarchitektur zum Hosten von nicht vertrauenswürdigen .NET-Anwendungen von Drittanbietern vorgestellt, die der Architektur des Bloomberg App Portal sehr ähnelt. Der beigefügte Quellcode (archive.msdn.microsoft.com/mag201308Plugins) enthält die Referenzimplementierung eines .NET-Plug-In-Hosts und eines Demo-Plug-Ins, wobei die Bloomberg-API zur Darstellung von historischen Preisinformationen für eine bestimmte Sicherheit verwendet wird.

Architektur des Plug-In-Hosts

Die in Abbildung 1 dargestellte Architektur besteht aus einem Hauptanwendungsprozess und einem Plug-In-Hosting-Prozess.

Architektur zum Hosten von .NET -Plug-Ins
Abbildung 1: Architektur zum Hosten von .NET -Plug-Ins

Entwickler, die eine Plug-In-Hosting-Infrastruktur implementieren, sollten die Vor- und Nachteile der Implementierung eines Plug-In-Hosts als separater Prozess sorgfältig abwägen. Im Falle des App Portal überwiegen unserer Ansicht nach klar die Vorteile dieses Ansatzes. Dennoch führen wir die wichtigsten Faktoren auf, damit Sie sich selbst ein Bild machen können.

Beispiele für die Vorteile der Implementierung eines Plug-In-Hosts als separater Prozess:

  • Es entkoppelt den Hauptanwendungsprozess von Plug-Ins und senkt infolgedessen die Gefahr, dass sich Plug-Ins negativ auf die Leistung oder Benutzerfreundlichkeit der Anwendung auswirken. Auch ist es dadurch weniger wahrscheinlich, dass Plug-Ins den UI-Thread der Hauptanwendung blockieren. Darüber hinaus kommt es wohl eher nicht vor, dass beim Hauptprozess Verluste von Speicher oder anderen wichtigen Ressourcen auftreten. Auch sorgt dieser Ansatz dafür, dass ein schlecht geschriebenes Plug-In kaum mehr den Hauptanwendungsprozess zu Fall bringen kann, indem unbehandelte Ausnahmen, die entweder „verwaltete“ oder „nicht verwaltete“ sind, erzeugt werden.
  • Er kann potenziell die Sicherheit der Gesamtlösung steigern, indem Sandbox-Technologien zum Einsatz kommen, die mit den Technologien der „The Chromium Projects“ vergleichbar sind (weitere Informationen unter bit.ly/k4V3wq).
  • Für den Hauptanwendungsprozess ist mehr virtueller Speicherplatz vorhanden. Dies ist besonders für 32-Bit-Prozesse wichtig, da hierfür lediglich 2 GB an virtuellem Speicherplatz für Code im Prozessbenutzermodus zur Verfügung stehen.
  • Sie können durch .NET-Plug-Ins die Funktionalität von Nicht-.NET-Anwendungen erweitern.

Die Nachteile gehen überwiegend mit einer insgesamt höheren Implementierungskomplexität einher:

  • Es muss ein separater IPC-Mechanismus (Inter-Process Communication) implementiert werden. Besondere Aufmerksamkeit ist dabei der Versionskontrolle der IPC-Benutzeroberfläche zu widmen, wenn der Hauptanwendungsprozess und der Plug-In-Host unterschiedliche Veröffentlichungs- oder Bereitstellungszyklen haben.
  • Sie sind für die Verwaltung der Gültigkeitsdauer des Plug-In-Hosting-Prozesses verantwortlich.         

Beim Entwerfen des Hosting-Prozesses für nicht vertrauenswürdige Plug-Ins von Drittanbietern sorgt man sich am ehesten um die Benutzersicherheit. Wie eine Sicherheitsarchitektur richtig entworfen wird, würde zu einem separaten Gespräch führen und sprengt den Rahmen dieses Artikels.

Die .NET-Anwendungsdomäne (System.AppDomain-Klasse) bietet eine umfassende und robuste Lösung für das Hosten von .NET-Plug-Ins.

Eine AppDomain hat folgende leistungsfähige Features:

  • Typsichere Objekte in einer AppDomain können nicht direkt auf Objekte in einer anderen AppDomain zugreifen. So kann der Host die Isolierung zwischen Plug-Ins erzwingen.
  • Eine AppDomain kann individuell konfiguriert werden, wodurch der Host durch verschiedene Konfigurationseinstellungen in der Lage ist, die AppDomain für unterschiedliche Plug-In-Typen anzupassen.
  • Eine AppDomain kann entladen werden. So kann der Host Plug-Ins und alle zugehörigen Assemblys mit Ausnahme von Assemblys, die als domänenunabhängig geladen wurden, entladen. Hierbei kommt die Ladeprogramm-Optimierungsoption „LoaderOptimization.Multi­Domain“ oder „LoaderOptimization.MultiDomainHost“ zum Einsatz. Dieses Feature macht den Hosting-Prozess robuster, weil der Host Plug-Ins entladen kann, die beim verwalteten Code nicht funktionieren.

Die Hauptanwendung und Plug-In-Hostprozesse können mit einem der zur Verfügung stehenden IPC-Mechanismen wie COM, Named Pipes oder Windows Communication Foundation (WCF) verbunden werden. In der von uns vorgeschlagenen Architektur besteht die Funktion des Hauptanwendungsprozesses darin, die Erstellung einer zusammengesetzten Benutzeroberfläche zu verwalten und verschiedene Anwendungsdienste für Plug-Ins bereitzustellen. Abbildung 2 zeigt die Bloomberg Launchpad-Ansicht, bei der es sich um solch eine zusammengesetzte Benutzeroberfläche handelt. Eine Komponente mit der Bezeichnung „Stealth Analytics” wird von einem WPF-basierten Plug-In, das auf dem Bloomberg App Portal gehostet wird, erstellt und gerendert. Alle anderen Komponenten werden hingegen von einer Win32-basierten Bloomberg Terminal-Anwendung erstellt und gerendert. Der Hauptanwendungsprozess sendet über ein Plug-In mit der Bezeichnung „Controller Proxy“ Befehle an einen Plug-In-Hosting-Prozess.

Beispiel einer zusammengesetzten Benutzeroberfläche
Abbildung 2: Beispiel einer zusammengesetzten Benutzeroberfläche

Ein Plug-In-Controller wird in der Standard-AppDomain des Plug-In-Hostprozesses ausgeführt und ist für die Verarbeitung von Befehlen, die vom Hauptanwendungsprozess eingegangen sind, für das Laden von Plug-Ins in dedizierten AppDomains und für das Verwalten der jeweiligen Gültigkeitsdauer zuständig.

Der Beispielsquellcode bietet eine Referenzimplementierung unserer Architektur und besteht aus der Hosting-Infrastruktur sowie aus den SAPP- und DEMO-Plug-Ins.

Verzeichnisstruktur für die Anwendung

Das Hauptanwendungsverzeichnis enthält, wie aus Abbildung 3 ersichtlich, drei Assemblys:

  • Main.exe stellt den Hauptanwendungsprozess dar und bietet die Benutzeroberfläche zum Starten von Plug-Ins.
  • PluginHost.exe ist der Plug-In-Hosting-Prozesses.
  • Hosting.dll enthält den PluginController, der für das Instanziieren von Plug-Ins und Verwalten der Gültigkeitsdauer zuständig ist.

Verzeichnisstruktur der Hauptanwendung
Abbildung 3: Verzeichnisstruktur der Hauptanwendung

API-Assemblys werden von den Plug-Ins in einem separaten Unterverzeichnis, das als „PAC“ bezeichnet wird, zur Nutzung bereitgestellt. PAC steht für „Private Assembly Cache“, einem Konzept, das mit .NET-GAC (Global Assembly Cache) vergleichbar ist, das aber – wie sein Name schon sagt – Elemente enthält, die für die Anwendung privat sind.

Jedes Plug-In wird im Plug-In-Ordner in einem eigenen Unterverzeichnis gespeichert. Der Name des Ordners entspricht der aus vier Buchstaben bestehenden Mnemonik des Plug-Ins, das zum Starten über die Befehlszeile der Benutzeroberfläche verwendet wird. Die Referenzimplementierung enthält zwei Plug-Ins. Das erste, das mit der Mnemonik „SAPP“ verknüpft ist, ist ein leeres WPF-UserControl-Steuerelement, das lediglich seinen Namen druckt. Das zweite, das mit der Mnemonik „DEMO“ verknüpft ist, zeigt ein Preishistoriediagramm für eine bestimmte Sicherheit, wobei die Bloomberg Desktop API (DAPI) zum Einsatz kommt.

In jedem Plug-In-Unterverzeichnis befindet sich eine Datei mit dem Namen „Metadata.xml“ sowie mindestens eine .NET-Assembly. Metadata.xml von SAPP enthält den Titel des Plug-Ins, der als Fenstertitel verwendet wird, und die Namen für das MainAssembly und die MainClass des Plug-Ins, durch die der Einstiegspunkt des Plug-Ins implementiert wird:

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

Starten von Plug-Ins

Ein Plug-In-Hosting-Prozess erstellt beim Start eine einzige Instanz von PluginController in der Standard-AppDomain. Der Hauptprozess der Anwendung nutzt .NET Remoting zum Aufrufen der Methode „PluginController.Launch(string[] args)“, durch die das Plug-In gestartet wird, das mit der vom Benutzer eingegebenen Mnemonik verknüpft ist (SAPP oder DEMO in der Beispielsreferenzimplementierung). Die PluginController-Instanz muss die InitializeLifetimeService-Methode überschreiben, die es vom System.MarshalByRefObject zur Verlängerung der Gültigkeitsdauer geerbt hat. Geschieht dies nicht, wird das Objekt nach fünf Minuten zerstört, denn dies ist die Standardgültigkeitsdauer von MarshalByRefObject.

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

PluginController legt das Basisverzeichnis für die neue App­Domain gemäß dem in Abbildung 3 dargestellten Verzeichnisstruktur fest.

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

Wird für die App­Domain ein anderes Basisverzeichnis verwendet, so hat dies folgende Auswirkungen:

  • Plug-Ins werden besser voneinander isoliert.
  • Die Entwicklung fällt leichter, da – wie bei einer eigenständigen .NET-Anwendung – der Speicherort des Hauptassemblys des Plug-Ins als Hauptverzeichnis verwendet wird.
  • Zum Auffinden und Laden von Infrastruktur und PAC-Assemblys, die sich außerhalb des Hauptverzeichnisses des Plug-Ins befinden, wird spezielle Hosting-Infrastrukturlogik benötigt.

Beim Starten eines Plug-Ins erstellen wir zuerst eine neue Plug-In-Hosting-AppDomain.

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

Dann laden wir Hosting.dll in die neu erstellte AppDomain, erstellen eine Instanz der PluginContainer-Klasse und rufen zum Instanziieren des Plug-Ins die Launch-Methode auf.

Diese Aufgaben lassen sich scheinbar am einfachsten mit der Methode „AppDomain.CreateInstanceFromAndUnwrap“ durchführen, weil hierbei der Speicherort der Assembly direkt angegeben werden kann. Allerdings wird Hosting.dll bei diesem Ansatz in den load-from-Kontext geladen und nicht in den Standardkontext. Die Verwendung des load-from-Kontexts hat einige geringfügige Nebenwirkungen. Zum Beispiel können weder systemeigene Bilder verwendet noch Assemblys als domänenunabhängig geladen werden. Als offensichtlich negativ gilt bei der Verwendung des load-from-Kontexts die Tatsache, dass das Starten von Plug-Ins länger dauert. Weitere Informationen über Assembly-Ladekontexte finden Sie auf der MSDN Library-Seite unter „Best Practices für das Laden von Assemblys“ (bit.ly/2Kwz8u).

Wesentlich besser wäre es jedoch, wenn Sie AppDomain.CreateInstanceAndUnwrap verwenden sowie den Speicherort von Hosting.dll und abhängiger Assemblys in den XML-Konfigurationsinformationen der AppDomain mit dem <codeBase>-Element bestimmen. In der Referenzimplementierung generieren wir Konfigurations-XML dynamisch und weisen es der neuen AppDomain über die AppDomainSetup.SetConfigurationBytes-Methode zu. Ein Beispiel des generierten XML ist in Abbildung 4 dargestellt.

Abbildung 4: Beispiel einer generierten AppDomain-Konfiguration

<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>

Die PluginContainer-Klasse aus dem System.MarshalBy­RefObject muss nicht, wie dies bei der PluginController-Klasse der Fall ist, die Standardgültigkeitsdauer-Verwaltung überschreiben, weil es sich nur um einen einzelnen Remoteaufruf, nämlich die Launch-Methode, kümmert. Dies geschieht gleich nach Erstellung.

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

Die Launch-Methode der PluginContainer-Klasse erstellt einen UI-Thread für das Plug-In und setzt den COM-Apartment-Status auf „Singlethread-Apartment“ (STA), wie dies für WPF erforderlich ist.

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

Die Run-Methode der PluginContainer-Klasse (siehe Abbildung 5) ist die Startup-Methode des UI-Thread des Plug-Ins. Sie extrahiert Namen der Haupt-Assembly des Plug-Ins, der Hauptklasse, die von MainAssembly bestimmt wird, und der MainClass-Elemente der Datei Metadata.xml. Außerdem lädt sie die Haupt-Assembly und nutzt Reflektion zum Auffinden des Einstiegspunkts in die Hauptklasse.

Abbildung 5: Run-Methode der PluginContainer-Klasse

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);
}

In der Referenzimplementierung ist der Einstiegspunkt als öffentliche, statische Methode der Hauptklasse definiert, die die Bezeichnung „CreateRootElement“ trägt. Dabei wird ein Zeichenfolgenarray als Startargument akzeptiert und eine Instanz von System.Windows.UIElement zurückgegeben.

Nach Aufruf der Einstiegspunktmethode wickeln wir den Rückgabewert in ein WPF-Fensterobjekt und Starten das Plug-In. Die Run-Methode der System.Windows.Application-Klasse, die in Abbildung 5 dargestellt ist, tritt in eine Meldungsschleife ein und verlässt diese erst, wenn das Hauptfenster des Plug-Ins geschlossen wird. Danach planen wir das Hochladen der AppDomain des Plug-Ins und bereinigen alle von ihr verwendeten Ressourcen.

DEMO-Plug-In

Die DEMO-Plug-In-Anwendung, die im Rahmen der Referenzimplementierung zur Verfügung gestellt wurde, kann über den Befehl „DEMO IBM Equity“ gestartet werden. Dies beweist, wie einfach sich überzeugende Anwendungen erstellen lassen, die für Finanzexperten konzipiert sind. Hierbei wird die von uns vorgeschlagene Architektur genutzt: Bloomberg-API und WPF.

Das DEMO-Plug-In zeigt historische Preisinformationen für eine bestimmte Sicherheit an. Diese Funktion ist in allen Finanzanwendungen vorhanden (siehe Abbildung 6). Vom DEMO-Plug-In wird die Bloomberg-DAPI verwendet, und hierfür ist ein gültiges Abonnement des Bloomberg Professional Service erforderlich. Weitere Informationen über die Bloomberg-API finden Sie unter openbloomberg.com/open-api.

DEMO-Plug-In
Abbildung 6: DEMO-Plug-In

Die in Abbildung 7 dargestellte XAML definiert die Benutzeroberfläche des DEMO-Plug-Ins. Interessante Aspekte sind hier die Instanziierung der Klassen „Chart“, „LinearAxis“, „DateTimeAxis“ und „LineSeries“ sowie die Einrichtung der Bindung für LineSeries DependentValuePath und IndependentValuePath. Wir haben uns entschieden, WpfToolkit für die Datenvisualisierung zu verwenden, weil es in einer teilweise vertrauenswürdigen Umgebung gut funktioniert, die erforderlichen Funktionen bietet und im Rahmen der Microsoft Public License (MS-PL) lizenziert ist.

Abbildung 7: DEMO-Plug-In 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>

Für den Zugang zur Bloomberg-API sollte eine Referenz auf die Bloomberglp.Blpapi-Assembly zur Projektreferenzliste und folgender Code zur Liste der using-Anweisungen hinzugefügt werden:

using Bloomberglp.Blpapi;

Die Anwendung erstellt zu Beginn eine neue API-Sitzung und bezieht ein RDS-Objekt (Reference Data Service), das für statische Preise, historische Daten sowie für Intraday-, Tick- und Bar-Anforderungen verwendet wird (siehe Abbildung 8).

Abbildung 8: Beziehen eines Reference Data Service-Objekts

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");
  }
}

Als nächstes müssen die historischen Preisinformationen für eine bestimmte Marktsicherheit angefordert werden.

Erstellen Sie ein Request-Objekt des Typs „HistoricalDataRequest“, und bilden Sie die Anforderung, indem Sie die Sicherheit, das Feld (PX_LAST -last price), die Häufigkeit sowie die Start- und Enddaten im Format YYYYMMDD angeben (siehe Abbildung 9).

Abbildung 9: Beziehen von historischen Preisinformationen

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);
}

Als letzter Schritt sind, wie in Abbildung 10 dargestellt, die RDS-Antwortnachricht asynchron zu verarbeiten, eine Zeitreihe zu erstellen und die Daten durch Einrichten der _chart.DataContext-Eigenschaft zu visualisieren.

Abbildung 10: Verarbeiten der Reference Data Service-Antwortnachricht

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;
  });
}

Erstellen einer eigenen Lösung

Wir haben eine Universalarchitektur zum Hosten von nicht vertrauenswürdigen, WPF-basierten .NET-Plug-Ins vorgestellt, die zur Implementierung der Bloomberg App Portal-Plattform eingesetzt wurde. Der Code, den Sie über diesen Artikel herunterladen können, kann Ihnen die Erstellung einer eigenen Plug-In-Hosting-Lösung erleichtern, er kann Sie aber auch dazu inspirieren, eine Anwendung für das Bloomberg App Portal mithilfe der Bloomberg-API zu erstellen.

Gennady Slobodsky *ist F & E-Leiter und Architekt bei Bloomberg L.P. Er spezialisiert sich auf die Entwicklung von Produkten, bei denen Microsoft- und Open-Source-Technologien zum Einsatz kommen. Da er in Gotham wohnt, genießt er lange Spaziergänge im Central Park und entlang der Museum Mile. *

Levi Haskell ist F & E-Teamleiter und Architekt bei Bloomberg L.P. Ihm macht es Spaß, .NET-Einbauten auseinanderzunehmen und Unternehmenssysteme zu entwickeln.

Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Reid Borsuk (Microsoft) und David Wrighton (Microsoft)