1. 前言

TypeConverter是XAML解释器的幕后功臣,它做了大量工作,从WPF诞生以来,几乎每一次XAML的运作都有它的参与。虽然UWP中TypeConverter已经彻彻底底退居幕后,连自定义TypeConverver都不可以,但了解TypeConverter的原理对理解XAML解析器的运作方式总是有帮助的。

2. .Net中的TypeConverter

TypeConverter在.NET 的早期版本中就已经存在,它可以将一种类型的值转换为其它类型,典型的用法是在数据类型和字符串之间转换。

假设要实现一个从字符串转换成目标类型的函数GetValue:

  1. private T GetValue<T>(string source)
  2. {
  3. var type = typeof(T);
  4. T result;
  5. if (type == typeof(bool))
  6. {
  7. result = (T)(object)Convert.ToBoolean(source);
  8. }
  9. else if (type == typeof(string))
  10. {
  11. result = (T)(object)source;
  12. }
  13. else if (type == typeof(short))
  14. {
  15. result = (T)(object)Convert.ToInt16(source);
  16. }
  17. else if (type == typeof(int))
  18. {
  19. result = (T)(object)Convert.ToInt32(source);
  20. }
  21. else
  22. {
  23. result = default(T);
  24. }
  25. return result;
  26. }

这个函数有很多明显的问题:代码冗余、支持的类型不多、难以维护、不符合开放封闭原则等。使用Convert.ChangeType可以将代码重构如下:

  1. private T GetValue<T>(string source)
  2. {
  3. return (T)Convert.ChangeType(source, typeof(T));
  4. }

只用一行代码,看上去简直完美。但想深一层,Convert类的注释是“将一个基本数据类型转换为另一个基本数据类型。”也就是说它只支持基础类型,事实上ChangeType函数的源码只是上面GetValue的高级版本而已:

  1. public static Object ChangeType(Object value, Type conversionType, IFormatProvider provider) {
  2. if( conversionType == null) {
  3. throw new ArgumentNullException("conversionType");
  4. }
  5. Contract.EndContractBlock();
  6. if( value == null ) {
  7. if(conversionType.IsValueType) {
  8. throw new InvalidCastException(Environment.GetResourceString("InvalidCast_CannotCastNullToValueType"));
  9. }
  10. return null;
  11. }
  12. IConvertible ic = value as IConvertible;
  13. if (ic == null) {
  14. if ( value.GetType() == conversionType) {
  15. return value;
  16. }
  17. throw new InvalidCastException(Environment.GetResourceString("InvalidCast_IConvertible"));
  18. }
  19. RuntimeType rtConversionType = conversionType as RuntimeType;
  20. if (rtConversionType==ConvertTypes[(int)TypeCode.Boolean])
  21. return ic.ToBoolean(provider);
  22. if (rtConversionType==ConvertTypes[(int)TypeCode.Char])
  23. return ic.ToChar(provider);
  24. if (rtConversionType==ConvertTypes[(int)TypeCode.SByte])
  25. return ic.ToSByte(provider);
  26. if (rtConversionType==ConvertTypes[(int)TypeCode.Byte])
  27. return ic.ToByte(provider);
  28. if (rtConversionType==ConvertTypes[(int)TypeCode.Int16])
  29. return ic.ToInt16(provider);
  30. if (rtConversionType==ConvertTypes[(int)TypeCode.UInt16])
  31. return ic.ToUInt16(provider);
  32. if (rtConversionType==ConvertTypes[(int)TypeCode.Int32])
  33. return ic.ToInt32(provider);
  34. if (rtConversionType==ConvertTypes[(int)TypeCode.UInt32])
  35. return ic.ToUInt32(provider);
  36. if (rtConversionType==ConvertTypes[(int)TypeCode.Int64])
  37. return ic.ToInt64(provider);
  38. if (rtConversionType==ConvertTypes[(int)TypeCode.UInt64])
  39. return ic.ToUInt64(provider);
  40. if (rtConversionType==ConvertTypes[(int)TypeCode.Single])
  41. return ic.ToSingle(provider);
  42. if (rtConversionType==ConvertTypes[(int)TypeCode.Double])
  43. return ic.ToDouble(provider);
  44. if (rtConversionType==ConvertTypes[(int)TypeCode.Decimal])
  45. return ic.ToDecimal(provider);
  46. if (rtConversionType==ConvertTypes[(int)TypeCode.DateTime])
  47. return ic.ToDateTime(provider);
  48. if (rtConversionType==ConvertTypes[(int)TypeCode.String])
  49. return ic.ToString(provider);
  50. if (rtConversionType==ConvertTypes[(int)TypeCode.Object])
  51. return (Object)value;
  52. return ic.ToType(conversionType, provider);
  53. }

TypeConverter的一个典型应用场景就是用于解决这个问题。使用TypeConverter重构这个函数如下:

  1. private T GetValue<T>(string source)
  2. {
  3. var typeConverter = TypeDescriptor.GetConverter(typeof(T));
  4. if (typeConverter.CanConvertTo(typeof(T)))
  5. return (T)typeConverter.ConvertFromString(source);
  6. return default(T);
  7. }

TypeConverter GetConverter(Type type) 返回指定类型的TypeConverter,此方法可查找通过查找相应的 TypeConverterAttribute, 如果找不到 TypeConverterAttribute, ,该代码遍历类的基类层次结构,直到它找到的基元类型。使用TypeConverter不需要担心可以转换的数据类型太少,BCL中已实现了一大堆继承TypeConverter的类,基本满足日常使用。除了这些已实现的TypeConverter,还可以实现自己的TypeConverter,扩展性方面完全没有问题。

值得一提的是,如果使用了错误的字符串,Convert.ChangeType只提示“输入字符串的格式不正确”。 而TypeConverter的错误提示则详细得多:"a 不是 Decimal 的有效值"。

3. WPF中的TypeConverter

XAML本质上是XML,其中的属性内容全部都是字符串。如果对应属性的类型是XAML内置类型(即Boolea,Char,String,Decimal,Single,Double,Int16,Int32,Int64,TimeSpan,Uri,Byte,Array等类型),XAML解析器直接将字符串转换成对应值赋给属性;对于其它类型,XAML解析器需做更多工作。

  1. <Grid.RowDefinitions>
  2. <RowDefinition Height="Auto"/>
  3. <RowDefinition Height="*"/>
  4. </Grid.RowDefinitions>

如上面这段XAML中的"Auto"和"*",XAML解析器将其分别解析成GridLength.Auto和new GridLength(1, GridUnitType.Star)再赋值给Height,它相当于这段代码:

  1. grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
  2. grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(1, GridUnitType.Star) });

为了完成这个工作,XAML解析器需要TypeConverter的协助。XAML解析器通过两个步骤查找TypeConverter:

1. 检查属性声明上的TypeConverterAttribute。

2. 如果属性声明中没有TypeConverterAttribute,检查类型声明中的TypeConverterAttribute。

属性声明上TypeConverterAttribute的优先级高于类型声明。如果以上两步都找不到类型对应的TypeConverterAttribute,XAML解析器将会报错:属性"*"的值无效。找到TypeConverterAttribute指定的TypeConverter后,XAML解析器调用它的object ConvertFromString(string text)函数将字符串转换成属性的值。

WPF内置的TypeConverter十分十分多,但有时还是需要自定义TypeConverter,一种情况是难以用字符串直接构建的类型,一种是为了简化XAML。

假设有三个类Email、Receiver、ReceiverCollection,结构如下:

  1. public class Email
  2. {
  3. public ReceiverCollection Receivers { get; set; }
  4. }
  5. public class Receiver
  6. {
  7. public string Name { get; set; }
  8. }
  9. public class ReceiverCollection : ObservableCollection<Receiver>
  10. {
  11. }

在XAML中构建一个Email对象及填充Receiver列表的代码如下:

  1. <local:Email x:Key="Email">
  2. <local:Email.Receivers>
  3. <local:ReceiverCollection>
  4. <local:Receiver Name="Zhao"/>
  5. <local:Receiver Name="Qian"/>
  6. <local:Receiver Name="Sun"/>
  7. <local:Receiver Name="Li"/>
  8. <local:Receiver Name="Zhou"/>
  9. <local:Receiver Name="Wu"/>
  10. </local:ReceiverCollection>
  11. </local:Email.Receivers>
  12. </local:Email>

语法这么复杂,这时候就需要考虑自定义一个ReceiverCollectionConverter了。自定义TypeConverter的基本步骤如下:

  • 创建一个继承自TypeConverter的类;
  • 重载virtual bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType);
  • 重载virtual bool CanConvertTo(ITypeDescriptorContext context, Type destinationType);
  • 重载virtual object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value);
  • 重载virtual object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType);
  • 使用TypeConverterAttribute 指示XAML解析器可用的TypeConverter;

代码如下:

  1. [TypeConverter(typeof(ReceiverCollectionConverter))]
  2. public class ReceiverCollection : ObservableCollection<Receiver>
  3. {
  4. public static ReceiverCollection Parse(string source)
  5. {
  6. var result = new ReceiverCollection();
  7. var tokens = source.Split(';');
  8. foreach (var token in tokens)
  9. {
  10. result.Add(new Receiver { Name = token });
  11. }
  12. return result;
  13. }
  14. public string ConvertToString()
  15. {
  16. var result = string.Empty;
  17. foreach (var item in this)
  18. {
  19. result += item.Name;
  20. result += ";";
  21. }
  22. return result;
  23. }
  24. }
  25. public class ReceiverCollectionConverter : TypeConverter
  26. {
  27. public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
  28. {
  29. if (sourceType == typeof(string))
  30. {
  31. return true;
  32. }
  33. return base.CanConvertFrom(context, sourceType);
  34. }
  35. public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
  36. {
  37. if (destinationType == typeof(string))
  38. {
  39. return true;
  40. }
  41. return base.CanConvertTo(context, destinationType);
  42. }
  43. public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
  44. {
  45. switch (value)
  46. {
  47. case null:
  48. throw GetConvertFromException(null);
  49. case string source:
  50. return ReceiverCollection.Parse(source);
  51. }
  52. return base.ConvertFrom(context, culture, value);
  53. }
  54. public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
  55. {
  56. switch (value)
  57. {
  58. case ReceiverCollection instance:
  59. if (destinationType == typeof(string))
  60. {
  61. return instance.ConvertToString();
  62. }
  63. break;
  64. }
  65. return base.ConvertTo(context, culture, value, destinationType);
  66. }
  67. }

结果上面那的那段XAML可以简化成一句代码:

  1. <local:Email Receivers="Zhao;Qian;Sun;Li;Zhou;Wu" x:Key="Email"/>

除了可以在类型上声明TypeConverterAttribute,还可以在属性上声明,属性上的声明优先级较高:

  1. public class Email
  2. {
  3. [TypeConverter(typeof(ReceiverCollectionConverterExtend))]
  4. public ReceiverCollection Receivers { get; set; }
  5. }

4. UWP中的TypeConverter

在UWP中TypeConverter已彻底退居幕后,要实现上面ReceiverCollectionConverter 的简化XAML效果, 可以用CreateFromStringAttribute(自Aniverssary Update(14393)后可用,但好像常常报错,直接升到Creators Update(15063)比较好):

  1. [Windows.Foundation.Metadata.CreateFromString(MethodName = "TypeConverterUwp.ReceiverCollection.Parse")]
  2. public class ReceiverCollection : ObservableCollection<Receiver>
  3. {
  4. public static ReceiverCollection Parse(string source)
  5. {
  6. var result = new ReceiverCollection();
  7. var tokens = source.Split(';');
  8. foreach (var token in tokens)
  9. {
  10. result.Add(new Receiver { Name = token });
  11. }
  12. return result;
  13. }
  14. }

CreateFromStringAttribute的效果和TypeConverterAttribute差不多,可是它只能用在类上,不能用于属性。即使提供了这个补偿方案,不能自定义TypeConverter对UWP的影响还是很大。UWP有XAML 固有数据类型的概念(即可以直接在XAML上使用的数据类型),只包含Boolean、String、Double、Int32四种,而内置的TypeConverter又十分少,导致连decimal都没有获得支持.

有趣的是VisualStudio的属性面板还天真地以为自己支持直接输入Decimal,甚至设计视图还可以正常显示,但编译报错。通过引用System.ComponentModel.TypeConverter的NuGet包连TypeConverterAttribute都可以添加,但这个Attribute没有任何实际效果。

  1. public class MyContentControl : ContentControl
  2. {
  3. /// <summary>
  4. /// 获取或设置Amount的值
  5. /// </summary>
  6. [TypeConverter(typeof(DecimalConverter))]
  7. public Decimal Amount
  8. {
  9. get { return (Decimal)GetValue(AmountProperty); }
  10. set { SetValue(AmountProperty, value); }
  11. }
  12. /// <summary>
  13. /// 标识 Amount 依赖属性。
  14. /// </summary>
  15. public static readonly DependencyProperty AmountProperty =
  16. DependencyProperty.Register("Amount", typeof(Decimal), typeof(MyContentControl), new PropertyMetadata(0m, OnAmountChanged));
  17. private static void OnAmountChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
  18. {
  19. MyContentControl target = obj as MyContentControl;
  20. Decimal oldValue = (Decimal)args.OldValue;
  21. Decimal newValue = (Decimal)args.NewValue;
  22. if (oldValue != newValue)
  23. target.OnAmountChanged(oldValue, newValue);
  24. }
  25. protected virtual void OnAmountChanged(Decimal oldValue, Decimal newValue)
  26. {
  27. Content = "Amount is " + newValue;
  28. }
  29. }

当看到如上图那样的错误信息,可以理解为UWP缺少对应类型的TypeConverter,只能在CodeBehind为属性赋值。如果一定要在XAML上为decimal赋值,可以用Binding。

  1. public class StringToDecimalBridge
  2. {
  3. public Decimal this[string key]
  4. {
  5. get
  6. {
  7. return Convert.ToDecimal(key);
  8. }
  9. }
  10. }
  1. <local:MyContentControl Amount="{Binding [10.3],Source={StaticResource StringToDecimalBridge}}"/>

5. 结语

因为本地化的文章提到TypeConverter,正好手头的工作要用到TypeConverter,所以才想写一篇文章介绍这个概念。结果才发现UWP的TypeConverter不能直接使用,偏偏这个概念对理解XAML解析器很重要,正好把WPF的内容也拿来讨论一下。

6. 参考

TypeConverter 类

TypeConverters 和 XAML

Type Converters for XAML Overview

TypeConverterAttribute Class

如何:实现类型转换器

XAML 固有数据类型

CreateFromStringAttribute Class

7. 源码

GitHub - TypeConverterSample

[UWP]了解TypeConverter的更多相关文章

  1. 【Win10】UAP/UWP/通用 开发之 x:Bind

    [Some information relates to pre-released product which may be substantially modified before it's co ...

  2. [UWP]了解模板化控件(2):模仿ContentControl

    ContentControl是最简单的TemplatedControl,而且它在UWP出场频率很高.ContentControl和Panel是VisualTree的基础,可以说几乎所有VisualTr ...

  3. [UWP]了解IValueConverter

    1. 前言 IValueConverter是用于数据绑定的强大的武器,它用于Value在Binding Source和Binding Target之间的转换.本文将介绍IValueConverter的 ...

  4. [UWP 自定义控件]了解模板化控件(2):模仿ContentControl

    ContentControl是最简单的TemplatedControl,而且它在UWP出场频率很高.ContentControl和Panel是VisualTree的基础,可以说几乎所有VisualTr ...

  5. UWP 律师查询 MVVM

    APP简介 律师查询是基于聚合数据的律师查询接口做的,这个接口目前处于停用状态,但是,由于我是之前申请的,所以,还可以用,应该是无法再申请了. 效果图 开发 一.HttpHelper 既然是请求接口的 ...

  6. 领域驱动和MVVM应用于UWP开发的一些思考

    领域驱动和MVVM应用于UWP开发的一些思考 0x00 起因 有段时间没写博客了,其实最近本来是根据梳理的MSDN上的资料(UWP开发目录整理)有条不紊的进行UWP学习的.学习中有了心得体会或遇到了问 ...

  7. UWP中实现自定义标题栏

    UWP中实现自定义标题栏 0x00 起因 在UWP开发中,有时候我们希望实现自定义标题栏,例如在标题栏中加入搜索框.按钮之类的控件.搜了下资料居然在一个日文网站找到了一篇介绍这个主题的文章: http ...

  8. UWP中新加的数据绑定方式x:Bind分析总结

    UWP中新加的数据绑定方式x:Bind分析总结 0x00 UWP中的x:Bind 由之前有过WPF开发经验,所以在学习UWP的时候直接省略了XAML.数据绑定等几个看着十分眼熟的主题.学习过程中倒是也 ...

  9. MVVM框架从WPF移植到UWP遇到的问题和解决方法

    MVVM框架从WPF移植到UWP遇到的问题和解决方法 0x00 起因 这几天开始学习UWP了,之前有WPF经验,所以总体感觉还可以,看了一些基础概念和主题,写了几个测试程序,突然想起来了前一段时间在W ...

随机推荐

  1. JS--我发现,原来你是这样的JS:面向对象编程OOP[2]--(创建你的那个对象吧)

    一.介绍 我们继续面向对象吧,这次是面向对象编程的第二篇,主要是讲创建对象的模式,希望大家能从博客中学到东西. 时间过得很快,还是不断的学习吧,为了自己的目标. 二.创建对象 1.前面的创建对象方式 ...

  2. [转载] 一致性hash算法释义

    转载自http://www.cnblogs.com/haippy/archive/2011/12/10/2282943.html 一致性Hash算法背景 一致性哈希算法在1997年由麻省理工学院的Ka ...

  3. 【RabbitMQ+Python入门经典】兔子和兔子窝 笔记

    RabbitMQ工业级的消息队列服务器. 兔子和兔子窝 动机来源:从生产环境的电子邮件处理流程当中分支出一个特定的离线分析流程. 解决方案1: 开始使用MySQL处理,将要处理的东西放在表里面,另一个 ...

  4. Ext3和Ext4文件系统区别

    inode http://www.cnblogs.com/itech/archive/2012/05/15/2502284.html Ex3使用15个inode查询数据块,前12个为直接数据块,直接指 ...

  5. 我从.net转到java的心得和体会

    前言:由于有不少人咨询过我如果从.net转java,有什么技巧吗,我现在就分享我从.net转java的历程,这里不涉及两门语言的比较,记录的都是我个人的观点. 一:从.net转java的初衷 我是20 ...

  6. Liunx find的运用

    find命令 一.根据 -name 查找 find[搜索范围][搜索条件] find /root -name a1 若是模糊查询,则使用通配符 *匹配任意字符{find /root -name &qu ...

  7. Spring4 事务管理

    Spring4 事务管理 本章是Spring4 教程中的最后一章,也是非常重要的一章.如果说学习IOC是知识的入门,那学习事务管理就是知识的提升.本章篇幅可能有一丢丢长,也有一丢丢难,需要读者细细品味 ...

  8. 为什么我的子线程更新了 UI 没报错?借此,纠正一些Android 程序员的一个知识误区

    开门见山: 这个误区是:子线程不能更新 UI ,其应该分类讨论,而不是绝对的. 半小时前,我的 XRecyclerView 群里面,一位群友私聊我,问题是: 为什么我的子线程更新了 UI 没报错? 我 ...

  9. js 获取每月有几周,根据年月周获取该周从周一到周日的日期等方法

    本文基于react-native 本人在用react-native写一个关于课程表的APP时需要课程表按照日期周期显示,网上查了许多方法,都没有达到自己想要的效果,根据一些方法的参考,再根据自己思维写 ...

  10. canvas画一个时钟

    效果图如下 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF ...