code it

Martins Tech Blog

MSN Live Messenger erweitern

Manchmal möchte man eben etwas mehr Funktionalität.... und manchmal geben einem die Programme auch die Möglichkeit, diese Funktionalität selbst hinzuzufügen. So auch beim Messenger. Das Ganze ist etwas tricky, aber wenn man weiß an welchen Stellen man aufpassen muss und was dann zu tun ist, dann ist es doch recht einfach.

Die erste Hürde besteht darin, Messenger dazu zu überreden, AddIns zu akzeptieren. Hierzu erstellt man in der Registry unter HKEY_CURRENT_USER\Software\Microsoft\MSNMessenger einen neuen Wert AddInFeatureEnabled, den man auf 1 setzt.

 

Das sorgt dafür, dass ab dem nächsten Start von Messenger unter Optionen auf wundersame Weise plötzlich die Seite "Add-ins" verfügbar ist

 

Nachdem nun geklärt ist, wo das Add-in geladen werden kann, muss nur noch eines entwickelt werden. Dazu erzeugt man sich eine neue Klassenbibliothek und in dieser Klassenbibliothek eine Klasse. Im Anschluss daran fügt man eine Referenz auf die MessengerClient.dll hinzu. Diese befindet sich im Verzeichnis der Messenger-Installation und beinhaltet unter anderem eine Schnittstellendefinition IMessengerAddIn. Die neu erstellte Klasse muss nun eben dieses Interface implementieren. Dadurch erhält die Klasse eine Funktion Initialize, die ein Messenger-Objekt als Parameter erhält.

namespace UniqueSoftware.LiveMessenger
{
 public class HeadlineTicker : IMessengerAddIn
 {
     private MessengerClient _client = null;

     public void Initialize(MessengerClient messenger)
     {
         Client = messenger;
         initAddin();
     }

     private void initAddin()
     {
     }

     private MessengerClient Client
     {
         get { return this._client; }
         set { this._client = value; }
     }
 }
}

Mit dem hier erhaltenen Messenger-Objekt kann auf einige Funktionen des Messengers zugegriffen werden. Die Eigenschaft LocalUser bietet die Möglichkeit des schreibgeschützten Zugriffes auf die Informationen des aktuellen Nutzers; AddInProperties gibt dem AddIn lesenden und schreibenden Zugriff auf einige Eigenschaften. Dazu stehen noch einige Ereignisse und Methoden zur Verfügung, die das Empfangen und Schreiben von Nachrichten ermöglichen.

 

Das wohl am häufigsten genannte Beispiel - vermutlich auch weil im Optionen-Dialog des Messengers der Text "...Sie können ein Add-in erhalten, welches mit Ihren Freunden chattet, wenn Sie nicht verfügbar sind..." steht, ist es, dass das Add-in als "Anrufbeantworter" fungiert. Ich fand es interessanter, ein Add-in zu entwickeln, das automatisch in einem bestimmten Intervall die Headline-Message ändert. Das meiste dafür ist mit dem oben gezeigten Codesnippet auch schon getan. Es fehlt noch ein Timer-Objekt, das beim Start initialisiert wird und ein Array mit Headline-Texten aus dem im Elapsed-Event ein Eintrag gewählt und in die Eigenschaft AddInProperties.PersonalStatusMessage damit zu befüllen (auch wenn LocalUser.PersonalStatusMessage meines Erachtens hier logischer gewesen wäre)

private Timer _timer = null;
private List<string> _statusmessages = null;
private bool _randomize = true;
private int _currentindex = -1;

private void initAddin()
{
 Client.AddInProperties.Creator = "Martin Hey";
 Client.AddInProperties.FriendlyName = "HeadlineTicker AddIn";
 Client.AddInProperties.Description = "This addin rotates the headline in Messenger.";
 this._statusmessages = new List<string>();
 this._statusmessages.Add("Viele Köche verderben den Brei.");
 this._statusmessages.Add("Man soll den Tag nicht vor dem Abend loben.");
 this._statusmessages.Add("Lebe deinen Traum.");

 this._timer = new Timer();
 this._timer.AutoReset = true;
 this._timer.Interval = 10000;
 this._timer.Elapsed += new System.Timers.ElapsedEventHandler(timer_Elapsed);
 this._timer.Start();
}

private List<string> StatusMessages
{
 get
 {
     return this._statusmessages;
 }
}

void timer_Elapsed(object sender, ElapsedEventArgs e)
{
 string newmessage;
 if (StatusMessages.Count > 0)
 {
     if (!_randomize)
     {
         _currentindex++;
         if (_currentindex > (StatusMessages.Count - 1))
         {
             _currentindex = 0;
         }
     }
     else
     {
         Random random = new Random();
         _currentindex = random.Next(0, (StatusMessages.Count - 1));
     }
     newmessage = StatusMessages[_currentindex];
  }
  else
  {
      newmessage = string.Empty;
  }
  try
  {
      Client.AddInProperties.PersonalStatusMessage = newmessage;
  }
  catch (Exception ex)
  {
      //throw;
  }
}
Wie auch immer... der Effekt ist der gleiche. Die Headline rotiert! Schön wäre es jetzt noch, die Informationen von außen konfigurierbar zu machen, aber das ist etwas umfangreicher, da das Add-in im Security-Kontext InternetZone läuft und damit minimale Berechtigungen im lokalen System hat.

Nun da das Add-in fertig ist, muss es noch geladen werden. Dabei ist zunächst unbedingt darauf zu achten, dass die Assembly genau so benannt wird, wie eben im Quellcode der Namespace und die Klasse benannt wurden.Wer darauf nicht achtet, wird beim Versuch das Add-in in den Messenger zu laden eine entsprechende Meldung erhalten. Die Auswahl selbst erfolgt über "Zu Messenger hinzufügen".

 

Danach kann das Add-in automatisch bei Statuswechsel aktiviert werden (sinnvoll bei automatischer Messagebeantwortung) oder manuell über das "Online"-Menü. Etwas schade finde ich, dass immer nur ein Add-in zu einer Zeit aktiviert sein kann und man so sehr viel Konfigurationsaufwand hat, wenn man mehrere Funktionen implementieren möchte.

Virtual Server weigert sich

In letzter Zeit hab ich häufiger Zeit damit verbracht, meinen Virtual Server dazu überreden, bereits vor einigen Wochen vorhandene und dann verschobene Virtuelle Rechner wieder aktivieren zu können. Ich wurde immer abgehalten mit der Meldung "The virtual machine could not be added. The virtual machine configuration could not be added. A configuration with this name already exists." Leider bietet die Oberfläche keine Möglichkeit, solche verwaisten Einträge zu editieren bzw. zu löschen.

Nach einer kurzen Suche hab ich bei Microsoft den Knowledgebase-Eintrag 555618 gefunden, der hier schnell Abhilfe schafft. Die bekannten Konfigurationen werden nämlich unter C:\Documents and Settings\All Users\Application Data\Microsoft\Virtual Server\Virtual Machines als Verknüpfung gespeichert. Wenn man hier alle verwaisten Verknüpfungen entfernt, kann man auch wieder eine neue mit gleichem Namen hinzufügen.

Update 14.09.2009: Der genannte Pfad bezieht sich auf Windows Server 2003. Unter Windows Vista ist der Pfad C:\ProgramData\Microsoft\Virtual Server\Virtual Machines.

Plugin für LiveWriter

Heute bin ich durch Zufall auf ein Plugin für den LiveWriter gestoßen mit dem man CodeSnippets in Blogs einfügen kann. Damit würden meine Posts besser lesbar werden.

Also: Das Teil heißt treffenderweise Code Snippet plugin for Windows Live Writer. Ich bin mal gespannt, was das alles so kann. Nach der Installation befindet sich im LiveWriter unter dem Menüpunkt Einfügen ein weiterer Punkt "CodeSnippet". Im oberen Fenster gibt man den Quellcode ein - oder kopiert ;) ihn dorthin. Dann sucht man sich das Syntax-Highlighting aus und schon erhält man schön formatierten Quellcode, den der Blog-Leser auch wieder einfach kopieren kann.

Im Folgenden mal ein Beispiel:

public class Auto
{
private string marke;

/// <summary>
/// Automarke. Beschreibt das Auto.
/// </summary>
public string Marke
{
    get { return this.marke; }
    set { this.marke = value; }
}
private int ps;

/// <summary>
/// Leistung des Motors.
/// </summary>
public int Ps
{
    get { return this.ps; }
    set { this.ps = value; }
}

/// <summary>
/// Startet den Motor.
/// </summary>
public void MotorStarten()
{
    throw new System.NotImplementedException();
}

/// <summary>
/// Stoppt den Motor.
/// </summary>
/// <param name="status">Abgewürgt, ja/nein</param>
public void MotorStoppen( bool status )
{
}
}

Ich werd es für meine nächsten Posts mal verwenden und dann wird sich zeigen, wie brauchbar das Ganze ist.

Das COM-.NET-PInvoke-Experiment

In den letzten Tagen hab ich mich intensiv mit COM-Interop in .NET beschäftigt. Ich muss sagen: Das Ganze war nicht ganz so einfach wie ich es mir vorgestellt hatte, aber wenn man weiß wie es geht, ist es gar nicht so schwer.

Was will ich machen: Ich brauche eine .NET-Komponente, die mittel PInvoke die Twain-Schnittstelle eines Scanners anspricht. .NET in diesem Fall, weil das System, das die Komponente verwendet in naher Zukunft auch auf .NET umgestellt werden sein wird und ich dann nicht nochmal mit einer Umstellung beginnen möchte. Leider sind einige Teile des Frontend noch auf VB6-Basis und deshalb muss meine Middleware-Komponente auch COM-Interop unterstützen.

PInvoke an sich scheint kein größeres Problem darzustellen - sollte es doch eines sein, dann werd ich zu diesem Zeitpunkt hier nochmal näher auf dieses Thema eingehen. Was mich deshalb mehr interessiert, war der Punkt mit COM-Interoperabilität.

Als verwöhnter Visual-Studio-Microsoft-Programmierer hab ich es mir ganz einfach vorgestellt: Ich erstelle eine Klassenbibliothek, setze in den Projekteigenschaften den Haken "für COM-Interop registrieren" und setze bei den entsprechenden Klassen, Methoden und Events das Attribut ComVisible auf true. Aber weit gefehlt.

Ich hab also meinen gerade genannten ersten Ansatz wieder verworfen und mich auf die Suche nach den perfekten funktionierenden Einstellungen im Internet gemacht.

Meinen Erkenntnissen nach einigen Stunden Foren-, Newsgroup- und MSDN-Suche sind folgende Einstellungen wichtig:

Zunächst ein Eintrag in der AssemblyInfo.cs - hier bin ich mir nicht sicher, ob der wirklich notwendig ist, aber es funktioniert so und deshalb lass ich die Einträge erstmal drin. Hier wird definiert, dass das Assembly an sich sichtbar für COM ist und mit welcher Guid.

[assembly: ComVisible(true)]

[assembly: Guid("a3b03ad9-34bf-4357-a41f-be532b904c81")]
[assembly: ClassInterface(ClassInterfaceType.AutoDual)]

Meine zu veröffentlichende Klasse selbst besitzt einige öffentliche Methoden und Eigenschaften sowie Events. Nun wird es interessant. Die Klasse selbst wird nicht für COM sichtbar gemacht, sondern nur ein Interface, das die Schnittstelle beschreibt. In meinem Fall sieht das so aus:

[Guid("F8E22BD6-1C73-467B-B6D2-DF33DFD6861F"), 
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ITwain
{
    [DispId(0x8001)]
    bool Scanning { get; }

    [DispId(0x8002)]
    int ImageCount { get; }

    [DispId(0x8101)]
    void SelectScanner();

    [DispId(0x8102)]
    void AcquireImage();

    [DispId(0x8103)]
    bool SaveImage(string location, ImageFormats format)
}

Das Interface erhält für COM das Guid-Attribut und das InterfaceType-Attribut. Jeder Methode und Eigenschaft sollte noch eine DispId zugewiesen werden. Einigen Foreneinträgen zufolge kommt es bei der automatischen Zuweisung bei der Kompilierung sonst zu seltsamen Nebeneffekten. In welchem Nummernbereich diese DispId's liegen sollten, ist mir noch nicht ganz klar. Man findet Beispiele, wo mit 1 angefangen und dann durchnummeriert wird und es finden sich ebenso auch Beispiele, in denen Nummernbereiche definiert werden wie ich sie verwendet hab.

Noch interessanter wird es bei den Events. Wichtig ist, dass die Delegates für COM unsichtbar sind. Weiterhin ist nicht der Event selbst für COM zu veröffentlichen. Das war mein erster Ansatz und er führte dazu, dass das "syntaktische Zuckerstück" Event in seine Basismethoden aufgeschlüsselt wird und diese 3 Methoden im COM sichtbar sind. Da das nicht das gewünschte Ergebnis ist, ist hier anders vorzugehen.

Auch für die Events wird wieder ein Interface definiert. Auch dieses Interface erhält für COM das Guid-Attribut und das InterfaceType-Attribut und für jeden Event wird eine Methode definiert, der eine DispId zugewiesen wird.

[Guid("AEF23281-AAC1-401D-B798-EC4B999CD85E"), 
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ITwainComEvents
{
    [DispId(0x8201)]
    void ScanComplete();

    [DispId(0x8202)]
    void ScanStarted();

    [DispId(0x8203)]
    void ScanAborted();

    [DispId(0x8204)]
    void ScanImageCompleted();
}

So weit so gut. Nun muss der eigentlichen implementierenden Klasse noch mitgeteilt werden, dass die Interfaces wichtig sind. Das Methoden-Property-Interface wird wie gewohnt angegeben. Das Event-Interface wird als ComSourceInterface verwendet. Wichtig ist hier noch, dass die eigentliche Klasse ihre Schnittstelle nicht exportieren darf (ClassInterfaceType.None).

[Guid("15BDE204-511E-411E-9A52-F117A8C8BAD7")]
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(ITwainComEvents))]
[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
public class Twain : ITwain, IDisposable

Und das war's auch schon. Nun wird die Assembly ganz normal kompiliert.

Wie verwendet man die Klasse nun aber im COM?

Damit das möglich ist, muss die TypeLib noch bereitgestellt werden. Hierfür kann man das Programm TlbExp verwenden. Damit erhält man eine tlb-Datei. Diese kann man dann mit RegAsm registrieren.

Wichtig in den "Verweisen" der COM-konsumierenden-Komponente ist nun, dass man auf die TypeLib verweist und nicht wie gewohnt auf die dll.

Exam 70-536: Microsoft .NET Framework - Application Development Foundation

Nach mehreren Wochen intensiver Vorbereitung und dem entsprechenden Kribbeln im Bauch, das unbekannten Situationen vorausgeht bin ich am letzten Freitag meiner ersten Microsoft-Zertifizierung entgegengetreten.

Um 8:00 Uhr sollte es soweit sein. Ich war etwas früher da und es gab noch einen Kaffee bevor ich dann zur Tat schritt. Ich hatte 45 Fragen vor mir und das große Ziel, mindestens 800 Punkte zu haben, damit ich ganz sicher sein kann, auch zu bestehen.

Als ich nach einer Stunde fertig war, wurde ich von der "Prüfungsaufsicht" noch gefragt, ob ich denn schon fertig sei, aber meine Meinung bei Prüfungen ist immer: Entweder man weiß es oder man weiß es nicht. Wenn ich hinterher noch mal über alles nachdenkt, dann lauf ich Gefahr, die richtigen Antworten infrage zu stellen und das hilft dann ja auch nicht.

Und was soll ich sagen... Microsoft said "passed".

Leider hab ich damit erstmal nur einen Zettel mit schwarzen Balken. Ein MCTS wäre mir lieber gewesen. Der kommt dann hoffentlich mit der nächsten Zertifizierungsprüfung.

Hello World

Erste Schritte mit einer neuen Programmiersprache beginnen wohl immer mit den bekannten Worten "Hello World". Und so auch meine ersten Schritte hier im Blog. Für mich ist Blogging noch etwas ziemlich Neues und auch ich sammle nun erste Erfahrungen damit.

Den Initialfunken dazu hat mein neuer Arbeitgeber gegeben, der auch einen Blog unterhält. Nun kann und möchte ich aber nicht alles dort bloggen - ganz einfach weil es dort nicht unbedingt reinpasst oder weil es sich um private Entdeckungen handelt.

Weshalb ich blogge? Nun ja, ich denke es ist eine Mischung von vielen Komponenten: Ich möchte meine Erfahrungen - vorranging im Entwickleralltag - beschreiben. Darunter zählt die Suche nach Informationen und die Erfolge und Misserfolge im täglichen Umgang mit Microsoft Technologien. ... und ein bisschen Hang zur Selbstdarstellung ist wohl auch mit dabei. Ich bin gespannt, zu welchen Freudensprüngen mich die Kollegen aus Redmond animieren können.