Montag, 5. Oktober 2009

Prevent expanding grouping in listviews

Recently I got the requirement from a customer to deactivate the auto-expanding of groups in a single AllItems.aspx-Default-Listview that was grouped and should show at least 700 items per page.
First attempts where in vain to deactivate viewStates from the control or change webpart-attributes. A more promising approach seemed to be deleting browser-cookies. Reloading Firefox after deleting cookies worked like a charm but I've got no success to reproduce this behaviour by code.

So I had to try another way and began to debug the SharePoint-JavaScript by using Firebug. That was a good decision because I discovered this method:

ProcessDefaultOnLoad(_spBodyOnLoadFunctionNames);

In here a simple array is iterated over that's items are getting executed by eval-function.
One of the array-items is "ExpGroupOnPageLoad". This function cares for expanding each group if it's found in the state-cookie.

A little bit googling about the JS-Variable _spBodyOnLoadFunctionNames gave me a little bit more overview and so I tried to remove the
ExpGroupOnPageLoad-entry in hope that the groups stayed collapsed after reload. And voila! It worked like a charm!

Just add this piece of code in your AllItems.aspx (or another view-file) and the list-groupings stay closed after page-reload, no matter, what groups the user expanded before:


<script type="text/javascript">
for (var i = 0; i < _spBodyOnLoadFunctionNames.length; i++) {
if (_spBodyOnLoadFunctionNames[i] == "ExpGroupOnPageLoad") {
// remove only this one element from array
_spBodyOnLoadFunctionNames.splice(i,1);
break;
}
}
</script>

Lange nichts gehört... / Long time ago...

Seit meinem letzten Post ist einige Zeit vergangen, in der ich auch ein paar Tage im Urlaub war. Schöne Grüße nach Mallorca, wo während meines Aufenthalts die schlimmsten Unwetter der letzten 40 Jahre herrschten ;-)

Seit letzter Woche bin ich zurück und ausgeruht um neue neue SharePoint-Abenteuer zu bestehen. Ich hoffe, dass ich zukünftige Postings in kürzeren Abständen schreiben kann. Diese werden dann, beginnend ab dem nächsten, komplett in Englisch sein. Ebenfalls habe ich vor, das etwas angestaubte Design des Blogs ein wenig zu überarbeiten.

Damit der Übergang zum Sprachwechsel nicht ganz so hart ausfällt, hier die englische Version:

English version:

Since my last post some time elapsed, which I used for making holidays and other lovely things.
Greetings to Mallorca, Spain that suffered from the heaviest thunderstorm since the last 40 years. ;-)

Now I'm back since last week and feel refreshed to look forward new SharePoint-Adventures. I hope to shorten the delays between my upcoming blogposts which I'll try to write henceforth completely in english. I also want to redesign the blog-layout a little bit.

So that the change-over for my readers isn't that hard, this posting is a mixture from german and english.

Dienstag, 18. August 2009

Länge des Mailbody in SPUtility.SendEmail begrenzt

Die Methode SendEmail der statischen Klasse SPUtility kann dazu verwendet werden, aus dem SharePoint-Kontext heraus E-Mails zu verschicken. Zu beachten ist dabei allerdings, dass der Mailbody nach 2048 Zeichen abgeschnitten wird. Für größere E-Mails, bzw. Inhalte mit aufwendigen HTML-Inhalten kann dies zum Problem werden.

Abhilfe schafft hier allerdings das Verwenden von New Lines innerhalb des Body-Strings.

Der Mailbody für SendEmail kann also mehr als 2048 Zeichen insgesamt verschicken, es dürfen allerdings nur maximal 2048 Zeichen pro Zeile stehen.


StringBuilder mailBody = new StringBuilder();
...
mailBody.AppendFormat("<tr style='{0}'><td style='padding-right:10px;'>{1}</td><td style='padding-right:10px;'>{2}</td><td style='padding-right:10px;'>{3}</td><td style='padding-right:10px;'>{4}</td></tr>", style, key, title, milestone, dueDate).AppendLine();
...

SPUtility.SendEmail(spWeb, false, false, toAddresses, "Subject", mailBody.ToString());

Donnerstag, 30. Juli 2009

Listeninstanzen - Cannot complete this action

Werden bei einer Solution ListInstances verwendet und sollen dann an speziell diese Listen CustomActions angehängt werden, empfiehlt es sich, anstelle der Standard-TemplateTypes (100, 101, etc..) eigene IDs ab 10000 zu verwenden.

Diese Änderung muss aber überall nachgezogen werden, sonst kommt es beim Erstellen der SiteCollection schnell zu der nichts sagenden Fehlermeldung "Cannot complete this action".



Die TemplateType-ID muss angepasst werden in
  • CustomAction
  • ListInstance
  • ListTemplate
  • Schema.xml

Donnerstag, 23. Juli 2009

verschwindendes WSPBuilder-Kontextmenü im Visual Studio

Ab und zu verschwindet nach dem Neustart des Visual Studios der Eintrag des WSPBuilders aus dem Projekt-Kontextmenü.



Den Grund dafür habe ich leider noch nicht gefunden. Abhilfe schafft hier aber folgender Prozess: Visual Studio schließen, WSPBuilder komplett deinstallieren und anschließend neuinstallatieren.
In den vorherigen Versionen war es dann noch nötig, die cablib.dll auszutauschen.
Seit der neuesten Version ist dies aber nicht mehr notwendig.

http://www.codeplex.com/wspbuilder

Mittwoch, 22. Juli 2009

Code-Debugging auf Tastendruck II

Um SPTimerJobs zu debuggen, muss sich zusätzlich an den OWSTIMER.EXE-Prozess angehängt werden.
Wie man das via Macro und ShortCut im Visual Studio bewerkstelligt, habe ich hier bereits einmal beschrieben.

Für den OWSTIMER gilt das gleiche Vorgehen; das Macro, welches den Debugprozess an w3wp.exe anhängt, kann bequem kopiert und leicht abgeändert werden:


' This subroutine attaches to owstimer.exe:
Sub AttachToOWSTIMER()
Dim attached As Boolean = False
Dim proc As EnvDTE.Process

For Each proc In DTE.Debugger.LocalProcesses
If (Right(proc.Name, 12) = "OWSTIMER.EXE") Then
proc.Attach()
attached = True
End If
Next

If attached = False Then
MsgBox("Couldn't find OWSTIMER.EXE")
End If

End Sub


In meinem Falle habe ich dann als ShortCut STRG+SHIFT+4 gewählt (STRG+SHIFT+3 hängt sich an w3wp.exe an), sodass ich dann die relevanten ShortCuts beinander habe.

Freitag, 17. Juli 2009

Erweitertes Debuggen

Oft stößt der SharePoint-Entwickler während seiner Arbeit auf solche Ärgernisse wie SharePoint-Fehlermeldungen á la "Unknown error" z.B. beim Erstellen einer SiteCollection mittels einer Solution. Wenn man dann noch richtig Glück hat, stehen in SharePoint-Log und EventViewer keine weiteren Details dazu, die Hilfe, die SharePoint hier von Haus aus bietet, ist in diesem Falle wertlos.

Hier empfiehlt es sich, ein paar zusätzliche Einstellungen im Visual Studio vorzunehmen, was den Erfassungsradius von auftretenden Exceptions wesentlich vergrößert und eine Hilfe sein kann, die Ursachen für eine Fehlermeldung wie die obige zu finden.

Zuerst wird unter Tools->Options->Debugging die Option "Enable Just My Code (Managed only)" deaktiviert. Dies weist den Debugger an, über den Tellerand des eigenen Codes zu schauen und auch andere Exceptions zu erfassen:




Als nächstes muss die Option "Common Language Runtime Exceptions" unter Debug->Exceptions aktiviert werden damit der Prozess auch weiß, bei welcher Art von Exceptions die Verarbeitung zusätzlich angehalten werden soll.



Damit sind die nötigen Vorbereitungen abgeschlossen. Nun wird sich an den w3wp-Prozess angehängt (wie man dafür einen VS-ShortCut anlegt, beschreibe ich hier) und wenn nun eine Exception auftritt, hat man, wie auf dem folgenden Bild zu sehen, die Möglichkeit, zusätzliche Informationen zur aktuellen Situation abzufragen:




Wer noch tiefer in das Thema "verbesserte Debugging-Möglichkeiten" einsteigen möchte, dem empfehle ich diese Seite, die das obige Vorgehen ebenfalls erläutert und noch weiter in die Materie eindringt: http://blog.thekid.me.uk/archive/2007/07/25/debugging-tips-for-sharepoint-and-wss-exceptions.aspx

Montag, 22. Juni 2009

Prüfung bestanden: TS: Microsoft Windows SharePoint Services 3.0 - Application Development (070-541)

Heute vormittag habe ich die Zertifizierung zu TS: Microsoft Windows SharePoint Services 3.0 - Application Development (070-541) bestanden. Hurray!

Die Fragen waren aber auch recht knackig, mein lieber Mann.

Diese Website von Microsoft (http://www.microsoft.com/learning/en/us/exam.aspx?ID=70-541&locale=en-us#tab2) listet das doch recht umfangreiche Themengebiet der 59 Fragen auf. Auf dem dritten Tab findet sich einiges verlinktes Material zum Inhalt der Fragen.
Und auch das Buch "Inside Microsoft® Windows® SharePoint® Services 3.0" deckt schon 60% des nötigen Vorwissens ab und kann ich sehr empfehlen: *click*

Freitag, 19. Juni 2009

Code-Debugging auf Tastendruck

Nach längerer Zeit habe ich endlich wieder einen Moment Ruhe, einen Eintrag zu schreiben.

Heute möchte ich erklären, wie man sich das Debuggen von im SharePoint-Kontext laufendem Code während des Entwickelns extrem vereinfachen kann:

Normalerweise muss man sich einfach an den laufenden w3wp-Prozess anhängen (Visual Studio -> Debug -> Attach to Process -> w3wp.exe(n) auswählen). Das ist nicht besonders schlimm, wird aber zu extrem nerviger Klickerei, wenn man zwischendurch mehrfach einen IISRESET ausführt und die Verbindung zum Prozess verloren geht.

Abhilfe schafft da ein kleines Macro im Visual Studio welches dann mittels Shortcut aktiviert werden kann.

Zu erst wird der Macro Explorer ausgewählt:



Über das Kontextmenü des VSDebuggers legt man ein neues Macro an:



Dadurch wurde VSDebugger ein neues Macro namens Macro1 hinzugefügt:



Dieser Code-Rumpf wird ersetzt durch den folgenden Code:


' This subroutine attaches to w3wp.exe:
Sub AttachToW3WP()
Dim attached As Boolean = False
Dim proc As EnvDTE.Process

For Each proc In DTE.Debugger.LocalProcesses
If (Right(proc.Name, 8) = "w3wp.exe") Then
proc.Attach()
attached = True
End If
Next

If attached = False Then
MsgBox("Couldn't find w3wp.exe")
End If

End Sub


Wird nun abgespeichert, sieht man im Macro-Explorer das neue Macro:



Damit ist das Macro einsatzbereit. Zu guter Letzt legen wir einen Shortcut in den Visual-Studio-Optionen (Tools-> Options) an um dessen Ausführung zu starten.



1) Unter Environment->Keyboard suchen wir zuerst nach dem Macronamen "attachtow3wp".
2) In der Auswahl erscheint nun unser neues Macro das angeklickt wird.
3) Der neue Shortcut wird dann in dem Feld "Press shortcut keys" gedrückt. Ich persönlich bevorzuge STRG+SHIFT+3 aber das kann jeder frei wählen.
4) Mittels des "Assign"-Buttons wird der Shortcut zugewiesen und als letztes
5) via OK global aktiviert.

Damit funktioniert STRG+SHIFT+3 jederzeit sobald der w3wp-Prozess verfügbar ist. Direkt nach einem IISRESET ist der w3wp-Prozess nicht verfügbar sondern erst, sobald man z.B. einmal die SiteCollection im Browser aufgerufen hat.

Dienstag, 26. Mai 2009

Zentraladministration über Objektmodell anzapfen

Mithilfe dieses Einzeilers kann bequem auf die Zentraladministration zugegriffen werden:

SPWebApplication webApplication = SPAdministrationWebApplication.Local


Dies ist z.B. sinnvoll um die geblockten Dateiendungen auszulesen:

SPWebApplication centralAdministraion = SPAdministrationWebApplication.Local;
Collection BlockedFileExtensions = centralAdministraion.BlockedFileExtensions;

Donnerstag, 16. April 2009

Speicherleichen (Memory Leaks) finden

Wer es noch nicht kannte, sollte sich unbedingt mal das kostenlose SharePoint Dispose Checker Tool von Microsoft (http://code.msdn.microsoft.com/SPDisposeCheck) herunterladen. Damit können die eigenen Assemblies auf nicht mehr benötigte aber trotzdem nicht freigegebene Ressourcen untersucht werden.

Solche Speicherleichen entstehen unter anderem, wenn z.B. SPSite- oder SPWeb-Objekte erzeugt aber nach Gebrauch nicht via Dispose oder mittels eines using-Konstrukts für den Garbage Collector freigegeben werden.

Näheres dazu gibt es im Best Practice von Microsoft auf http://msdn.microsoft.com/en-us/library/aa973248.aspx

Dienstag, 24. März 2009

Attachments auslesen

Um programmatisch an die Attachments eines Listeneintrags heranzukommen, kann nicht einfach die SPAttachmentCollection ausgelesen werden, da diese nur Strings enthält.

Die angehängten Dateien bekommt man über diesen Aufruf:

SPFolder folder = spWeb.Folders["Lists"].SubFolders[spList.Title].SubFolders["Attachments"].SubFolders[spListItem.ID.ToString()];


foreach (SPFile spFile in folder.Files)
{
// code to work with spFile...
}



Wie auf dem Screenshot zu sehen, scheint es eine Art virtuelle Liste "Attachments" unterhalb der Liste "test" zu geben, in der wiederum in einem SubFolder "1" (die ID des ListenItems "Attachment") das Attachment "code.txt" abgelegt wird.

Montag, 23. März 2009

Reservierte Namen in der Dokumentenbibliothek

Vorsicht ist geboten bei der Namenswahl für Folder in einer Dokumentenbibliothek.
Nicht jeder Name kann auf jeder Ebene vergeben werden. Leider sind die reservierten Worte, bzw. Wortbestandteile nirgends aufgelistet oder ich habe sie noch nicht gefunden.

Nicht benutzt werden sollten:

* forms (auf Root-Ebene), da dieser Folder reserviert ist für die .aspx-Seiten, mit denen Einträge in der Dokumentenbibliothek bearbeitet werden können
* die Endungen _file, _files, .files (alle Ebenen). Benutzt werden können diese Wortbestandteile allerdings, wenn nach ihnen noch Zeichen kommen, z.B. _files2. Sollte es solch ein Verzeichnis geben, wird es von SharePoint nach dem Anlegen automatisch umbenannt und ein Unterstrich angefügt. Außerdem scheint es das Objektmodell zu verwirren denn bei Operationen auf dem SPFolder-Objekt erscheinen dann z.B. solche Fehlermeldungen: "This operation can only be performed on a file; "Documents/folder1/01_Source_Files/Folder2" is a folder."

Sollte ich auf weitere in Dokumentenbibliotheken "verbotene" Worte stoßen, werde ich sie hier posten.

Donnerstag, 12. März 2009

Mit SPQuery nach Items in DocumentLibrary suchen

Eine Anforderung, die es gestern umzusetzen galt, bestand darin, nach Items (in diesem speziellen Falle Subfoldern) in einer DocumentLibrary zu suchen und diesen Wert einem speziellen Lookup-Feld als Standardauswahl anzugeben. Die normale SPQuery-Abfrage sucht leider nur auf der obersten Ebene. Abhilfe schafft hier das ViewAttribute Scope="RecursiveAll". Dieses sucht rekursiv in der DocumentLibrary und zwar nach allem, also Dokumenten und Foldern.

Die Code-Abfrage lautet dann so:


SPFieldLookup spFieldLookup = (SPFieldLookup)spListItem.Fields[property];
SPList spLookupList = spListItem.Web.Lists[new Guid(spFieldLookup.LookupList)];

SPQuery query = String.Format("{1}", spLookupFieldName, value);
query.ViewAttributes += " Scope=\"RecursiveAll\"";
SPListItemCollection results = spLookupList.GetItems(query);


spListItem und property sind das SPListItem welches ich updaten möchte und der Name des Lookup-Feldes, welches benutzt werden soll. value ist der Foldername, nach dem gesucht werden soll.

Mehr zum dem Attribut "Scope" gibt es auf http://msdn.microsoft.com/en-us/library/ms438338.aspx.

Montag, 2. März 2009

Gruppen in Quicklaunch-Navigation eintragen

Einer der Unterschiede von programmatisch angelegten Benutzergruppen zu denen, die über die Oberfläche erzeugt wurden, ist der, dass letztere in der Quicklaunch-Navigation angezeigt werden, erstere aber nicht.

Witzigerweise stehen diese Gruppen auch nicht per Default zur Verfügung, wenn man in "All People" versucht, einen Benutzer gleich einer Gruppe hinzuzufügen.

Abhilfe schafft hier das Property "vti_associategroups" vom Rootweb, dem man semikolonsepariert die IDs der Benutzergruppen zuweisen muss:


List quickLaunchGroupIDs = new List();
...
// create a new spGroup here and add its ID to the list
quickLaunchGroupIDs.Add(spGroup.ID.ToString());

rootWeb.AllProperties["vti_associategroups"] = string.Join(";", quickLaunchGroupIDs.ToArray());

rootWeb.Update();

Mittwoch, 18. Februar 2009

Formeln für berechnete Felder (calculated fields)

Mal ein interessanter Link zwischendurch: auf dieser Website werden sehr übersichtlich und mit Beispielen versehen die Formeln angezeigt, die in berechneten Feldern verwendet werden dürfen: http://www.gothamweb.com/support/manual/wsshelp/html/EgForm.htm http://camp.gob.mx/_vti_bin/help/1033/sts/html/EgForm.htm

Viele dieser Formeln lassen z.B. Feldmanipulationen auf EventReceiver-Seite überflüssig werden.

Mittwoch, 21. Januar 2009

"Insufficient memory to continue the execution of the program" und "System.OutOfMemoryException" bei Upload in eine Dokumentbibliothek

Im Moment arbeite ich an dem Projekt zur Migration der Daten aus einem Livelink- in ein SharePoint-System. Eine der Herausforderungen ist das Migrieren einer vorhandenen Dateistruktur - bestehend aus Foldern, Dokumenten (mit mehreren Versionen) und URLs - in eine Dokumentbibliothek.

Neben der Tatsache, dass SharePoint stark begrenzte Vorgaben was die Maximallänge von Datei- und Verzeichnisnamen sowie deren Syntax betrifft - dazu vielleicht in einem anderen Post einmal mehr -, gibt es auch das Problem, dass Dateien nicht beliebig groß sein dürfen.

In der Zentraladministration unter Application Management > Web Application General Settings die Einstellung Maximum Upload Size. Standardmäßig sollten dort 50 MB eingestellt sein.

Laut Microsoft kann man nun Dateien von einer Umgebung in eine Dokumentenbibliothek laden mittels diesen Codes:


FileStream fStream = File.OpenRead(srcUrl);
byte[] contents = new byte[fStream.Length];
fStream.Read(contents, 0, (int)fStream.Length);
fStream.Close();

EnsureParentFolder(site, destUrl);
site.Files.Add(destUrl, contents);


Das funktioniert auch wunderbar. Wird versucht, eine Datei hochzuladen, die größer ist als die eingestellte Maximum Upload Size wirft SharePoint eine Exception, die auch aussagt, dass das File Size Limit Exceeded ist.

Eine weitere Projektanforderung besagt, dass Dokumente bis 200 MB hochgeladen werden sollen. Also erhöhte ich den Wert in der ZA. Das funktionierte eine Zeit lang auch ganz gut aber irgendwann tauchten solche Fehlermeldungen auf:

Insufficient memory to continue the execution of the program.
at Microsoft.SharePoint.Library.SPRequestInternalClass.PutFile(String bstrUrl, String bstrWebRelativeUrl, Object varFile, PutFileOpt PutFileOpt, String bstrCreatedBy, String bstrModifiedBy, Int32 iCreatedByID, Int32 iModifiedByID, Object varTimeCreated, Object varTimeLastModified, Object varProperties, String bstrCheckinComment, UInt32& pdwVirusCheckStatus, String& pVirusCheckMessage)
at Microsoft.SharePoint.Library.SPRequest.PutFile(String bstrUrl, String bstrWebRelativeUrl, Object varFile, PutFileOpt PutFileOpt, String bstrCreatedBy, String bstrModifiedBy, Int32 iCreatedByID, Int32 iModifiedByID, Object varTimeCreated, Object varTimeLastModified, Object varProperties, String bstrCheckinComment, UInt32& pdwVirusCheckStatus, String& pVirusCheckMessage)
at Microsoft.SharePoint.SPFileCollection.AddInternal(String urlOfFile, Object file, PutFileOpt fileOpt, String createdBy, String modifiedBy, Int32 createdByID, Int32 modifiedByID, DateTime timeCreated, DateTime timeLastModified, Object varProperties, String checkInComment, SPVirusCheckStatus& virusCheckStatus, String& virusCheckMessage)
at Microsoft.SharePoint.SPFileCollection.Add(String urlOfFile, Byte[] file, SPUser createdBy, SPUser modifiedBy, DateTime timeCreated, DateTime timeLastModified)...


Und zwar genau in der Codezeile, in der der SPFileCollection mit der Methode .add die neue Datei hinzugefügt werden sollte.

Die Irritation war groß zumal laut der oben verlinkten MSDN-Seite und dem dort vorgegebenen Code Dateigrößen bis 2 GB möglich sein sollten.

Ein paar Zeilen tiefer in meinem Logfile fand sich auch diese Fehlermeldung:

Exception of type 'System.OutOfMemoryException' was thrown.

Und zwar in der Codezeile, in der das Bytearray deklariert wurde:

byte[] contents = new byte[fStream.Length];

Das Problem an dieser Stelle ist ausnahmsweise mal nicht SharePoint sondern das Betriebssystem, bzw. .net. Anscheinend sind Byte-Arrays jenseits der ~100 MB-Grenze zu groß für den Speicher sodass es zu den oben genannten Fehlermeldungen kommt.

Glücklicherweise, und das verschweigt die MSDN in ihrem Artikel voll und ganz, kann als zweiter Parameter in der SPFileCollection.add-Methode anstelle eines Byte-Arrays auch ein Stream benutzt werden:


FileStream fStream = File.OpenRead(fileToUpload);
spFile = spFileCollection.Add(newDocumentName, fStream, fileCreator, fileModifier, DateTime.Parse(fileCreated), DateTime.Parse(fileModified));
fStream.Close();


Durch diese Methode wird nicht nur der Speicher geschont sondern werden auch weniger Codezeilen benötigt.