19Jun

… oder der lange Weg zum Glück

Manchmal hat man als Sharepoint-Entwickler das Bedürfnis, sich in einer benutzerdefinierten Liste an das Hinzufügen oder Ändern eines Items anzuhängen und die Aktion abzubrechen und dabei eine nette benutzerdefinierte Fehlermeldung ausgeben. Das sollte eigentlich überhaupt kein Problem sein, man erstellt einfach einen EventReceiver für ItemAdding bzw. ItemUpdating und setzt dort die ErrorMessage sowie die Cancel Property:

image

Nun erscheint allerdings leider nicht wie erwartet die benutzerdefinierte Fehlermeldung, sondern stattdessen eine böse Exception (DataFormWebPartException “The data source control failed to execute the insert command”):

clip_image002

Google verhilft uns zur Erkenntnis, dass dies ein Known Bug in WSS 3.0 ist (wird auch durch WSS 3.0 Service Pack 1 nicht behoben). Der Bug tritt offenbar nur in benutzerdefinierten Listen auf. Für dieses Problem ist ein Hotfix verfügbar. Der zugehörige Knowledge Base Artikel incl. Möglichkeit zum Anfordern des Hotfixes findet sich unter http://support.microsoft.com/default.aspx?scid=kb;en-us;949749

Aber Vorsicht! Nachdem man die Hürde genommen hat, die Mail mit dem Hotfix zu erhalten (Tipp: unter Junk E-Mail nachschauen) und frohgemut den Hotfix installiert hat (wobei man natürlich als gelernter Microsoft-Entwickler auch das iisreset nicht vergessen hat), geht hinterher auf dem Sharepoint Server erstmal gar nichts mehr:

image

Abhilfe schafft die Ausführung des Sharepoint Products and Technologies Configuration Wizard. Nach der Ausführung funktioniert nicht nur unser Sharepoint wieder, sondern nun wartet der EventReceiver auch mit unserer benutzerdefinierten Fehlermeldung auf:

image


21Apr

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);
Technorati Tags: , ,

17Apr

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);
Technorati Tags: , , , , ,

26Mrz

Die Standard-MasterPage von SharePoint (default.master) wurde ohne Angabe eines DOCTYPE geschrieben. Dadurch fällt der IE beim Rendern in den Quirks Mode zurück. Entsprechend wurden SharePoint-eigene Funktionalitäten wie das Verschieben eines WebParts auf Grundlage des Quirks Mode geschrieben.

Wenn man sich nun aber an Standards halten, und einen DOCTYPE angeben möchte, bekommt man u.U. Probleme in SharePoint.

So bringt zum Beispiel des Einfügen der Angabe

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

einen Fehler beim Ausführen der JavaScript-Methode “MSOLayout_GetRealOffset” hervor, wenn sich der WebPart in einem relativ positionierten DIV befindet:

Message: Objekt erforderlich
Line: 1572
Char: 3
Code: 0
URI: http://SERVER/_layouts/1031/ie55up.js?rev=Ni7%2Fj2ZV%2FzCvd09XYSSWvA%3D%3D

Mögliche Lösungen für dieses Problem sind:

1. Anpassen der JavaScript-Methode

Dies sollte jedoch nicht direkt in der <12>\TEMPLATE\LAYOUTS\1033\IE55UP.JS geschehen, da es dort durch Updates überschrieben werden kann. Und spätestens beim Deployen auf einer Farm wird es umständlich.

Daher sollte man die Anpassung lieber in seiner Masterpage vornehmen. Dazu überschreibt man einfach die “fehlerhafte” Methode indem man nach SPWebPartManager folgenden Code einfügt:

<script language="javascript" type="text/javascript">
   1: 
   2:     function MSOLayout_GetRealOffset(StartingObject, OffsetType, EndParent)
   3:     {
   4:         var realValue=0;
   5:         if (!EndParent) EndParent=document.body;
   6:         for (var currentObject=StartingObject; currentObject && currentObject !=EndParent && currentObject != document.body; currentObject=currentObject.offsetParent)
   7:         {             var offset = eval('currentObject.offset'+OffsetType);
   8:             if (offset) realValue+=offset;
   9:         }
  10:         return realValue;
  11:     }
</script>

oder 2. Vermeiden von position:relative bei Containern für WebParts

Stattdessen kann man z.B. float verwenden. Dazu gibt es diverse MasterPage-Vorlagen.

oder 3. Entfernen der DOCTYPE-Angabe in der MasterPage

Das ist eine schnelle Lösung, aber kein optimales HTML. Eine Triple-A Conformance für “Premium”-Barrierefreiheit wird man dadurch nicht erreichen können. Andererseits: Die default.master wird ja auch so ausgeliefert… ;-)

Englischsprachige Quellen zu diesem Thema:

Technorati Tags: , , ,

23Jan

Konvertierung von Freemind-Mindmaps in Mindmanager-Mindmaps und umgekehrt

Neben der bekannten, kommerziellen Lösung Mindmanager (von Mindjet) zum Bearbeiten von Mindmaps hat sich mittlerweile ein freies Tool etablieren können. Freemind ist eine Open Source Lösung, lizensiert unter GPL, die viele Funktionen des Mindmanagers abbildet. Die Bedienung von Freemind ist teilweise nicht ganz so komfortabel wie die des Mindmanagers, auch werden Mindmaps nicht automatisch angeordnet, dafür speichert Freemind in weiterverwendbare XML-Dateien und es entstehen keine weiteren Lizenzkosten.

Um Freemind-Mindmaps in Mindmanager-Mindmaps (und umgekehrt) zu konvertieren existieren mehrere Ansätze:

  • Installatation eines Plug-Ins (vorher kostenlose Registrierung notwendig) im Mindmanager, mit dessen Hilfe Mindmaps, die mit Freemind erstellt wurden, geöffnet werden können. Zur Konvertierung in die andere Richtung bietet Freemind die Importfunktion von Mindmanager-Dateien. Achtung: diese Funktion ist erst ab Freemind-Version 0.9 verfügbar.
  • Konvertierung durch XSLTs. Diese Konvertierung ist recht fehleranfällig und umständlich. Durch die neuen Funktionen und Plugins wie oben beschrieben wird dieser Ansatz zur Konvertierung obsolet.
  • Konvertierung ohne zusätzlichen Installationsaufwand, aber etwas „schmutzig“ (Diese Lösung bietet bietet sich vor allem für die schnelle, einmalige Konvertierung an):
    • Export der Mindmap (Alle Zweige müssen beim Export vollständig ausgeklappt sein) mit Freemind in eine HTML-Seite
    • Öffnen und Abspeichern der HTML-Seite in Microsoft Word
    • Import in Mindmanager.

Weil es thematisch gerade passt füge ich an dieser Stelle noch zwei interessante Links zu Mindmapping Tools ein: Artikel vollständig lesen »


08Jan

Dafür gibt es (theoretisch) eine denkbar einfache Lösung: SPListItem bietet eine Methode CopyTo(destinationUrl) an (http://msdn2.microsoft.com/en-us/library/microsoft.sharepoint.splistitem.copyto.aspx) - leider scheint diese aber nicht (in jedem Fall?) zu funktionieren. Zumindest in meinem Fall (benutzerdefinierte Liste mit benutzerdefiniertem Inhaltstyp UND Attachments - vielleicht erwarte ich einfach auch zu viel von Sharepoint…) tat sie es nicht. Stattdessen erhielt ich folgende Exception: “Source item cannot be found. Verify that the item exist and that you have permission to read it.” Eine rasche Recherche bei Google brachte mir die Erkenntnis, dass andere Leute das gleiche Problem auch schon hatten - leider ohne verwertbare Lösungsvorschläge…

Also erstellen wir uns eben selbst eine kleine statische Methode, die das gewünschte tut:

Die Methodensignatur erwartet ein Quellelement und einen Listenname und gibt das kopierte Zielelement zurück:

image

Zuerst erstellen wir das Zielelement in der angegebenen Liste. Dann gehen wir Schritt für Schritt alle Fields des Quellelementes durch und kopieren diese zum Zielelement:

image

Achtung! Wir sollten nicht versuchen, readonly Fields zu kopieren und auch die Attachments lassen sich nicht auf diese Weise “abfertigen”. Diese behandeln wir folgendermaßen:

image

Nun noch schnell das Zielelement gespeichert und zurückgegeben - fertig :-)

image

So könnte z.B. der Aufruf der Methode aussehen:

image

Zum besseren Kopieren hier das Ganze nochmal als Text:

public static SPListItem CopyItem(SPListItem sourceItem, string destinationListName)
{
//copy sourceItem to destinationList
SPList destinationList = sourceItem.Web.Lists[destinationListName];
SPListItem targetItem = destinationList.Items.Add();
foreach (SPField f in sourceItem.Fields)
{
if (!f.ReadOnlyField && f.InternalName != “Attachments”)
{
targetItem[f.InternalName] = sourceItem[f.InternalName];
}
}
//copy attachments
foreach (string fileName in sourceItem.Attachments)
{
SPFile file = sourceItem.ParentList.ParentWeb.GetFile(sourceItem.Attachments.UrlPrefix + fileName);
byte[] imageData = file.OpenBinary();
targetItem.Attachments.Add(fileName, imageData);
}
targetItem.Update();
return targetItem;
}


07Jan

Wenn man in Sharepoint einen benutzerdefinierten Inhaltstyp erstellt und diesen einer Liste zuweist, muss man sich im Idealfall nicht um die Formulare für das Anlegen, Anzeigen und Editieren der Listeneinträge kümmern - dies erledigt Sharepoint anhand der zum Inhaltstyp zugehörigen Spalten freundlicherweise automatisch für uns.

Nun kann es aber passieren, dass wir mit dem von Sharepoint generierten Standardformular nicht zufrieden sind (sei es, weil uns das Layout nicht gefällt oder weil wir z.B. zusätzliche Validierungen und/oder Buttons einbauen wollen…). Auch für diesen Fall wurde vorgesorgt - wir können ein eigenes aspx-Formular erstellen (ja, ich weiß, man könnte auch Infopath nehmen…) und für das Anlegen, Anzeigen oder Editieren der Listeneinträge verwenden. Allerdings habe ich in der Benutzeroberfläche in Sharepoint keine Stelle gefunden, an der man dem Inhaltstyp mitteilen könnte, welche Formulare er jetzt verwenden soll. Hier kann man aber glücklicherweise mit ein paar Zeilen Quellcode (z.B. in einer Konsolenanwendung) Abhilfe schaffen:

image

Es empfiehlt sich, die Formulare unterhalb des Sharepoint LAYOUTS Verzeichnisses (standardmäßig C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\LAYOUTS) abzulegen, dies ist aber nicht erforderlich.

Natürlich kann auch z.B. nur für das Anzeigeformular eine eigene aspx-Datei verwendet werden - dann bleiben die restlichen Properties einfach leer (dies ist der Standard) bzw. werden gar nicht erst gesetzt.

Damit das Ganze nicht zu einfach wird, gibt es zum Schluss noch einen kleinen Pferdefuß: Wenn der Inhaltstyp schon einer Liste zugeordnet ist, muss er von der Liste entfernt und wieder zugeordnet werden, damit die Einstellung in dieser Liste wirksam wird!

Technorati Tags: , , , ,

03Jan

Wer für Sharepoint Web Forms und/oder Controls entwickelt, hatte vielleicht auch schon einmal das Bedürfnis, eine Sharepoint Liste als Datenquelle für z.B. eine RadioButton List zu verwenden. Dies lässt sich glücklicherweise mit geringem Aufwand umsetzen:

Hier ist erst einmal unsere Sharepoint Beispielliste:

image

Und hier die RadioButton List, an die wir die Liste binden wollen:

image image

Wichtig sind hier die Properties DataTextField und DataValueField, die wie angezeigt zu belegen sind.

Nun benötigen wir noch einige wenige Zeilen Quellcode, um den Inhalt der Liste in ein Dictionary einzulesen und dieses als Datenquelle an unsere RadioButton List zu binden:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Text;
   4: using Microsoft.SharePoint;
   5: using System.Web.UI.WebControls;
   6: using System.Web.UI;
   7:
   8: public static class CommonFunctions
   9: {
  10:     public static void BindDataBoundControl(DataBoundControl dataBoundList, SPList list)
  11:     {
  12:          //Einträge aus der Liste in ein Dictionary einlesen
  13:          IDictionary<int, string> listItems = new Dictionary<int, string>();
  14:          foreach (SPListItem item in list.Items)
  15:          {
  16:              listItems.Add(item.ID, item.Title);
  17:          }
  18:          //Dictionary an dataBoundList binden
  19:          dataBoundList.DataSource = listItems;
  20:          dataBoundList.DataBind();
  21:     }
  22: }

Zu guter Letzt noch der Aufruf für unsere Beispielliste:

   1: //"web" ist das SPWeb, in dem sich die Liste befindet
   2: SPList geschlechtList = web.Lists["PersonenGeschlecht"];
   3: CommonFunctions.BindDataBoundControl(Geschlecht, geschlechtList);

Das war’s :-).


21Dez

In verschiedenen Anwendungsfällen kann es vorkommen, dass eine Liste mit den dazugehörigen Einträgen dem Nutzer der Site verborgen bleiben soll. Trotzdem muss der Nutzer einen Workflow mit Instantiation-Form starten können und ein entsprechendes Listenelement anlegen können. Für diesen Beitrag wird von einem vorhandenen Infopath-Formular ausgegangen, welches als Instantiation-Form verwendet wird.

Hier die Schritt-für-Schritt-Anleitung:

1. ASPX-Seite oder Webpart zum Anlegen des List-Items
Zum Anlegen des List-Items, für den der Workflow gestartet werden soll, kann ein einfaches ASPX-Formular oder ein Webpart zum Einsatz kommen. Dem Formular muss als Parameter die Listen-ID mitgegeben werden.

2. Anlegen der Eingabefelder für das Listenelement
Das ASPX-Formular sollte nun alle Datenfelder abfragen und ein neues Listenelement mit entsprechenden Werten anlegen. Dies muss unbedingt mit erhöhten Privilegien geschehen:

SPSecurity.RunWithElevatedPrivileges(delegate()

{

    using (SPSite site = new SPSite(web.Site.ID))

    {

    // implementation details omitted

    }

});

Zu beachten ist, dass keine Kontext-Objekte (SPContext) verwendet werden dürfen, da diese mit den Rechten des aktuellen Nutzers laufen.

3. Weiterleitung zum Instatiation-Form
Nachdem das Item angelegt wurde (Update nicht vergessen), kann der Workflow gestartet werden. Voraussetzung ist, dass dieser bereits mit der Liste verknüpft ist und neue Instanzen gestartet werden dürfen. Der Aufruf des Instantiation-Form erfolgt mittels Response.Redirect. Folgende Url muss übergeben werden:

"_layouts/IniWrkflIP.aspx?List=<listID>&ID=<itemID>&TemplateID=<WFAID>&Source=<webUrl>"

Folgende Parameter müssen ersetzt werden:
<listID> - ID der Liste (siehe 1.)
<itemID> - ID des soeben erzeugten Items (siehe 2.)
<WFAID> - ID der WorkflowAssoziation (diese hängt an der Liste)
<webUrl> - Url an die weitergeleitet wird, nachdem der Workflow gestartet wurde

Den Rest erledigen Infopath und die aufgerufene ASPX-Datei.


26Nov

Das Programm STSADM ist ein sehr nützliches Tool für häufig wiederkehrende Aufgaben auf einem Sharepoint-Server. Einer der großen Vorteile ist es, dass man diese Aufgaben per Kommandozeile und damit z.B. per Batch abarbeiten kann.

An manchen Stellen kommt aber auch der dieses Programm an seine Grenzen. Glücklicherweise kann der beherzte Entwickler hier selbst eingreifen und sich eigene Erweiterungen schreiben, die genau den Bedarfsfall abbilden, der gerade vorliegt.

Die Entwicklung einer eigenen Erweiterung ist gar nicht so kompliziert, wie es sich anhört. Dazu sind folgende Schritte notwendig:

Zunächst erstellt man sich ein neues DLL-Projekt und verlinkt die Microsoft.Sharepoint-Assembly. Jede Klasse, die das ISPStsadmCommand-Interface implementiert, kann als Erweiterung fungieren.

new StsadmCommand

Das Interface beinhaltet 2 Funktionen. Einerseits GetHelpMessage - diese Funktion wird dann aufgerufen, wenn man stsadm -help <befehl> aufruft; andererseits  Run - diese Funktion wird aufgerufen, wenn man stsadm -o <befehl> in der Kommandozeile eingibt.

Implementation ISPStsadmCommand 

Beide Methoden beinhalten einen Parameter command. Dieser beinhaltet zur Ausführungszeit den Befehl - vermutlich aus dem Grund, da eine implementierende Klasse auf mehrere Befehle gemappt werden kann. Das Mapping selbst ist recht einfach. Dazu erzeugt man sich eine neue XML-Datei mit dem Namen stsadmcommands.<eigenername>.xml, die später unter “Gemeinsame Dateien\Microsoft Shared\web server extensions\12\CONFIG” abgelegt wird.

Im Beispiel wird das command copynewfiles auf die eben erstellte Klasse CopyFiles gemappt.

stsadmconfig

Nachdem die DLL nun in den GAC deployed ist, kann der Aufruf erfolgen.

output