Beiträge von Dorrit Riemenschneider

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


27Feb

Als netter ASP.Net Programmierer mutet man dem Anwender wegen eines Klicks in eine CheckBox (in diesem Fall als Item einer CheckBoxList) kein Postback/Reload der Seite zu. Nun hat man ja aber vielleicht doch den Wunsch, das eine oder andere beim Klick zu erledigen - clientseitig per Javascript. Wenn möglich, will man den einzelnen CheckBoxList Items noch ein paar Attribute mitgeben, die in der Javascript-Funktion ausgewertet werden können und im Idealfall weiß die Javascript-Funktion auch gleich, welcher Item geklickt wurde. Das sollte eigentlich kein Problem sein - denkt man.

Also versucht man es erstmal ganz einfach mit folgendem Codeschnipsel im C# CodeBehind der Seite:

image

-> keine Reaktion :-(

Das gleiche noch einmal mit “onclick” anstelle “onchange” -> selbes Ergebnis :-(

Nun hilft alles nichts - man fängt an nachzudenken… Ein Blick in den Seitenquellcode ist ziemlich aufschlussreich: Die CheckBoxList Items haben einen <span> um den eigentlichen <input>-Tag des Items stehen - und in diesem landen unsere Attribute:

image

So funktioniert das also leider nicht. Ein kurzes Googlen zeigt nicht nur, dass andere das Problem auch schon hatten, sondern auch eine Lösung: Das “onclick” darf nicht an die einzelnen Items, sondern muss an die CheckBoxList gebunden werden:

image

Welcher Item geklickt wurde, kann man nun leider nicht mehr einfach an die Javascript-Funktion übergeben. Dies muss man über eine for-Schleife herausfinden. Die Attribute für die einzelnen Items können aber mit einem kleinen Trick trotzdem in der Javascript-Funktion abgefragt werden: Man legt sich zusätzlich zum Array mit den CheckBoxList Items (<input>-Tags) noch ein Array für die Attribute (<span>-Tags) an, das natürlich die gleiche Länge hat und über den gleichen Index abgefragt werden kann.

Alles in allem sieht die Javascript-Funktion in der Seite dann so aus:

function CheckboxChanged()
{
var checkBoxList = document.getElementById(’<%= SampleCheckBoxList.ClientID %>’);
//Array für die CheckBoxList Items
var checkboxes = checkBoxList.getElementsByTagName(’input’);
//Array für die Attribute der CheckBoxList Items
var checkboxAttributes = checkBoxList.getElementsByTagName(’span’);
for (var i=0; i<checkboxes.length;i++)
{
alert(checkboxes[i].checked);
alert(checkboxAttributes[i].code);
alert(checkboxAttributes[i].text);
}
}

Und der C# CodeBehind für das Hinzufügen des “onclick” und der Attribute so:

//set attributes for the items of SampleCheckBoxList (reqired for javascript function)
foreach (ListItem item in SampleCheckBoxList.Items)
{
item.Attributes.Add(”code”, item.Value);
item.Attributes.Add(”text”, item.Text);
}
//set the javascript function to be called at a checkbox item click
SampleCheckBoxList.Attributes.Add(”onclick”, “CheckboxChanged()”);

Das sollte wirklich kein Problem sein… :-)

Technorati Tags: , , , ,

25Feb

Manchmal hat man ein Problem, und wenn man es dann gelöst hat, ist die Lösung so einfach, dass man es fast nicht glauben mag. So ging es mir mit folgender Aufgabe:

Gegeben ist ein String, der ein XML Document repräsentiert:

<books>
<book author=”Meier”>Lexikon der Meierei</book>
<book author=”Muster”>Patterns in der Schneiderstube</book>
</books>

Diesen String möchte man (möglichst browserunabhängig, wenigstens soll es aber für Internet Explorer und Firefox funktionieren) per Javascript in ein XML DOM Object parsen, um dieses dann, wie auch immer, weiter zu verwenden. Und das geht ganz einfach so:

function MyParseXml(xmlString)
{
var xmlDoc;
//for IE
if (window.ActiveXObject)
{
xmlDoc = new ActiveXObject(”Microsoft.XMLDOM”);
xmlDoc.async = “false”;
xmlDoc.loadXML(xmlString);
}
//for Mozilla, Firefox, Opera, etc.
else if (document.implementation && document.implementation.createDocument)
{
var parser = new DOMParser();
xmlDoc = parser.parseFromString(xmlString,”text/xml”);
}
var x=xmlDoc.getElementsByTagName(’book’); //oder wasauchimmer
}

Fertig! :-)

Technorati Tags: , ,

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

04Jan

Web User Controls (*.ascx) kann man zur Designzeit sehr bequem per Drag & Drop auf ein Web Form (*.aspx) ziehen - den Rest erledigt Visual Studio für uns.Wenn man das Gleiche dynamisch zur Laufzeit erreichen möchte (weil man z.B. unterschiedliche ascx-Dateien verwenden oder einunddieselbe ascx-Datei mehrmals nacheinander laden will), kann man das folgendermaßen bewerkstelligen:

Unser User Control heißt “SampleControl” und besteht aus einem Label, einer TextBox und einem Button:

<%@ Control Language=”C#” AutoEventWireup=”true” CodeFile=”SampleControl.ascx.cs” Inherits=”SampleControl” %>
<asp:Label ID=”Label1″ runat=”server” Text=”Label”></asp:Label>
<asp:TextBox ID=”TextBox1″ runat=”server”></asp:TextBox>
<asp:Button ID=”Button1″ runat=”server” Text=”Button” />
<br />

image

Außerdem gibt es eine string property “Header”, mit der im Page_Load der Text des Labels belegt wird:

protected void Page_Load(object sender, EventArgs e)
{
Label1.Text = Header;
}

Unser Web Form besteht nur aus einem PlaceHolder und einem Button für das dynamische Laden des User Controls:

<%@ Page Language=”C#” AutoEventWireup=”true” CodeFile=”SampleForm.aspx.cs” Inherits=”SampleForm” %>
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “
http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>
<html xmlns=”
http://www.w3.org/1999/xhtml” >
<head runat=”server”>
<title>Untitled Page</title>
</head>
<body>
<form id=”form1″ runat=”server”>
<div>
<asp:PlaceHolder ID=”PlaceHolder1″ runat=”server”></asp:PlaceHolder>
<br />
<asp:Button ID=”Button1″ runat=”server” OnClick=”Button1_Click” Text=”Lade SampleControl” />
</div>
</form>
</body>
</html>

image

Um das Control dynamisch zur Laufzeit laden zu können, müssen wir die ascx-Datei in SampleForm.aspx registrieren. Dafür fügen wir die folgende Register-Direktive (z.B. unter der Page-Direktive) ein:

<%@ Register src=”SampleControl.ascx” TagName=”SampleControl” TagPrefix=”uc1″ %>

Das dynamische Laden des Controls wird mittels der Methode “LoadControl” implementiert:

private void LoadSampleControl(int i)
{
SampleControl sc = (SampleControl)LoadControl(”SampleControl.ascx”);
sc.Header = “Test ” + i.ToString();
PlaceHolder1.Controls.Add(sc);
}
protected void Button1_Click(object sender, EventArgs e)
{
LoadSampleControl(++counter);
}

Das Control wird nun zur Laufzeit geladen, die Header property gesetzt und das Control zum PlaceHolder hinzugefügt. So weit - so gut. Wenn nun allerdings ein Postback erfolgt, werden die bereits in die Seite geladenen Instanzen des Sample Controls nicht automatisch geladen und damit auch nicht mehr angezeigt :-(.

Dies müssen wir im Page_Load selbst erledigen:

protected void Page_Load(object sender, EventArgs e)
{
if (IsPostBack)
{
for (int i = 0; i < counter; i++)
{
LoadSampleControl(i);
}
}
}

Fertig - das Ergebnis sieht wie folgt aus:

image

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 :-).


16Okt

Wer mit Visual Studio.Net Sharepoint Projekte (Webparts, Workflows etc.) entwickelt, weiß wie oft man während der Entwicklung ein IISRESET machen muss und auch, dass dies immer eine Weilchen Zeit in Anspruch nimmt. Besonders interessant wird es natürlich, wenn das IISRESET auf einem System ausgeführt wird, auf dem auch noch andere Nutzer arbeiten, denen dann förmlich der IIS “unter den Füßen weggezogen” wird…

Eine deutlich maßvollere Alternative ist es, stattdessen nur den Application Pool der Sharepoint Webanwendung neu zu starten:

cscript c:\windows\system32\iisapp.vbs /a “%SharePointAppPool%” /r

Dies ist nicht nur sauberer, sondern geht auch deutlich schneller… :-)


12Okt

Für alle Vista-Nutzer, die sich auch schon darüber geärgert haben, dass sie bei jedem Öffnen, Speichern, Einchecken usw. eines Office-Dokumentes aus einer Sharepoint Dokumentenbibliothek (2003 oder 2007) 3 mal nach ihrem Passwort gefragt werden, findet sich in der MS Knowledge Base die tröstliche Erklärung, das dieses Verhalten „by design“ ist: http://support.microsoft.com/kb/932118/en

Immerhin gibt es aber einen Workaround dafür: Seite im Internet Explorer als vertrauenswürdige Seite eintragen!

******** UPDATE 19.10.07 ************

Der oben angegebener Workaround scheint (zumindest bei mir) leider nur sporadisch zum Ziel zu führen. Dafür findet sich im offiziellen Microsoft Sharepoint Blog (http://blogs.msdn.com/sharepoint/archive/2007/10/16/known-issue-office-
2007-on-windows-vista-prompts-for-user-credentials-when-opening-documents-in-a-sharepoint-2007-site.aspx
) ein wirklich funktionierender (wenn auch etwas chaotisch anmutender) Workaround:

-> IE7 -> Extras -> Internetoptionen -> Verbindungen -> LAN Einstellungen:

Checkboxen für “Proxyserver verwenden” und “Proxyserver für lokale Adressen umgehen” aktivieren und “fake proxy” (ohne Anführungsstriche) als Proxy eintragen. Unter -> Erweitert “*” als Ausnahme hinzufügen:

IE7ProxySettings

IE7ProxySettingsErweitert

Klingt sehr gewagt - funktioniert aber! Wer sowieso einen (echten) Proxy verwendet, muss nur “Proxyserver für lokale Adressen umgehen” bzw. “Automatische Suche der Einstellungen” aktivieren.

 ******** UPDATE 09.11.07 ************

Man merke auf: Microsoft hat sich selbst korrigiert! Es gibt einen neuen Eintrag im offiziellen Microsoft Sharepoint Blog, wonach der (bei mir inzwischen doch bewährte) Workaround mit dem Fake Proxy nun um Himmels willen nicht mehr angewendet werden soll: http://blogs.msdn.com/sharepoint/archive/2007/10/19/known-issue-office-2007-on-windows-vista-prompts-for-user-credentials-when-opening-documents-in-a-sharepoint-2007-site.aspx

Dafür gibt’s einen neuen Workaround, der in Gestalt eines Hotfix und einiger Registry-Einträge daherkommt:

Der Hotfix (KnowledgeBase Artikel 907306) behebt verschiedene Probleme, die auftreten können, wenn Webordner verwendet werden, um eine Verbindung auf einem Server mittels Web Distributed Authoring und Versioning (WebDAV) herzustellen.

Die Registry-Einträge dienen dazu, die Office-Anwendungen im Windows XP SP 2 Kompatibilitätsmodus laufen zu lassen. Beispielhaft hier die Einträge dür Word, Excel und Powerpoint:

[HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\AppCompatFlags\Layers]
“C:\\Program Files\\Microsoft Office\\Office12\\WINWORD.EXE”=”WINXPSP2″
“C:\\Program Files\\Microsoft Office\\Office12\\EXCEL.EXE”=”WINXPSP2″
“C:\\Program Files\\Microsoft Office\\Office12\\POWERPNT.EXE”=”WINXPSP2″

Einfach in eine Datei mit der Endung “reg” kopieren und diese ausführen. Bei mir hat der Workaround erst funktioniert, als ich HKEY_LOCAL_MACHINE durch HKEY_CURRENT_USER ersetzt habe - dann aber prima :-)

PS: nicht vergessen, den Fake Proxy wieder aus den IE Einstellungen rauszunehmen…

Technorati Tags: , ,

04Jul

Wer schon einmal händig die default.master für eine Site (z.B. im Sharepoint Designer) angepasst hat und diese Änderungen für eine größere Menge vorhandener Sites übernehmen wollte, weiß, wie zeit- und nervtötend dies sein kann: jede Site muss einzeln im Sharepoint Designer aufgerufen und die Masterpage ersetzt werden.Mit einer kleinen Konsolenanwendung kann man hier Abhilfe schaffen: Die Anwendung läuft rekursiv durch alle Sites und Subsites ab einer übergebenen URL, lädt die als Datei auf dem Sharepoint Server (MOSS oder WSS) bzw. einem Fileshare gespeicherte Masterpage in die Masterpage-Galerie der Site hoch und setzt sie als Default Masterpage.

Anwendung: auf dem Sharepoint Server einfach die geänderte Masterpage als Datei speichern und die Anwendung ausführen, dabei die URL der Site, ab der die Masterpage aktualisiert werden soll und den Speicherort der geänderten Masterpage als Parameter übergeben:

ApplyMasterpage.exe “http:/myserver/mysite” “C:\mydefault.master”

ApplyMasterpage

Download der Exe: applymasterpage.exe

Download Quellcode: applymasterpage.zip