У Беа Столніц була хороша публікація в блозі про використання розширення розмітки для цього під заголовком "Як я можу встановити кілька стилів у WPF?"
Цей блог зараз мертвий, тому я відтворюю публікацію тут
WPF та Silverlight пропонують можливість вивести стиль із іншого стилю через властивість “BasedOn”. Ця функція дозволяє розробникам організовувати свої стилі, використовуючи ієрархію, подібну до успадкування класів. Розглянемо наступні стилі:
<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>
За допомогою цього синтаксису кнопка, яка використовує RedButtonStyle, матиме властивість переднього плану встановлено на Red, а його властивість Margin - 10.
Ця функція існує вже давно в WPF, і вона нова в Silverlight 3.
Що робити, якщо ви хочете встановити на елементі більше одного стилю? Ні WPF, ні Silverlight не дають рішення для вирішення цієї проблеми поза рамками. На щастя, є способи реалізації такої поведінки у WPF, про які я розповім у цій публікації в блозі.
WPF та Silverlight використовують розширення розмітки для надання властивостей зі значеннями, для отримання яких потрібна певна логіка. Розширення розмітки легко впізнати за наявністю фігурних дужок, що оточують їх у XAML. Наприклад, розширення розмітки {Binding} містить логіку для отримання значення з джерела даних та оновлення його, коли відбуваються зміни; розширення розмітки {StaticResource} містить логіку, щоб захопити значення зі словника ресурсів на основі ключа. На щастя для нас, WPF дозволяє користувачам писати власні розширення для розмітки. Ця функція ще не існує в Silverlight, тому рішення в цьому блозі застосовне лише до WPF.
Інші написали чудові рішення для об'єднання двох стилів, використовуючи розширення розмітки. Однак я хотів рішення, яке надало можливість об'єднати необмежену кількість стилів, що трохи складніше.
Написання розширення розмітки є простим. Перший крок - створити клас, що походить від MarkupExtension, і використовувати атрибут MarkupExtensionReturnType, щоб вказати, що значення, яке повертається з розширення розмітки, має бути типу Style.
[MarkupExtensionReturnType(typeof(Style))]
public class MultiStyleExtension : MarkupExtension
{
}
Визначення входів до розширення розмітки
Ми хотіли б дати користувачам нашого розширення розмітки простий спосіб вказати стилі, які потрібно об’єднати. По суті є два способи, за допомогою яких користувач може вказати входи до розширення розмітки. Користувач може встановити властивості або передати параметри конструктору. Оскільки в цьому сценарії користувачеві потрібна можливість задавати необмежену кількість стилів, моїм першим підходом було створення конструктора, який приймає будь-яку кількість рядків за допомогою ключового слова «парами»:
public MultiStyleExtension(params string[] inputResourceKeys)
{
}
Моя мета полягала в тому, щоб вміти писати вклади наступним чином:
<Button Style="{local:MultiStyle BigButtonStyle, GreenButtonStyle}" … />
Зауважте, кома розділяє різні клавіші стилю. На жаль, спеціальні розширення розмітки не підтримують необмежену кількість параметрів конструктора, тому такий підхід призводить до помилки компіляції. Якби я заздалегідь знав, скільки стилів я хочу об'єднати, я міг би використати той самий синтаксис XAML з конструктором, який взяв потрібну кількість рядків:
public MultiStyleExtension(string inputResourceKey1, string inputResourceKey2)
{
}
Як вирішення, я вирішив, щоб параметр конструктора взяв єдиний рядок, який визначає імена стилів, розділені пробілами. Синтаксис не надто поганий:
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.");
}
}
Обчислення виходу розширення розмітки
Щоб обчислити вихід розширення розмітки, нам потрібно перекрити метод із MarkupExtension під назвою "ProvideValue". Значення, повернене цим методом, буде встановлено в цілі розширення розмітки.
Я почав зі створення методу розширення для Style, який вміє об'єднати два стилі. Код цього методу досить простий:
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];
}
}
З вищенаведеною логікою перший стиль модифікується, щоб включати всю інформацію з другого. Якщо виникають конфлікти (наприклад, обидва стилі мають сетер для однієї властивості), другий стиль виграє. Зауважте, що окрім копіювання стилів та тригерів, я також врахував значення TargetType та BasedOn, а також будь-які ресурси, які може мати другий стиль. Для TargetType об'єднаного стилю я використовував той тип, який є більш похідним. Якщо другий стиль має стиль BasicOn, я зливаю його ієрархію стилів рекурсивно. Якщо у нього є ресурси, я копіюю їх у перший стиль. Якщо для цих ресурсів посилається {StaticResource}, вони статично вирішуються до виконання цього коду злиття, і тому переміщувати їх не потрібно. Я додав цей код, якщо ми використовуємо DynamicResources.
Метод розширення, показаний вище, включає наступний синтаксис:
style1.Merge(style2);
Цей синтаксис корисний за умови, що у мене є екземпляри обох стилів у ProvideValue. Ну, я не хочу. Все, що я отримую від конструктора, - це список рядкових клавіш для цих стилів. Якби підтримка парам в параметрах конструктора, я міг би використати наступний синтаксис, щоб отримати фактичні екземпляри стилю:
<Button Style="{local:MultiStyle {StaticResource BigButtonStyle}, {StaticResource GreenButtonStyle}}" … />
public MultiStyleExtension(params Style[] styles)
{
}
Але це не працює. І навіть якби обмеження для парами не існувало, ми, мабуть, потрапили б до іншого обмеження розширень розмітки, де нам би довелося використовувати синтаксис властивості-елемента замість синтаксису атрибутів для визначення статичних ресурсів, який є багатослівним і громіздким (я пояснюю це помилка краще в попередній публікації блогу ). І навіть якщо обох цих обмежень не існувало, я все-таки краще напишу список стилів, використовуючи лише їх імена - він коротший і простіший для читання, ніж StaticResource для кожного.
Рішення полягає у створенні StaticResourceExtension за допомогою коду. З огляду на стиль стилю рядка типу та постачальника послуг, я можу використовувати StaticResourceExtension для отримання фактичного екземпляра стилю. Ось синтаксис:
Style currentStyle = new StaticResourceExtension(currentResourceKey).ProvideValue(serviceProvider) as Style;
Тепер у нас є всі фрагменти, необхідні для написання методу 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;
}
Ось повний приклад використання розширення розмітки 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" />