LINQ to Office? Cast!

Jeder, der mich auch nur ein bißchen als Entwickler kennt, weiß, dass ich LINQ liebe – Mengenoperationen in der prozeduralen Programmierung, was will man mehr? Vielleicht genau dort einsetzen, wo viele von uns, mich eingeschlossen, Objektmengen zum ersten Mal erlebt haben, nämlich in den Office-Objektmodellen?

Eine Aufgabe, die sich zum Beispiel beim Programmieren von Office-Lösungen häufig stellt, ist das „Auffinden“ eines Objekts an Hand seines Namens oder anderer Eigenschaften. Oft finden sich solche Konstrukte im Code, hier am Beispiel von Excel:

	public Worksheet GetWorksheetByNameClassic(string name)
        { 
            Workbook wbkAktiv = Globals.ThisAddIn.Application.ActiveWorkbook;

            foreach (Worksheet ws in wbkAktiv.Worksheets)
            {
                if (ws.Name == name)
                {
                    return ws;
                }
            }
            return null;
        }

Nicht schön, erzielt aber das gewünschte Ergebnis.

Ein erster Versuch, diese Methode in LINQ zu implementieren, schlägt fehl:

"Could not find an implementation of the query pattern for source type 'Microsoft.Office.Interop.Excel.Sheets'. 'Where' not found." lautet in Visual Studio die zugehörige Fehlermeldung. Nunja, wie so oft nicht wirklich hilfreich, insbesondere für LINQ-Neulinge.

Hintergrund ist der Umstand, das LINQ nur verwendet werden kann, wenn die abzufragende Objektmenge auch LINQ-fähig ist. Weil unsere VSTO-Klassen aber lediglich Verkapselungen von COM32-Objekten sind – sie sind ja nicht einmal „richtige“ Klassen, sondern in den meisten Fällen lediglich Interfaces -, sind die Auflistungen in VSTO nicht LINQ-fähig.

Schade.

Quatsch, wir machen sie einfach LINQ-fähig! Das Zauberwort heißt „Casting“ und ist jedem Entwickler, der Option Strict On verwendet, schon längst hinlänglich bekannt. LINQ bringt dafür unter anderem eine eigene Extension Method mit, und plötzlich weiß nicht nur die Intellisense von Visual Studio, was wir meinen, sondern es funktioniert auch das spätere Kompilat ohne Zicken:

        public Worksheet GetWorksheetByName(string name)
        {
            Workbook wbkAktiv = Globals.ThisAddIn.Application.ActiveWorkbook;

            var q = from ws in wbkAktiv.Worksheets.Cast<Worksheet>()
                    where ws.Name == name
                    select ws;

            return q.FirstOrDefault();
        }

Die Zeilenzähler unter den Lesern werden jetzt natürlich sagen: „Schön. Wir machen uns also die Mühe und bauen ein ausgefuchstes LINQ Statement, nur um am Ende im Grunde die gleiche Methode zu haben wie zuvor, und haben damit nicht einmal eine Codezeile gewonnen?“ – Gegen solche Argumente hilft naturgemäß kein Verweis auf die wesentlich elegantere Struktur von LINQ, verbessertes Speichermanagement und die Unsauberkeit abgebrochener For-Schleifen.

Stattdessen eher diese eine(!) Codezeile:

Worksheet wsNamensTest = wbkAktiv.Worksheets.Cast<Worksheet>()
                                            .Where(ws => ws.Name == "Test")
                                            .FirstOrDefault();

Eingesetzt an der Stelle, wo unsere GetWorksheetByName-Funktion aufgerufen wird, und die ganze Funktion ist komplett überflüssig (und alle ihre Geschwister, die ähnliche Aufgaben erledigen)!