In einem größeren SharePoint-Projekt ist unser Engineering vor einigen Tagen auf ein kurioses Verhalten des Search Crawlers gestoßen: Während der größte Teil der SharePoint-Farm korrekt durchsucht wird, bricht der Crawler bei einer einzelnen Site Collection bereits am Anfang mit folgender Fehlermeldung im Crawl-Log ab:
 

Data is Null. This method or property cannot be called on Null values

 
Nachdem das Team nach mehreren Tagen ziemlich jedes eventuelle Problem (fehlerhaftes AAM, falscher Crawl-Account, kaputte Content Source usw.) ausgeschlossen hatte, begann ich das Problem per Reverse Engineering anzugehen. Zunächst lieferte mir das Engineering die Verbose ULS-Logs des Search-Servers mit folgendem Logeintrag, der bei dem Crawl der Site Collection geschrieben wurde:
 

Exception Type: System.Web.Services.Protocols.SoapException *** Message : Exception of type ‘Microsoft.SharePoint.SoapServer.SoapServerException’ was thrown. *** StackTrace:
at System.Web.Services.Protocols.SoapHttpClientProtocol.ReadResponse(SoapClientMessage message, WebResponse response, Stream responseStream, Boolean asyncCall)
at System.Web.Services.Protocols.SoapHttpClientProtocol.Invoke(String methodName, Object[] parameters)
at Microsoft.Office.Server.Search.Internal.Protocols.SiteData.SiteData.GetContent(ObjectType objectType, String objectId, String folderUrl, String itemId, Boolean retrieveChildItems, Boolean securityOnly, String& lastItemIdOnPage)
at Microsoft.Office.Server.Search.Internal.Protocols.SharePoint2006.SiteDataImpl.GetWSSContent(ObjectType objectType, String objectId, String folderUrl, String itemId, Boolean retrieveChildItems, Boolean securityOnly, String& lastItemIdOnPage)
at Microsoft.Office.Server.Search.Internal.Protocols.SharePoint2006.CSTS3Helper.InitSite(String strSiteUrl)
at Microsoft.Office.Server.Search.Internal.Protocols.SharePoint2006.CSTS3Helper.GetSite(String strSiteUrl, sSite3& site)

 
Einen Decompile und eine Codeanalyse später konnte ich sagen, dass das wohl der Versuch des Crawlers war, für die betroffene Site Collection den SiteData-Webservice aufzurufen. Anhand des Timestamps des Fehlers hat nun unser Engineering einen Blick in die IIS-Logs der Webfrontends geworfen. Und tatsächlich: Es gab genau zu diesem Zeitpunkt auf einen Aufruf von SiteData.asmx, der vom Server mit 500 beantwortet wurde.
 
Motiviert von diesem ersten Erfolg seit mehreren Tagen gruben wir also weiter. Zusammen mit der Timestamp und dem IIS-Logeintrag suchten wir auf dem Webfrontend tiefer in den Verbose-Logs und fanden tatsächlich einen weiteren Hinweis:
 

SOAP exception: System.Data.SqlTypes.SqlNullValueException: Data is Null. This method or property cannot be called on Null values.
at System.Data.SqlClient.SqlBuffer.get_String()
at Microsoft.SharePoint.SoapServer.SiteDataImpl.GetSiteGroupsXml(XmlTextWriter writer, SPWeb spWeb)
at Microsoft.SharePoint.SoapServer.SiteDataImpl.GetSiteXml(XmlTextWriter w, SPSite spSite, Boolean fEnumerate, SPChangeToken changeToken, Boolean ignoreSecurityIfInherit)
at Microsoft.SharePoint.SoapServer.SiteDataImpl.GetContent(ObjectType objectType, String objectId, String folderUrl, String itemId, Boolean retrieveChildItems, Boolean securityOnly, String& lastItemIdOnPage)
at Microsoft.SharePoint.SoapServer.SiteData.GetContent(ObjectType objectType, String objectId, String folderUrl, String itemId, Boolean retrieveChildItems, Boolean securityOnly, String& lastItemIdOnPage)

 
Nun wurde die Sache richtig interessant, da einerseits die Fehlermeldung genau mit dem Eintrag in den Crawl-Logs übereinstimmte und der Methodenaufruf “GetSiteGroupsXml” nun auch einen konkreten Hinweis darauf, dass es wohl etwas mit den Gruppen der Site Collection zu tun hatte.
 
Ein weiterer Decompile der entsprechenden Assembly brachte folgendes Fragment zum Vorschein:
 

while (sqlDataReader.Read())
{
int num = 0;
SiteDataImpl.GroupInfo groupInfo = new SiteDataImpl.GroupInfo();
groupInfo.ID = sqlDataReader.GetInt32(num++);
groupInfo.Name = sqlDataReader.GetString(num++);
groupInfo.Description = sqlDataReader.GetString(num++);
groupInfo.OwnerId = sqlDataReader.GetInt32(num++);
groupInfo.OwnerIsUser = sqlDataReader.GetBoolean(num++);
dictionary[groupInfo.ID] = groupInfo;
}

 
Nun Watson… was sagt uns das? “Data is Null” in Kombination mit GetString und einem fehlenden IsDBNull lässt eigentlich nur noch den Schluss zu, dass im entsprechenden Datenbankfeld null eingetragen ist und damit eine SqlNullValueException auslöst. Folgendes kurzes PowerShell-Script bestätigte diese Vermutung für die betroffene Site Collection:
 

$web = Get-SPWeb http://url.to.root.web
$web.SiteGroups |? { $_.Description -eq $null }

 
Und da war sie: Die Wurzel allen Übels in Form einer programmatisch angelegten SharePoint-Gruppe namens “Externe Mitarbeiter”. Eine unserer programmatischen Lösungen legt die Gruppe über folgendes Codefragment neu an:
 

web.SiteGroups.Add(groupName, owner, null, null)

 
Weder die SharePoint-API noch die Benutzeroberfläche haben ein Problem damit, dass als Beschreibung der Gruppe null gesetzt wird. Lediglich der SiteData-Webservice läuft durch diesen Wert (für mich ein klarer Bug in der SiteData-Implementierung) in einen Fehler und bricht somit auch den Crawl für die Site Collection ab.
 
Die letzte und entscheidende Frage nun noch: Wie behebt man das Problem? Für die Außenstehenden mag es wie Magie ausgehsehen haben, als der Entwickler das tagelang bekämpfte Problem mit einigen wenigen PowerShell-Befehlen endgültig erlegte:
 

$web = Get-SPWeb http://url.to.root.web
$groups = $web.SiteGroups |? { $_.Description -eq $null }
$groups |% { $_.Description = “”; $_.Update() }

 
Crawl gestartet – Crawl lief durch – alle sind glücklich. Ach ja, Microsoft … da fehlt ein IsNull-Check in der API oder dem Webservice.

Custom Actions und der ListViewWebPart

Veröffentlicht: 16. Juni 2012 in SharePoint

Ich hatte heute ein kurioses Verhalten beim Anpassen des Ribbons. Ziel war es, der Ribbon-Gruppe “Actions” (LocationId Ribbon.ListItem.Actions) in Listen des Typs “Contacts” (RegistrationId 105) einen neuen Button “Anrufen” unterzuschieben. Der Button war schnell hinzugefügt und auch die Logik zum Aufbau des Anrufs über die Sipgate API funktionierte nach anfänglichen Schwierigkeiten.

Allerdings viel mir später auf, dass der Ribbon-Button zwar angezeigt wird, wenn ich mich direkt in der Liste befinde, jedoch nicht, wenn ich auf der Startseite über einen ListViewWebPart einen Kontakt auswähle. Nach über einer Stunde Debugging und Ausprobieren fand ich schließlich die Lösung:

Aus einem Grund, der sich mir nicht ganz erschließt, scheinen solche Anpassungen am Ribbon nur dann angezeigt zu werden, wenn in den WebPart-Einstellungen des ListViewWebParts der gewählte Toolbar Type (dt. Symbolleistentyp) auf “Full Toolbar” (dt. “Vollständige Symbolleiste”) umgestellt wird. Wird ein ListViewWebPart hinzugefügt, so werden Anpassungen zunächst nicht angezeigt, da die Standardeinstellung des WebParts dies nicht für notwendig hält.

Was man sich dabei wohl gedacht hat?

Als SharePoint-Dienstleister entwickeln wir für unsere Kunden Lösungen und stellen je nach Anforderung Benutzeroberflächen verschiedener Komplexität bereit. Seit SharePoint 2010 setzten wir inzwischen auch Silverlight verstärkt als UI-Technologie ein.

Die Interaktion zwischen SharePoint und Silverlight findet normalerweise über das Silverlight Client Object Model statt. Wie auch sonst in Silverlight spielt das Model-View-ViewModel Pattern eine tragende Rolle, stellt den Entwickler jedoch gerade bei komplexeren Anwendungen vor besondere Herausforderungen. Zu diesem Thema, den Problemen und einer möglichen Lösung – dem asynchronen ViewModel – werde ich in den nächsten Wochen einen Artikel verfassen.

In diesem Artikel möchte ich auf eine Möglichkeit der Interaktion zwischen Silverlight und der SharePoint-Benutzeroberfläche hinweisen. Im Gegensatz zum Silverlight ClientOM bietet das ECMA ClientOM direkten Zugriff auf die neuen Steuerelemente wie Notifications oder das Dialog-Framework.

Der Aufruf bzw. die Ausführung von JavaScript wird durch die Klasse HtmlPage im Namespace System.Windows.Browser ermöglicht. Durch HtmlPage.Window.Eval(“JAVASCRIPT”) wird der Code direkt im Kontext der aktuellen Website ausgeführt, d.h. alle verfügbaren JavaScript-Methoden und Objekte können angesprochen werden. In unserem Fall ebenfalls die Methoden der Datei ECMA-Scriptbibliothek “SP.js”.

Folgende kleine Zeile im Click-Handler des Speichern-Button führt zu einem Notification-Popup (siehe Screenshot) wie wir es von der restlichen SharePoint-UI her kennen:

HtmlPage.Window.Eval(“SP.UI.Notify.addNotification(‘Aufgabe 1 als Entwurf gespeichert’, false);”);

Der Zugriff auf andere Steuerelemente wie ModalDialog oder PopupMenu funktionieren analog. Die Kombination aus JavaScript und Silverlight bietet gerade unter Hinzunahme von Bibliotheken wie SP.js oder jQuery eine Vielzahl neuer Möglichkeiten, mit SharePoint zu interagieren bzw. die Benutzeroberfläche zu erweitern.

Ich bin letzte Woche über einen äußerst unerfreulichen Bug in der SharePoint-Oberfläche gestoßen: Bei langen Listenformularen führt ein Klick auf “Datei anfügen” dazu, dass die Scrollbar nicht korrekt aktualisiert wird, wenn man in das Listenformular zurückkehrt. Will man bei einem langen Formular nach ganz nach unten scrollen stellt man fest, dass die letzten Zeilen unter dem Browserrand verschwinden. Verändert man die Größe des Browserfensters wird auch die Scrollbar wieder korrekt berechnet, da dadurch anscheinend ein Resize des s4-workspace-Elements ausgelöst wird.

Die Ursache dafür liegt offensichtlich in einem Fehler in init.js (14/TEMPLATE/LAYOUTS/1031/init.js), da diese beim Öffnen und Schließen des “Datei anfügen”-Dialogs einen JavaScript-Fehler produziert. Wir haben uns das Problem in init.debug.js entsprechend angesehen und mussten leider feststellen, dass wir es nicht direkt fixen können, da an dieser Stelle die benötigten Daten fehlen (genauer gesagt ist window.FrameElement fälschlicherweise null).

Allerdings konnten wir uns mit einem kleinen Workaround behelfen. Es genügt wenn man auf dem Listenformular (z.B. durch einen Inhaltseditor-WebPart bzw. CEWP oder direkt durch SharePoint Designer) folgendes JavaScript einfügt:

<script>
var workspace = document.getElementById(“s4-workspace”);
workspace.onmouseover = function() { FixRibbonAndWorkspaceDimensions(); };
</script>

Dieses Skript führt dazu, dass bei die Größe von s4-workspace immer dann korrigiert wird, wenn man mit der Maus darüber geht. Die Funktion “FixRibbonAndWorkspaceDimensions” wird von SharePoint 2010 bereitgestellt und führt das aus, was man bei dem Namen auch erwarten darf.

Ich hoffe, dass dieser “Dirty-Hack” mit dem nächsten Update nicht mehr nötig ist.

Ich beschäftige mich derzeit mit der Konvertierung von OfficeOpenXML-Dateien (z.B. DOCX von Word 2007/2010) in PDF-Dateien. Im Rahmen eines kleinen Prototypen habe ich ein PowerShell-Skript geschrieben, welches mittels COM und dem Bullzip PDF Printer für eine festgelegte DOCX-Datei eine PDF-Datei erstellt.

Der Bullzip PDF Printer hat zwei Vorteile: Zunächst ist er bis 10 Benutzer sowohl kommerziell als auch privat kostenlos, was für meinen Anwendungsfall wichtig ist. Ausserdem hat er eine dokumentierte API, mit der es möglich wird die Komponente aus .NET heraus zu konfigurieren.

Da für die Umwandlung sowohl Word als auch Bullzip benötigt werden, habe ich mir in einer virtuellen Maschine eine kleine Testumgebung geschaffen. Da in der Dokumentation in einem kleinen Beispiel gezeigt wird, wie man mittels .NET, Interop und der Bullzip-Assembly aus C# heraus eine PDF-Datei erzeugt, kam mir die Idee dies testweise in PowerShell zu umzusetzen. Das folgende Skript kann entsprechend verwendet werden, sofern Word und Bullzip korrekt installiert wurden.

[void][System.Reflection.Assembly]::LoadWithPartialName(“Bullzip.PdfWriter”);

# Specifies the input and output path
$fileIn = “C:\\Users\\Testuser\\Desktop\\User_Guide_de-DE.docx”;
$fileOut = “C:\\Users\\Testuser\\Desktop\\out.pdf”;

# Creates and sets up Word application object
$wordApp = new-object -ComObject word.application
$wordApp.ActivePrinter = “Bullzip PDF Printer”;
$wordApp.Visible = $false;

# Opens word document
$document = $wordApp.Documents.Open($fileIn);

# Setup Bullzip printer
$settings = new-object Bullzip.PdfWriter.PdfSettings;
$settings.PrinterName = “Bullzip PDF Printer”;
$settings.SetValue(“Output”, $fileOut);
$settings.SetValue(“ShowPDF”, “no”);
$settings.SetValue(“ShowSettings”, “never”);
$settings.SetValue(“ShowSaveAS”, “never”);
$settings.SetValue(“ShowProgress”, “no”);
$settings.SetValue(“ShowProgressFinished”, “no”);
$settings.SetValue(“ConfirmOverwrite”, “no”);
$settings.WriteSettings([Bullzip.PdfWriter.PdfSettingsFileType]::RuneOnce);

# Sends document to spooler
$document.PrintOut();
# Closes document to prevent SaveAs dialog
$document.Close();

# Closes and cleans up Word application
$wordApp.Quit();
$wordApp = $null;
[gc]::collect();
[gc]::WaitForPendingFinalizers();

Das Skript lässt sich auch für Excel oder andere Office-Produkte verwenden, wenn man das entsprechende Application-Objekt erzeugt.

Ich bin seit heute an einem Problem, was mich wieder einige Nerven gekostet hat. Auf zwei getrennten SharePoint 2010 Installationen habe ich versucht aus einem Besprechungsarbeitsbereich eine brauchbare Website-Vorlage zu erstellen.

Wird in einem Kalender ein Termin erstellt und aus der Vorlage ein neuer Arbeitsbereich für diesen Termin, so funktioniert dies auch. Lege ich aber nun einen weiteren Termin an und verknüpfe diesen ebenfalls mit dem zuvor erstellten Arbeitsbereich wird zwar die Termin-Auswahl auf der linken Seiten angezeigt, doch funktioniert diese nicht.

Dies äußert sich dadurch, dass ein Klick auf einen Termin nicht etwa zum Wechsel auf die entsprechende Instanz führt, sondern zu einem JavasScript-Fehler: ‘g_thispagedata’ is undefined. Nachdem ich alles aufgefahren habe, was mir zum Thema JavaScript- und SharePoint-Debugging eingefallen ist, habe ich inzwischen eine recht gute Vorstellung, wo des Problem liegt.

Der WebPart, welcher für die Navigation zwischen den Meetings zuständig ist, ruft bei einem Klick auf ein Datum eine Funktion auf, die per JavaScript die URL mit den entsprechenden Parametern (z.B. “InstanceID”)  zusammenbaut. Die Funktion versucht dabei auf die Variable g_thispagedata zuzugreifen und genau da liegt das Problem. Aus einem mir unbekannten Grund (möglicherweise ein Bug) ist diese Variable bei Arbeitsbereichen, die aus Vorlagen erzeugt wurden nicht belegt. Arbeitsbereich die über die Weboberfläche als neue Webseite angelegt wurden, setzten g_thispagedata korrekt.

Um das Problem zu umgehen habe ich mir einen kleinen Workaround ausgedacht: Eigentlich genügt es, wenn die Variable g_thispagedata nicht undefined ist. Erreichen kann man das, indem man einen kleinen Content Editor WebPart (dt. Inhalts-Editor WebPart) auf dem Arbeitsbereich platziert und als HTML-Content folgendes Skript setzt:

<script>var g_thispagedata = "";</script>

Zwar fehlen dadurch die URL-Parameter, die normalerweise durch g_thispagedata gesetzt werden sollten, aber zumindest funktioniert die Auswahl der Instanzen wieder.

Ich hatte gestern wieder die Freude mich mit einem nervigen Bug in SharePoint Foundation 2010 herumzuschlagen. Das derzeitige Projekt umfasst einen Workflow, der unter anderem Aufgaben an einzelne Anwender verteilt. Bewerkstelligt wird dies durch die Aktivität “CreateTaskWithContentType” und einem entsprechenden Content Type mit einem angepassten Formular.

Um dem Anwender eine möglichst klare Beschreibung zu geben, was er zu tun hat, führen wir zusätzliche eine Anpassung von

CreateTaskWithContentType.TaskProperties.EmailBody

durch. Bis hierhin ist die Sache auch nicht sonderlich spannend, allerdings fiel uns dann auf, dass die von SharePoint generierte Aufgaben-Benachrichtigung einen unschönen Bug enthält.

Bei unserer SPF2010-Installation führt ein Klick auf die markierte Schaltfläche dazu, dass eine nette und absolut nutzlose Fehlermeldung erscheint anstatt unser Aufgabenformular zu öffnen. Eine kurze Google-Suche nach “SharePoint Open this task” ergab, dass es sich offensichtlich um einen Bug in SPF2010 handelt. Die angebotenen Lösungen reichten dabei von einem Registry-Hack über einen Hotfix, einer Anpassung des Exchange-Servers und der Aussage von Microsoft “Benutzen Sie die Schaltfläche nicht, sondern verwende den Link im E-Mail-Body”.

Nachdem alle Lösungsvorschläge fehlgeschlagen waren und es sich zeigte, dass die Schaltfläche selbst bei einfachen Designer-Workflows nicht funktionierte, machten wir uns einige Gedanken wie man das Problem umgehen könnte. Die Überlegung ging dabei in die Richtung, die bestehende Schaltfläche durch einen Link auf das Formular in unserem angepassten E-Mail-Body zu ersetzen. Mehrere Stunden SharePoint-API und .NET Reflector später hatten wir schließlich eine Lösung, die “Diese Aufgabe öffnen…” ersetzen konnte.

Das Problem, vor dem wir standen war allerdings, dass wir mindestens die ListItemID der Aufgabe benötigten um einen funktionierenden Link zu erzeugen. Diese steht allerdings innerhalb des MethodInvoking-Handlers von CreateTask und CreateTaskWithContentType noch nicht zur Verfügung, sondern wird erst danach mit der Erstellung der Aufgabe generiert.

Folgender kleiner Code-Ausschnitt innerhalb des MethodInvoking-Handlers von CreateTask bzw. CreateTaskWithContentType brachte schließlich die Lösung, bereits vor der Erstellung der Aufgabe durch die Aktivität an die ID des zu erzeugenden Items zu gelangen.

MethodInfo[] methods = typeof(SPWorkflow).GetMethods(
BindingFlags.Instance | BindingFlags.NonPublic);
MethodInfo method = methods.First<MethodInfo>(curMethod =>
(curMethod.Name == “GetReservedItemId”) &&
(curMethod.GetParameters().Length == 2));
int itemID = (int)method.Invoke(this.workflowProperties.Workflow,
new object[] { this.workflowProperties.TaskList, taskId });

Dieser kleine Block ruft unter Verwendung von LINQ und Reflection die Methode GetReservedItemId der aktuellen SPWorkflow-Instanz auf. Die aufgerufene Methode reserviert für die angegebene TaskID (diese wird innerhalb des MethodInvoking-Handlers erstellt) in der Workflow-Aufgabenliste eine freie ListItemID und gibt diese zurück. Im Gegensatz zu anderen Lösungen, die sich über SQL-Queries oder der Abfrage der Aufgabenliste die nächste Item-ID berechnen, greift dieser Ansatz auf etwas zurück, was CreateTask und CreateTaskWithContentType sowieso später tun würden.

Ein Blick mit dem .NET Reflector in SPWorkflow zeigt, dass die Methode von CreateTask und CreateTaskWithContentType  dazu verwendet wird, eine neue ListItemID für die angegebene TaskID zu erstellen sofern dies nicht bereits geschehen ist. Der obige Code-Ausschnitt tut also nichts anderes, als die Reservierung der ListItemID etwas vorzuziehen und diese so bereits innerhalb des MethodInvoking-Handlers verfügbar zu machen. Aus der so erhaltenen ID und den restlichen Informationen aus SPWorkflowActivationProperties ist es daraufhin kein Problem mehr, einen Link auf die Formulare für die zu erstellende Aufgabe zusammenzubauen und diese in den angepassten E-Mail-Body der Aufgabe zu platzieren.