Lang:German


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!

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.

Gerade einen Fehler behoben der mich sehr viel Zeit gekostet hat. Beim Publishen eines Projektes erhielt ich den Fehler

The target "_CopyBinDeployableAssemblies" does not exist in the project.

Ursache war ein modifiziertes .target File das nicht durch das ServicePack 1 von Visual Studio upgedated wurde.

Zum Glück hatte ich eine Maschine, bei der die Installation von SP1 funktioniert hatte. Habe die Daten aller .target Files verglichen und fand dabei eine Datei deren Datum abwich. Das auch noch eine Kopie der Datei mit der Endung .target.org daneben lag, half mir dann auch noch auf die Sprünge Smiley

Mit der nächsten .Net Version wird eine deutliche Vereinfachung der asynchronen Programmierung einher gehen.

C# und VB.Net werden um die Schlüsselwörter async und await erweitert. Async markiert eine Methode oder einen Lambda Ausdruck und await wartet auf den Abschluss eines Tasks ohne zu blockieren.

Beispiel:

public async void ProcessPage(Uri url)
{
  Task<string> downloader =  new WebClient().DownloadStringTaskAsync(url);
  string pageData = await downloader;
  // ... process ...
}

Await erlaubt es asynchronen Code so zu schreiben, das die eigentliche Programmlogik nicht mehr unter Tonnen von boilerplate Code verborgen ist.

Eines der wesentlichen Konzepte für die Benutzung von async und await ist die Task Struktur, die mit .Net 4 eingeführt wurde. Task ist eine Repräsentation des asynchronen Vorgangs. Task ist das Handle mit dem man auf den Vorgang zugreifen kann. Es ist nicht der Thread und es ist nicht das Resultat des Vorgangs. Task bietet Zugang zum Status des Vorgangs, zum Ergebnis und zu evtl. Fehlern.

Microsoft bietet eine CTP zum Download an, mit der man die neuen Keywords ausprobieren kann. (s.u.) Auf dieser Seite findet man auch die wichtigsten Links u.a. zu sehr informativen Videos auf Channel 9. Zusammen mit der CTP werden eine Reihe Dokumente installiert und jede Menge Beispiele. Das wichtigste ist das 101 Async Samples. Ein wahrer Schatz! Danke Microsoft, diese Samples erleichtern den Einstieg so sehr.

Links

Visual Studio Async CTP http://msdn.microsoft.com/en-us/vstudio/async.aspx

Async Readme (wird mit Async CTP installiert unter My Documents > Microsoft Visual Studio Async CTP)

Eric Lippert‘s Blog http://blogs.msdn.com/b/ericlippert/

Jon Skeet‘s Blog http://msmvps.com/blogs/jon_skeet/default.aspx

Slides zum Vortrag

Mal ein Beitrag vollkommen Off-Topic. Wir waren gerade mit unserer Tochter in der Alice im Wunderland Inszenierung des Jugendtheaters Kleinenberg. Ehrlich gesagt hatten wir eine Aufführung auf dem Niveau der üblichen Schulaufführungen erwartet.

Gesehen haben wir ein freches und witziges Musical für Kinder und Erwachsene. Schauspieler und Stück haben sehr gut gefallen, alle Rollen waren sehr gut besetzt. (Natürlich merkt man insbesondere bei den kleineren Nebenrollen, das es keine Profischauspieler sind, aber das ist bei völlig Ok) Das Stück ist eine behutsam modernisierte Musicalfassung des zeitlosen Kinderbuches von Lewis Carroll.  Das Publikum wird immer wieder auf gekonnte Art und Weise in das Stück einbezogen. Da hüpft zwischendurch auch schon mal das weiße Kaninchen auf Sprungschuhen an  den Zuschauern vorbei.

Die Bühne ist sehr klein, aber die Art und Weise wie der verfügbare Raum immer wieder neu genutzt wird, mal mit verschiebbaren Wänden, mal mit Tüchern, mal mit dem sehr gut dosiertem Einsatz von Projektionen, ist wirklich beeindruckend.

Mein Fazit: Hat sich auf jeden Fall gelohnt, nicht nur unsere Tochter, auch wir hatten viel Spaß an ihrem ersten Theaterbesuch. Wir kommen auf jeden Fall wieder.

imageEs ist nicht besonders selten, das wir Entwickler Listen mit Gruppierungen in irgendeiner Form darstellen müssen. Leider ist das auf dem Windows Phone nicht out-of-the-box möglich. Meine ersten Ansätze mit CollectionViewSource zu arbeiten wie von Bea Stollnitz beschrieben, schlugen fehl weil Silverlight auf dem Phone die GroupDescription nichtunterstützt. PagedCollectionViewSource geht auch nicht, so was tun?

Schlussendlich folgte die Lösung dem Motto ‘Back to the Basics’.

Zuerst brauchen wir eine gruppierte Liste als Basis. Hier verwende ich eine einfache ViewModelBase Klasse als Basisklasse, aber im Grunde ginge auch INotifyPropertyChanged (hab ich aber nicht getestet). Die Gruppierung erzeuge ich über LINQ on the fly aus einer flachen Liste:

       /// <summary>
        /// A collection for ItemViewModel objects.
        /// </summary>
        public ObservableCollection<ItemViewModel> Items { get; private set; }

       /// <summary>
        /// A collection for Category and ItemViewModel objects.
        /// </summary>
        public ObservableCollection<ViewModelBase> CategoriesAndItems { get; private set; }

        /// <summary>
        /// Creates and adds a few ItemViewModel objects into the Items collection.
        /// </summary>
        public void LoadData()
        {
            // Sample data; replace with real data
            this.Items.Add(new ItemViewModel() { 
                                  Id=Items.Count, 
                                  Group="Group 1", 
                                  LineOne = "runtime one",
                                  LineTwo = "Maecenas ... bibendum" });
                // ....
            this.Items.Add(new ItemViewModel() { 
                                 Id = Items.Count,
                                 Group = "Group 4", 
                                 LineOne = "runtime sixteen", 
                                 LineTwo = "Nascetur....t pulvinar" });

            var categories = from i in Items
                                     group i by i.Group into category
                                     select new CategoryViewModel(category.Key, category);
            foreach (var category in categories)
            {
               CategoriesAndItems.Add(category);
                foreach (var item in category.Items)
                {
                    CategoriesAndItems.Add(item);
                }
            }
            this.IsDataLoaded = true;

            NotifyPropertyChanged("ItemsView");
        }

Jetzt brauchen wir nur noch eine Listbox und ein Datatemplate pro Datatype. Dummerweise gibt es implizite Styles erst ab Silverlight 4, also helfen wir uns mit TemplateSelector … welchen es auf dem Phone nicht gibt. Also kramen wir noch mal unser Basiswissen raus. TemplateSelector ist ein Control welches Content mit passemdem Template darstellt. Also nur ein ContentControl mit zusätzlicher Logik zur Auswahl des Templates. ContentControl bietet eine Property ContentTemplate und eine Methode, die aufgerufen wird, wenn sich der Content geändert hat. Also führen wir eine kleine Konvention ein, dass das Datatemplate so heißen soll, wie der Type des Contents.

<Application.Resources>
        <DataTemplate x:Key="TestGroupedList.CategoryViewModel">
            <TextBlock Text="{Binding CategoryText}"
                       Style="{StaticResource PhoneTextGroupHeaderStyle}"
                       Margin="0,18,0,0"/>
        </DataTemplate>
        <DataTemplate x:Key="TestGroupedList.ItemViewModel">
            <StackPanel Margin="32,0,0,17"
                        Width="432">
                <TextBlock Text="{Binding LineOne}"
                           TextWrapping="Wrap"
                           Style="{StaticResource PhoneTextExtraLargeStyle}" />
                <TextBlock Text="{Binding LineTwo}"
                           TextWrapping="Wrap"
                           Margin="12,-6,12,0"
                           Style="{StaticResource PhoneTextSubtleStyle}" />
            </StackPanel>
        </DataTemplate>
    </Application.Resources>

Die Selector Klasse selbst ist dann sehr einfach:

public class DataTemplateSelector : ContentControl
{
    protected override void OnContentChanged(object oldContent, object newContent)
    {
        if (newContent == null)
       {
           ContentTemplate = null;
        } else {
            var type = newContent.GetType();
            if (Application.Current.Resources.Contains(type.FullName))
            {
                ContentTemplate = App.Current.Resources[type.FullName] as DataTemplate;
            }
         }
    }
}

Unser XAML für die Listbox sieht so aus:

<ListBox x:Name="MainListBox"
		 Margin="0,0,-12,0"
	                 ItemsSource="{Binding CategoriesAndItems}"
		SelectionChanged="MainListBox_SelectionChanged">
	<ListBox.ItemTemplate>
		<DataTemplate>
			<TestGroupedList:DataTemplateSelector Content="{Binding}" />
		</DataTemplate>
	</ListBox.ItemTemplate>
</ListBox>

In diesem Fall möchte ich nicht das ein Klick auf einen Gruppenheader irgendwas bewirkt, also navigiere ich nur wenn das ausgewählte Item ein ItemViewModel ist. Je nach Anwendungsfall kann ich hier natürlich auch zu Gruppeneinstellungen navigieren o.ä.

 var item = MainListBox.SelectedItem as ItemViewModel;
 if (item != null)
 {
  // Navigate to the new page
  NavigationService.Navigate(new Uri("/DetailsPage.xaml?Id=" + item.Id,
                                                   UriKind.Relative));

   // Reset selected index to -1 (no selection)
    MainListBox.SelectedIndex = -1;
}

Der größte Nachteil dieses Ansatzes soll nicht verschwiegen werden. Da die Auflösung des Templates erst zur Laufzeit stattfindet, kann weder Visual Studio noch Expression Blend eine Vorschau zeigen.

In diesem Fall hatte ich mich entschieden, das die Gruppierung rein für die Darstellung dient, die Logik sollte davon weitgehend unbeeinflusst bleiben. Alternativ hätte ich eine reine Kategorieliste nehmen können und die Items  über HeaderedItemsControl darzustellen. In dem Fall sind Features wie auf- und zuklappen usw. viel einfacher zu implementieren.

Beispielprojekt

Dieses Mal:

Grundkurs funktionale Programmierung mit Scala von Lothar Piepmeyer, Hanser

Recht gut geschrieben, aber für meinen Geschmack ist die Einführung der funktionalen Welt mit Hilfe von Java und der Schwenk im zweiten Teil des Buches auf Scala ein bisschen am Ziel vorbei. Die Sprachkonstrukte von Scala konnten sich m.E. so nicht richtig festigen. Für jemanden, der ohne Wissen über funktionale Sprachen von Java (oder auch C#) kommt, erklärt der Ansatz aber sehr gut funktionale Konstrukte anhand bekannter Mechanismen und deren Vor- und Nachteile. Allerdings hätte das Buch ruhig noch ein bisschen stärker in die Tiefe gehen können.

Fazit:

Ein gutes Buch, aber nicht so richtig für mich geeignet. Von mir 3 von 5 Funktionen.

Next Page »