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;
}


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


05Dez

Mithilfe des Site-Plugins ist es möglich ohne großen Aufwand eine Projekt Homepage mit allen wichtigen Informationen zu erstellen. Neben generierten Code Audits, Unit-Test Reports oder SourceCode Reviews können auch eigene Inhalte problemlos mit eingebunden werden, was die Projektsite zu einer universellen Informationsplattform für Entwickler macht.

Leider gibt es bei Projekten mit mehrenen Modulen häufig Probleme in der Verlinkung der generierten Html-Seiten. Da die Super-Pom zwar alle Sub-Modules kennt aber nicht umgekehrt, sind die Module auf der Projekt-Page auch nur in eine Richtung verlinkt. Möchte man nun zu einem Parent-Modul navigieren, muss man entweder die Browser-Hierarchie bemühen oder die Startseite erneut aufrufen.

Direkte Modulverlinkung

Die Lösung für dieses Problem ist die site.xml. In dieser Datei kann das Aussehen und die Navigation der Projektseite individuell angepasst werden. Den generellen Aufbau der site.xml findet man hier. Damit nun die Verlinkung funktioniert muss in jedem Modul im Verzeichnis src/ ein Ordner site, zusammen mit der Datei site.xml angelegt werden. Eine leere site.xml erzeugt eine blank-page, daher muss für die Standardfunktionalität folgender Content eingebunden werden:

<?xml version="1.0" encoding="ISO-8859-1"?>
<project name="Maven">
  <body>
    <menu ref="parent"/> <!-- Erzeugt Link zum Parent-Modul-->
    <menu ref="modules"/>
    <menu ref="reports"/>
  </body>
</project>

Mit mvn site-deploy werden nun die zugehörigen Projektseiten erzeugt. Ein Aufruf der Startseite überrascht noch nicht, es werden wie gewohnt alle Module angezeigt.

                       

Auf den Seiten der Sub-Modules hingegen, erscheint nun ein Link zum jeweiligen Parent-Modul oben in der Navigationsleiste.

                       

 Krümelpfad Verlinkung

Die direkte Verlinkung erfüllt zwar ihren Zweck, ist aber gerade bei mehreren Hierarchiestufen nicht gerade praktisch. Besser wäre da doch ein Krümelpfad mit dem man beliebig zwischen den Seiten wechseln kann. Wieder hilft dabei die site.xml

<?xml version="1.0" encoding="ISO-8859-1"?>
<project name="Maven">
  <body>
    <breadcrumbs>
      <item name="<ModulName>" href="index.html" mce_href="index.html"/>
    </breadcrumbs>

    <menu ref="modules"/>
    <menu ref="reports"/>
  </body>
</project>

In dem Item-Tag wird dabei nur der Name des aktuellen Moduls eingetragen und als href die index.html. Maven baut dann automatisch den Pfad synchron zur Modulhierarchie zusammen. Die nachfolgenden Abbildungen zeigen eine Projektseite mit drei Hierarchieebenen.

                       

                       

                       

Technorati Tags: , ,

26Nov

Das Programm STSADM ist ein sehr nützliches Tool für häufig wiederkehrende Aufgaben auf einem Sharepoint-Server. Einer der großen Vorteile ist es, dass man diese Aufgaben per Kommandozeile und damit z.B. per Batch abarbeiten kann.

An manchen Stellen kommt aber auch der dieses Programm an seine Grenzen. Glücklicherweise kann der beherzte Entwickler hier selbst eingreifen und sich eigene Erweiterungen schreiben, die genau den Bedarfsfall abbilden, der gerade vorliegt.

Die Entwicklung einer eigenen Erweiterung ist gar nicht so kompliziert, wie es sich anhört. Dazu sind folgende Schritte notwendig:

Zunächst erstellt man sich ein neues DLL-Projekt und verlinkt die Microsoft.Sharepoint-Assembly. Jede Klasse, die das ISPStsadmCommand-Interface implementiert, kann als Erweiterung fungieren.

new StsadmCommand

Das Interface beinhaltet 2 Funktionen. Einerseits GetHelpMessage - diese Funktion wird dann aufgerufen, wenn man stsadm -help <befehl> aufruft; andererseits  Run - diese Funktion wird aufgerufen, wenn man stsadm -o <befehl> in der Kommandozeile eingibt.

Implementation ISPStsadmCommand 

Beide Methoden beinhalten einen Parameter command. Dieser beinhaltet zur Ausführungszeit den Befehl - vermutlich aus dem Grund, da eine implementierende Klasse auf mehrere Befehle gemappt werden kann. Das Mapping selbst ist recht einfach. Dazu erzeugt man sich eine neue XML-Datei mit dem Namen stsadmcommands.<eigenername>.xml, die später unter “Gemeinsame Dateien\Microsoft Shared\web server extensions\12\CONFIG” abgelegt wird.

Im Beispiel wird das command copynewfiles auf die eben erstellte Klasse CopyFiles gemappt.

stsadmconfig

Nachdem die DLL nun in den GAC deployed ist, kann der Aufruf erfolgen.

output


15Nov

Bei der Verwendung von Spring stellt sich im Buildmanagement die Frage wie mit Properties umgegangen wird. So hat jeder Test-, Live- und Entwicklungsserver seine eigene Datenbank oder andere unterschiedliche Einstellungen. Eine oft gewählte Variante ist die Ersetzung der Properties durch den Buildprozess. Dies hat allerdings den Nachteil, dass für eine Änderung der Properties der Buildprozess neu ausgeführt werden muss.

Mit Spring bietet sich dabei eine Alternative: Hier kann ein Propertyfile welches z. B. im Classpath liegt eingebunden werden:

<bean id="propertyPlaceholder" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:hibernate.properties" />
</bean>

Die Properties, die dann in dem File ‘hibernate.properties’ definiert sind, können innerhalb der Springkonfiguration genutzt werden:

...
<property name="hibernateProperties">
<props>
<prop key="hibernate.show_sql">${hibernate.show_sql}</prop>
<prop key="hibernate.dialect">${hibernate.dialect}</prop>
<prop key="hibernate.hbm2ddl.auto">${hibernate.hbm2ddl.auto}</prop>
...
</props>
</property>
...

Das Property File selbst ist dabei einfach wie erwartet:

hibernate.show_sql=true
hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
hibernate.hbm2ddl.auto=update


09Nov

Damit man gezielt versionsbezogenen Support leisten kann, muss der Kunde die aktuelle Version z.B. eines SharePoint Webparts mitteilen können.

Es bietet sich an, dafür die Version der Assembly zu verwenden. Per Default ist diese jedoch auf [assembly: AssemblyVersion("1.0.0.0")] eingestellt und ändert sich nicht von allein. Man kann durch Einsatz von Sternchen diese Versionierung automatisieren. Visual Studio zählt dann von selbst hoch. (Mehr Informationen: AssemblyVersionAttribute-Klasse)

Für schnellen und gezielten Support wäre es aber besser, wenn der Kunde die Subversion Revisionsnummer des Quellcodes nennen könnte.

Dazu sind folgende Schritte notwendig:

  1. In der AssemblyInfo.cs den Eintrag
    [assembly: AssemblyDescription("")]
    zu
    [assembly: AssemblyDescription("$Rev$ $Author$ $LastChangedDate$")]
    ändern.
  2. Im SVN-Client die Property “svn:keywords” für die AssemblyInfo.cs anlegen und den Wert auf “Rev Author LastChangedDate” setzen.

    Subversion Property

  3. Nun setzt Subversion zukünftig immer die aktuellen Werte bei Commit der AssemblyInfo.cs ein.

    Achtung: Die Ersetzung in der AssemblyInfo.cs findet wirklich nur bei Commit genau dieser Datei statt. Ändert sie sich nicht, sondern wurden z.B. nur Änderungen an anderen Dateien vorgenommen, so wird auch nichts ersetzt.

    Dieses Problem lässt sich wie folgt lösen:

    1. Vor jedem Release sollte vom Entwickler der Wert von AssemblyVersion nach einem festgelegtem Muster erhöht werden. Z.B. erster Teil “bei neuer Hauptversion”, zweiter Teil “bei Erweiterungen”, dritter Teil “bei Bugfixes”, vierter Teil “immer 0″.So hat man zusätzlich zu der eher maschinennahen Revisionsnummer auch noch eine menschenlesbare Nummer, an der man schnell Umfang und Art der Änderungen ablesen kann.

      oder

    2. Ist auf dem Entwickler-Rechner TortoiseSVN installiert, so kann man dessen Kommandozeilentool SubWCRev.exe verwenden. Dann muss nicht erst eine manuelle Änderung in der AssemblyInfo.cs vorgenommen werden.

      Dazu kopiert man die AssemblyInfo.cs zu AssemblyInfo.cs.Template, ändere den Platzhalter in der AssemblyInfo.cs.Template von
      [assembly: AssemblyDescription("$Rev$ $Author$ $LastChangedDate$")]
      zu
      [assembly: AssemblyDescription("$WCREV$ $WCDATE$")]
      und rufe im Pre-Build Event das Tool wie folgt auf:

      Pre-Build

      C:\Program Files\TortoiseSVN\bin\SubWCRev.exe $(SolutionDir) $(ProjectDir)\Properties\AssemblyInfo.cs.Template $(ProjectDir)\Properties\AssemblyInfo.cs

      Wichtig: Man man muss hier mit einem Template arbeiten, da sonst die Ersetzung nur beim ersten Mal funktioniert.

      Jetzt hat man bei jedem Build eine aktuelle AssemblyInfo.cs ohne das man erst jedes Mal ein Commit ausführen muss.

    Da im Gegensatz zu CVS bei SVN die Revisionsnummer nicht für jede Datei einzeln erhöht wird, sondern für das gesamte Repository, haben wir mit der Revisionsnummer eine optimale Angabe um den Versionsstand des Kunden nachzustellen. Der Kunde kann die Version nun in der gelieferten DLL ablesen. (Rechte Maustaste auf Eigenschaften…)

    DLL properties

  4. Um dem Kunden das Ablesen zu vereinfachen, zeigt man die Information aber besser im Impressum der Anwendung an, führt sie in Logfiles mit, etc. So funktioniert das Auslesen mit C#:

    Versionsnummer der Assembly:
    Assembly.GetExecutingAssembly().GetName().Version

    Subversion-Informationen:
    string svnInfo = string.Empty;
    object[] attributes = Assembly.GetExecutingAssembly().GetCustomAttributes(typeof(AssemblyDescriptionAttribute), true);
    if ( attributes.Length > 0 ) {
    AssemblyDescriptionAttribute descriptionAttribute = attributes[0] as AssemblyDescriptionAttribute ;
    svnInfo = descriptionAttribute.Description;
    }

Nun geht’s schneller beim nächsten Bug… ;-)

Technorati Tags: ,

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

16Okt

Um schnell Datenbankschemata zu visualisieren bietet sich Schemaspy (veröffentlicht unter LGPL) an. Es wird auf der Kommandozeile zum Beispiel wie folgt aufgerufen:

java -jar schemaSpy.jar -cp postgresql-8.1-408.jdbc3.jar -t pgsql -db beispiel_db -u improve -s public -host beispielport -port 5431 -o .

Damit die Generierung der Tabellen und Diagramme erfolgreich verläuft, müssen 2 Bedingungen erfüllt werden:

  1. ein Treiber für den entsprechenden Datenbanktyp muss vorhanden sein. Der Pfad zum Treiber kann entweder in durch den Parameter -cp spezifiziert werden (siehe oben) oder in den [databaseType].properties von SchemaSpy angegeben werden
  2. zum Rendern der Graphiken muß der Dot-Renderer von Graphviz im selben Ordner wie Schemaspy liegen, ansonsten wird die folgende Fehlermeldung angezeigt:
    Warning: Failed to run dot.
    Download dot version 2.2.1 or versions greater than 2.4 from www.graphviz.org and make sure that dot is in your path.

    Momentan wird von Graphiz nur die Version 2.14.x bereitgestellt, mit der jedoch ebenfalls die Graphiken erzeugt werden können: Einfach die Dateien dot.exe, z.dll, jepg.dll und png.dll aus dem bin-Ordner von Graphviz in das Verzeichnis der schemaspy.jar kopieren.

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