Bea Stollnitz đã có một bài đăng blog tốt về việc sử dụng tiện ích mở rộng đánh dấu cho mục này, dưới tiêu đề "Làm cách nào tôi có thể đặt nhiều kiểu trong WPF?"
Blog đó đã chết, vì vậy tôi đang sao chép bài viết ở đây
Cả WPF và Silverlight đều cung cấp khả năng lấy ra một Phong cách từ một Phong cách khác thông qua thuộc tính của Dựa trên cơ sở. Tính năng này cho phép các nhà phát triển sắp xếp các kiểu của họ bằng cách sử dụng một hệ thống phân cấp tương tự như kế thừa lớp. Hãy xem xét các phong cách sau:
<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>
Với cú pháp này, một Nút sử dụng RedButtonStyle sẽ có thuộc tính Tiền cảnh được đặt thành Đỏ và thuộc tính Margin của nó được đặt thành 10.
Tính năng này đã xuất hiện trong WPF từ lâu và nó mới xuất hiện trong Silverlight 3.
Điều gì nếu bạn muốn đặt nhiều hơn một kiểu trên một thành phần? Cả WPF và Silverlight đều không cung cấp giải pháp cho vấn đề này. May mắn thay, có nhiều cách để thực hiện hành vi này trong WPF, mà tôi sẽ thảo luận trong bài đăng trên blog này.
WPF và Silverlight sử dụng các phần mở rộng đánh dấu để cung cấp các thuộc tính với các giá trị yêu cầu một số logic để có được. Phần mở rộng đánh dấu có thể dễ dàng nhận ra bởi sự hiện diện của dấu ngoặc nhọn bao quanh chúng trong XAML. Ví dụ: tiện ích mở rộng đánh dấu {Binding} chứa logic để tìm nạp một giá trị từ nguồn dữ liệu và cập nhật nó khi có thay đổi; tiện ích mở rộng đánh dấu {StaticResource} chứa logic để lấy giá trị từ từ điển tài nguyên dựa trên khóa. May mắn cho chúng tôi, WPF cho phép người dùng viết các phần mở rộng đánh dấu tùy chỉnh của riêng họ. Tính năng này chưa có trong Silverlight, vì vậy giải pháp trong blog này chỉ áp dụng cho WPF.
Những người khác đã viết các giải pháp tuyệt vời để hợp nhất hai phong cách sử dụng tiện ích mở rộng đánh dấu. Tuy nhiên, tôi muốn một giải pháp cung cấp khả năng hợp nhất số lượng kiểu không giới hạn, khó hơn một chút.
Viết một phần mở rộng đánh dấu là đơn giản. Bước đầu tiên là tạo một lớp xuất phát từ MarkupExtension và sử dụng thuộc tính MarkupExtensionReturnType để chỉ ra rằng bạn dự định giá trị được trả về từ tiện ích mở rộng đánh dấu của bạn là Kiểu.
[MarkupExtensionReturnType(typeof(Style))]
public class MultiStyleExtension : MarkupExtension
{
}
Chỉ định đầu vào cho phần mở rộng đánh dấu
Chúng tôi muốn cung cấp cho người dùng tiện ích mở rộng đánh dấu của chúng tôi một cách đơn giản để chỉ định các kiểu sẽ được hợp nhất. Về cơ bản có hai cách mà người dùng có thể chỉ định đầu vào cho tiện ích mở rộng đánh dấu. Người dùng có thể đặt thuộc tính hoặc truyền tham số cho hàm tạo. Vì trong kịch bản này, người dùng cần khả năng chỉ định số lượng kiểu không giới hạn, cách tiếp cận đầu tiên của tôi là tạo một hàm tạo lấy bất kỳ số chuỗi nào bằng cách sử dụng từ khóa param params:
public MultiStyleExtension(params string[] inputResourceKeys)
{
}
Mục tiêu của tôi là có thể viết các đầu vào như sau:
<Button Style="{local:MultiStyle BigButtonStyle, GreenButtonStyle}" … />
Lưu ý dấu phẩy phân tách các phím kiểu khác nhau. Thật không may, tiện ích mở rộng đánh dấu tùy chỉnh không hỗ trợ số lượng tham số hàm tạo không giới hạn, vì vậy cách tiếp cận này dẫn đến lỗi biên dịch. Nếu tôi biết trước có bao nhiêu kiểu tôi muốn hợp nhất, tôi có thể đã sử dụng cùng một cú pháp XAML với một hàm tạo lấy số chuỗi mong muốn:
public MultiStyleExtension(string inputResourceKey1, string inputResourceKey2)
{
}
Như một giải pháp thay thế, tôi đã quyết định để tham số hàm tạo lấy một chuỗi xác định tên kiểu được phân tách bằng dấu cách. Cú pháp không quá tệ:
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.");
}
}
Tính toán đầu ra của phần mở rộng đánh dấu
Để tính toán đầu ra của tiện ích mở rộng đánh dấu, chúng ta cần ghi đè một phương thức từ MarkupExtension có tên là Cung cấpValue. Giá trị được trả về từ phương thức này sẽ được đặt trong mục tiêu của tiện ích mở rộng đánh dấu.
Tôi đã bắt đầu bằng cách tạo một phương thức mở rộng cho Style để biết cách hợp nhất hai kiểu. Mã cho phương pháp này khá đơn giản:
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];
}
}
Với logic ở trên, kiểu đầu tiên được sửa đổi để bao gồm tất cả thông tin từ kiểu thứ hai. Nếu có xung đột (ví dụ: cả hai kiểu đều có setter cho cùng một thuộc tính), kiểu thứ hai sẽ thắng. Lưu ý rằng ngoài việc sao chép kiểu và kích hoạt, tôi cũng đã tính đến các giá trị TargetType và Dựa trên cũng như bất kỳ tài nguyên nào mà kiểu thứ hai có thể có. Đối với TargetType của kiểu được hợp nhất, tôi đã sử dụng loại nào có nguồn gốc nhiều hơn. Nếu kiểu thứ hai có kiểu Dựa trên, tôi hợp nhất phân cấp kiểu của nó theo cách đệ quy. Nếu nó có tài nguyên, tôi sao chép chúng sang kiểu đầu tiên. Nếu các tài nguyên đó được đề cập đến bằng cách sử dụng {StaticResource}, chúng sẽ được giải quyết tĩnh trước khi mã hợp nhất này thực thi và do đó không cần thiết phải di chuyển chúng. Tôi đã thêm mã này trong trường hợp chúng tôi đang sử dụng DynamicResource.
Phương thức mở rộng được hiển thị ở trên cho phép cú pháp sau:
style1.Merge(style2);
Cú pháp này hữu ích với điều kiện tôi có các phiên bản của cả hai kiểu trong ProvValue. Vâng, tôi không. Tất cả những gì tôi nhận được từ hàm tạo là một danh sách các khóa chuỗi cho các kiểu đó. Nếu có hỗ trợ cho các tham số trong các tham số của hàm tạo, tôi có thể đã sử dụng cú pháp sau để lấy các thể hiện kiểu thực tế:
<Button Style="{local:MultiStyle {StaticResource BigButtonStyle}, {StaticResource GreenButtonStyle}}" … />
public MultiStyleExtension(params Style[] styles)
{
}
Nhưng điều đó không hiệu quả. Và ngay cả khi giới hạn params không tồn tại, có lẽ chúng ta sẽ gặp giới hạn khác của tiện ích mở rộng đánh dấu, trong đó chúng ta sẽ phải sử dụng cú pháp phần tử thuộc tính thay vì cú pháp thuộc tính để chỉ định tài nguyên tĩnh, dài dòng và cồng kềnh (tôi giải thích điều này lỗi tốt hơn trong một bài viết trên blog trước đó ). Và ngay cả khi cả hai giới hạn đó không tồn tại, tôi vẫn thà viết danh sách các kiểu chỉ sử dụng tên của chúng - nó ngắn hơn và đơn giản hơn để đọc so với TĩnhResource cho mỗi kiểu.
Giải pháp là tạo một StaticResourceExtension bằng mã. Đưa ra khóa kiểu của chuỗi kiểu và nhà cung cấp dịch vụ, tôi có thể sử dụng StaticResourceExtension để truy xuất thể hiện kiểu thực tế. Đây là cú pháp:
Style currentStyle = new StaticResourceExtension(currentResourceKey).ProvideValue(serviceProvider) as Style;
Bây giờ chúng ta có tất cả các phần cần thiết để viết phương thức ProvValue:
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;
}
Dưới đây là một ví dụ đầy đủ về việc sử dụng tiện ích mở rộng đánh dấu 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" />