Bea Stollnitz zamieściła dobry post na blogu o używaniu do tego rozszerzenia znaczników, pod nagłówkiem „Jak ustawić wiele stylów w WPF?”
Ten blog jest teraz martwy, więc odtwarzam ten wpis tutaj
WPF i Silverlight oferują możliwość wyprowadzenia stylu z innego stylu za pomocą właściwości „BasedOn”. Ta funkcja umożliwia programistom organizowanie stylów przy użyciu hierarchii podobnej do dziedziczenia klas. Rozważ następujące style:
<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>
W tej składni Button, który używa RedButtonStyle, będzie miał właściwość Foreground ustawioną na Red, a właściwość Margin ustawioną na 10.
Ta funkcja jest dostępna w WPF od dłuższego czasu i jest nowa w Silverlight 3.
A co jeśli chcesz ustawić więcej niż jeden styl na elemencie? Ani WPF, ani Silverlight nie zapewniają rozwiązania tego problemu po wyjęciu z pudełka. Na szczęście istnieją sposoby na zaimplementowanie tego zachowania w WPF, które omówię w tym wpisie na blogu.
WPF i Silverlight używają rozszerzeń znaczników, aby zapewnić właściwości z wartościami, które wymagają pewnej logiki do uzyskania. Rozszerzenia znaczników są łatwo rozpoznawalne dzięki obecności nawiasów klamrowych otaczających je w języku XAML. Na przykład rozszerzenie znaczników {Binding} zawiera logikę do pobierania wartości ze źródła danych i aktualizowania jej w przypadku wystąpienia zmian; rozszerzenie znaczników {StaticResource} zawiera logikę do pobierania wartości ze słownika zasobów na podstawie klucza. Na szczęście dla nas WPF umożliwia użytkownikom pisanie własnych niestandardowych rozszerzeń znaczników. Ta funkcja nie jest jeszcze obecna w Silverlight, więc rozwiązanie w tym blogu ma zastosowanie tylko do WPF.
Inni napisali świetne rozwiązania do scalania dwóch stylów za pomocą rozszerzeń znaczników. Zależało mi jednak na rozwiązaniu dającym możliwość łączenia nieograniczonej liczby stylów, co jest nieco trudniejsze.
Pisanie rozszerzenia znaczników jest proste. Pierwszym krokiem jest utworzenie klasy, która pochodzi od MarkupExtension i użycie atrybutu MarkupExtensionReturnType, aby wskazać, że zamierzasz, aby wartość zwracana z rozszerzenia znaczników była typu Style.
[MarkupExtensionReturnType(typeof(Style))]
public class MultiStyleExtension : MarkupExtension
{
}
Określanie danych wejściowych do rozszerzenia znaczników
Chcielibyśmy dać użytkownikom naszego rozszerzenia znaczników prosty sposób określania stylów do scalenia. Zasadniczo istnieją dwa sposoby określania danych wejściowych rozszerzenia znaczników przez użytkownika. Użytkownik może ustawić właściwości lub przekazać parametry do konstruktora. Ponieważ w tym scenariuszu użytkownik potrzebuje możliwości określenia nieograniczonej liczby stylów, moim pierwszym podejściem było utworzenie konstruktora, który pobiera dowolną liczbę ciągów za pomocą słowa kluczowego „params”:
public MultiStyleExtension(params string[] inputResourceKeys)
{
}
Moim celem było napisanie danych wejściowych w następujący sposób:
<Button Style="{local:MultiStyle BigButtonStyle, GreenButtonStyle}" … />
Zwróć uwagę na przecinek oddzielający różne klucze stylu. Niestety, niestandardowe rozszerzenia znaczników nie obsługują nieograniczonej liczby parametrów konstruktora, więc takie podejście powoduje błąd kompilacji. Gdybym wiedział z góry, ile stylów chcę scalić, mógłbym użyć tej samej składni XAML z konstruktorem pobierającym żądaną liczbę ciągów:
public MultiStyleExtension(string inputResourceKey1, string inputResourceKey2)
{
}
Aby obejść ten problem, zdecydowałem, że parametr konstruktora będzie przyjmował pojedynczy ciąg, który określa nazwy stylów oddzielone spacjami. Składnia nie jest taka zła:
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.");
}
}
Obliczanie danych wyjściowych rozszerzenia znaczników
Aby obliczyć dane wyjściowe rozszerzenia znaczników, musimy zastąpić metodę z MarkupExtension o nazwie „ProvideValue”. Wartość zwrócona przez tę metodę zostanie ustawiona w miejscu docelowym rozszerzenia znaczników.
Zacząłem od stworzenia metody rozszerzenia dla Style, która wie, jak połączyć dwa style. Kod tej metody jest dość prosty:
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];
}
}
Zgodnie z powyższą logiką, pierwszy styl jest modyfikowany w celu uwzględnienia wszystkich informacji z drugiego. Jeśli występują konflikty (np. Oba style mają metodę ustawiającą dla tej samej właściwości), wygrywa drugi styl. Zauważ, że oprócz kopiowania stylów i wyzwalaczy, wziąłem również pod uwagę wartości TargetType i BasedOn, a także wszelkie zasoby, które mógł mieć drugi styl. W przypadku typu TargetType połączonego stylu użyłem tego, który typ jest bardziej pochodny. Jeśli drugi styl ma styl BasedOn, rekursywnie scalam jego hierarchię stylów. Jeśli ma zasoby, kopiuję je do pierwszego stylu. Jeśli odwołujemy się do tych zasobów za pomocą {StaticResource}, są one statycznie rozwiązywane przed wykonaniem tego kodu scalającego i dlatego nie jest konieczne ich przenoszenie. Dodałem ten kod na wypadek, gdybyśmy używali DynamicResources.
Przedstawiona powyżej metoda rozszerzenia umożliwia następującą składnię:
style1.Merge(style2);
Ta składnia jest przydatna pod warunkiem, że mam wystąpienia obu stylów w ramach ProvideValue. Cóż, ja nie. Wszystko, co otrzymuję od konstruktora, to lista kluczy ciągów dla tych stylów. Gdyby w parametrach konstruktora istniała obsługa params, mógłbym użyć następującej składni, aby uzyskać rzeczywiste wystąpienia stylu:
<Button Style="{local:MultiStyle {StaticResource BigButtonStyle}, {StaticResource GreenButtonStyle}}" … />
public MultiStyleExtension(params Style[] styles)
{
}
Ale to nie działa. I nawet gdyby ograniczenie parametrów nie istniało, prawdopodobnie trafilibyśmy na inne ograniczenie rozszerzeń znaczników, gdzie musielibyśmy użyć składni elementu właściwości zamiast składni atrybutu, aby określić zasoby statyczne, co jest rozwlekłe i kłopotliwe (wyjaśniam to błąd lepiej w poprzednim poście na blogu ). I nawet gdyby oba te ograniczenia nie istniały, nadal wolałbym pisać listę stylów używając tylko ich nazw - jest krótsza i prostsza do odczytania niż StaticResource dla każdego z nich.
Rozwiązaniem jest utworzenie StaticResourceExtension przy użyciu kodu. Biorąc pod uwagę klucz stylu typu string i dostawcę usług, mogę użyć StaticResourceExtension, aby pobrać rzeczywistą instancję stylu. Oto składnia:
Style currentStyle = new StaticResourceExtension(currentResourceKey).ProvideValue(serviceProvider) as Style;
Teraz mamy wszystkie elementy potrzebne do napisania metody ProvideValue:
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;
}
Oto pełny przykład użycia rozszerzenia znaczników MultiStyle:
<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" />