• 博客园logo
  • 会员
  • 周边
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
Dreama
只想要简简单单的快乐!
博客园    首页    新随笔    联系   管理     
XAML语法详解

原文:XAML Syntax In Detail


本文规范了描述XAML(Extensible Application Markup Language,可扩展应用程序标记语言)语法元素的专用术语体系。

该术语体系的建立旨在为不同XAML实现方案提供统一的概念基准,确保跨框架技术文档在语义表达上的准确性和一致性。

这些术语将在后续技术文档中被高频引用,其适用范围包括:

1、Windows Presentation Foundation (WPF) 技术文档
2、基于System.Xaml基础架构并支持XAML语言核心概念的框架
3、其他任何采用XAML作为开发语言的框架体系

XAML语言规范

作为基于XML语言进行结构化扩展的标记语言,XAML既继承了XML的基础语法规则,又针对特定技术场景进行了增强设计。

其术语系统呈现以下特征:

1、核心语法术语在XAML规范中独立定义,形成完整的语言描述体系
2、部分基础术语与XML语言规范保持兼容,特别是涉及文档对象模型(DOM)的公共概念
3、扩展术语兼顾跨框架应用的通用性,确保不同XAML实现方案在语法描述层面具有可对照性

这种术语设计策略既维护了与XML技术生态的延续性,又为XAML特有的声明式编程范式建立了精准的语义描述基础。

XAML 和 公共语言运行时(CLR)

XAML本质上是一种标记语言。

公共语言运行时(CLR)的核心职能在于实现运行时执行,但XAML并非CLR可直接解析执行的常规编程语言。

XAML构建了独立的自洽类型系统,其运行机制通过以下技术路径实现:

1、在WPF技术栈中,XAML解析体系构建于CLR及其类型系统基础之上;
2、当WPF的XAML文档被解析时,XAML类型会被映射至CLR类型以实例化运行时对象模型。

因此,本文件后续的语法讨论仍将包含对CLR类型系统的引用——尽管在XAML语言规范层级,其语法描述本身并不依赖于特定类型系统。

技术说明:
根据XAML语言规范的定义,XAML类型理论上可映射至任意其他类型系统,此过程并不强制依赖CLR,但实现非CLR映射需配套开发专用的XAML解析器,这将重构整个技术实现架构

类型成员和类继承

在WPF类型系统中,作为XAML成员呈现的属性和事件,其定义往往继承自基类层级结构。以典型示例说明:

<Button Background="Blue" />

此处设置的Background属性并非直接定义在Button类中,而是继承自基类Control。通过以下技术验证手段可证实该结论:

1、类定义分析:查看Button类源码,未见Background属性直接声明
2、反射验证:执行typeof(Button).GetProperty("Background")将返回null
3、文档溯源:MSDN文档显示Background属性实际定义于基类Control

WPF XAML元素的类继承行为与基于XML模式(Schema)的强制解析范式存在本质性差异。
当存在以下情形时:

1、中间基类为抽象类(如Grid继承Panel抽象类)
2、涉及接口实现(如INotifyPropertyChanged在数据绑定中的应用)

类继承关系可能呈现高度复杂性。这正是XAML元素及其允许使用的属性难以通过DTD或XSD等传统XML编程模式类型实现精准完整表征的重要原因。
另一关键原因在于XAML语言自身的可扩展性与类型映射特性,使得任何针对允许类型及成员的固定表征方案都难以实现完整覆盖。

对象元素语法

对象元素语法是通过声明XML元素实例化CLR类或结构的XAML标记规则,其语法范式表现为:<类型名 属性1="值1" ... 属性N="值N">,
这种语法与其他标记语言(如HTML)的元素语法结构具有相似性。
对象元素语法以左尖括号(<)起始,其后紧接待实例化的类或结构对应的类型名称。
在类型名称之后可包含零个或多个空格,对象元素上可声明零到多个属性,各属性名="值"对之间需以一个或多个空格分隔。
最后,必须满足下列闭合要求条件之一:
1、元素及其标签必须通过正斜杠(/)与右尖括号(>)的连续组合完成闭合,即采用/>闭合符,例如:

<TextBox Text="{Binding Name}"/>

2、起始标签必须通过右尖括号(>)完成闭合,其后可包含以下内容:

1、其他对象元素(如 <Grid>)
2、属性元素(如 <Button.Content>)
3、内部文本(如 <Run>Hello</Run>)

允许包含的具体内容通常受元素对象模型的约束,例如:

<!-- StackPanel对象模型允许子元素 -->
<StackPanel>
    <Button/> <!-- 合法 -->
    "文本"    <!-- 非法(不接受原始字符串) -->
</StackPanel>

对象元素的等效闭合标签必须存在,且需与其他起始/闭合标签对保持正确的嵌套平衡,例如:

<!-- 正确嵌套 -->
<Grid>
    <StackPanel> <!-- 层级1 -->
        <TextBlock/> <!-- 层级2 -->
    </StackPanel>
</Grid>

<!-- 错误嵌套(引发XamlParseException) -->
<Grid>
    <StackPanel>
</Grid>          <!-- 提前闭合破坏层级 -->
</StackPanel>

<!-- 显式闭合形式 -->
<ListBox>
    <ListBoxItem Content="Item1"/>
</ListBox>

.NET实现的XAML遵循以下映射规则体系:

1、对象元素 → 映射至.NET类型
2、元素属性 → 映射至类型属性/事件(类型的成员)
3、XAML命名空间 → 组合映射至CLR命名空间+程序集

当在XAML中引用CLR类型时,还可访问该类型的继承成员。
例如,以下示例展示了通过对象元素语法实例化Button类新实例,并为其指定Name属性及对应值的完整过程:

<Button Name="CheckoutButton"/>

下列示例展示同时包含对象元素语法与XAML内容属性语法的用法,其内部文本将用于设置TextBox的XAML内容属性(Text):

<TextBox>请输入用户名</TextBox>
<!--以下是不使用 内容属性 语法的完整示例-->
<TextBox xmlns:sys ="clr-namespace:System;assembly=mscorlib">
	<TextBox.Text>
    		<sys:String>请输入用户名</sys:String>
	</TextBox.Text>
</TextBox>

内容模型

某类可能在语法层面支持作为XAML对象元素使用,但其在应用程序或页面中需置于内容模型或元素树的预期位置才能正常运作。典型示例如下:

<!-- 合法使用 -->
<Menu>
    <MenuItem Header="文件">
        <MenuItem Header="新建"/>
    </MenuItem>
</Menu>

<!-- 非法使用(导致运行时异常) -->
<StackPanel>
    <MenuItem Header="无效位置"/> <!-- 违反MenuBase容器约束 -->
</StackPanel>

说明:MenuItem必须作为MenuBase派生类(如Menu/ContextMenu)的子元素,父容器通过ItemsControl.ItemsSource实现子项管理

可对特定元素的内容模型进行文档化,其内容作为控件及其他WPF类(可用作XAML元素)的类说明备注的一部分,以下是Button类说明备注(Remark)中有关于Button内容模型的描述:

参考原文:Button类

image

对象元素的属性

在XAML中,可通过多种语法格式来设置对象的属性,例如:

特性语法:<Button Content="OK" Width="100"/>
属性元素语法:

<Button>
 <Button.Content>
   <Image Source="icon.png"/>
 </Button.Content>
</Button>

附件属性语法:<Button Grid.Row="1" Content="Yes"/>中的Grid.Row="1"

根据所设置属性的底层类型系统特性不同,特定属性可使用的语法也会有所差异,
比如支持数据绑定(Binding)、样式(Style)、模板(Template)的依赖属性设置及数据类型转换系统(如Background="Blue")等。
通过设置属性值,你可以为运行时对象图(或逻辑树)中存在的对象实例添加功能或特性。
无参数构造函数的行为决定了对象元素创建对象实例的初始状态。

  • XAML限制:XAML解析器必须能够通过new T()创建实例
  • 设计模式应对:
public class MyClass
{
  // 必需的无参构造
  public MyClass() {}
  // 实际初始化方法
  public void Initialize(string param) {...}
}

通常来讲,应用程序使用的对象实例不会是完全默认的。

特性(Attribute)语法(属性)

特性语法是XAML标记语法,通过在现有对象元素上声明特性(Attribute)来设置属性(Property)值。

注意:Attribute是XAML标记层面的,而Property是CLR类型系统层面的,注意这两者的区别。

特性名称必须与支撑该对象元素对应类的CLR成员名称完全匹配。特性名称后需接赋值运算符(=),特性值必须是包含在引号内的字符串,例如:

<!-- 有效语法 -->
<TextBox Text="{Binding Path=Name}"/>
<!-- 无效语法(缺少=) -->
<TextBox Text {Binding Path=Name}/>

注意:你可以使用交替引号在特性(Attribute)值中包含字面量引号。例如,当字符串(string)内需要包含双引号字符时,可以使用单引号声明该字符串,例如:
ToolTip='Press "OK" to continue' 提示信息:Press "OK" to continue
ToolTip="Press 'OK' to continue" 提示信息:Press 'ok' to continue
无论使用单引号还是双引号,必须保持起始和结束引号的配对一致性。
XAML语法还提供转义序列或其他技术手段来规避特定字符限制,
例如:&quot;(双引号)、&amp;(&符号)、&#x0d;或&#13;(回车)、&#x0A;或&#10;(换行)

要通过特性语法设置属性,该属性在支撑类型系统中必须满足以下条件:

  1. 公共成员
  2. 可写性
  3. 其属性值若为值类型则可直接赋值;若为引用类型,则必须能被XAML处理器实例化(必须是无参的构造函数)或引用。
    例如:
<!-- 错误1:私有属性不能被访问 -->
<local:CustomControl PrivateProp="Value"/>
<!-- 错误2:只读依赖属性不能被设置 -->
<local:CustomControl ReadOnlyProp="Value" />
<!-- 错误3:不能在CRL类型系统中被实例化 -->
<local:CustomControl/> <!-- 触发XAML解析异常 -->
public class CustomControl:FrameworkElement
{
	//错误1:私有属性不能被访问
	private string PrivateProp {get{...}set{...}}
	//错误2:只读依赖属性不能被设置
	public string ReadOnlyProp {get{...}}
	//错误3:不能在XAML中被实例化
	public CustomControl(string vallue){...}
}

在WPF的XAML事件处理中,作为特性名称引用的事件必须满足双重公开性要求:

  1. 事件本身需声明为public访问级别;
  2. 事件对应的委托类型必须具有public可访问性。
    举个例子:
    //对应1:这里的类声明必须为public[或internal(最新已验证)]
    public class TestElement : FrameworkElement
    {
	public static readonly RoutedEvent TestEvent = EventManager.RegisterRoutedEvent("Test", RoutingStrategy.Bubble, typeof(TestHalder), typeof(TestElement));
	//对应2:这里的事件委托必须为public[或internal(最新已验证)]
	//以下这句可移到与TestElement同一层的位置
	//对应1与对应2的访问修饰符必须是一致的,如类声明为internal,那么事件委托声明也必须为internal
	public delegate void TestHalder(object? seder, TestRoutedEventArgs e);
	public event TestHalder? Test
        {
            add => AddHandler(TestEvent, value);
            remove => RemoveHandler(TestEvent, value);
        }
        protected virtual void OnTest(TestRoutedEventArgs e) { RaiseEvent(e); }
        protected override void OnInitialized(EventArgs e)
        {
            base.OnInitialized(e);
            OnTest(new TestRoutedEventArgs("Test", TestEvent));
        }
    }
    public class TestRoutedEventArgs:RoutedEventArgs
    {
        public TestRoutedEventArgs(string? testParam, RoutedEvent e):base(e) { TestParam = testParam; }
        public string? TestParam { get; }
    }

属性或事件必须是包含对象元素所实例化的类或结构体的直接成员,例如:

<!--必须是Button的直接成员(包括继承的),不能是成员的成员-->
<Button Content="Hello World" Parent.Width="100/>

特性(Attribute)值的处理

包含在左引号和右引号之间的字符串值由 XAML 处理器处理。对于属性,默认处理行为由其底层 CLR 属性类型决定。属性值的填充遵循以下处理顺序:

  1. 若 XAML 处理器检测到大括号 {} 或继承自 MarkupExtension 的对象元素:
    优先解析引用的标记扩展(而非将值视为字符串),并将标记扩展返回的对象作为属性值。
    • 多数情况下,标记扩展返回的对象可能是对现有对象的引用,或是延迟到运行时计算的表达式,而非新实例化的对象。
    • 标记扩展(Markup Extension)​
      本质是运行时返回对象的工厂模式,例如:
      {Binding} → 生成数据绑定表达式对象(非立即计算)
      {x:Static} → 引用静态属性(编译时解析)
      与普通对象的区别:需继承 MarkupExtension 并重写 ProvideValue。
  2. 若属性或属性值类型通过特性(Attribute)声明了 TypeConverter:
    将属性的字符串值提交给类型转换器作为输入,转换器会返回一个新的对象实例。
    需为特定类型实现 TypeConverter 的子类,例如:
    public class ColorConverter : TypeConverter
    {
    	public override object ConvertFromString(string value)
    	{ // 实现 "Blue" → Colors.Blue 的转换逻辑 }
    }
    
    应用限制:需通过 [TypeConverter(typeof(ColorConverter))] 显式声明。
  3. 若无 TypeConverter:
    尝试直接将属性值转换为目标类型。
    • 最终转换基于 XAML 语言原语类型的解析器原生值层面的直接转换,或检查枚举中已命名的常量名称(解析器会访问匹配的枚举值)。
    • 解析器原生类型(Parser-Native Types)​
      XAML内置支持的基元类型,如 Boolean, Int32, String以及枚举。
      例外处理:若尝试将 "abc" 赋值给 int 属性,直接在此阶段抛出 XamlParseException。

枚举(Enumeration)属性值

XAML中的枚举由XAML解析器内部处理,应通过指定枚举命名常量之一的字符串名称来指定枚举成员,例如:
以仪器通信接口的为例子,C#代码如下:

    enum InstrumentInterface
    {
        RS232,USB,LAN,GPIB
    }

在XAML中,需使用<local:Instrument x:Key="dcSource" Interface="USB"/>中Interface="USB"的设置特性值的方式在CLR类型系统中指定枚举类型为InstrumentInterface.USB。

对于非标志枚举值(以上例子就为非Flags特性枚举),原生行为是处理属性值的字符串并将其解析为枚举值之一。不需要像在代码中那样使用"Enumeration.Value"格式,而只需指定"Value",枚举类型将通过设置的属性类型推断出来。如果使用"Enumeration.Value"形式,将无法正确解析。

对于标志枚举,其行为基于Enum.Parse方法。可以通过逗号分隔为标志枚举指定多个值,但不能组合非标志的枚举值,将以上例子增加Flags特性,如下:

    [Flags]
    [TypeConverter(typeof(InstrumentTypeConverter))]
    enum InstrumentInterface
    {
        RS232,USB,LAN,GPIB
    }

在XAML中,可以将Interface从USB修改为Interface="Interface="RS232,USB"。
在WPF中支持XAML可设置属性的标志枚举很少见,StyleSimulations是其中之一。例如可以使用逗号分隔的标志属性语法修改Glyphs类的示例:StyleSimulations = "BoldSimulation"可改为StyleSimulations = "BoldSimulation,ItalicSimulation"。
KeyBinding.Modifiers是另一个可以指定多个枚举值的属性,但ModifierKeys枚举使用加号(+)作为分隔符的特殊类型转换器,这种转换支持传统的Windows按键组合语法如"Ctrl+Alt"。
ModifierKeys枚举它有Flags特性,同时还有TypeConverter特性,依据特性值处理的方式,将先处理TypeConverter特性,因此,不会用到默认的使用逗号隔开后用Enum.Parse的方法对枚举进行处理。
我们可以看看ModifierKeys枚举:

    [Flags]
    [TypeConverter(typeof(ModifierKeysConverter))]
    [ValueSerializer(typeof(ModifierKeysValueSerializer))]
    public enum ModifierKeys
    {
        None = 0,
        Alt = 1,
        Control = 2,
        Shift = 4,
        Windows = 8
    }

就可以知道它是比较特殊的。
若我们要实现跟ModifierKeys枚举一样的功能,我们可以自定义自己的TypeConverter,继续以InstrumentInterface枚举作为例子,代码如下:

    [Flags]
    [TypeConverter(typeof(InstrumentTypeConverter))]
    enum InstrumentInterface
    {
        RS232,USB,LAN,GPIB
    }

    class InstrumentTypeConverter:TypeConverter
    {
        public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
        {
            if (value is string valueString && valueString.Split([',', '|']) is string[] values)
            {
                InstrumentInterface result = InstrumentInterface.RS232;
                foreach (var item in values)
                {
                    result |= (InstrumentInterface)Enum.Parse(typeof(InstrumentInterface), item);
                }
                return result;
            }
            return base.ConvertFrom(context, culture, value);
        }
    }

以上就可以实现Interface="Interface="RS232,USB|GPIB"这样的特性值赋值方法。

属性和事件成员名称引用

当指定特性时,你可以引用包含对象元素对应的CLR类型中定义的任何成员属性或事件。或者,你也可以引用与包含对象元素无关的附加属性或附加事件。(附加属性将在后续章节中详细讨论。)

包含对象元素:准确反映XAML中父级元素包含子元素的层级结构(如<Grid><Button/></Grid>中,Grid是包含Button的对象元素);
与包含对象元素无关:明确附加属性(如Canvas.Left)不依赖父级元素,而是由外部类型定义,突出其“附加”特性。

你还可以通过使用 类型名.事件 的部分限定名(partially qualified name),引用默认命名空间中可访问的任何对象的任何事件。

限定名语法(TypeName.Event)允许父元素监听子元素事件,即使父元素自身未定义该事件。

此语法支持为路由事件附加处理程序,尤其适用于以下场景:当父元素本身在其成员表中不包含某个事件时,但需要处理从子元素路由传递到父元素的该事件,例如:
若父元素Grid需处理子元素Button的Click事件,但Grid自身无Click成员,可使用Button.Click限定名语法:

<Grid Button.Click="HandleButtonClick">
    <Button Content="Submit"/>
</Grid>

虽然这种语法形式上类似于附加事件(attached event)语法,但此处引用的事件并非真正的附加事件,而是通过限定名(qualified name)引用的事件。
在某些场景下,属性名(property names)会作为特性值(attribute value)而非特性名(attribute name)提供,例如:

  • 作为特性值提供:
<Style TargetType="Button">
<!--Foreground是特性值,特性名是Property-->
    <Setter Property="Foreground" Value="Blue" />
</Style>
  • 而非特性名:
<!--Foreground是特性名-->
<Button Foreground="Blue"/>

此类属性名也可以包含限定符(qualifiers),例如以 所有者类型.依赖属性名(ownerType.dependencyPropertyName)形式指定的属性。这种用法在编写XAML样式(styles)或模板(templates)时十分常见,例如:

<Style TargetType="Button">
<!--此处Grid.Row作为属性名通过所有者类型.依赖属性名形式引用,即使Button本身不直接定义该属性-->
    <Setter Property="Grid.Row" Value="1" />
</Style>

将属性名作为特性值传递时,其处理规则与常规特性不同,具体由被设置的属性类型(type of the property being set)或特定WPF子系统的行为(如样式系统或模板系统)决定。
属性名的另一种用法是:当特性值(attribute value)描述属性-属性关系(property-property relationship)时。
此功能用于数据绑定(data binding)和演示图板目标(storyboard targets),并通过 PropertyPath 类及其类型转换器(type converter)实现。
例如:

  • 数据绑定
<!--Path 特性值描述了从 Customer 到 TotalAmount 的属性链-->
<TextBlock Text="{Binding Path=Customer.Order.TotalAmount}" />
  • 演示图板
<Storyboard>
    <!--通过 PropertyPath 指定 RenderTransform 的 Angle 属性为目标-->
    <DoubleAnimation
        Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
        To="45" Duration="0:0:1"/>
</Storyboard>

属性元素语法

属性元素语法是一种与基础XML元素语法规则存在差异的特殊语法形式。在XML中,属性的值本质上只能是字符串类型,唯一的变体仅体现在字符串编码格式的选择上。而在XAML中,可以通过属性元素语法将其他对象元素作为属性的值进行赋值。这一功能通过以下方式实现:属性不再以特性(Attribute)的形式直接写在元素标签内,而是通过以元素类型名.属性名(elementTypeName.propertyName)形式命名的起始标签声明,属性的值在标签内部定义,最后通过闭合标签结束声明。

具体语法规则如下:

  1. 起始标签结构:
    以左尖括号(<)开头,紧跟着包含该属性的类或结构体的类型名称(elementTypeName),然后是一个点号(.)和属性名称(propertyName),最后以右尖括号(>)结束。
    格式示例:
    <Button>
      <Button.ContextMenu>
         <ContextMenu>
            <MenuItem Header="1">First item</MenuItem>
            <MenuItem Header="2">Second item</MenuItem>
         </ContextMenu>
      </Button.ContextMenu>
      Right-click me!
    </Button>
    
  2. 属性合法性要求:
    该属性必须在目标类型(elementTypeName)的已声明公共成员中存在。
  3. 属性值定义:
    属性的值在起始标签与闭合标签之间定义。通常,该值会被赋予一个或多个对象元素,因为属性元素语法的核心设计目标正是支持以对象作为属性值的场景。
  4. 闭合标签结构:
    必须提供与起始标签完全对应的闭合标签,格式为</elementTypeName.propertyName>,且需确保标签的嵌套和层级关系正确。

补充说明

  1. 内部文本作为属性值:

    • 如果属性的类型是原始值类型(如String)或枚举,其值也可以直接以内部文本形式定义。
    • 此类用法相对少见,因为特性语法(Attribute Syntax)通常更简洁。但以下场景需使用属性元素语法:
      • 保留空白字符:
        当属性用于表示包含换行符等特殊空白字符的UI文本时,属性元素语法可在启用有效空格保留规则后维持这些字符(详见XAML中的空白处理)。例如:
            <TextBlock xml:space="preserve">
         	<TextBlock.Text>
         	First Line
         	Second Line
         	</TextBlock.Text>
         </TextBlock>
        
        必须使用xml:space="preserve"才能使空白符生效
        1. 若使用特性语法(Text="First Line Second Line"),换行符会被压缩为空格(其他空白符保留)
          如果删除xml:space="preserve",空白符号只会保留一个(空格),不管是连续的还是换行
        2. 若使用属性元素语法,所有空白符都会被保留
      • 本地化支持:
        通过在属性元素上应用x:Uid指令,可将值标记为需要本地化的内容(例如通过WPF的BAML输出或其他技术实现)。
  2. 逻辑树无关性:

    • 属性元素不会出现在WPF逻辑树中。它仅是一种语法形式,不对应任何实际对象实例(详见WPF中的树形结构)。
  3. 与特性语法的对比:

    • 若某个属性同时支持特性语法和属性元素语法,两种语法通常效果相同,但空白字符处理等细节可能略有差异。

集合语法

To Be Continued.

posted on 2025-03-30 15:10  Dreamma  阅读(152)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3

© 本文章版权归 Dreama 所有, 转载授权请联系: cnxy@88.com

如果本文对您有帮助,欢迎支持原创

支付宝

支付宝扫码支持

微信

微信赞赏支持