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


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