Bea Stollnitz มีบล็อกโพสต์ที่ดีเกี่ยวกับการใช้ส่วนขยายมาร์กอัปสำหรับสิ่งนี้ภายใต้หัวข้อ "ฉันจะตั้งค่ารูปแบบหลายอย่างใน 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 จะมีคุณสมบัติ Foreground ที่ตั้งค่าเป็นสีแดงและตั้งค่าคุณสมบัติระยะขอบเป็น 10
คุณลักษณะนี้อยู่ใน WPF มาเป็นเวลานานและเป็นคุณสมบัติใหม่ใน Silverlight 3
ถ้าคุณต้องการตั้งค่าสไตล์มากกว่าหนึ่งองค์ประกอบ WPF และ Silverlight ไม่มีวิธีแก้ปัญหาสำหรับปัญหานี้นอกกรอบ โชคดีที่มีวิธีการใช้พฤติกรรมนี้ใน WPF ซึ่งฉันจะพูดถึงในโพสต์บล็อกนี้
WPF และ Silverlight ใช้ส่วนขยายมาร์กอัปเพื่อจัดเตรียมคุณสมบัติที่มีค่าที่ต้องใช้ตรรกะในการจัดหา ส่วนขยายมาร์กอัปสามารถจดจำได้ง่ายโดยการมีวงเล็บปีกกาล้อมรอบพวกเขาใน XAML ตัวอย่างเช่นส่วนขยายมาร์กอัป {Binding} มีตรรกะเพื่อดึงค่าจากแหล่งข้อมูลและอัปเดตเมื่อมีการเปลี่ยนแปลงเกิดขึ้น ส่วนขยายมาร์กอัป {StaticResource} มีตรรกะเพื่อรับค่าจากพจนานุกรมทรัพยากรโดยยึดตามคีย์ โชคดีสำหรับเรา WPF อนุญาตให้ผู้ใช้เขียนส่วนขยายมาร์กอัปที่กำหนดเองของตัวเอง คุณลักษณะนี้ยังไม่ปรากฏใน Silverlight ดังนั้นโซลูชันในบล็อกนี้จึงสามารถใช้กับ WPF เท่านั้น
คนอื่น ๆได้เขียนวิธีแก้ปัญหาที่ดีในการผสานสองลักษณะโดยใช้ส่วนขยายมาร์กอัป อย่างไรก็ตามฉันต้องการโซลูชันที่ให้ความสามารถในการผสานสไตล์ไม่ จำกัด จำนวนซึ่งเป็นเรื่องยากเล็กน้อย
การเขียนส่วนขยายมาร์กอัปนั้นตรงไปตรงมา ขั้นตอนแรกคือการสร้างคลาสที่มาจาก MarkupExtension และใช้แอตทริบิวต์ MarkupExtensionReturnType เพื่อระบุว่าคุณต้องการให้ค่าที่ส่งคืนจากส่วนขยายมาร์กอัปของคุณเป็นประเภทของสไตล์
[MarkupExtensionReturnType(typeof(Style))]
public class MultiStyleExtension : MarkupExtension
{
}
การระบุอินพุตให้กับส่วนขยายมาร์กอัพ
เราต้องการให้ผู้ใช้ส่วนขยายมาร์กอัปของเราเป็นวิธีที่ง่ายในการระบุสไตล์ที่จะผสาน โดยพื้นฐานแล้วมีสองวิธีที่ผู้ใช้สามารถระบุอินพุตเป็นส่วนขยายมาร์กอัป ผู้ใช้สามารถตั้งค่าคุณสมบัติหรือส่งพารามิเตอร์ไปยังตัวสร้าง เนื่องจากในสถานการณ์นี้ผู้ใช้ต้องการความสามารถในการระบุสไตล์ไม่ จำกัด วิธีแรกของฉันคือการสร้างนวกรรมิกที่ใช้สตริงจำนวนเท่าใดก็ได้โดยใช้คีย์เวิร์ด“ params”:
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” ค่าที่ส่งคืนจากวิธีนี้จะถูกตั้งค่าในเป้าหมายของส่วนขยายมาร์กอัป
ฉันเริ่มต้นด้วยการสร้างวิธีการขยายสำหรับสไตล์ที่รู้วิธีผสานสองสไตล์ รหัสสำหรับวิธีนี้ค่อนข้างง่าย:
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 ของสไตล์ที่ผสานฉันใช้ชนิดใดก็ได้ที่ได้รับมากกว่า หากสไตล์ที่สองมีสไตล์ BasedOn ฉันจะรวมลำดับชั้นของสไตล์ไว้ซ้ำ หากมีทรัพยากรฉันจะคัดลอกไปยังรูปแบบแรก หากทรัพยากรเหล่านั้นถูกอ้างถึงโดยใช้ {StaticResource} พวกเขาจะได้รับการแก้ไขแบบคงที่ก่อนที่โค้ดผสานนี้จะทำงานและดังนั้นจึงไม่จำเป็นต้องย้าย ฉันเพิ่มรหัสนี้ในกรณีที่เราใช้ DynamicResources
วิธีการส่วนต่อขยายที่แสดงด้านบนเปิดใช้งานไวยากรณ์ต่อไปนี้:
style1.Merge(style2);
ไวยากรณ์นี้มีประโยชน์โดยมีเงื่อนไขว่าฉันมีอินสแตนซ์ของทั้งสองสไตล์ภายใน ProviderValue ฉันทำไม่ได้ ทั้งหมดที่ฉันได้รับจากนวกรรมิกคือรายการของคีย์สตริงสำหรับสไตล์เหล่านั้น หากมีการสนับสนุนพารามิเตอร์ในพารามิเตอร์คอนสตรัคเตอร์ฉันสามารถใช้ไวยากรณ์ต่อไปนี้เพื่อรับอินสแตนซ์สไตล์จริง:
<Button Style="{local:MultiStyle {StaticResource BigButtonStyle}, {StaticResource GreenButtonStyle}}" … />
public MultiStyleExtension(params Style[] styles)
{
}
แต่นั่นไม่ได้ผล และแม้ว่าข้อ จำกัด params ไม่มีอยู่เราก็อาจจะมีข้อ จำกัด อีกอย่างหนึ่งของส่วนขยายมาร์กอัปซึ่งเราจะต้องใช้ไวยากรณ์องค์ประกอบองค์ประกอบแทนไวยากรณ์แอตทริบิวต์เพื่อระบุทรัพยากรคงที่ซึ่งเป็น verbose และยุ่งยาก (ฉันอธิบายสิ่งนี้ บั๊กดีกว่าในโพสต์บล็อกก่อนหน้า ) และแม้ว่าข้อ จำกัด ทั้งสองนั้นไม่มีอยู่ฉันก็ยังอยากจะเขียนรายการของสไตล์โดยใช้เพียงชื่อของมัน - มันสั้นกว่าและอ่านง่ายกว่า StaticResource สำหรับแต่ละข้อ
วิธีแก้ไขคือการสร้าง StaticResourceExtension โดยใช้รหัส รับคีย์สไตล์ของสตริงประเภทและผู้ให้บริการฉันสามารถใช้ StaticResourceExtension เพื่อดึงอินสแตนซ์ของสไตล์ที่แท้จริง นี่คือไวยากรณ์:
Style currentStyle = new StaticResourceExtension(currentResourceKey).ProvideValue(serviceProvider) as Style;
ตอนนี้เรามีชิ้นส่วนทั้งหมดที่จำเป็นในการเขียนวิธี 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;
}
นี่คือตัวอย่างที่สมบูรณ์ของการใช้งานส่วนขยายมาร์กอัป 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" />