Bea Stollnitz hatte einen guten Blogbeitrag über die Verwendung einer Markup-Erweiterung unter der Überschrift "Wie kann ich mehrere Stile in WPF festlegen?"
Dieser Blog ist jetzt tot, also reproduziere ich den Beitrag hier
WPF und Silverlight bieten beide die Möglichkeit, einen Stil über die Eigenschaft „BasedOn“ von einem anderen Stil abzuleiten. Mit dieser Funktion können Entwickler ihre Stile mithilfe einer Hierarchie ähnlich der Klassenvererbung organisieren. Betrachten Sie die folgenden Stile:
<Style TargetType="Button" x:Key="BaseButtonStyle">
<Setter Property="Margin" Value="10" />
</Style>
<Style TargetType="Button" x:Key="RedButtonStyle" BasedOn="{StaticResource BaseButtonStyle}">
<Setter Property="Foreground" Value="Red" />
</Style>
Bei dieser Syntax wird für eine Schaltfläche, die RedButtonStyle verwendet, die Foreground-Eigenschaft auf Red und die Margin-Eigenschaft auf 10 festgelegt.
Diese Funktion gibt es in WPF schon lange und sie ist neu in Silverlight 3.
Was ist, wenn Sie mehr als einen Stil für ein Element festlegen möchten? Weder WPF noch Silverlight bieten eine sofort einsatzbereite Lösung für dieses Problem. Glücklicherweise gibt es Möglichkeiten, dieses Verhalten in WPF zu implementieren, die ich in diesem Blogbeitrag diskutieren werde.
WPF und Silverlight verwenden Markup-Erweiterungen, um Eigenschaften mit Werten zu versehen, für deren Abruf eine gewisse Logik erforderlich ist. Markup-Erweiterungen sind leicht an geschweiften Klammern in XAML zu erkennen. Beispielsweise enthält die Markup-Erweiterung {Binding} eine Logik zum Abrufen eines Werts aus einer Datenquelle und zum Aktualisieren, wenn Änderungen auftreten. Die Markup-Erweiterung {StaticResource} enthält Logik zum Abrufen eines Werts aus einem Ressourcenwörterbuch basierend auf einem Schlüssel. Zum Glück können Benutzer mit WPF ihre eigenen benutzerdefinierten Markup-Erweiterungen schreiben. Diese Funktion ist in Silverlight noch nicht vorhanden, daher gilt die Lösung in diesem Blog nur für WPF.
Andere haben großartige Lösungen geschrieben, um zwei Stile mithilfe von Markup-Erweiterungen zusammenzuführen. Ich wollte jedoch eine Lösung, die die Möglichkeit bietet, eine unbegrenzte Anzahl von Stilen zusammenzuführen, was etwas schwieriger ist.
Das Schreiben einer Markup-Erweiterung ist unkompliziert. Der erste Schritt besteht darin, eine Klasse zu erstellen, die von MarkupExtension abgeleitet ist, und das MarkupExtensionReturnType-Attribut zu verwenden, um anzugeben, dass der von Ihrer Markup-Erweiterung zurückgegebene Wert vom Typ Style sein soll.
[MarkupExtensionReturnType(typeof(Style))]
public class MultiStyleExtension : MarkupExtension
{
}
Angeben von Eingaben für die Markup-Erweiterung
Wir möchten Benutzern unserer Markup-Erweiterung eine einfache Möglichkeit bieten, die zusammenzuführenden Stile anzugeben. Grundsätzlich gibt es zwei Möglichkeiten, wie der Benutzer Eingaben für eine Markup-Erweiterung angeben kann. Der Benutzer kann Eigenschaften festlegen oder Parameter an den Konstruktor übergeben. Da der Benutzer in diesem Szenario die Möglichkeit haben muss, eine unbegrenzte Anzahl von Stilen anzugeben, bestand mein erster Ansatz darin, einen Konstruktor zu erstellen, der eine beliebige Anzahl von Zeichenfolgen mit dem Schlüsselwort "params" verwendet:
public MultiStyleExtension(params string[] inputResourceKeys)
{
}
Mein Ziel war es, die Eingaben wie folgt schreiben zu können:
<Button Style="{local:MultiStyle BigButtonStyle, GreenButtonStyle}" … />
Beachten Sie das Komma zwischen den verschiedenen Stilschlüsseln. Leider unterstützen benutzerdefinierte Markup-Erweiterungen keine unbegrenzte Anzahl von Konstruktorparametern, sodass dieser Ansatz zu einem Kompilierungsfehler führt. Wenn ich im Voraus gewusst hätte, wie viele Stile ich zusammenführen wollte, hätte ich dieselbe XAML-Syntax mit einem Konstruktor verwenden können, der die gewünschte Anzahl von Zeichenfolgen verwendet:
public MultiStyleExtension(string inputResourceKey1, string inputResourceKey2)
{
}
Um dieses Problem zu umgehen, habe ich beschlossen, dass der Konstruktorparameter eine einzelne Zeichenfolge verwendet, die die durch Leerzeichen getrennten Stilnamen angibt. Die Syntax ist nicht schlecht:
private string[] resourceKeys;
public MultiStyleExtension(string inputResourceKeys)
{
if (inputResourceKeys == null)
{
throw new ArgumentNullException("inputResourceKeys");
}
this.resourceKeys = inputResourceKeys.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
if (this.resourceKeys.Length == 0)
{
throw new ArgumentException("No input resource keys specified.");
}
}
Berechnung der Ausgabe der Markup-Erweiterung
Um die Ausgabe einer Markup-Erweiterung zu berechnen, müssen Sie eine Methode aus MarkupExtension mit dem Namen "ProvideValue" überschreiben. Der von dieser Methode zurückgegebene Wert wird im Ziel der Markup-Erweiterung festgelegt.
Ich habe zunächst eine Erweiterungsmethode für Style erstellt, mit der zwei Stile zusammengeführt werden können. Der Code für diese Methode ist recht einfach:
public static void Merge(this Style style1, Style style2)
{
if (style1 == null)
{
throw new ArgumentNullException("style1");
}
if (style2 == null)
{
throw new ArgumentNullException("style2");
}
if (style1.TargetType.IsAssignableFrom(style2.TargetType))
{
style1.TargetType = style2.TargetType;
}
if (style2.BasedOn != null)
{
Merge(style1, style2.BasedOn);
}
foreach (SetterBase currentSetter in style2.Setters)
{
style1.Setters.Add(currentSetter);
}
foreach (TriggerBase currentTrigger in style2.Triggers)
{
style1.Triggers.Add(currentTrigger);
}
// This code is only needed when using DynamicResources.
foreach (object key in style2.Resources.Keys)
{
style1.Resources[key] = style2.Resources[key];
}
}
Mit der obigen Logik wird der erste Stil so geändert, dass er alle Informationen aus dem zweiten enthält. Wenn es Konflikte gibt (z. B. haben beide Stile einen Setter für dieselbe Eigenschaft), gewinnt der zweite Stil. Beachten Sie, dass ich neben dem Kopieren von Stilen und Triggern auch die TargetType- und BasedOn-Werte sowie alle Ressourcen des zweiten Stils berücksichtigt habe. Für den TargetType des zusammengeführten Stils habe ich den Typ verwendet, der stärker abgeleitet ist. Wenn der zweite Stil einen BasedOn-Stil hat, füge ich seine Stilhierarchie rekursiv zusammen. Wenn es Ressourcen hat, kopiere ich sie in den ersten Stil. Wenn auf diese Ressourcen mit {StaticResource} verwiesen wird, werden sie statisch aufgelöst, bevor dieser Zusammenführungscode ausgeführt wird. Daher ist es nicht erforderlich, sie zu verschieben. Ich habe diesen Code hinzugefügt, falls wir DynamicResources verwenden.
Die oben gezeigte Erweiterungsmethode aktiviert die folgende Syntax:
style1.Merge(style2);
Diese Syntax ist nützlich, vorausgesetzt, ich habe Instanzen beider Stile in ProvideValue. Nun, ich nicht. Alles, was ich vom Konstruktor bekomme, ist eine Liste von Zeichenfolgenschlüsseln für diese Stile. Wenn die Konstruktorparameter Parameter unterstützen würden, hätte ich die folgende Syntax verwenden können, um die tatsächlichen Stilinstanzen abzurufen:
<Button Style="{local:MultiStyle {StaticResource BigButtonStyle}, {StaticResource GreenButtonStyle}}" … />
public MultiStyleExtension(params Style[] styles)
{
}
Das geht aber nicht Und selbst wenn die Parameterbeschränkung nicht vorhanden wäre, würden wir wahrscheinlich eine andere Einschränkung der Markup-Erweiterungen treffen, bei der wir die Eigenschaftselementsyntax anstelle der Attributsyntax verwenden müssten, um die statischen Ressourcen anzugeben, die ausführlich und umständlich sind (ich erkläre dies Fehler besser in einem früheren Blog-Beitrag ). Und selbst wenn diese beiden Einschränkungen nicht existieren würden, würde ich die Liste der Stile lieber nur mit ihren Namen schreiben - sie ist kürzer und einfacher zu lesen als eine StaticResource für jeden.
Die Lösung besteht darin, eine StaticResourceExtension mithilfe von Code zu erstellen. Bei einem Stilschlüssel vom Typ Zeichenfolge und einem Dienstanbieter kann ich StaticResourceExtension verwenden, um die tatsächliche Stilinstanz abzurufen. Hier ist die Syntax:
Style currentStyle = new StaticResourceExtension(currentResourceKey).ProvideValue(serviceProvider) as Style;
Jetzt haben wir alle Teile, die zum Schreiben der ProvideValue-Methode benötigt werden:
public override object ProvideValue(IServiceProvider serviceProvider)
{
Style resultStyle = new Style();
foreach (string currentResourceKey in resourceKeys)
{
Style currentStyle = new StaticResourceExtension(currentResourceKey).ProvideValue(serviceProvider) as Style;
if (currentStyle == null)
{
throw new InvalidOperationException("Could not find style with resource key " + currentResourceKey + ".");
}
resultStyle.Merge(currentStyle);
}
return resultStyle;
}
Hier ist ein vollständiges Beispiel für die Verwendung der MultiStyle-Markup-Erweiterung:
<Window.Resources>
<Style TargetType="Button" x:Key="SmallButtonStyle">
<Setter Property="Width" Value="120" />
<Setter Property="Height" Value="25" />
<Setter Property="FontSize" Value="12" />
</Style>
<Style TargetType="Button" x:Key="GreenButtonStyle">
<Setter Property="Foreground" Value="Green" />
</Style>
<Style TargetType="Button" x:Key="BoldButtonStyle">
<Setter Property="FontWeight" Value="Bold" />
</Style>
</Window.Resources>
<Button Style="{local:MultiStyle SmallButtonStyle GreenButtonStyle BoldButtonStyle}" Content="Small, green, bold" />