这个文章来源于这样一个问题。下面的两种做 法,在效果上有什么不同?(注:Header是一个自定义DP。)
<TabItem Header="{Binding Header, ElementName=window}">
<TextBox Text="{Binding Header, ElementName=window}"
Name="headerName"/>
</TabItem>
和
<DataTemplate x:Key="TabTemplate">
<TextBox Text="{Binding Header, ElementName=window}"
Name="headerNameInTemplate"/>
</DataTemplate>
<TabItem Header="{Binding Header, ElementName=window}"
ContentTemplate="{StaticResource TabTemplate}"/>
一种是直接在TabItem里放一个TextBox,另一种是通过DataTemplate间接地放进去。这两种方式,从外观上看,没有区别。拿到XamlPad里看VisualTree,控件的结构上也没有区别。
图1. 直接使用TextBox
图2. Template中的TextBox
在开发WPF程序的时候,常常会使用到DataTemplate,以通过MVVM模式实现从数据生成UI。我们当然也会希望用与不用DataTemplate的行为最好也是一样的。而且目前来看,这两种界面看上去一样,VisualTree也是一样的。行为应该也会一样的。
但是愿望是美好的,实现却是残酷的。两种不同的实现方式,虽然看上去一样,但是行为却是不同的。这里有一个示例程序,看看大家能不能找到两种实现方式有什么不同呢?
图3. 示例运行图
如果你还没有猜到原因,或是懒得研究示例代码,就继续看下面的分析吧。
上面的分析,一直都是建立在空间相似性的比较上。两种实现方式,从空间结构的角度上来说的确是一样。我们再来考虑一下过程。
先来了解一下TabControl和示例的情况,算是先给大家一些提示。
1. 当直接在TabItem里放TextBox时,TabItem的Content就是一个实实在在的控件。
2. 当使用DataTemplate时,这上面的示例代码中,TabItem的Content是空的。
3. TabControl虽然有多个TabItem,但是当前显示的TabItem只有一个。而且更重要的是,无论TabItem如何切换,TabControl一直是用同一个ContentPresenter把当前TabItem里的Content显示出来的。参考图一、二中的蓝色部分,那个控件,就是TabControl的主体部分,在切换Tab时是不会变的。
当在TabControl里切换不同的TabItem时,如果发现Content是一个控件,比如示例中的TextBox,那就把这个TextBox显示出来。如果发现Content是个Object,而同时使用了DataTemplate。就会应用这个DataTemplate。
问题就在于,DataTemplate是如何被应用的?
微软关于DataTemplate的官方文档只介绍了如何使用DataTemplate,却似乎并没有介绍DataTemplate的工作原理(也许有,不过一直没有找到)。
文档上是如此解释DataTemplate的。
“The WPF data templating model provides you with great flexibility to define the presentation of your data.”
就是说,为从数据展示(或者说是生成)界面提供了极大的灵活性。
但是通读全文,微软也没有讲过,用DataTemplate和不用DataTemplate有什么不同。基本上通篇就是在介绍DataTemplate的优点。没有介绍一个缺点。但是说实在了,我们在比较不同技术的时候,通常都会去比较其优点和缺点。如果之前没有了解过一个技术的缺点就贸然使用,势必会遇到无法预期的情况,给开发的进度造成影响。
以下对于DataTemplate原理的分析,是笔者根据DataTemplate的实际效果分析出来的,没有官方的资料确认,可能有不少错误,欢迎大家指正。
从DataTemplate的目标和效果来看。DataTemplate是一种运行时行为。就是在程序加载数据之前,DataTemplate是没有起作用的。而这时其它非DataTemplate控件都已经加载了。加载数据之后,WPF会根据数据的类型或控件本身指定DataTemplate资料来进一步加载DataTemplate中的控件。
考虑下面的几种情况:
1. 两个控件,使用了相同的DataTemplate。拿前文中的例子来说,两个TabItem都用了同一个DataTemplate,DataTemplate里有一个TextBox,那么最后生成的界面中,两个TabItem里,是两个不同的TextBox还是同一个TextBox呢?
2. 还是拿上面的例子,稍微改一下,让两个TabItem用不同的DataTemplate。那么在两个TabItem之间切换的时候,看不到的那个TabItem的DataTemplate Generated 控件是不是存在的,还是已经被回收了?
大家可以先想一下再继续看。
对于前文的例子而言。问题1 的答案是,是同一个TextBox。那第2个问题也就不用回答了。
但是对于WPF整体而言,答案是不确定的。问题的答案取决于DataTemplate是否设置了x:Shared属性(这个属性很奇怪,在VS的Intellisence是无法自动出来的)和DataTemplate使用在什么控件上。上文的例子,没有设置这个属性,默认的值是True,而又由于TabControl只有一个ContentPresenter,所以DataTemplate里的东西会共享。呜,好复杂。
如果将x:Shared属性设置为False,那第1个问题的答案就是不同的TextBox了。对于第2 个问题,答案是会被回收掉。但是要知道,如果在TabItem里直接放TextBox,那么在切换TabItem时,每个TabItem里的TextBox是不会回收的。
DataTemplate的这些特性有什么问题吗?有,而且对于要求比较高的商业软件来说,还不小呢。目前发现的主要有下面两个。
1. 光标的位置丢失。
2. DataBinding不会UpdateSource。
先来解释一下第一个问题。光标的位置丢失,这个问题说大不大,说小不小。我们打开Visual Studio,再多打开几个源代码文件,然后在各个文件间切换一下,看看光标的位置会不会被保存下来呢?然而,使用了DataTemplate之后,无论x:Shared如何设置(结果会有不同),光标的位置都不会正确。我们就不得不再去维护这个光标的位置。
问题二,分类讨论一下。如果x:Shared为True,就是默认情况。两个TabItem里用的其实就是同一个TextBox,那么在切换Tab时,就不会触发TextBox的LostFocus事件,DataBinding就不会更新DataSource。如果x:Shared为False,由于切换Tab时,前一个Tab里的TextBox会被回收丢。那么DataBinding能不能UpdateSource,就变成了,TextBox被回收之前,会不会先触发一次LostFocus。经过实验,TextBox在被回收前,没有触发LostFocus事件。(在此要感谢一下我的同事Shelvin Yuan,是他发现的这个问题。才有了这篇文章。)
对于问题二的解决方案就多了,可以设置DataBinding的UpdateSourceTrigger属性,可以自定义一个TextBox,在回收时,手工触发一次LostFocus,同时设置x:Shared为False,还可以自己在切换Tab时手工地UpdateSource。
但是这些解决方案看上去都不太完美。DataTemplate就不能没有这些副作用吗?很遗憾,笔者还没有找到最佳的解决方案,使得使用DataTemplate和不使用DataTemplate的效果完成一样。
目前而言,DataTemplate完全是一种运行时行为。笔者能想到一个方案就是让程序员DataTemplate可以设置的行为,既可以是运行时的行为,也可以是静态的类似预编译指令的行为。比如可以通过给DataTemplate添加加一个x:IsStatic属性来设置其行为。还是拿上面的代码为例,编译器如果发然这个属性是Ture,就把DataTemplate里的东西展开,内联到TabItem中,就和直接使用TextBox一样了。
不过这只是一个美好的愿望。目前只能去找Workaround的方案了。