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能做更多的工作,编译器会替你做很多事情!简单的一个赋值操作,背后门道很多!初学者如果不了解这些门道,就感到一片茫然!本文初步揭示了赋值操作背后的一些内幕,希望你读后,有豁然开朗的感觉!

 


posted @ 2018-10-15 14:47 源之缘 阅读(...) 评论(...) 编辑 收藏