Feb 072012
 

Bei WPF kann man mit Styles, ControlTemplates etc. wirklich viel erreichen. Ich bin großer Fan davon, viel “Standard-Style” in applikationsweit gültige StaticResources auszulagern, und nur noch ganz wenig “Custom-Styling” in der View vorzunehmen.
Anders ist es IMHO nicht möglich, in größeren Teams Wildwuchs zu vermeiden oder später “DEN” Style nochmal großflächig zu ändern.

Ein großes Defizit gibt es aber: es ist unglaublich lästig, eine Style-Hierarchie zu pflegen, z.B. einen Firmenstandard in Styles zu definieren, und dann aber für eine Einzelanwendung für Kunde X ein kleines Delta obendrauf zu setzen. Noch lästiger ist es, bei Komponenten von Fremdherstellern die Styles anpassen zu wollen, da hier oft diese Möglichkeit gar nicht vorgesehen ist.

Neulich wollte ich z.B. beim Toolbar im PropertyGrid von Mindscape einige Tooltipps eindeutschen, die leider in der vorliegenden Version nicht anpassbar waren. Dafür hätte ich das gesamte ControlTemplate für das PropertyGrid kopieren müssen, dann die 4 Texte ändern und die geänderte Version in meine Applikationsstyles integrieren müssen. Ein neuer Style mit BasedOn oder das Hinterlegen eines Styles nur für die Teilkomponente “Toolbar” ging nicht.

  1. Copy/Paste widerspricht DRY, und man kann dem Style hinterher nicht mehr ansehen, welcher Teil 1:1 kopiert und welcher angepasst ist (Ja, ich weiß dass Kommentare auch in XML möglich sind, aber ich weiß auch dass die keiner liest oder gar pflegt)
  2. Irgendwas an dem kopierten XAML hat dem WPF-Designer nicht geschmeckt, und da er in den applikationsweiten Ressourcen sein sollte, hat das den WPF-Designer gleich für die gesamte Solution ausgehebelt.
  3. Dadurch wurde die gesamte Ressource erst von der Fremdhersteller-Lib und dann nochmal von meiner App eingelesen, auch unnötig.

Also braucht es einen anderen Weg, mein kleines Delta in den Style bzw. das ControlTemplate einzubauen.
Und zwar wollte ich “OnApplyTemplate” das angewandte ControlTemplate im Code-Behind manipulieren. Dafür muss man natürlich den Visual Tree herunterlaufen und die anzupassenden Elemente heraussuchen. Da ich sowieso eine Ableitung des UserControls hatte, konnte ich mich problemlos in das Event einhängen – alternativ hätte ich auch ein "Behavior" bauen können, was sich ins Event einklinkt.
Folgende Extension-Methode funktioniert für sowas wunderbar:

public static class WPFTreeUtils {

  public static IEnumerable FindVisualChildren(this DependencyObject obj, bool applyTemplateIfNeeded) 
    where T : DependencyObject {
      if (obj != null) {
        int childrenCount = VisualTreeHelper.GetChildrenCount(obj);
        if (childrenCount==0 && applyTemplateIfNeeded) {
          var fe = obj as FrameworkElement;
          if (fe != null) {
            if (fe.ApplyTemplate()) {
              childrenCount = VisualTreeHelper.GetChildrenCount(obj);
            }
          }
        }
        for (int i = 0; i < childrenCount; i++) {
          var child = VisualTreeHelper.GetChild(obj, i);
          if (child is T) {
            yield return (T)child;
          }
          var childOfChild = child.FindVisualChildren();
          if (childOfChild != null) {
            foreach (T c in childOfChild) {
              yield return c;
            }
          }
        }
      }
    }
}

Die rekursive Suche mit dem VisualTreeHelper ist relativ straight forward. Tricky ist die kleine If-Abfrage: Bevor man mit leeren Händen nach Hause geht, sollte man erst noch gucken, ob durch "ApplyTemplate" nicht doch noch eine Unterstruktur gefüllt wird, in der man fündig wird. So konnte ich meine Buttons herauspicken und die Tooltipps durch eine lokalisierte Variante ersetzen und noch einen Labeltext ändern. Auf diese Art kann man, statt immer neue, immer größere ResourceDictionaries zu erzeugen zur Laufzeit die Änderungen anbringen. Natürlich ist das nicht für alle Anwendungen geeignet, aber in diesem Fall z.B. wird das Control nur selten instanziiert und es kostet nicht die Welt, jedesmal dann Hand anzulegen.

n/a