Beiträge von Martin Hey

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: , , , , ,

28Feb

Hin und wieder kommt es vor, dass man Dateien im BLOB-Format in einer Tabelle ablegt. Wenn man nun “schnell” auf diese Daten zugreifen will, steht man vor einem Problem, denn der Standard-SQL-Befehlssatz bietet keine Möglichkeit, Spalten mit Binärdaten zu füllen bzw. diese Daten wieder auszulesen und in eine Datei zu speichern.

Nun hat man nicht immer eine Entwicklungsumgebung zu Hand, um ein Programm zu schreiben, das per ADO oder ADO.NET diese Spalten befüllt. Auch der SQL-Server selbst bietet Möglichkeiten, um hier z.B. mittels Management-Studio tätig zu werden. Im Folgenden beschreibt ein Beispiel das generelle Vorgehen:

Als vorbereitende Maßnahme legt man eine Datenbank namens [pictures] an, die eine Tabelle mit dem Namen [images] enthält. Diese Tabelle enthält einfachheitshalber nur einen Primärschlüssel und das eigentliche Datenfeld. Wichtig hierbei ist, dass das Datenfeld den Datentyp [varbinary](max) hat.

CREATE DATABASE pictures
GO
USE pictures
GO  

CREATE TABLE [dbo].[images](
[imageid] [int] IDENTITY(1,1) NOT NULL,
[imageblob] [varbinary](max) NOT NULL
PRIMARY KEY CLUSTERED ([imageid]))
GO

Nun wird die Tabelle mittels INSERT befüllt. Mittels OPENROWSET in Kombination mit der Option BULK ist es möglich, Spalteninhalte als Binärstrom aus einer Datei (im Beispiel: bild1.jpg) zu lesen.

INSERT [dbo].[images]([imageblob])
SELECT BulkColumn
FROM OPENROWSET( BULK 'C:\bild1.jpg',
SINGLE_BLOB) as ExternalFile
GO

Führt man nun einen SELECT auf die Tabelle aus, sieht man, dass die Binärdaten gespeichert wurden.

image

Der umgekehrte Weg ist leider nicht ganz so einfach. Hierzu muss man das Programm bcp bemühen. Um bcp per SQL aufzurufen, muss der Server konfiguriert werden, dass Shell-Aufrufe möglich sind. Auch das ist (notwendige Berechtigungen vorausgesetzt) per SQL-Statement möglich:

EXECUTE sp_configure 'show advanced options', 1
RECONFIGURE WITH OVERRIDE
GO
EXECUTE sp_configure 'xp_cmdshell', '1'
RECONFIGURE WITH OVERRIDE
GO
EXECUTE sp_configure 'show advanced options', 0
RECONFIGURE WITH OVERRIDE
GO

Für den eigentlichen Export ist noch eine Konfigurationsdatei erforderlich. Diese beinhaltet die Formatbeschreibung. Wird diese nicht verwendet hat die resultierende Datei zwar die passende Größe, kann aber nicht gelesen werden, da bcp beim Export einige automatische Zeichenersetzungen vornimmt. Die Formatdatei kann auf Kommandozeilenebene mit bcp erstellt und dann angepasst oder direkt mittels Editor erstellt werden.

image

Die resultierende Datei sollte final wie dargestellt aussehen:

image

Nun kann mittels eines BCP-Befehls eine Datei erstellt werden. Wichtig hierbei ist, dass immer nur 1 Spalte und eine Zeile ausgegeben wird, da a) nur eine Datei geschrieben wird und b) dies auch in der eben erstellten Formatdatei so beschrieben ist.

exec master..xp_cmdshell ‘bcp “select [imageblob] from [pictures].[dbo].[images] where imageid = 1″ queryout C:\kopiebild1.jpg -T -S . -f c:\imageblob.fmt’

image

Die Datei liegt nun als kopiebild1.jpg wieder vor.

Technorati Tags: , , , , ,

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


06Nov

In ASP.NET gibt es die doch sehr nützlichen Validator-Controls. Diese bieten die Möglichkeit, Werte von Web-Formularen zu prüfen. Besonders sinnvoll ist aus meiner Sicht die clientseitige Valisierung, weil hier Postbacks zum Server schon im Vorab unterbunden werden. Allerdings gibt es hier besonders beim CustomValidator einiges zu beachten, worauf ich in diesem Blog-Eintrag etwas eingehen möchte.

Mein konkreter Anwendungsfall war, zu prüfen, ob in einem TreeView mit Checkboxen mindestens 1 Knoten angehakt ist.

Wenn man sich die verfügbaren Eigenschaften des CustomValidators anschaut, ist man versucht
a) in ControlToValidate den Namen des zu validierenden Controls anzugeben und
b) unter ClientValidationFunction eine JavaScript-Funktion anzugeben, die true zurückgibt, wenn die Validierung erfolgreich war und false, wenn nicht

Das funktioniert aber leider nicht :(

Wichtig sind folgende Sachverhalte:

  1. Die Signatur der JavaScript-Funktion:
    Es wird eine Funktion benötigt, die 2 Parameter erwartet. Der erste Parameter (sender) ist der Validator selbst, der uns hier erstmal nicht weiter interessiert. Der zweite Parameter (args) sind die EventArgs. Und hier müssen wir einhaken, denn diese haben eine Eigenschaft IsValid, die im Verlauf unserer Funktion auf true bzw. auf false gesetzt werden kann.
  2. Kein ControlToValidate festlegen. Der Zugriff auf die Controls erfolgt innerhalb der JavaScript-Funktion

Hier die Beispiel-JavaScript-Funktion:

<script type=”text/javascript”>
function ValidateTreeview(src, args)
{
args.IsValid = IsTreeviewNodeChecked()
}
function IsTreeviewNodeChecked()
{
var treeView = document.getElementById(‘<% =MyTreeView.ClientID %>’);
var checkboxes = treeView.getElementsByTagName(‘input’);
for (var i=0; i<checkboxes.length;i++)
{
if (checkboxes[i].checked)
{
return true;
}
}
return false;
}
</script>

Dem CustomValidator muss nun nur ValidateTreeview als ClientValidationFunction und eine passende ErrorMessage hinterlegt werden und fertig ist die clientseitige Validierung.

Technorati Tags: ,

02Okt

Eine Konfigurationsdatei für Business Data Catalogs ist eine XML-Datei, die dem Schema bdcmetadata.xsd entspricht. Zum Binden der XSD-Datei an die neue XML-Datei gibt es mehrere Möglichkeiten:

Wenn man die Datei im Visual Studio entwickeln möchte kopiert man am besten die Datei bdcmetadata.xsd in die Visual Studio 2005 Schema Library. Die Datei ist im Order Program Files\Microsoft Office Servers\12.0\Bin zu finden und sollte in den Ordner Program Files\Microsoft Visual Studio 8\Xml\Schemas kopiert werden. Alternativ kann die Datei auch direkt von den Microsoft-Servern heruntergeladen werden (http://schemas.microsoft.com/office/2006/03/BusinessDataCatalog/BDCMetadata.xsd) oder die XML-Datei an die Datei unter der angegebenen URL gebunden werden.

Im Visual Studio wird nun eine neue XML-Datei generiert. In den Dokumenteigenschaften wird die XML-Datei an das Schema bdcmetadata.xsd gebunden.

Zudem sollte das Schema auch als Attribut des LobSystem-Knotens angegeben werden. Hier ist es dann sinnvoll, gleich die Entsprechung auf den Microsoft-Servern zu verwenden.

<LobSystem
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://schemas.microsoft.com/office/2006/03/BusinessDataCatalog BDCMetadata.xsd"
    Type="Database"
    Version="1.0.0.0"
    Name="DocumentArchive"
    xmlns="http://schemas.microsoft.com/office/2006/03/BusinessDataCatalog">
</LobSystem>

Der im Attribut Name angegebene Name wird dann später im Sharepoint als Name der Applikation verwendet.

Als nächstes müssen die Verbindungseigenschaften festgelegt werden. Hierzu legt man als Childnode von LobSystem einen Knoten LobSystemInstances an, der die Verbindungseinstellungen zur Datenbank enthält.

<LobSystemInstances>
    <LobSystemInstance Name="ArchiveDatabase">
        <Properties>
            <Property Name="AuthenticationMode" Type="System.String">PassThrough</Property>
            <Property Name="DatabaseAccessProvider" Type="System.String">SqlServer</Property>
            <Property Name="RdbConnection Data Source" Type="System.String">DEV-EV-01</Property>
            <!– The name of your server hosting the database–>
            <Property Name="RdbConnection Initial Catalog" Type="System.String">Archive</Property>
            <!– The name of the database. –>
            <Property Name="RdbConnection Integrated Security" Type="System.String">SSPI</Property>
        </Properties>
    </LobSystemInstance>
</LobSystemInstances>

Im Knoten LobSystem\Entities werden die verschiedenen Entitäten festgelegt, die abrufbar sind. In der Entität wird eine Methode definiert, die für das Abrufen der Daten verantwortlich ist.

<Entities>
    <Entity Name="Document">
        <Methods>
            <Method Name="GetDocuments">
                <Properties>
                    <!– add properties here –>
                </Properties>
                <Parameters>
                    <!– add parameters here –>
                </Parameters>
                <MethodInstances>
                    <!– add method instances here –>
                </MethodInstances>
            </Method>
        </Methods>
    </Entity>
</Entities>

Dabei bestimmt die Eigenschaft RdbCommandText das SQL-Statement zum Abrufen der Daten. Hierbei ist darauf zu achten, dass Sonderzeichen wie „>“ und „<“ zu codieren sind (&lt; bzw. &gt;).

<Properties>
    <Property Name="RdbCommandText" Type="System.String">
        SELECT [ID]
        ,[OriginalPath]
        ,[OriginalFileName]
        ,[Size]
        FROM [dbo].[Documents]
        WHERE [ID] &gt; 0
    </Property>
    <Property Name="RdbCommandType" Type="System.Data.CommandType">Text</Property>
</Properties>

Unter dem Knoten Parameters der Methode ist nun der Rückgabetyp zu deklarieren. Bei Datenbankabfragen handelt es sich hier um einen DataReader (= Collection), der DataRecords enthält. Unterhalb des DataRecords werden die einzelnen Rückgabespalten deklariert.

<Parameters>
    <Parameter Direction="Return" Name="Documents">
        <TypeDescriptor TypeName="System.Data.IDataReader, System.Data, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" IsCollection="true" Name="DocumentDataReader">
             <TypeDescriptors>
                <TypeDescriptor TypeName="System.Data.IDataRecord, System.Data, Version=2.0.3600.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" Name="DocumentDataRecord">
                    <TypeDescriptors>
                        <TypeDescriptor TypeName="System.Int32" Name="ID"/>
                        <TypeDescriptor TypeName="System.String" Name="OriginalPath"/>
                        <TypeDescriptor TypeName="System.String" Name="OriginalFileName"/>
                        <TypeDescriptor TypeName="System.Int32" Name="Size"/>
                    </TypeDescriptors>
                </TypeDescriptor>
            </TypeDescriptors>
        </TypeDescriptor>
    </Parameter>
</Parameters>

Für die einzelnen Spalten können jetzt noch Anzeigenamen deklariert werden. Hierzu deklariert man unterhalb des TypeDescriptors für die jeweilige Spalte jeweils einen LocalizedDisplayName für die gewünschte Sprache.

<TypeDescriptor TypeName="System.String" Name="OriginalPath">
    <LocalizedDisplayNames>
        <LocalizedDisplayName LCID="1033">Path to File</LocalizedDisplayName>
        <LocalizedDisplayName LCID="1031">Pfad zur Datei</LocalizedDisplayName>
    </LocalizedDisplayNames>
</TypeDescriptor>

Nun wird noch ein Knoten MethodInstances erzeugt, der den Finder deklariert. Dieser ist notwendig, damit Instanzen der Entities erzeugt werden können. Sharepoint benutzt die Finder-Methode, um alle Instanzen der Entitiy abzurufen und die SpecificFinder-Methode um bestimmte Entities abzurufen. Bei der Deklaration bezieht man sich auf die eben deklarierten Rückgabewerte.

<MethodInstances>
    <MethodInstance Name="DocumentFinderInstance" Type="Finder" ReturnParameterName="Documents" ReturnTypeDescriptorName="DocumentDataReader"/>
    <MethodInstance Name="DocumentSpecificFinderInstance" Type="SpecificFinder" ReturnParameterName="Documents" ReturnTypeDescriptorName="DocumentDataReader"/>
</MethodInstances>

Damit der SpecificFinder die Datensätze auch finden kann, muss noch der Identifier der Entity deklariert werden. Der Identifier ist in der Regel der Primärschlüssel der Tabelle.

<Identifiers>
    <Identifier Name="ID" TypeName="System.Int32" />
</Identifiers>

Dem entsprechenden Feld im Resultset wird nun noch die Eigenschaft IdentifierName zugewiesen und damit eine Referenz erzeugt. Damit sieht nun der TypeDescriptor-Tag wie folgt aus:

<TypeDescriptor TypeName="System.Int32" IdentifierName="DocumentID" Name="ID">
    <LocalizedDisplayNames>
        <LocalizedDisplayName LCID="1033">Document ID</LocalizedDisplayName>
        <LocalizedDisplayName LCID="1031">Dokument Nummer</LocalizedDisplayName>
    </LocalizedDisplayNames>
</TypeDescriptor>

Wenn man die Konfigurationsdatei für die Applikation nun mal hochlädt kann man sich das Ergebnis in einer Business Data List anschauen.

image 

Die Elemente sind schon sortier- und filterbar.

Um die Filtermöglichkeiten als Eingabefelder freizuschalten sind noch einige Änderungen an der Konfiguration notwendig.

Im Beispiel soll eine Like-Suche auf den Dokument-Namen ermöglicht werden. Zunächst wird der SQL-Command angepasst, so dass hier Parameter angenommen werden.

<Property Name="RdbCommandText" Type="System.String">
    SELECT [ID]
    ,[OriginalPath]
    ,[OriginalFileName]
    ,[Size]
    FROM [dbo].[Documents]
    WHERE [OriginalFileName] LIKE @Name
</Property>

Im Anschluss daran muss das Wildcard-Zeichen des Backends festgelegt werden. Das Frontend-Wildcard-Zeichen „*“ wird durch dieses Zeichen ersetzt. Im SQL-Server ist das „%“. Die entsprechende Einstellung nimmt man in den Eigenschaften des LobSystems vor.

<Properties>
    <Property Name="WildcardCharacter" Type="System.String">%</Property>
</Properties>

Nun erfolgen noch einige Anpassungen an der Methode, die die Daten liest. Zunächst wird der im SQL-Statement verwendete Parameter als Eingabeparameter bekannt gegeben. Hierzu fügt man einen weiteren Parameter-Tag im Parameters-Tag der Methode ein. Für jeden Finder wird ein Standardwert für diesen Parameter festgelegt.

<Parameter Direction="In" Name="@Name">
    <TypeDescriptor TypeName="System.String" AssociatedFilter="Name" Name="Name">
        <DefaultValues>
            <DefaultValue MethodInstanceName="DocumentFinderInstance" Type="System.String">%</DefaultValue>
            <DefaultValue MethodInstanceName="DocumentSpecificFinderInstance" Type="System.String">%</DefaultValue>
        </DefaultValues>
    </TypeDescriptor>
</Parameter>

Der Parameter bezieht sich auf den Filter mit dem Namen „Name“, der bisher noch nicht existiert. Die Filter werden im Unterknoten FilterDescriptors der Methode definiert.

<FilterDescriptors>
    <FilterDescriptor Type="Wildcard" Name="Name">
        <Properties>
            <Property Name="UsedForDisambiguation" Type="System.Boolean">true</Property>
        </Properties>
    </FilterDescriptor>
</FilterDescriptors>

Als Ergebnis erhält man bei der Anzeige des Webparts jetzt eine Eingabemaske in der man nach dem eben definierten Filter filtern kann.

image