Das ContentEditorWebPart ist eigentlich eine feine Sache um bei einer SharePoint Applikation Content auf eine Seite zu packen, den der User nachträglich noch anpassen kann. Man findet auch einige Beispiele im Netz, wie dies programmatisch gemacht werden muss, aber alle Beispiele beschränken sich darauf, unformatierten Text zu setzen. Hier möchte ich zeigen wie man XHTML Content setzen kann.

 

Zur Erinnerung, der prinzipielle Vorgang beim Erzeugen und Hinzufügen ist wie folgt:

public void AddEditableContent(SPWeb web, string pageName, string zone, string title, string content)
{
    using (var elevatedSite = new SPSite(web.Site.ID))
    using (SPWeb elevatedWeb = elevatedSite.AllWebs[CurrentWeb.ID])
    using (SPLimitedWebPartManager webPartManager =
            elevatedWeb.GetLimitedWebPartManager(
                SPUtility.ConcatUrls(elevatedWeb.Url, pageName), 
                PersonalizationScope.Shared))
    {
        var xmlDoc = new XmlDocument();
        XmlElement container = xmlDoc.CreateElement("container");
        container.InnerText = content;

        var editable = new ContentEditorWebPart { Title = title, Content = container };

        webPartManager.AddWebPart(editable, zone, 1);
        webPartManager.SaveChanges(editable);
        web.Update();
    }
}

Content erwartet ein XmlElement, dabei ist der Elementname völlig egal. Logische Annahme wäre also, man setzt (X-)Html als InnerXml statt InnerText. Ersetzen wir also container.InnerText durch container.InnerXml und probieren als content Parameter mal einen Html String:

<h2>Willkommen</h2><p><b>Fettes</b> Hallo!</p>

mit dem Ergebnis:

Willkommen Fettes Hallo! ohne jede Formatierung.  Grummel! Offensichtlich wird der Value des Xml/Html in das Content Element des Webparts kopiert, dabei geht dann jede Struktur verloren Trauriges Smiley

Nach ein bisschen Rumprobieren kam ich schließlich auf die Lösung. Damit die Formatierung nicht verloren geht, muss man sie vor der Interpretation durch XML schützen. Dies geht entweder via SPEncode.XmlRemoveControlChars oder durch eine CDATA Section. Die endgültige Version von Zeile 12 lautet daher:

container.InnerXml = string.Format("<![CDATA[{0}]]>", content);

Jetzt kriegen wir:

Willkommen


Fettes Hallo!

Man macht also relativ aufwändig ein XmlDokument, kapselt die Html Struktur in einer Xml sicheren Textversion und übergibt dann ein XmlElement… Wer hat das entworfen???? Was hat der geraucht??? Wieviel sinnvoller wäre es doch, einfach einen String zu übergeben!

Advertisements

Die Tage musste ich in eine mir weitgehend unbekannte SharePoint Solution eine neue Übersetzung einpflegen. Leider war die Struktur der ResX Datei beim Übersetzer zerstört worden und zurück kam eine schwach strukturierte Mischung verschiedener Resourcefiles in Form eines Excel Files bei der einzelne Übersetzung gleich komplett fehlten. Der ZetaResource Editor stürzte angeblich bei den Resourcen ab, was ich dummerweise einfach geglaubt habe. Da ich ziemlich viel Zeit verschwendet habe, bis ich alles mit Hilfe eines Kollegen (Danke Mario) gerade gebogen hatte, möchte ich hier die Lehren daraus zur Diskussion stellen.

Resourcen in SharePoint

SharePoint benötigt seine Resourcen je nach Kontext an verschiedenen Stellen. Features und Backend Solution können sich eine Resourcedatei teilen, sofern diese im Hive in …/14/Resources liegt.Alternativ kann man die Featureresourcen wie die Namen von SiteColumns, ListDefinitions usw. auch auf Featurescope lassen. SharePoint nutzt hier die $Resources: Syntax in den elements.xml usw. Nutzt man eine gemeinsame/globale Resource muss man darauf achten den Namen der Resourcedatei mit anzugeben, also z.B. $Resources:Wulf.Absence,Start_SiteColumnDisplayName.

Backendlogik kann die globale Resourdatei entweder über einen ResourceManager oder über die Codegenerierung der Resource nutzen. Ich persönlich ziehe den Zugriff über die generierte Klasse vor, da sie Typsicherheit und Intellisense bietet. In diesem Fall wurde aber eine Zugriffsklasse mit hunderten von const string Feldern benutzt, was die gleichen Vorteile bietet, aber manuell gepflegt werden muss. 

Application Pages, visuelle WebParts oder Usercontrols brauchen eine Resourcendatei in AppResources und nutzen ebenfalls die $Resources: Syntax. Diesmal muss man bei der Referenzierung auf die Endung .AppResource achten also z.B.  Text=”<%$Resources:Wulf.Absence.AppResource,Page_Title%>”

Aufbereitung

Um erst einmal eine laufende Übersetzung zu bekommen kopierte ich das ganze Sammelsurium aus Excel in die neue Sprachresource. Dann habe ich sortiert und Feld für Feld verglichen.

Grober Patzer! Der viel einfachere und bessere Weg ist es, die Namensspalten von Original und Übersetzung nach dem Sortieren in je eine Textdatei zu kopieren. Danach kann man sie mit der Vergleichsfunktion von VisualStudio 12, CodeComparer, WinMerge oder andere Vergleicher vernünftig auf Unterschiede kontrollieren. Leider habe ich eine Weile gebraucht, bis ich mich an den einfachen Weg erinnert habe.

In diesem Schritt sind dann erst mal alle Übersetzungen rausgeflogen, die nicht dazugehörten und die fehlenden aus dem Original kopiert. Zum Glück war ich in der Lage in diesem Falle die fehlenden Übersetzungen selber zu machen, normalerweise würde man hier eine Liste für den Übersetzer machen (Verbot sich hier aus Zeitgründen)

Kontrolle im Code

Um eine Liste aller tatsächlich verwendeten Resource Keys zu bekommen, muss man sie aus dem Code  bzw. den XML Dateien extrahieren. Das geht prima mit einer Suche in Visual Studio. Um sicher zu stellen, das auch alle Zugriffe  auf die globale Resource die Datei mit angeben nutze ich Suchen und Ersetzen mit Regular Expressions:

Suche \$Resources\:{:i};

Ersetze durch $Resources:Wulf.Absence,\1;

image

(wir erzeugen die XML Dateien normalerweise über die SharePoint Software Factory, die legt die Texte in den Featurespezifischen Resourcen ab. Nicht sicher, ob das eine Einstellungssache ist)

Alles was der $Resouce: Datei, Key Syntax folgt, findet man mit der Suche nach

\$Resources\:{:i},{:i};

Das Suchergebnis packt man wieder in eine Textdatei, befreit es von Rauschen über Suchen und Ersetzen mit

Suchen: ^{.*}\$Resources\:{:i},{:i};{.*}$

Ersetzen: \2, \1

Dann hat man Einträge der Form Key, Datei. Alternativ kann man auch mit \2 ersetzen, dann hat man wieder nur die Keys und kann die via WinMerge mit den Keys in der Resource abgleichen.

Fehlersuche

Natürlich lief die Solution nach dem Deployment auf Fehler wegen fehlender Resourcen (Ich hatte ja von Hand verglichen Trauriges Smiley) Hier ein paar Tips, die man dann als erstes kontrollieren sollte.

  1. AppPool recyclen oder IISReset /noforce
  2. Liegen die aktuellen Resourcen im Hive? Die globale Resource muss im Hive in Program Files\Common files\Microsoft Shared\Web Server Extensions\14\Resources liegen. Die AppResource unterhalb von c:\Inetpub\wwwroot\wss\<Ordner der Webapplication>\AppResources
  3. Liegen Resoucen tatsächlich im Ordner der richtigen Webapplication? Unsere Lokalisierungen liegen als eigene Solution vor, damit man bei neuen Übersetzungen die Hauptapplication nicht neu deployen muss. Deshalb darauf achten, das Resource und Applikation auch auf die gleiche Webapplikation deployed werden.
  4. ULS Log via ULSViewer beim Aufruf mitlaufen lassen. Fehlende Resourcen werden als MissingResourceManifestException gemeldet. Wenn man weiß wo man suchen muss,  mit WinMerge die Keys vergleichen
  5. Debuggen für den Zugriff auf fehlende Resourcen vom Code. In diesem Fall ging jeder Zugriff über eine gemeinsame Methode. Die wurde von mir erweitert, das sie für jede fehlende Resource einen Eintrag ins Log machte und dann den Schlüssel zurückgab. Allein durch den Fallback auf den Schlüssel fielen eine ganze Reihe fehlender Felder sofort auf.