code it

Martins Tech Blog

Erweiterungen für STSADM debuggen

Bei der Entwicklung von Erweiterungen für STSADM kommt man auch irgendwann an die Stelle an der man seine Entwicklung testen möchte. STSADM selbst läuft aber viel zu schnell durch als dass man "Attach to process" auswählen könnte.

Die Lösung ist recht trivial: Im Debug-Reiter des Projektes gibt man unter "Start external program" den Pfad zur stsadm.exe (c:\program files\common files\microsoft shared\web server extensions\12\bin\stsadm.exe) an und in "Command line arguments" die gewünschten Parameter.

Benutzerinformationen elegant aus Listitems extrahieren

Gelegentlich werden in Spalten von ListItems auch Benutzerinformationen gespeichert - als Beispiel sollen hier "erstellt von" und "geändert von" dienen. Die zugehörigen Felder des Items beinhalten meist Zeichenketten im Format <BenutzerID>;#<Benutzername>.

Bevor man sich nun in String-Operationen versucht - z.B. durch Split an der Stelle ";#" und Erzeugung der Objekte über die extrahierte ID - sollte man sich mit den Klassen SPFieldUser bzw. SPFieldUserValue auseinandersetzen, die hier viel Arbeit und Fehleranfälligkeit abnehmen können, da hier sehr einfach Objekte vom Typ SPUser bzw. SPGroup zurückgegeben werden können.

Im Folgenden 2 beispielhafte Methoden, die den Zugriff verdeutlichen sollen:

private SPUser GetSPUser(SPListItem item, Guid fieldid) 
{ 
    SPUser retval = null; 

    #region check params 
    if (item == null) 
    { 
        throw new ArgumentNullException("item"); 
    } 
    if (fieldid == Guid.Empty) 
    { 
        throw new ArgumentException("Empty Guid is not allowed.", "fieldid"); 
    } 
    #endregion 

    try 
    { 
        SPFieldUser field = item.Fields[fieldid] as SPFieldUser; 
        if (field != null) 
        { 
            SPFieldUserValue fieldValue = field
                .GetFieldValue(item[fieldid].ToString()) as SPFieldUserValue; 
            if (fieldValue != null) 
            { 
                retval = fieldValue.User; 
            } 
        } 
    } 
    catch (Exception ex) 
    { 
        throw ex; 
    } 

    return retval; 
}
private SPGroup GetSPGroup(SPListItem item, Guid fieldid) 
{ 
    SPGroup retval = null; 

    #region check params 
    if (item == null) 
    { 
        throw new ArgumentNullException("item"); 
    } 
    if (fieldid == Guid.Empty) 
    { 
        throw new ArgumentException("Empty Guid is not allowed.", "fieldid"); 
    } 
    #endregion 

    try 
    { 
        SPFieldUser field = item.Fields[fieldid] as SPFieldUser; 
        if (field != null) 
        { 
            SPFieldUserValue fieldValue = field
                .GetFieldValue(item[fieldid].ToString()) as SPFieldUserValue; 
            if (fieldValue != null) 
            { 
                string groupName = fieldValue.LookupValue; 
                retval = item.Web.SiteGroups[groupName]; 
            } 
        } 
    } 
    catch (Exception ex) 
    { 
        throw ex; 
    } 

    return retval; 
}

Der Aufruf ist nun recht trivial:

SPUser modifiedbyuser = GetSPUser(tasklistitem, SPBuiltInFieldId.Editor); 
DateTime modifiedat = Convert.ToDateTime(tasklistitem[SPBuiltInFieldId.Modified]); 
SPUser assignedto = GetSPUser(tasklistitem, SPBuiltInFieldId.AssignedTo);

Zugriff auf erweiterte Eigenschaften einer Workflowaufgabe

Bei der Entwicklung komplexer Workflows für den SharePoint spielen auch Workflowaufgaben für die Benutzerinteraktion während der Ausführung des Workflows eine nicht unbedeutende Rolle. Eine solche Aufgabe hat nicht nur die Standardeigenschaften wie einen Titel oder eine Beschreibung sondern auch erweiterte Eigenschaften. Das praktische daran ist, dass diese kein eigenes Feld in der Liste benötigen sondern die Daten in den bereits vorhandenen Feldern gespeichert werden. Und wenn man weiß, wie es geht, ist der Zugriff auf diese Daten auch sehr einfach.

1.) bei der Erstellung
Während der Erstellung der Aufgaben verwendet man in der Regel den Typ WssTaskActivity. Dieser verfügt bereits über eine Eigenschaft ExtendedProperties, die vom Typ Hashtable ist und in die man seine gewünschten Daten schreiben kann.

createTask.TaskId = Guid.NewGuid();
createTask.TaskProperties.Title = "Bitte genehmigen";
createTask.TaskProperties.ExtendedProperties["AdditionalText"] 
    = "Bitte persönlich bearbeiten!";

2.) über den Item der TaskList
Möchte man nun später noch einmal darauf zugreifen, sieht man sich vor die Aufgaben gestellt, die Daten wieder aus dem SPListItem der TaskList zu lesen und nach der Änderung auch wieder zu schreiben. Sieht man sich das SPListItem genauer an, das die Aufgabe repräsentiert, so erkennt man, dass es ein Feld ExtendedProperties enthält, in dem die Daten auch enthalten sind - allerdings sind hier einfach die XML-Attribute die die Eigenschaften repräsentieren angegeben:

"AdditionalText='Bitte persönlich bearbeiten!' Department='Einkauf'"

Die große Frage ist jetzt, wie man einfach auf diese Daten zugreift, diese verändert und wieder abspeichert. Die Hashtable die bei der Erstellung wäre hierfür ein adäquates Objekt, das hier aber wie es zunächst scheint nicht vorhanden ist. Aber man muss hier nicht verzagen - auch dafür gibt es eine bereits vorhandene Lösung, auf die man aber erst kommen muss. Der Typ SPWorkflowTask aus dem NameSpace Microsoft.SharePoint.Workflow verfügt über 2 interessante statische Methoden: GetExtendedPropertiesAsHashtable hat als Rückgabewert genau die eben noch vermisste Hashtable und AlterTask verfügt über die Möglichkeiten, den Item mit den geänderten Daten auch wieder zu aktualisieren.

// get the extended properties hashtable
Hashtable taskItemExtProps = SPWorkflowTask
    .GetExtendedPropertiesAsHashtable(this.TaskListItem);
// write new values to the hashtable
taskItemExtProps["AdditionalText"] = "Mein Kommentar: genehmigt!";
// update the task item with new values
SPWorkflowTask.AlterTask(this.TaskListItem, taskItemExtProps, true);

InfoPath-Formulare als Eingabe für SharePoint-Daten verwenden

Heute sollte mein SharePoint dazu überredet werden, InfoPath-Formulare zur Dateneingabe zu verwenden. Dazu hab ich ein paar gute Blog-Einträge von Fabian gefunden, die die relevanten Schritte abdecken und glücklicherweise auch schon in deutsch geschrieben sind.

InfoPath Forms Services Teil 1: Forms Server einrichten
InfoPath Forms Services Teil 2: InfoPath Formular in einer SharePoint Bibliothek bereitstellen und übers Web veröffentlichen

Erstellung eines eigenen Listen-Templates auf Basis eines bestehenden Listen-Templates

In meinem letzten Post hab ich darüber geschrieben, wie man einen EventReceiver im SharePoint erstellt und an eine Dokumentenbibliothek bindet. Der Nachteil dieses Vorgehens ist, dass der Receiver dann bei allen Dokumentenbibliotheken in der Site in der das Feature aktiviert wurde anschlägt. Manchmal möchte man aber nur, dass bestimmte Dokumentenbibliotheken betroffen sind.

In der Featuredefinition wird der EventReceiver immer an eine bestimmte ListTemplateId gebunden. Um das Ziel zu erreichen, muss man ein eigenes ListTemplate erstellen. Die bestehenden ListTemplates sind als Features vorhanden. Ein eigenes Template kann man erstellen, indem man sich Feature-Dateien selbst erzeugt oder indem man einfach eine bestehende Definition kopiert und anpasst. Als Beispiel soll die Standard-Dokumentenbibliothek dienen. Das dazugehörige Feature findet sich unter 12/Template/Features/DocumentLibrary. Zur Erstellung einer angepassten Kopie des Features kopiert man den kompletten Ordner und passt sich diesen nach seinen Wünschen an.

Zunächst wird der Hauptordner so umbenannt, wie das Feature künftig im stsadm angesprochen werden soll. Im Beispiel soll das "MyOwnDocumentLibrary" sein.
Im Anschluss daran öffnet man die feature.xml und vergibt hier einen sprechenden Titel, eine passende Beschreibung und eine neue Guid.

Die feature.xml verweist auf die Datei DocumentLibrary.xml. Auch in dieser sind Anpassungen vorzunehmen. Unter DisplayName vergibt man eine sprechende Bezeichnung, bei Description einen passenden Beschreibungstext. Und nun wird es interessant: Type ist die neue ListTemplateId. Hier verwendet man am besten eine Id größer als 10000, um nicht mit den eigenen Ids des SharePoint in Konflikt zu geraten. Auch bei Name sollte man einen neuen Namen vergeben.

Wichtig ist, dass der hier vergebene Name dem Namen des Ordners im Dateisystem entspricht, in dem die schema.xml liegt. Deshalb ist im Beispiel auch dieser Ordner in mydoclib umbenannt worden. Da die Spalten und Eigenschaften der Standard-Dokumentenbibliothek übernommen werden sollen, werden die Dateien innerhalb dieses Ordners nicht angepasst. Die finale Struktur sollte ähnlich der folgenden aussehen.

Das war auch schon die Feature-Definition. Nun muss das Feature noch installiert und aktiviert werden. Dazu kopiert man den Ordner unter dem neuen Namen wieder nach 12/Template/Features. und aktiviert und installiert das neue Feature mit den passenden stsadm-Befehlen.

stsadm -o installfeature -name MyOwnDocumentLibrary -force
stsadm -o activatefeature -name MyOwnDocumentLibrary -url http://localhost

Nun steht unter Create der neue Bibliothekstyp zu Verfügung.

Alle Listen dieses Typs haben jetzt die neue TemplateId an die man spezielle Features (z.B. EventReceiver) binden kann.

Erstellung eines eigenen EventReceivers in SharePoint

Heute soll es mal um die Erstellung eines eigenen EventReceivers gehen. Als Grundlage für die Erstellung hab ich einen Post von Miguel Moreno zum gleichen Thema verwendet, der das prinzipielle Vorgehen sehr gut erklärt wie ich finde.

Als kleinen Anwendungsfall soll der EventReceiver beim Hinzufügen eines Items zu einer Dokumentenbibliothek einen Abriss der Eigenschaften auf die Festplatte schreiben. Bei der finalen Implementierung wären hier noch Benutzerberechtigungen zu beachten. Darauf möchte ich hier nicht näher eingehen.

1. Erstellung eines neuen Projekts in Visual Studio
Zunächst startet man Visual Studio und erstellt eine neues Projekt für eine Klassenbibliothek.

Als Name soll SharePointEventHandler dienen.

2. Referenz hinzufügen
Im nächsten Schritt wird ein Verweis auf Windows SharePoint Services hinzugefügt.

3. Klasse benennen
Der enthaltenen Klasse wird nun noch ein Name gegeben. In meinem Fall ist das ItemEventreceiver.

 

4. Eigene Logik implementieren
Es werden SharePoint-Funktionalitäten und Zugriff auf das Dateisystem benötigt. Deshalb werden using-Direktiven auf System.IO und Microsoft.SharePoint hinzugefügt. Die erstellte Klasse erbt von SPItemEventReceiver.

using System;
using System.Collections.Generic;
using System.Text;

using System.IO;
using Microsoft.SharePoint;

namespace SharePointEventHandler
{
    public class ItemEventReceiver : SPItemEventReceiver
    {
    }
}

Nun wird ein StreamWriter-Objekt deklariert, das später die Datei schreiben soll.

Der EventReceiver soll auf das Ereignis ItemAdded reagieren. Aus diesem Grund wird die gleichnamige Methode überschrieben und mit der gewünschten Logik gefüllt. Die finale Klasse sieht wie folgt aus:

using System;
using System.Collections.Generic;
using System.Text;

using System.IO;
using Microsoft.SharePoint;

namespace SharePointEventHandler
{
    public class ItemEventReceiver : SPItemEventReceiver
    {
        private StreamWriter SW;

        public override void ItemAdded(SPItemEventProperties properties)
        {
            base.ItemAdded(properties);

            try
            {
                SW=File.CreateText(@"C:\eventoutput.txt");

                SW.WriteLine("CurrentUserId: " + properties.CurrentUserId.ToString());
                SW.WriteLine("BeforeUrl: " + properties.BeforeUrl.ToString());
                SW.WriteLine("SiteId: " + properties.SiteId.ToString());
                SW.WriteLine("ListId: " + properties.ListId.ToString());
                SW.WriteLine("ListItem: " + properties.ListItem.ToString());
                SW.WriteLine("ListTitle: " + properties.ListTitle.ToString());
                SW.WriteLine("ReceiverData: " + properties.ReceiverData.ToString());
                SW.WriteLine("WebUrl: " + properties.ToString());

                SW.Close();
            }
            catch (Exception ex)
            {
                properties.Cancel = true;
                properties.ErrorMessage = ex.Message;
                throw ex; 
            }
            finally
            {
                this.SW.Dispose();
            } 
        }
    }
}

5. Assembly signieren und in den Global Assembly Cache installieren
Dem Assembly muss nun ein Schlüssel zugewiesen werden, damit es strong-named ist und im GAC installiert werden kann. Dazu wird zunächst ein Schlüssel ausgewählt.

Im Anschluss daran kann man das Ergebnis der Kompilierung in den GAC kopieren. Entweder man nutzt dafür den Explorer oder man fügt diesen Task in den PostBuild-Event ein.

"C:\Program Files\Microsoft Visual Studio 8\SDK\v2.0\Bin\GacUtil.exe" -i "$(TargetPath)"

6. Feature definieren
SharePoint-Features benötigen nicht nur die DLL, sondern müssen über Konfigurationsdateien im Ordern "12" noch bekannt gemacht werden. Die benötigte Ordnerstruktur legt man am besten gleich im Projekt an.

Hierbei ist auf die Benennung der Ordner zu achten, da der Ordnername "SharePointEventHandler" im späteren Verlauf noch zur Identifizierung des Features verwendet wird.

Unterhalb dieses Ordners sind zwei XML-Dateien anzulegen:

a) feature.xml
Diese Datei beinhaltet die Hauptkonfiguration des Features. Hier ist eine neue GUID, ein Name und eine Beschreibung zu vergeben.

<?xml version="1.0" encoding="utf-8" ?>
<Feature
   Id="4B2B6531-A043-43f8-B58F-CDC7D35C4517"
   Title="SharePointEventHandler"
   Description="This feature is a dummy event handler."  
   Scope="Web"
   Hidden="FALSE"
   AlwaysForceInstall="TRUE"
   ImageUrl="components.gif" xmlns="http://schemas.microsoft.com/sharepoint/">

    <ElementManifests>
        <ElementManifest Location="elements.xml" />
    </ElementManifests>
</Feature>

b) elements.xml
Diese Datei beinhaltet Detailinformationen zum Feature. Hier sind die Events aufgelistet, für die der EventReceiver registriert wird sowie das Assembly.

<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
    <Receivers ListTemplateId="101">
        <Receiver>
            <Name>SharePointEventHandler</Name>
            <Type>ItemAdded</Type>
            <SequenceNumber>1000</SequenceNumber>
            <Assembly>
                SharePointEventHandler,
                Version=1.0.0.0,
                Culture=Neutral,
                PublicKeyToken=1244abcbb537729f
            </Assembly>
            <Class>SharePointEventHandler.ItemEventReceiver</Class>
            <Data />
        </Receiver>
    </Receivers>
</Elements>

Die ListTemplateId 101 definiert, dass sich der EventReceiver auf Dokumentenbibliotheken bezieht. Unter Assembly ist das eben erstellte Assembly anzugeben. Der PublicKeyToken ist ersichtlich, wenn man sich die Eigenschaften des Assemblies im GAC ansieht.

7. Feature installieren und aktivieren
Die Ordnerstruktur muss nun in den 12-Ordner des SharePoints kopiert werden und im Anschluss daran mit stsadm installiert werden. Das kann einerseits über Windows-Explorer und Kommandozeile oder ebenfalls über das PostBuild-Ereignis vorgenommen werden

cd "$(ProjectDir)"
xcopy "12" "%CommonProgramFiles%\Microsoft Shared\web server extensions\12\" /ys

cd "%CommonProgramFiles%\Microsoft Shared\web server extensions\12\BIN"
stsadm -o installfeature -name SharePointEventHandler -force

iisreset

Nachdem das Feature nun installiert ist, muss es noch aktiviert werden. Auch hier hat man wieder mehrere Möglichkeiten. Entweder über die Kommandozeile mit stsadm und dem Befehl activatefeature oder über das UI des SharePoint-Portals (Site Settings -> Site Administration -> Site Features)

8. Test des Features
Wenn man nun in der SharePoint-Site eine Dokumentenbibliothek anlegt und dieser per "New" oder "Upload" eine Datei hinzufügt, wird eine Textdatei geschrieben.

Erste Schritte in LINQ

Nachdem das .NET-Framework 3.5 nun schon eine Weile auf dem Markt ist hab auch ich mich begonnen mit den Neuerungen zu beschäftigen. Eines der Hauptfeatures ist ja LINQ. Meine ersten Gehversuche hab ich mit einfachen Arrays durchgeführt - und ich muss sagen, dass ich bisher recht angetan bin von dem was ich da an Ergebnissen produzieren konnte.

Mein Array enthält zunächst Elemente vom Typ string und ich möchte beispielsweise alle Elemente ermitteln, die mit "0" beginnen. Hier erstmal der Quellcode dazu:

string[] myarray = new string[] { "10", "01", "25", "02", "03" };
var queryresults = from s in myarray where s.StartsWith("0") select s;
foreach (string queryresult in queryresults)
{
    Debug.WriteLine(queryresult);
}

Interessant hier ist die zweite Zeile. Zunächst betrachten wir mal den Ausdruck hinter dem "=". Abfrageausdrücke müssen mit from beginnen. Am Anschluss daran deklarieren wir eine Bereichsvariable s mit der wir auf die einzelnen Elemente der Liste zugreifen können. Hinter dem where-Schlüsselwort verwenden wir auch gleich diese Variable und prüfen, dass nur die Elemente zurückgegeben werden die mit "0" beginnen. Die Select-Klausel bewirkt in diesem einfachen Beispiel, dass die Elemente einfach zurückgegeben werden.

Wenn man sich nun den Ausdruck vor dem "=" betrachtet, fällt das var-Schlüsselwort auf. Das erinnert im ersten Eindruck stark an nicht typisierte Variablen aus JavaScript und sorgt zunächst für ein ungutes Bauchgefühl. Die Befürchtungen sind aber unnötig. var ist die kurze Schreibweise für IEnumerable<T> und ist damit stark typisiert und zeigt eines der weiteren Features von .NET 3.5, indem an vielen Stellen an denen klar ist, welcher Typ zurückgeliefert wird keine explizite Typangabe à la IEnumerable<string> notwendig ist.

Im zweiten Beispiel möchte ich nur die Elemente haben, die konvertierbar sind in einen int.

object[] myarray = new object[] { "10", 1, 25, 2, "03" };
var queryresults = from s in myarray.OfType<int>() select s;
foreach (int queryresult in queryresults)
{
    Debug.WriteLine(queryresult);
}

Weil strings nicht implizit in int-Werte konvertierbar sind, ist das Ergebnis dieser Abfrage nur 1, 25 und 2.

Aufgabenliste in der Visual Studio IDE aus Kommentaren erzeugen

Heute möchte ich mal auf die Aufgabenliste in der IDE etwas näher eingehen. Hier kann man gut die noch zu erledigenden Aufgaben eintragen, verwalten und abarbeiten.

Weniger bekannt, aber mindestens genauso sinnvoll, ist die Möglichkeit, Aufgabenlisten auch aus Quellcodekommentaren zu erzeugen. Dazu kann man einfach im Kommentar die Schlüsselwörter TODO, HACK oder UNDONE verwenden.


In der Aufgabenliste sieht man diese Aufgaben dann im Bereich Comments.

Zusätzlich zu diesen drei Token kann man unter Tools/Options/Environment/Task List neue Token anlegen. Vorteil dieser selbst angelegten Token: Bei diesen kann die Priorität geändert werden.