XAML属性赋值转换之谜(WPF XAML语法解密)
XAML与XML类似,就是XML延伸过来的。为了更好的表达一些功能,WPF对XML做了扩展,有些功能是WPF在后台悄悄的替你做了。有时候,虽然实现了某个功能,但是对实现原理还是很茫然。今天就讲讲XAML中赋值操作。
1 通过类型转换赋值
赋值是最简单最常见的操作,举例:
<Button Width="200" Height="100">
</Button>
这里把Width值赋值为200;用代码实现赋值,则为Button.With = 200; 这种赋值操作很直接,大家都能理解。但是仔细想想,感觉有点不对劲。XAML表达式Width="200",这里200是字符串,Width类型是double。字符串200怎么就转换成double了!你会说,200很明显可以转换为double类型,有什么大惊小怪的!
有时,程序实现的逻辑操作很傻瓜,人很容易理解的事,程序并不一定能理解。需要你告诉XAML编译器,怎么把字符串型转换成double型。确实有 一个转换类悄悄的把字符串型转换成了double型。
通过元文件,可以查到Width属性定义。
//
// 摘要:
// 获取或设置元素的宽度。
//
// 返回结果:
// 元素的宽度,单位是与设备无关的单位(每个单位 1/96 英寸)。默认值为 System.Double.NaN。此值必须大于等于 0.0。有关上限信息,请参见“备注”。
[Localizability(LocalizationCategory.None, Readability = Readability.Unreadable)]
[TypeConverter(typeof(LengthConverter))]
public double Width { get; set; }
Width属性定义[TypeConverter(typeof(LengthConverter))]。这句话就表明width转换类型是LengthConverter。当XAML编译器看到Width赋值操作,就会调用LengthConverter。输入是字符串,返回就是double。
你可能感觉到,对这个属性讲解有点啰嗦。我这里是想告诉你:几乎所有的赋值操作,都需要这种转换。
引申: 更深一步讲,如果我们定义了一个属性,这个属性是一个复杂的类型。在XAML如何赋值? 比如自己定义了类型如下:
public class MyPointItem
{
public double Latitude { get; set; }
public double Longitude { get; set; }
}
有一个类包含此属性:
public class MyClass
{
public MyPointItem Item { get; set; }
}
在XAML语法中如何对Item赋值,XAML语法只认识字符串型。这时需要参考上文Width处理方式。需要自己定义些转换类。定义一个类型继承TypeConverter,实现里面的函数。
比如这样赋值MyClass.Item = "123,456";你需要告诉编译器,如何将"123,456"转化成类型MyPointItem。这里字符串用逗号分隔,你可以用别的符号分隔;比如“#”,只要你的转换函数能处理就行。完整的处理函数如下:
//定义转换类型
public class MyPointItemConverter : TypeConverter
{
public override bool CanConvertFrom( ITypeDescriptorContext context, Type sourceType)
{
if (sourceType is string)
return true;
return base.CanConvertFrom(context, sourceType);
}
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType is MyPointItem)
return true;
return base.CanConvertTo(context, destinationType);
}
public override object ConvertFrom(ITypeDescriptorContext context,
CultureInfo culture, object value)
{
if (value is string)
{
try
{
return MyPointItem.Parse(value as string);
}
catch (Exception ex)
{
throw new Exception(string.Format("Cannot convert '{0}' ({1}) because {2}", value, value.GetType(), ex.Message), ex);
}
}
return base.ConvertFrom(context, culture, value);
}
public override object ConvertTo(ITypeDescriptorContext context,
CultureInfo culture, object value, Type destinationType)
{
if (destinationType == null)
throw new ArgumentNullException("destinationType");
MyPointItem gpoint = value as MyPointItem;
if (gpoint != null)
if (this.CanConvertTo(context, destinationType))
return gpoint.ToString();
return base.ConvertTo(context, culture, value, destinationType);
}
}
//自定义类型
[TypeConverter(typeof(MyPointItemConverter))]
public class MyPointItem
{
public double Latitude { get; set; }
public double Longitude { get; set; }
internal static MyPointItem Parse(string data)
{
if (string.IsNullOrEmpty(data))
return new MyPointItem();
string[] items = data.Split(','); //用逗号分隔,和XAML赋值中字符串分隔符保持一致
if (items.Count() != 2)
throw new FormatException("should have both latitude and longitude");
double lat, lon;
try
{
lat = Convert.ToDouble(items[0]);
}
catch (Exception ex)
{
throw new FormatException("Latitude value cannot be converted", ex);
}
try
{
lon = Convert.ToDouble(items[1]);
}
catch (Exception ex)
{
throw new FormatException("Longitude value cannot be converted", ex);
}
return new MyPointItem() { Latitude=lat, Longitude=lon };
}
}
转换类型不是万能的: 只有类型转换,也会遇到难以处理的情况。比如MyClass.Item = "null"。我的意思是将Item赋值为null。但是编译不会这么处理,仍然会调用转换类型MyPointItemConverter,结果就会抛出异常!WPF为此又引入了扩展标识符的概念。
2 扩展标识符
扩展标识符有特殊的语法,如果属性赋值为null,语法如下:
MyClass.Item ="{x:Null}"; 这里的Null其实是一个类型,继承自MarkupExtension;
// // 摘要: // 实现 XAML 标记以返回 null 对象,可使用该对象在 XAML 中将值显式设置为 null。 [MarkupExtensionReturnType(typeof(object))] [TypeForwardedFrom("PresentationFramework, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")] public class NullExtension : MarkupExtension { // // 摘要: // 初始化 System.Windows.Markup.NullExtension 类的新实例。 public NullExtension(); // // 摘要: // 提供要用作值的 null 作为此标记扩展的输出。 // // 参数: // serviceProvider: // 可为标记扩展实现提供服务的对象。 // // 返回结果: // 空引用。 public override object ProvideValue(IServiceProvider serviceProvider); }
MyClass.Item ="{x:Null}"这句话的意思就是:编译器生成类型NullExtension,调用函数ProvideValue,将此返回值赋值给MyClass.Item;
再举个例子:
Height="{x:Static SystemParameters.IconHeight}”; 编译器处理逻辑是:生成类型StaticExtension,将字符串“SystemParameters.IconHeight”传给构造函数,调用函数ProvideValue,返回double类型。
其实StaticExtension会将字符串“SystemParameters.IconHeight”认为一个静态变量。XAML眼里只有字符串!
绑定 -- 一种很常用的扩展标识符类型
看如下语法:
<Button Width="200" Height="200" Content="{Binding Height,RelativeSource={RelativeSource Self}}"> </Button>
对content的赋值,是不是感到一头雾水! binding其实也是扩展标识,最终继承自MarkupExtension;
Binding : BindingBase --> BindingBase : MarkupExtension;
所以binding的作用也是将字符串转换成我们需要的类型。不过binding的参数比较多,有时候需要转好几个弯,才能找到真的源头!
对于上面的赋值,咱做个分析,来看看编译器处理的步骤:
1)生成Binding类型,构造函数传入“Height”,
2)Binding有一个属性为RelativeSource,参见元文件
// // 摘要: // 通过指定绑定源相对于绑定目标的位置,获取或设置绑定源。 // // 返回结果: // 一个 System.Windows.Data.RelativeSource 对象,该对象指定要使用的绑定源的相对位置。默认值为 null。 [DefaultValue(null)] public RelativeSource RelativeSource { get; set; }
仔细看看代码,属性类型和变量名称都是RelativeSource,这是c#语法允许的。当然,这样做会使人困惑!
RelativeSource={RelativeSource Self},第一个RelativeSource其实是Binding的属性名称,第二个是类型名。Self是一个枚举值。
这句话的意思就是,生成一个类型RelativeSource,构造函数是枚举值Self;将这个变量赋值给属性RelativeSource。
3) 当Content需要值时,就会调用Binding的ProvideValue。这个函数就会把Button的属性Height返回!
当然这里绕了很大一圈,只实现了一个简单的操作:将Button的高度显示出来!感觉好费劲!
但是:绑定有一个特点,可以感知“源”变量的变化!举例如下
<StackPanel> <Button x:Name="btnTest" Width="200" Height="30" Content="{Binding Height,RelativeSource={RelativeSource Self}}"> </Button> <Button Margin="10" Width="200" Height="30" Click="Button_Click">增加高度</Button> </StackPanel>
Button_Click函数:
private void Button_Click(object sender, RoutedEventArgs e) { btnTest.Height += 10; }
当执行Button_Click时,btnTest的高度增加10,显示内容也随之变化。是不是很神奇!为什么会变化?这里需要了解WPF特殊的属性“依赖属性”。这里就不深入讲解了!
当然绑定的优点不仅仅是这些,WPF会用到大量绑定,如果这些绑定都用代码来实现,太繁琐,也不易理解。
总结:微软为了让XAML好用,费了很多心思。为了XAML能做更多的工作,编译器会替你做很多事情!简单的一个赋值操作,背后门道很多!初学者如果不了解这些门道,就感到一片茫然!本文初步揭示了赋值操作背后的一些内幕,希望你读后,有豁然开朗的感觉!