Beiträge von Dorrit Riemenschneider

21Nov

… oder wie bei Microsoft das Basteln abgeschafft wurde
Wer schon einmal vor dem Problem stand, an eine SQL Server Prozedur oder Funktion eine Liste von Werten oder gar eine Tabelle zu übergeben, der wird für dieses Problem bestimmt eine Lösung gefunden haben: Ob man nun die Werte mit einem geeigneten Trennzeichen versehen (ich bevorzuge Pipes ;-)) alle in eine varchar(max)-Variable packt und mittels einer benutzerdefinierten Funktion wieder “auseinanderschnipselt” oder gleich den XML-Datentyp verwendet - es gibt die verschiedensten Bastellösungsansätze, dieser Problematik Herr zu werden.

SQL Server 2008 offeriert mit Table Valued Parameters diese Funktionalität nun gänzlich ohne Bastelei. Die Technik dafür ist denkbar simpel:

Es wird angenommen, dass eine Tabelle Employees mit folgender Struktur existiert:

Nun muss man

  1. einen benutzerdefinierter Datentyp vom Typ Table erzeugen:
  2. eine SQL Server Prozedur oder Funktion erstellen, die den neuen Typ als Input-Parameter verwendet:
  3. eine Variable von diesem Typ erzeugen und mit Daten befüllen:
  4. die Variable an die Prozedur oder Funktion übergeben:

Fertig ;-)

Für alle, die es gerne schnell mal ausprobieren möchten, hier nochmal zum Kopieren:

CREATE TABLE Employees(
EmployeeID int NOT NULL,
LastName nvarchar(50) NOT NULL,
FirstName nvarchar(50) NOT NULL)
go

CREATE TYPE T_Employees AS TABLE (EmployeeID int, LastName nvarchar(50), FirstName nvarchar(50))
go

CREATE PROCEDURE P_InsertEmployees
@employees T_Employees READONLY
AS
INSERT INTO Employees (EmployeeID, LastName, FirstName)
SELECT EmployeeID, LastName, FirstName
FROM @employees
go

DECLARE @emps T_Employees
INSERT INTO @emps (EmployeeID, LastName, FirstName) VALUES (1, ‘Davolio’, ‘Nancy’)
INSERT INTO @emps (EmployeeID, LastName, FirstName) VALUES (2, ‘Fuller’, ‘Andrew’)

EXEC P_InsertEmployees @emps

Hier noch ein paar Dinge, die zu beachten bzw. wissenswert sind:

  • Der INPUT-Parameter für die Prozedur muss READONLY sein.
  • TVP können nicht als Return-Variable für eine Funktion verwendet werden.
  • Microsoft empfielt die Verwendung von TVP bis zur Anzahl von 1000 Datensätzen, für alles, was drüber ist, sollte BULK INSERT verwendet werden.
  • TVP werden als temporäre Tabellen in der tempdb gespeichert.

Und zum Abschluss noch ein kleiner Leckerbissen für ADO.Net Programmierer: TVP werden auch hier vollständig unterstützt, ein Beispiel für den Aufruf der oben erstellte Prozedur in C# könnte so aussehen:

SqlConnection sqlConn…
DataTable dt = new DataTable();
dt.Columns.Add….
dt.Rows.Add…..
SqlCommand cmd = new SqlCommand(”P_InsertEmployees”, sqlConn);
cmd.Parameters.AddWithValue(”@employees”, dt);
cmd.ExecuteNonQuery();

Technorati Tags: , , ,

16Nov

Wer sich schon immer mal darüber geärgert hat, dass mehrere SQL-Anweisungen nötig waren, um in einer Tabelle, abhängig von den Bedingungen in einer anderen Tabelle, Daten einzufügen, zu ändern und/oder zu löschen, der wird über die neue MERGE-Anweisung in SQL Server 2008 hocherfreut sein - … vorausgesetzt, er kann sich für komplexe SQL-Anweisungen begeistern ;-) .

Das absolute Standard-Szenario für die Anwendung von MERGE ist die Aktualisierung von Daten in einem Data Warehouse auf Basis der Daten aus dem Produktivsystem: Im Data Warehouse sollen

  • Adressdaten, die im Produktivsystem vorhanden sind, aber nicht im Data Warehouse, eingefügt werden (INSERT)
  • Adressdaten, die im Produktivsystem und im Data Warehouse vorhanden sind, geändert werden (UPDATE)
  • Adressdaten, die im Data Warehouse, nicht aber im Produktivsystem vorhanden sind, gelöscht werden (DELETE)

Dafür waren bisher 3 Anweisungen der folgenden Art nötig (je eine für INSERT, UPDATE und DELETE):

Mit dem MERGE-Befehl nun ist das alles in einer Anweisung unterzubringen:

Das kommt nicht nur deutlich eleganter daher, sondern ist auch ressourcensparend, da nur eine anstelle mehrerer Lookup-Operationen durchgeführt wird.

Wichtig: Die MERGE-Anweisung muss immer mit einem Semikolon abgeschlossen werden.

Natürlich ist die MERGE-Anweisung noch deutlich flexibler als hier abgebildete Variante, z.B. kann die USING-Klausel auch eine Unterabfrage (dann mit einem Alias) enthalten oder die MATCHED-Klausel beliebig mit weiteren logischen Ausrücken kombiniert werden, des Weiteren sind beliebig viele MATCHED-Klauseln von jeder Art möglich. Ein etwas komplexeres Beispiel könnte z.B. so aussehen:

Ausführliche Syntaxinformationen finden sich unter http://msdn.microsoft.com/de-de/library/bb510625.aspx.

Technorati Tags: , ,

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