head.WriteLine()

Mittwoch, Januar 30, 2008

Ein Command-Modell für System.AddIn, Teil 2

Nachdem ich im ersten Teil bereits mein Command-Modell und dessen Integration in die Host-Anwendung beschrieben habe, soll es nun um die Add-In-Seite gehen.

Während der Aktivierung ruft die AddInHost-Komponente die InitializeCommands()-Methode des Add-In auf. Hierbei wird eine Instanz von ICommandContext übergeben, über die das Add-In Zugriff auf ICommandService und IUIContextService hat. Über diese Interfaces läuft im weiteren Verlauf die Kommunikation. Die folgende Abbildung zeigt den grundlegenden Ablauf.

CommandCommunication

Das Add-In muss nun zunächst die vom Host angebotene Command-Struktur ermitteln, um sich im Anschluss in diese integrieren zu können.

public override void InitializeCommands(ICommandContext context)
{
  // CommandService und UICommandService ermitteln
  ICommandService cmdSrv = context.CommandService;
  IUICommandService cmdUISrv=context.UICommandService;

  // File-Commands ermitteln
  ICommand fileCmd = cmdSrv.GetCommand("Host.File");
  IUICommand fileUICmd = cmdUISrv.GetUICommand(
      fileCommand, UICommandType.MenuCommand);

  // Toolbar ermitteln
  IUICommand toolbarUICmd = cmdUISrv.GetMainToobar();

...

}

Hier werden die ICommand- und IUICommand-Objekte des File-Menüs, sowie die primäre Toolbar ermittelt. Daraufhin können neue Elemente eingefügt werden:

// "Neu"-Command erstellen
ICommand fileNewCmd = cmdSrv.CreateCommand(
  "FirstAddIn.File.New",
  fileCmd);

Hierbei werden ein eindeutiger Name, sowie das jeweils übergeordnete Command-Objekt angegeben. Dazu passend können nun entsprechende Menü- und Toolbar-UICommands erstellt werden:

// "Neu"-UICommand erstellen
IUICommand fileNewUICmd = UICmdSrv.CreateMenuItem(
  fileNewCmd,
  "&Neu",
  fileUICmd,
  0,
  ToolStripItemDisplayStyle.ImageAndText,
  UICmdSrv.GetIconArray(Properties.Resources.New));

In diesem Fall wird ein Menüelement erstellt und hierbei das zugehörige ICommand-Objekt, ein Anzeigetext, das übergeordnetes IUICommand-Objekt, ein Index, sowie der Darstellungsstil und ein Symbol angegeben. Da Bilder in Form von Byte-Arrays über die AppDomain-Grenze geschickt werden, müssen sie über die GetIconArray()-, bzw. GetImageArray()-Methode entsprechend konvertiert werden.

Darüber hinaus können zusätzliche Eigenschaften wie Visible, Checked oder ShortcutKeys festgelegt werden.

Die Erstellung von Toolbar-Elementen erfolgt analog, nur dass hierfür die CreateToolbar()-Methode aufzurufen ist. Darüber hinaus können mit der CreateSeparator()-Methode Separatorelemente für Menü und Toolbar erstellt werden.

Wurde ein Command auf Host-Seite aktiviert, so ruft der Host die NotifyCommandExecuted()-Methode des Add-In auf und übergibt hierbei die jeweilige ICommand-Instanz.

Das Add-In kann daraufhin beispielsweise seine Oberfläche anzeigen. Hierfür bietet die CommandContext-Klasse die ShowSurface()-Methode, welche als Parameter eine WindowProxy-Instanz übergeben bekommt (Details hier).

public override void NotifyCommandExecuted(ICommand command)
{
  if (command == m_fileNewCommand)
  {
    m_surface = new Surface();
    m_context.ShowSurface(new WindowProxy(m_surface));
  }
}

Surface ist ein User Control, welches von AddInSurface ableitet und die Oberfläche des Add-In enthält. Die Einzelheiten dieser Komponente habe ich bereits hier beschrieben.

Die Sourcen inkl. Beispielanwendung gibt's hier.

Ein Command-Modell für System.AddIn, Teil 1

Nachdem ich hier und hier bereits die Grundlage für erweiterbare Anwendungen auf Basis von Windows Forms gelegt habe, möchte ich nun einen Schritt weiter gehen. Denn die Integration von Add-Ins in die Host-Anwendung macht erst so richtig Sinn, wenn ein gemeinsames Command-Modell existiert, über das eine Bereitstellung von Menü- und Toolbar-Elementen möglich ist.

Im ersten Teil möchte ich zunächst das Modell und die Host-Integration beschreiben, während es im zweiten Teil um die Add-In-Seite gehen soll.

Die Basis des Command-Modells bilden die Interfaces ICommand und UICommand, deren Member in der folgenden Abbildung dargestellt sind:

Commands

Während ICommand die allgemeinen Eigenschaften einer hierarchischen Command-Struktur definiert, legt IUICommand die grafische Repräsentation fest.

Hierbei bildet der Host zunächst seine Menü- und Toolbar-Struktur über Commands ab und übergibt diese an die Add-Ins. Diese können sich daraufhin in die Hierarchie integrieren und eigene Commands anbieten. Für die Manipulation der Struktur werden die Interfaces ICommandService und IUICommandService bereitgestellt. Beide werden über ICommandContext zusammengefasst und während der Aktivierung an das Add-In übergeben.

CommandServices

Während mit ICommandService die benötigten Command-Objekte erstellt werden, ist IUICommandService für die Generierung einer grafischen Repräsentation in Form von Menü- und Toolbar-Elementen verantwortlich. Über die ShowSurface()-Methode von ICommandContext hat das Add-In zusätzlich die Möglichkeit seine Oberfläche an den Host zu übertragen (beispielsweise wenn ein Command aktiviert wurde).

Die Kommunikation zwischen Host und Add-Ins erfolgt über das IAddInCommandContract-Interface, welches von der entsprechenden Host-View-Klasse implementiert wird. Zusätzlich findet sich dessen Signatur auch im jeweiligen Add-In-Contract (hier IAddInContract genannt).

CommandContracts

Nachdem ein Add-In geladen wurde, ruft der Host die InitializeCommands()-Methode auf und übergibt ein ICommandContext-Objekt. Daraufhin registriert sich das Add-In in der Command-Hierarchie. Beim Auslösen eines Commands informiert der Host das Add-In über die NotifyCommandExecuted()-Methode. Optional hat die Host-Anwendung über die GetSurface()-Methode die Möglichkeit, die Oberfläche des Add-In explizit anzufordern.

Was nun noch fehlt ist eine Komponente die auf Host-Seite die Integration der Commands in die jeweilige Menü- und Toolbar-Struktur übernimmt. Hier kommt AddInHost zum Einsatz. Es leitet von Component ab und kann daher direkt aus der Toolbox auf eine Form gezogen werden. AddInHost bekommt über die Eigenschaften MainMenuStrip und MainToolStrip die primäre Menüleiste bzw. Toolbar übergeben. Zusätzlich kann ihr eine Instanz von WindowProxyPanel übergeben werden, welches zur Darstellung der Add-In-Oberfläche dient (Details hier). Auf diese Weise kann AddInHost nicht nur die entsprechenden Commands in Menü und Toolbar anlegen, sondern die Oberfläche auch automatisch binden, wenn das Add-In dies über ICommandContext.ShowSurface() veranlasst. Zusätzlich bietet die Komponente über die ShowAddInManager()-Methode die Möglichkeit den Add-In-Konfigurationsdialog zu öffnen, der für das Laden und Entladen der Add-Ins zuständig ist (Details hier).

CommandComponents

Durch die enge Verzahnung der Komponenten, wird die Implementierung des Add-In-Supports für die Host-Anwendung zum Kinderspiel. Sie muss lediglich die folgenden Komponenten zu Verfügung stellen:

  • MenuStrip
  • ToolStrip
  • WindowProxyPanel
  • AddInHost

Nachdem alle Komponenten mit AddInHost verbunden wurden, sind lediglich die folgenden Codezeilen erforderlich:

// Add-In-Pfad zuweisen
addInHost1.AddInPath = Environment.CurrentDirectory;

// Add-In-Store erstellen/aktualisieren
AddInStore.Rebuild(addInHost1.AddInPath);

// Registrierte Add-Ins vom Typ AddInHostView ermitteln
addInHost1.AvailableAddIns = AddInStore.FindAddIns(
    typeof(AddInHostView), addInHost1.AddInPath);

// Commands für Menü- u. Toolbar-Elemente erstellen
addInHost1.CreateCommands();

Hier wird zunächst der Add-In-Store neu erstellt und daraufhin die verfügbaren Add-Ins über AddInStore.FindAddIns() ermittelt. Die zurückgegebene Liste von AddInToken-Objekten kann daraufhin an AddInHost übergeben werden.

Durch den Aufruf von CreateCommands() erstellt AddInHost automatisch entsprechende ICommand- und IUICommand-Objekte für die vorhandenen Menü- und Toolbar-Elemente.

Für die Erstellung eines Commands wird ein eindeutiger Name benötigt, über den das Add-In später Referenzieren kann. Hierfür stellt AddInHost die Extender Property CommandName bereit. Sie wird automatisch jedem ToolStripItem-Element der Form angehängt und ermöglicht so eine leichte Zuordnung.

CommandNameExtender

Darüber hinaus können gleichartige Menü- und Toolbar-Elemente unter einem Command zusammengefasst werden. Wenn ein Add-In beispielsweise den Command "Host.File.New" deaktiviert (Command.Enabled-Eigenschaft), wird sowohl das Menü, als auch der zugehörige Toolbar-Button automatisch deaktiviert.

Zusätzlich abonniert AddInHost das Click-Event des jeweiligen Elements und informiert seinerseits über das CommandExecuted-Event über dessen Aktivierung. Daher benötigen Sie lediglich einen Event Handler für alle Menü- und Toolbar-Elemente.

Das Öffnen des Add-In-Manager-Dialogs kann beispielsweise über einen Menüpunkt erfolgen und durch den folgenden Event Handler verarbeitet werden:

private void addInHost1_CommandExecuted(
    object sender, CommandExecutedEventArgs e)
{
  if (e.Command.Name == "Host.Extras.AddInManager")
  {
    // Add-In-Manager-Dialog anzeigen
    addInHost1.ShowAddInManager(this, false);

  }
}

Der Dialog kümmert sich nun automatisch darum, die vom Benutzer ausgewählten Add-Ins zu aktivieren und über die InitializeCommands()-Methode die entsprechenden Commands anzufordern. AddInHost integriert daraufhin die bereitgestellten Commands in Menü und Toolbar und somit schließt sich der Kreis.

Für den Host wird die Sache somit denkbar einfach. Welche Arbeiten auf Add-In-Seite zu verrichten sind, erkläre ich im zweiten Teil.

Sourcen inkl. Beispielanwendung gibt's hier.

Dienstag, Januar 29, 2008

Thinktectured'

Now it's official: I'm joining Thinktecture. I know Christian, Ingo, Dominick and Neno a lot of years and I'm glad to work with these fantastic guys!

Watch my profile for further information.

Let's Rock!

Montag, Januar 21, 2008

Grafische Verwaltung von Add-Ins

Wie ich hier bereits beschrieben habe, bietet System.AddIn keine Unterstützung für Windows Forms-basierte Add-Ins. Gleiches gilt auch für die grafische Verwaltung in einer Anwendung. Einen Add-In-Manager-Dialog, wie man ihn aus Visual Studio oder Office kennt, ist zurzeit nicht verfügbar.

Daher habe ich einen generischen Dialog entwickelt, der die Aktivierung und Verwaltung von Add-Ins zur Laufzeit stark vereinfacht. Zusätzlich stelle ich zwei Dialoge bereit, in denen Aktivierungsoptionen eingestellt werden können, bzw. die Informationen über geladene Add-Ins bieten. Diese sind jedoch in erster Linie für den Entwickler und weniger für den Anwender gedacht, doch dazu später mehr.

Da ja bekanntlich ein Bild mehr als tausend Worte sagt, hier die Bestandteile in grafischer Form:

AddInManager

1)  AddInManagerForm beinhaltet den eigentlichen Verwaltungsdialog. Für die Darstellung der Add-In-Liste greift er auf das AddInManagerControl zurück. In ihr werden die verfügbaren Add-Ins gelistet.

2)  AddInManagerControl ist ein User Control für die Anzeige der verfügbaren Add-Ins, bzw. zu dessen Aktivierung.

3)  AddInActivationOptionsForm bietet Steuerungsoptionen zur Add-In-Aktivierung. Hier kann beispielsweise eingestellt werden, ob das Add-In in einer AppDomain oder einem separaten Prozess gehostet werden soll.

4)  AddInDetailsForm stellt Informationen über bereits geladene Add-Ins zu Verfügung. Er stellt beispielsweise Informationen über die zugehörige Add-In-Assembly bereit.

Alle Komponenten sind in der WindowsFormsAddInProxy.dll vereint, die ich bereits hier und hier beschrieben habe.

Verwendung

Durch die Verwendung des Add-In-Manager-Dialogs reduziert sich der Code in der Anwendung deutlich, da dieser nicht nur eine grafische Oberfläche bereitstellt, sondern ebenfalls die entsprechende Aktivierung und Deaktivierung übernimmt. Damit dies funktioniert, müssen jedoch einige Voraussetzungen erfüllt sein.

Zunächst müssen zwei Listen geführt werden, die alle verfügbaren, bzw. geladenen Add-Ins enthalten. Diese können beispielsweise als statische Member in der Program-Klasse hinterlegt werden.

static class Program
{
    public static Collection<AddInToken> AvailableAddIns;
    public static AddInDictionary LoadedAddIns;
    public static string AddInPath;
   
    [STAThread]
    static void Main()
    {
        LoadedAddIns = new AddInDictionary();
        AddInPath = Environment.CurrentDirectory;
       
        // Vorhandene Add-Ins ermitteln
        AvailableAddIns =
           AddInStore.FindAddIns(typeof(AddInHostView),
           Program.AddInPath);

        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new MainForm());
    }
}

Beim Programmstart werden zunächst die verfügbaren Add-Ins in der Main()-Methode ermittelt. Für die Liste der geladenen Add-Ins kommt eine Instanz der AddInDictionary-Klasse zum Einsatz. Diese leitet von Dictionary<AddInToken, IAddInManager> ab und  enthält für jedes geladene Add-In ein Token und die entsprechende Instanz. IAddInManger ist ein Marker-Interface, das keinerlei Logik enthält und ausschließlich für den generischen Umgang mit den entsprechenden Add-In-View-Instanzen verwendet wird. Dies ist nötig, da AddInManagerForm keinerlei Informationen über die konkreten View-Klassen hat. Diese müssen nun lediglich von IAddInManger ableiten und können daraufhin generisch verwaltet werden. Im Beispielprojekt sieht die wie folgt aus:

public abstract class AddInHostView : IAddInManager
{
    public abstract WindowProxyBase GetSurface();
}

Nach dieser Vorarbeit kann der Dialog aufgerufen werden:

AddInManagerForm frm = new AddInManagerForm();
frm.AddInPath = Program.AddInPath;
frm.AvailableAddIns = Program.AvailableAddIns;
frm.LoadedAddIns = Program.LoadedAddIns;
if (frm.ShowDialog(this) == DialogResult.OK)
{
    // ...
}

AddInMangerForm muss nun der Pfad des Add-In-Verzeichnisses, sowie die Liste der verfügbaren und geladenen Add-Ins übergeben werden, alles Weitere übernimmt der Dialog.

AddInManagerForm

Aktiviert nun der Benutzer ein Add-In, wird dieses automatisch geladen und ist im Anschluss über Program.LoadedAddIns verfügbar. In der Beispielanwendung werden die geladenen Add-Ins in eine Liste aufgenommen, über die sie gebunden werden können.

...
if (frm.ShowDialog(this) == DialogResult.OK)
{
    // Liste der geladenen Add-Ins füllen
    this.addInsListView.Items.Clear();
    foreach (AddInToken token in Program.LoadedAddIns.Keys)
    {
        ListViewItem item =
           this.addInsListView.Items.Add(token.Name);
        item.Tag = token;
    }
}

AddInList

Zum Binden der Oberfläche wird zunächst die entsprechende View-Instanz über Program.LoadedAddIns ermittelt und auf dieser die GetSurface()-Methode aufgerufen.

private void bindAddInButton_Click(object sender, EventArgs e)
{
    AddInToken token =
       this.addInsListView.SelectedItems[0].Tag as AddInToken;
    AddInHostView instance =
       Program.LoadedAddIns[token] as AddInHostView;
    if (instance != null)
    {
        // Oberfläche des selektierten Add-Ins binden
        WindowProxyBase window = instance.GetSurface();
        if (window != null)
        {
            this.windowsProxyPanel1.SetWindow(window);
            this.windowsProxyPanel1.Select();
        }
    }
}

Das Resultat sehen Sie hier:

AddInBinding 

Zusätzliche Optionen

Wie oben bereits erwähnt, kann die Aktivierung eines Add-Ins durch einen Optionsdialog unterstützt werden. Hier können Hosting-Modell (AppDomain oder Prozess) und der Sicherheitskontext eingestellt werden.

ActivationOptionsForm

Der Dialog ist jedoch in erster Linie für den Entwickler und Anwendungsbetreuer gedacht und sollte dem Endbenutzer nicht zu Verfügung stehen. Daher kann die Anzeige über die ShowActivationOptions-Eigenschaft der AddInManagerForm-Klasse gesteuert werden.

Wurde die Option deaktiviert, so können Sie über die Eigenschaften AddInActivationType und AddInSecurityLevel der Aktivierungstyp, bzw. der Sicherheitskontext festgelegt werden.

Die Sourcen, inkl. Beispielprojekt finden Sie hier. Für Feedback bin ich wie immer dankbar.

Montag, Januar 14, 2008

Update von WindowsFormsAddInProxy

Jesse Kaplan, einer der Architekten von System.AddIn, hat mich darauf aufmerksam gemacht, dass IWindowProxy in meiner Implementierung nicht unbedingt notwendig ist, da in System.AddIn.Contract bereits ein entsprechendes Interface existiert. Daher habe ich IWindowProxy kurzerhand über Bord geworfen und verwende nun statt dessen INativeHandleContract.

Anstelle einer Handle-Eigenschaft, definiert es die GetHandle()-Methode. Das Prinzip bleibt jedoch das Gleiche: Die Übertragung eines Window Handles über die AppDomain-Grenze.

Die aktualisierten Sourcen, inkl. Demo finden Sie hier.

Montag, Januar 07, 2008

Windows Forms Support für System.AddIn, Teil 2

Nachdem ich im ersten Teil den grundlegenden Aufbau der WindowsFormsAddInProxy.dll beschrieben habe, soll es nun um die Details der Implementierung gehen. Zum besseren Verständnis der beteiligten Komponenten, hier zunächst einmal der Aufbau der Assembly:

WindowsFormsAddInProxy

Wie ich im ersten Teil bereits erwähnte, ist das Hauptproblem bei der Übertragung von UI-Elementen über AppDomain-Grenzen, dass Windows Forms-Klassen weder serialisierbar sind, noch per Reference übertragen werden können. Daher wird lediglich das Handle der Add-In-Oberfläche übertragen und auf Host-Seite „umgehängt“. Hierfür stellt die Win32-API die SetParent()-Methode zu Verfügung. Sie nimmt das jeweilige Handle, sowie das Handle des gewünschten Elternfensters entgegen. Die Add-In-Oberfläche wird somit zum Kindelement des entsprechenden Host-Controls.

Aus Sicherheitsgründen ist das Umhängen von Handles jedoch nur innerhalb eines Win32-Prozesses möglich. Somit kann diese Technik nicht eingesetzt werden, wenn die Add-Ins in einem separaten Prozess gehosted werden. Für Client-seitige Add-Ins ist die AppDomain-Isolation jedoch vollkommen ausreichend.

Für das Übertragen des Handle ist das IWindowProxy-Interface zuständig, welches einzig die Handle-Eigenschaft definiert. IWindowProxy kann nun im jeweiligen Add-In-Contract verwendet werden, um Oberflächen-Inhalte über die AppDomain-Grenze zu transferieren. Im Beispielprojekt wird IWindowProxy in der GetSurface()-Methode eingesetzt, mit der der Host die Oberfläche des Add-In ermittelt.

Die eigentliche Kommunikation findet jedoch über die Klassen AddInSurface (Add-In-Seite) und WindowProxyPanel (Host-Seite) statt. Letztere bekommt über die SetWindow()-Methode eine WindowProxyBase-Instanz übergeben und „kapert“ daraufhin das zugehörige Fenster.

public void SetWindow(WindowProxyBase window)
{
    if (window != null && window.Handle != IntPtr.Zero)
    {
        SetParent(window.Handle, this.Handle);
    }
}

Weitere Herausforderungen

Somit ist das Hauptproblem schon mal gelöst. Doch nach einem kurzen Test treten weitere Herausforderungen auf. Da das Umhängen des Handles per Win32-Funktion erfolgte, hat die Windows Forms-Infrastruktur keine Information über die Existenz des Controls. Daher werden viele Nachrichten nicht ordnungsgemäß an dieses weiter geleitet. Dies führt zu Problemen in den folgenden Bereichen:

  1. Automatisches Resizing
  2. Vererbung von Font-Einstellungen
  3. Fokussteuerung
  4. Verarbeitung von Accelerator Keys

Zur Lösung dieser Probleme müssen die Nachrichten manuell von WindowProxyPanel an AddInSurface weitergeleitet werden. Die folgende Abbildung zeigt die Kommunikation zwischen den beiden Controls:

HandleCapturing

1. Automatisches Resizing

Das Resizing-Problem ist relativ einfach gelöst. Hierfür muss in WindowProxyPanel lediglich die OnResize()-Methode überschrieben und in ihr die aktuelle Größe über die Win32-Funktion MoveWindow() aktualisiert werden.

protected override void OnResize(EventArgs e)
{
    base.OnResize(e);
    if (m_window != null &&
        m_window.Handle != IntPtr.Zero)
    {
        MoveWindow(m_window.Handle, 0, 0,
            this.Width, this.Height, true);
    }
}

2. Vererbung von Font-Einstellungen

In Windows Forms werden Font- Einstellungen automatisch an die entsprechenden Kindelemente vererbt, wenn diese nicht explizit einen Font festlegen. Diese Vererbungslogik muss ebenfalls manuell nachgebaut werden, da die Windows Forms-Infrastruktur ja nichts von unserem Kindfenster weis.

Dies ist jedoch mit einem einfachen SendMessage()-Aufruf erledigt:

SendMessage(m_window.Handle, WM_SETFONT, this.Font.ToHfont().ToInt32(), 0);

Der Aufruf findet sowohl in der SetWindow()-Methode, als auch in der überschriebenen OnFontChanged()-Methode statt, um auch Font-Änderungen zur Laufzeit entsprechend durchzureichen. In der überschriebenen WndProc()-Methode von AddInSurface wird die WM_SETFONT-Message daraufhin abgefangen und der Font entsprechend gesetzt.

3. Fokussteuerung

Eine richtig dicke Nuss hatte ich mit der Fokussteuerung zu knacken. Zwar kann in der entsprechenden AddInSurface-Ableitung die TabIndex-Eigenschaft der enthaltenen Controls gesetzt werden, die Kontrolle hat hierbei jedoch das hostende Fenster. Und da dieses das Control nicht kennt (da es nicht in seiner Controls-Hierarchie auftaucht) werden die entsprechenden Elemente auch nicht bei Fokusänderungen berücksichtigt.

Zur Lösung ist die WndProc()-Methode sowohl in WindowProxyPanel als auch in AddInSurface überschrieben. Über sie kann direkt in die Win32 Message Loop des Fensters eingegriffen werden. Zum Verarbeitung der Tab-Taste wird die Message WM_GETDLGCODE in WindowProxyPanel abgefangen und manuell an AddInSurface weitergeleitet.

protected override void WndProc(ref Message m)
{
    const int WM_GETDLGCODE = 0x87;

    if (m_window != null &&
        m_window.Handle != IntPtr.Zero)
    {
        if (m.Msg == WM_GETDLGCODE)
        {
            SendMessage(
               m_window.Handle, WM_GETDLGCODE,
               m.WParam.ToInt32(),m.LParam.ToInt32());
            return;
        }
    }
    base.WndProc(ref m);
}

Auf Add-In-Seite wird diese Message wiederum in AddInSurface gefangen und an eine Methode zur Fokussteuerung übergeben:

protected override void WndProc(ref Message m)
{
    const int WM_GETDLGCODE = 0x87;
    const int VK_TAB = 0x09;

    if (m.Msg == WM_GETDLGCODE)
    {
        if (GetAsyncKeyState(VK_TAB) != 0)
        {
            this.TabInto(
               Control.ModifierKeys != Keys.Shift);
        }
    }

    base.WndProc(ref m);
}

Über die Win32-Funktion GetAsyncKeyState() wird hierbei geprüft, ob die Tab-Taste gedrückt wurde. Ist dies der Fall, wird die selbst implementierte TabInto()-Methode aufgerufen, welche für die eigentliche Fokussteuerung zuständig ist. Hierbei wird über Control.MofifierKeys abgefragt, ob zusätzlich die Shift-Taste gedrückt wurde, um eine entsprechende Rückwärtsbewegung des Fokus zu veranlassen.

4. Verarbeitung von Accelerator Key

Accelerator Keys werden ja bekanntlich aktiviert, indem die Alt-Taste zusammen mit einem bestimmten Zeichen gedrückt wird. Sie werden beispielsweise in Labels deklariert um dem Benutzer ein schnelles Springen in das zugehörige Control zu ermöglichen. Auch diese Steuerung ist auf Ebene der Form-Klasse implementiert, sodass hier ebenfalls eine manuelle Implementierung nötig ist. Diese ist in AddInSurface zu finden und ist der Fokussteuerung sehr ähnlich. Doch bevor auf das Drücken von Accelerator Keys reagiert werden kann, müssen diese erst einmal ermittelt werden. Hierfür wurde die OnHandleCreated()-Methode überschrieben, die bei der initialen Erstellung des Control aufgerufen wird.

protected override void OnHandleCreated(EventArgs e)
{
    base.OnHandleCreated(e);
    this.InitializeAcceleratorKeys(this);
}

Hier wird die InitializeAcceleratorKeys()-Methode aufgerufen, in der die enthaltenen Controls Collections rekursiv durchlaufen und nach Accelerator Keys durchsucht werden. Die gefundenen Keys werden daraufhin in einem Dictionary gespeichert, welches später ausgewertet werden kann.

Das Abfangen von gedrückten Accelerator Keys erfolgt wie gesagt sehr ähnlich wie bei der Focus-Steuerung. WindowProxyPanel sendet die WM_GETDLGCODE-Message, welche von AddInSurface abgefangen und verarbeitet wird. Der entsprechende Code sieht hierbei wie folgt aus:

protected override void WndProc(ref Message m)
{
    const int WM_GETDLGCODE = 0x87;

    if (m.Msg == WM_GETDLGCODE)
    {
        if (Control.ModifierKeys == Keys.Alt)
        {
            this.ProcessAcceleratorKeys();
        }
    }

    base.WndProc(ref m);
}

In der ProcessAcceleratorKeys()-Methode wird daraufhin das Dictionary der Accelerator Keys durchlaufen und der Fokus entsprechend gesetzt.

Den Source Code von WindowsFormsAddInProxy können Sie inkl. Demoprojekt hier herunter laden. Für Feedback bin ich stets dankbar.

PS: Wenn Sie mehr über System.AddIn erfahren möchten, schauen Sie auf der diesjährigen BASTA! Spring Edition vorbei. Dort führen Dominick Baier und ich in der Session „.NET-Anwendungen sicher und robust durch Add-ins erweitern“ in das Thema ein.

Windows Forms Support für System.AddIn, Teil 1

.NET 3.5 führt mit System.AddIn ist ein neues System zur Erstellung von erweiterbaren Anwendungen ein. Dariusz hat die grundlegenden Details bereits hier und hier beschrieben. Das System hat im Moment jedoch einen Haken: Wenn ein Add-In eine grafischen Oberfläche anbietet möchte, muss dieses – ebenso wie die hostende Anwendung – auf WPF basieren. Eine Unterstützung für Windows Forms existiert zurzeit nicht, was verwunderlich ist, da Windows Forms momentan eine deutlich größere Verbreitung hat.

Da ich das System aber gerne jetzt und nicht erst in der nächsten WPF-Anwendung nutzen wollte, habe ich mir ein Herz gefasst und die nötige Windows Forms-Funktionalität selbst implementiert. Doch bevor ich zu den technischen Details komme, zunächst einige Grundlagen zum besseren Verständnis.

System.AddIn-Architektur

Bei System.AddIn kommunizieren Host und Add-Ins durch eine sogenannte Pipeline, die aus Contracts, Views und Adaptern besteht, wie die folgende Abbildung zeigt.

SystemAddInPipeline

In der Mitte der Pipeline stehen die Contracts, über die Host und Add-Ins miteinander kommunizieren. Darüber hinaus verfügen beide Seiten jeweils über Views und Adapter. Diese dienen der Isolation und ermöglichen eine sichere und Versions-unabhängige Kommunikation.

Die Add-Ins werden in einer separaten AppDomain oder einem separaten Prozess ausgeführt. Die Kommunikation findet hierbei über den IPC-Channel von .NET-Remoting statt.

Das Hauptproblem bei der Übertragung von UI-Elementen besteht nun darin, dass die Klassen im System.Windows.Forms-Namespace nicht serialisierbar sind und somit nicht direkt übertragen werden können. Die leiten zwar von MarshalByRefObject ab, können aber auch nicht per Referenz übertragen werden. Doch wie sonst soll die Kommunikation erfolgen?

Der Windows Forms Support

Der Trick besteht darin, die Windows Forms-Elemente nicht in serialisierter Form über die Leitung zu schicken, sondern lediglich das Handle der Container-Instanz (z.B. eines User Controls). Dieses kann daraufhin vom Host „umgehängt“ werden. Der Contract für die Kommunikation könnte somit wie folgt aussehen:

public interface IWindowProxy
{
    IntPtr Handle { get; set; }
}

Analog zur Pipeline-Architektur wird zusätzlich eine abstrakte Basisklasse für die View-Ebene, sowie eine entsprechende Ableitung zur Verwendung in Add-In und Host benötigt. Hierzu dienen die Klassen WindowProxyBase und WindowProxy, welche dieselbe Signatur wie IWindowProxy aufweisen.

Zusätzlich werden Adapter-Klassen für die Add-In- und die Host-Seite benötigt. Hierfür werden die Klassen WindowProxyViewToContractAddInAdapter und WindowProxyViewToContractHostAdapter bereit gestellt.

Da das alles auf den ersten Blick etwas verwirrend wirken kann, soll das folgende Schaubild für die nötige Transparenz sorgen. Es zeigt den Aufbau der WindowFormsAddInProxy.dll, die alle nötigen Klassen enthält und von den jeweiligen Pipeline-Assemblies referenziert wird:

WindowsFormsAddInProxy

Das Demo-Projekt

Hat man den Aufbau erst einmal verstanden, ist die Verwendung in einem Add-In-Projekt relativ einfach. Zur Veranschaulichung habe ich ein kleines Demoprojekt erstellt, in dem ein sehr einfacher Contract verwendet wird:

[AddInContract]
public interface IAddInContract : IContract
{
      IWindowProxy GetSurface();
}

Die GetSurface()-Methode wird vom Host aufgerufen, um die Oberfläche des jeweiligen Add-In zu ermitteln. Sie liefert eine IWindowProxy-Instanz zurück, sprich das Handle eines User Controls.

Analog dazu werden zwei View-Klassen für die Add-In- und die Host-Seite definiert. Für die Add-In-Seite ist dies die Klasse AddInView:

[AddInBase]
public abstract class AddInView
{
    public abstract WindowProxyBase GetSurface();
}

Diese definiert ebenfalls die GetSurface()-Methode, gibt jedoch eine WindowProxyBase-Instanz zurück. Auf Host-Seite wird die Methode durch die AddInHostView-Klasse bereit gestellt.

public abstract class AddInHostView
{
    public abstract WindowProxyBase GetSurface();
}

Fehlen noch die entsprechenden Adapter-Klassen. Zunächst die AddInViewToContractAdapter-Klasse der Add-In-Seite:

[AddInAdapter]
public class AddInViewToContractAdapter
    : ContractBase, IAddInContract
{
    private AddInView m_view;

    public AddInViewToContractAdapter(AddInView view)
    {
        m_view = view;
    }

    public IWindowProxy GetSurface()
    {
        WindowProxyBase proxy =
          this.m_view.GetSurface();
        return new
          WindowProxyViewToContractAddInAdapter(proxy);
    }
}

Hier kommt die WindowProxyViewToContractAddInAdapter-Klasse aus der WindowsFormsAddInProxy.dll zum Einsatz, um eine IWindowProxy-Implementierung einer bestimmten Version bereit zu stellen.

Analog dazu stellt AddInContractToHostViewAdapter den entsprechenden Adapter für die Host-Seite bereit.

[HostAdapter]
public class AddInContractToHostViewAdapter
    : AddInHostView
{
    private IAddInContract m_contract;
    private ContractHandle m_handle;

    public AddInContractToHostViewAdapter
        (IAddInContract contract)
    {
        m_contract = contract;
        m_handle = new ContractHandle(contract);
    }

    public override WindowProxyBase GetSurface()
    {
        IWindowProxy proxy = m_contract.GetSurface();
        return new 
           WindowProxyViewToContractHostAdapter(proxy);
    }
}

Hier kommt die WindowProxyViewToContractHostAdapter-Klasse zum Einsatz, die für die Bereitstellung einer entsprechenden WindowProxyBase-Instanz sorgt.

Add-In erstellen

Nachdem die Infrastruktur steht, kann es an die Implementierung der Add-Ins gehen. Hierfür muss zunächst einmal eine entsprechende Oberfläche bereit gestellt werden. Die WindowsFormsAddInProxy.dll stellt hierfür die Basisklasse AddInSurface bereit. Sie leitet von UserControl ab und enthält die nötige Logik zum Transfer der Oberfläche über die AppDomain-Grenze. Durch die Ableitung von UserControl kann der Entwurf der Oberfläche bequem über den Control Designer erfolgen, ohne das irgendwelche Add-In-spezifischen Details berücksichtigt werden müssen. Im Demoprojekt habe ich einfach eine Handvoll Controls aus der Toolbox gezogen auf der Oberfläche platziert:

Surface

Nachdem die Entwurfsphase abgeschlossen ist, kann es an die Implementierung gehen. Da diese im Wesentlichen nur aus der GetSurface()-Methode besteht, ist das Ganze recht überschaubar:

[System.AddIn.AddIn("Add-In 1",
    Version="1.0.0.0",
    Description="Add-In Nr. 1",
    Publisher="Jörg Neumann")]
public class FirstAddIn : AddInView
{
    private Surface m_surface;

    public override WindowProxyBase GetSurface()
    {
        m_surface = new Surface();
        return new WindowProxy(m_surface);
    }
}

In GetSurface() wird eine Instanz von Surface erzeugt. Surface leitet wiederum von AddInSurface ab und enthält die gerade entworfene Oberfläche. Hierbei ist zu beachten, dass die entsprechende Variable auf Klassenebene deklariert wird, da sonst der Garbage Collector zuschlägt und die Oberfläche nach kurzer Zeit entsorgt. Für die Rückgabe muss nun noch eine WindowProxyBase-Instanz erstellt werden. Dies ist jedoch mit einem Einzeiler erledigt, da WindowProxy ein AddInSurface-Objekt im Konstruktor entgegen nimmt.

Add-In im Host laden

Zum Einbinden der Add-In-Oberfläche in die Host-Anwendung stellt die WindowsFormsAddInProxy.dll die Komponente WindowProxyPanel bereit. Sie leitet von UserControl ab und kann somit direkt auf die Form der Anwendung gezogen werden. WindowProxyPanel hostet die über AddInSurface bereitgestellte Oberfläche und kümmert sich hinter den Kulissen um die Synchronisation der entsprechenden Windows Messages (näheres dazu im nächsten Teil).

Wie die folgende Abbildung zeigt, ist die Oberfläche der Demoanwendung relativ überschaubar.

HostApp

Sie besteht lediglich aus einer WindowProxyPanel-Instanz, sowie einem Button zur Aktivierung. Der Code zum Laden des Add-Ins liegt der Einfachheit halber direkt im Click-Event Handler:

private void bindAddInButton_Click(
    object sender, EventArgs e)
{
    Collection<AddInToken> availableAddIns =
        AddInStore.FindAddIns(
            typeof(AddInHostView),
            Environment.CurrentDirectory);
    if (availableAddIns.Count > 0)
    {
        instance =
            availableAddIns[0].Activate<AddInHostView>
            (AddInSecurityLevel.Host);
        if (instance != null)
        {
            WindowProxyBase window =
                instance.GetSurface();
            if (window != null)
            {
                this.windowsProxyPanel1.
                    SetWindow(window);
                this.windowsProxyPanel1.Select();
            }
        }
    }
}

Hier werden zunächst über die FindAddIns()-Methode die verfügbaren Add-Ins ermittelt. Daraufhin wird das erste gefundene Add-In über die Activate()-Methode aktiviert, wobei über den Wert AddInSecurityLevel.Host festgelegt wird, dass das Add-In im Sicherheitskontext der Anwendung laufen soll.

Nun kommen wir zum spannenden Teil: Auf der erzeugten Instanz wird über die GetSurface()-Methode die Oberfläche des Add-In ermittelt. Das zurückgegebene WindowProxyBase-Objekt wird daraufhin an das WindowProxyPanel gebunden. Hierzu nimmt dessen SetWindow()-Methode die entsprechende WindowProxyBase-Instanz entgegen.

Im Vergleich zum getriebenen Aufwand sieht das Ergebnis dann doch relativ unspektakulär aus:

HostInRuntime

Bei dieser Art der Verwendung gibt es jedoch noch einen kleinen Schönheitsfehler. Da die Oberfläche über die Windows API „umgehängt“ wurde, hat Windows Forms keine Informationen über die enthaltenen Controls in der Add-In-Oberfläche. Dies führt dazu, dass bei einem Tab-Wechsel der Fokus lediglich zwischen Button und WindowProxy wechselt und dabei die eingebetteten Controls ignoriert. Zwar wurde eine entsprechende Fokussteuerung in AddInSurface implementiert, diese wird jedoch vom Hostfenster übersteuert. Als Workaround kann die ProcessTabKey()-Methode der Form wie folgt überschrieben werden.

protected override bool ProcessTabKey(bool forward)
{
    if (this.ActiveControl == this.windowsProxyPanel1)
    {
        this.windowsProxyPanel1.TabInto();
        return false;
    }
    return base.ProcessTabKey(forward);
}

Hier wird geprüft, ob die WindowProxyPanel-Instanz gerade aktiviert ist. Wenn ja, wird dieser die Tab-Steuerung durch einen Aufruf der TabInto()-Methode übergeben.

In der Praxis erweist sich dieser kleine Schönheitsfehler jedoch als eher unbedeutend, da das Hauptfenster einer komplexen Anwendung meist aus Menüs, Toolboxen und List Bars besteht und diese nicht über die klassische Tab-Steuerung erreichbar sind.

Im nächsten Teil gehe ich näher auf die Implementierung der WindowsFormsAddInProxy.dll ein und zeige wie die Win32-Kommunikation zwischen Add-In und Host funktioniert.

Den Source Code von WindowsFormsAddInProxy können Sie inkl. Demoprojekt hier herunter laden. Für Feedback bin ich stets dankbar.

PS: Wenn Sie mehr über System.AddIn erfahren möchten, schauen Sie auf der diesjährigen BASTA! Spring Edition vorbei. Dort führen Dominick Baier und ich in der Session „.NET-Anwendungen sicher und robust durch Add-ins erweitern“ in das Thema ein.