首先,我介绍一下今天登场的两位主角——Loaded事件与OnApplyTemplate方法,排名不分先后(热烈鼓掌ING~)。
微软是这么解释Loaded事件的:
Code
// Summary:
// Occurs when the element has completed layout passes, has rendered, and is
// ready for interaction.
public event RoutedEventHandler Loaded;
我想这几句鸟语就不用我翻译了,意思就是说这个事件是发生在这个控件的布局已经搞定并且可以对用户输入做出反应的那个时间点上。
然后我再说说这个ApplyTemplate(),再看微软的说明:
Code
// Summary:
// When overridden in a derived class, is invoked whenever application code
// or internal processes (such as a rebuilding layout pass) call System.Windows.Controls.Control.ApplyTemplate().
[SecuritySafeCritical]
public virtual void OnApplyTemplate();
这几句鸟语的意思是说,当你重载一个子类的时候,这个方法会在代码或者内部操作(比如重新构造布局)调用ApplyTemplate()方法的时候被调用。
那么问题来了,当一个有Template的Control在加载的时候,是先ApplyTemplate呢,还是先触发Loaded事件呢?
答案是不一定,为此我专门写了代码进行测试,首先我重载了两个通用控件,然后重写了它们的OnApplyTemplate()方法,代码如下:
Code
public class MyButton:Button
{
public override void OnApplyTemplate()
{
Debug.WriteLine("Button Applied at" + DateTime.Now.ToString("o"));
base.OnApplyTemplate();
}
}
public class MyCheckBox : CheckBox
{
public override void OnApplyTemplate()
{
Debug.WriteLine("CheckBox Applied at " + DateTime.Now.ToString("o"));
base.OnApplyTemplate();
}
}
然后在Xaml中,我如此定义:
Code
<UserControl x:Class="SilverlightApplication2.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:abcd="clr-namespace:SilverlightApplication2"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="300" Loaded="UserControl_Loaded">
<Grid x:Name="LayoutRoot" Background="White">
<abcd:MyButton Loaded="MyButton_Loaded" HorizontalContentAlignment="Center">
<abcd:MyButton.Content>
<abcd:MyCheckBox Loaded="InsideCheckBox_Loaded"/>
</abcd:MyButton.Content>
</abcd:MyButton>
<abcd:MyCheckBox Loaded="OutsideCheckBox_Loaded"/>
</Grid>
</UserControl>
在后台代码中:
Code
private void MyButton_Loaded(object sender, RoutedEventArgs e)
{
Debug.WriteLine("Button Loaded at " + DateTime.Now.ToString("o"));
}
private void InsideCheckBox_Loaded(object sender, RoutedEventArgs e)
{
Debug.WriteLine("Inside CheckBox Loaded at " + DateTime.Now.ToString("o"));
}
public override void OnApplyTemplate()
{
Debug.WriteLine("Page Applied at " + DateTime.Now.ToString("o"));
base.OnApplyTemplate();
}
private void UserControl_Loaded(object sender, RoutedEventArgs e)
{
Debug.WriteLine("Page Loaded at " + DateTime.Now.ToString("o"));
}
private void OutsideCheckBox_Loaded(object sender, RoutedEventArgs e)
{
Debug.WriteLine("Outside CheckBox Loaded at " + DateTime.Now.ToString("o"));
}
最终运行时截张VS的图:
可能到了这大家还没怎么看懂这些代码和这个图说明了什么,那么我解释一下。首先,Page类的OnApplayTemplate()方法没有执行,是因为它没有Template;然后,从Button、CheckBox以及Page的Loaded事件来看,CheckBox和Button作为Page的子控件,先Load完毕,而作为Parent的Page要更晚一些Load;再然后,Button和CheckBox的Loaded事件都比ApplyTemplate早;最后,作为Button的Content的CheckBox加载最晚,而且它ApplyTemplate要比Loaded早。
这个最后一条,就是为什么我说Loaded与ApplyTemplate的早晚不一定的原因。从上述的先后顺序上,我们至少可以看出来三点:第一,子控件总是先于父控件触发Loaded事件的;第二,应用了ContentPresender的控件,它的Content如果也是一个控件,那么这个控件是要比父控件Loaded晚的;第三,第二种情况下,Content那个控件的ApplyTemplate要比Loaded早,而不在那种情况下,控件的Loaded事件是比ApplyTemplate早的。
有些事情微软做得总是让人匪夷所思,比如说ApplyTemplate居然比Loaded晚这个事情,我在WPF上测了同样的东西,ApplyTemplate总是要比Loaded早的,这也合情合理,应用完模版之后再告诉别人我已经加载完了,这是正常人都有的逻辑,但是在Silverlight中,大部分情况都是反过来的。我当然不相信设计Silverlight那哥们在写架构的时候把调用顺序给弄倒了,微软这么做总是有它的理由的,只是理由是什么呢?是不是桌面上和浏览器上绘图会有区别呢?这些我就不得而知了,没准只有微软写OS和IE内核的人才知道……
最后,我说说Silverlight这种机制的问题,就是比如我写一个控件,然后在generic.xaml中为这个控件写了Template,我需要通过重写OnApplyTemplate方法调用GetTemplateChild来获得Template中的子控件的的实例,由于这个过程比Loaded事件要晚,因此如果我在Loaded事件触发时就为控件模板中的子控件的某些属性赋值的话,由于子控件还没有初始化,所以这样会引发NullReferenceException。解决方法并不是没有,只需要我们手动设用ApplyTemplate方法即可,但是这样破坏了类的默认行为,我认为并不是很好。
对于上述观点,由于测试得比较仓促,也许会有一些我没有发现在问题在里面,也欢迎大家对我进行指正。