一、前言
WPF使用XAML来对界面进行编写,界面与后台逻辑分离。我们也可以写Style、Trigger来实现一些界面效果,
这些都是通过Name来定位控件的,例如Setter.TargetName、Trigger.SourceName和Binding的ElementName等。
而这些Name都是通过设置控件的x:Name来定义的,如<Button x:Name="Button1" />
但是,XAML中有x:Name和Name这两个属性,究竟它们有什么区别呢?
本专题就来探究一下x:Name和Name的区别,它们的本质又是什么?
二、XAML与Code-Behind
在编写WPF程序时,通常需要分别编写前台XAML代码和后台Code-Behind代码(不使用MVVM时)。
WPF通过一个partial关键字,将一个类的定义切分为两部分:XAML和Code-Behind。
其中XAML交给设计师设计,Code-Behind交给程序员写业务逻辑,从而实现分离(虽然大部分时候全部都是程序员完成的)。
我们在XAML上写标签,其实与后台写代码是等效的。只要你想,完全可以只使用XAML或者只是用Code-Behind来写程序。
示例:
只使用xaml编写一个窗体
<!-- 只使用xaml编写一个窗体 -->
<!-- 只使用一个单独的xaml文件 -->
<Window x:Class="Cnblog.OnlyXaml"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="OnlyXaml"
Width="300"
Height="300">
<Grid>
<Button Width="100"
Height="100"
Click="ButtonClick">
Button
</Button>
</Grid>
<x:Code>
private void ButtonClick(object sender, RoutedEventArgs e)
{
MessageBox.Show("Button Click");
}
</x:Code>
</Window>
只使用xaml编写一个窗体
只使用Code-Behind编写一个窗体
namespace Cnblog
{
// 只使用Code-Behind编写一个窗体
// 只使用一个单独的OnlyCode.cs文件
public class OnlyCode :Window
{
public OnlyCode()
{
// button
var button = new Button { Content = "Button",Width = 100, Height = 100};
button.Click += ButtonClick;
// grid
var grid = new Grid();
grid.Children.Add(button);
this.Width = 300;
this.Height = 300;
this.Title = "OnlyCode";
this.Content = grid;
}
void ButtonClick(object sender, RoutedEventArgs e)
{
MessageBox.Show("Button Click");
}
}
}
只使用Code-Behind编写一个窗体
上面例子,分别只使用XAML和Code-Behind来定义一个窗体,但是最终的效果都是一样的。
当然,如果单独使用其中一种,必然让编程变得很痛苦,这里我只是想表明有这个可能性(因为下面会用到),实际中不会这么做。
结论:虽然编码方式不一样,但是效果是一样的,编译器其实对XAML进行编译生成BAMP,根据标签创建相应对象。
这个与本专题无关,但是下面要将要用到相关内容,先说明一下。
三、XAML中x:Name和Name最终效果相同
如果你在xaml中创建一个控件,并同时对x:Name和Name两个属性进行赋值,那么编译器就会提醒你:Name被设置了多次。
当然,如果你觉得这个不够有说服力,那么下面这段程序可能也能够佐证:
<!-- 两个Button分别使用x:Name和Name -->
<Window x:Class="Cnblog.SetName"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="SetName"
Width="300"
Height="300">
<StackPanel>
<Button x:Name="Button1" Loaded="ButtonLoaded" />
<Button Name="Button2" Loaded="ButtonLoaded" />
</StackPanel>
<x:Code>
private void ButtonLoaded(object sender, RoutedEventArgs e)
{
var button = (Button)sender;
button.Content = button.Name;
}
</x:Code>
</Window>
两个Button分别使用x:Name和Name
效果图:
下面是用IL的截图,两者无区别。
结论:
XAML中使用Name其实被映射到了x:Name,x:Name才是XAML中唯一的标识,所以它们效果相同。
四、不同于控件的Name属性
在WPF中,很多控件拥有Name属性,例如上面我们使用Button的Name来设置Content,button.Content=button.Name。
是因为它们的父类FrameworkElement中都定义了Name属性,以下用SomeWpfType来代替这些类型(便于表述)。
下面,我们不使用XAML和x:Name,使用Code-Behind和SomeWpfType.Name来测试一下。
namespace Cnblog
{
// 使用Button的Name属性
public class SetNameByCodeBehind : Window
{
public SetNameByCodeBehind()
{
// Buttons
var button1 = new Button { Name = "Button1" };
button1.Loaded += ButtonLoaded;
var button2 = new Button { Name = "Button2" };
button2.Loaded += ButtonLoaded;
// StackPanel
var contentPanel = new StackPanel();
contentPanel.Children.Add(button1);
contentPanel.Children.Add(button2);
// Window
this.Title = "Set Name By Code-Behind";
this.Height = 100;
this.Width = 300;
this.Content = contentPanel;
}
void ButtonLoaded(object sender, RoutedEventArgs e)
{
var button = (Button)sender;
button.Content = button.Name;
}
}
}
使用Button的Name属性
效果图:
下面,区别终于有了,我们再来看IL的内容:
因为不是使用XAML书写的,所以缺少一些函数和对象,但是更重要的是:缺少两个Button对象的定义。
如果我们修改一下代码,再来看一下,就能清楚的知道原因了!
namespace Cnblog
{
public class SetNameByCodeBehind : Window
{
// 修改为internal的对象,取代之前的局部变量
internal Button Button1;
internal Button Button2;
public SetNameByCodeBehind()
{
// Buttons
Button1 = new Button { Name = "Button1" };
Button1.Loaded += ButtonLoaded;
Button2 = new Button { Name = "Button2" };
Button2.Loaded += ButtonLoaded;
// StackPanel
var contentPanel = new StackPanel();
contentPanel.Children.Add(Button1);
contentPanel.Children.Add(Button2);
// Window
this.Title = "Set Name By Code-Behind";
this.Height = 100;
this.Width = 300;
this.Content = contentPanel;
}
void ButtonLoaded(object sender, RoutedEventArgs e)
{
var button = (Button)sender;
button.Content = button.Name;
}
}
}
使用Button的Name属性2
再来看一下IL的内容:
结论:
x:Name不是SomeWpfType.Name,当我们设置了x:Name后(假设为ElementName),
其实做了两件事情:
1. 创建一个internal的对象,对象名字为ElementName,它其实是一个对此SomeWpfType类型对象的引用;
internal SomeWpfType ElementName = new SomeWpfType();// 假设SomeWpfType为我们定义的类型。
2. 设置此对象的Name属性,
ElementName.Name = "ElementName";。
五、为什么XAML中Name与x:Name效果相同
上面,我们分析SomeWpfType.Name和x:Name有很大区别,而且XAML中的设置Name不就是SomeWpfType.Name吗?
同Width,在XAML中设置Width,就是设置SomeWpfType.Width。
那么我们就去翻一下FrameworkElement的源码看看吧。
namespace System.Windows
{
[StyleTypedProperty(Property = "FocusVisualStyle", StyleTargetType = typeof(Control))]
[XmlLangProperty("Language")]
[UsableDuringInitialization(true)]
public partial class FrameworkElement : UIElement, IFrameworkInputElement, ISupportInitialize, IHaveResources, IQueryAmbient
{ /// <summary>
/// The DependencyProperty for the Name property.
/// </summary>
[CommonDependencyProperty]
public static readonly DependencyProperty NameProperty =
DependencyProperty.Register(
"Name",
typeof(string),
_typeofThis,
new FrameworkPropertyMetadata(
string.Empty, // defaultValue
FrameworkPropertyMetadataOptions.None, // flags
null, // propertyChangedCallback
null, // coerceValueCallback
true), // isAnimationProhibited
new ValidateValueCallback(System.Windows.Markup.NameValidationHelper.NameValidationCallback));
/// <summary>
/// Name property.
/// </summary>
[Localizability(LocalizationCategory.NeverLocalize)]
[MergableProperty(false)]
[DesignerSerializationOptions(DesignerSerializationOptions.SerializeAsAttribute)]
public string Name
{
get { return (string) GetValue(NameProperty); }
set { SetValue(NameProperty, value); }
}
// a lot of code
}
}
namespace System.Windows
{
[RuntimeNamePropertyAttribute("Name")]
public partial class FrameworkElement
{
// a lot of code...
}
}
FrameworkElement
Name属性上貌似没有什么特别,但是另外一个局部类的定义中找到了个名为RuntimeNameProperty的特性。
我们就来看下它的代码吧。
namespace System.Windows.Markup
{
[AttributeUsage(AttributeTargets.Class)]
[TypeForwardedFrom("WindowsBase, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
public sealed class RuntimeNamePropertyAttribute : Attribute
{
private string _name;
public string Name
{
[TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")] get
{
return this._name;
}
}
[TargetedPatchingOptOut("Performance critical to inline this type of method across NGen image boundaries")]
public RuntimeNamePropertyAttribute(string name)
{
this._name = name;
}
}
}
RuntimeNamePropertyAttribute
可以看出来这个特性使用在Class上面的,也就是这个特性,使得XAML处理Name时,映射到了x:Name,才有了前面的结论。
我们再来查看以下还有哪些类使用了这个特性。
在4个Assembly中找到了13个,比较眼熟的有:
Timeline、BeginStoryboard、FrameworContentElement、FramewordElement、VisualState、VisualStateGroup,
这些也基本都是WPF中很多常用类型的基类了。
结论:RuntimeNameProperty特性,使得XAML中使用Name和x:Name效果一样,当编译器遇到此特性后,
就将Name映射到x:Name,执行一样的操作。
六、XAML中x:Name与Name并不完全等价。
不是所有类型都可以使用Name,但是任何类型都可以使用x:Name。
只有拥有Name属性,才可以在XAML中使用Name。不同于x:Name,因为这个是附加属性。
并且该类型、或者其父类型标记了RuntimeNameProperty特性,才拥有与x:Name一样的效果。
例如:<SolidColorBrush Color="Transparent" Name="ddd"/>便会报错,因为SolidColorBrush没有Name属性。
只能使用x:Name。<SolidColorBrush Color="Transparent" x:Name="ddd"/>
七、其他
1、分析为什么要有x:Name
前面提到,XAML中经常需要通过名字来定位某个控件或对象,而SomeWpfType的Name属性,只是一个DP,我们可以设置两个控件拥有相同的Name属性。
那么这样就非常不利于定位控件,因为Name不是一个唯一的标识了。
使用对象的引用有两个好处:
1.在特定的范围域内,能够保证它的唯一性;
2.在视图树中查找某个对象时,通过引用对象的名称比查找Name属性更加简单。