在 WPF 中, 控件有 Loaded 和 Initialized 两种事件. 初始化和加载控件几乎同时发生, 因此这两个事件也几乎同时触发. 但是他们之间有微妙且重要的区别. 这些区别很容易让人误解. 这里介绍我们设计这些事件的背景. (不仅适用于 Control 类, 同样在通用类如 FrameworkElement 和 FrameworkContentElement 类也适用.)
下面是个小故事:
- Initialized 事件只说: 这个元素已经被构建出来,并且它的属性值都被设置好了,所以通常都是子元素先于父元素触发这个事件.当一个元素的 Initialized 事件被触发, 通常它的子树都已经初始化完成, 但是父元素还未初始化. 这个事件通常是在子树的 Xaml 被加载进来后触发的. 这个事件与 IsInitialized 属性相互绑定.
- Loaded 事件说: 这个元素不仅被构造并初始化完成,布局也运行完毕,数据也绑上来了,它现在连到了渲染面上(rendering surface),秒秒钟就要被渲染的节奏.到这个时候,就可以通过 Loaded 事件从根元素开始画出整棵树. 这个事件与 IsLoaded 属性绑定.
如果你不确定该用哪个事件, 而且也不想继续读下去, 那就用 Loaded 事件好了, 通常它都是对的.
然后, 就是整个故事了.
Initialized 事件
这个事件在所有子元素都被设置完成时触发. 具体来说, FrameworkElement/FrameworkContentElement 实现了 ISupportInitialize 接口, 当该接口的 EndInit 方法调用时, IsInitialized 值被设置为 true. 事件就被触发了.
ISupportInitialize 在 WPF 之前就存在了. 有这个接口, 你就可以在设置 control 的某个属性时,提前告知它你要开始执行一个批处理,之后再告诉它你已经做完了.这样实现了这个接口的对象就可以推迟它的属性值修改事件的处理直到 EndInit 被调用. 在 WPF 中, 不只是 element 用这个接口来触发 Initialized 事件, 其他对象如 DataSourceProvider 也实现这个接口.
槽点是, 到底什么时候调用 EndInit 方法? 起点在 Xaml 加载器.(如果你懂 Baml 的话, 这个方法 Baml 加载器也会调用.) Xaml 加载器在构造对象时就调用 BeginInit. (也就是看见了起始标签), 然后在结束标签那里调用 EndInit 方法. 例子如下:
<Button Width="100"> Hello </Button>...创建一个 Button 对象, 调用 BeginInit, 设置宽度属性, 设置内容属性, 调用 EndInit 方法.
如果用代码来构建元素, 你也可以自己调用 BeginInit/EndInit 方法. 有个问题就是, 不用 Xaml 构造, 也不自己调用 EndInit 方法, 那初始化事件还能触发吗? 答案必须是 yes. 所以我们提供了一些别的方式来设置 IsInitialized 值, 触发事件.
- 当一个未初始化的元素被加到可视化树中时, 初始化事件被触发. 这个方法对于所有的非根元素都有效. 至于根元素, 所有的根元素都是从 PresentationSource 中来的, 所以你懂的...
- 当一个未初始化元素被加入到 PresentationSource 中的时候, 初始化事件会被近似的触发.
从 Initialized 事件的定义中, 可以看出, 这个事件必定是由下向上触发的, 也就是说父元素不应该被初始化直到子元素被初始化完成. 所以通常情况下都是子元素先于父元素被初始化. 不过这一点无法保证, 因为任何人都有可能调用 ISupportInitialize. 从 Xaml 中加载元素的话, 这点倒是可以保证.
另一个槽点是, 元素要这个事件干嘛用? 元素无法获取别处定义的 styles 直到初始化事件触发. 例如 Button1 会从这个 style 中获取一个蓝色的背景. 但是在初始化事件之前, 这个背景是null.
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" > <Page.Resources> <Style TargetType="Button"> <Setter Property="Background" Value="Blue" /> </Style> </Page.Resources> <Button Name="Button1">Clack</Button> </Page>
Loaded 事件
Loaded 事件在元素即将要被渲染时触发. 设计这个事件是考虑你可能需要在程序加载期间做一些初始化操作.
用 Initialized 事件也可以满足这个要求, 因为这个事件意味着元素已经被构建出来, 而且它的元素值也被设置过. 但这个事件还是少了点东西. 举个例子, 你可能需要知道一个元素的 AcutualWidth 属性值, 但是初始化事件触发时, 实际宽度还没有计算出来. 或者你想要看数据绑定的值, 这个值一样也还没有设定.
所以, 我们提供了 Loaded 事件. 它可以在窗口渲染完成, 但是还没有执行任何交互时触发. 我们原本以为控件在可以接受输入的时候做加载是初始化操作就够了. 但是当我们开始在加载事件中触发动画时, 我们发现了一个问题. 有那么一小会, 你会发现元素内容在渲染时没有动画效果, 过后你才会看到动画效果. 你可能没有发现这个问题, 但是这个问题在远程运行程序时会很明显.
所以我们移动了这个事件, 保证在这个事件之前数据绑定和布局有充足的事件执行,同时保证在第一次渲染前触发.(注意如果你要在加载事件中做任何使布局失效的操作, 那一定要记得在渲染前重新运行下布局. )
因为整棵元素数在同一时间走到 Loaded 事件,这个事件会在整棵树内广播. 广播从根元素开始, 所以加载事件是从父元素到子元素.
属性是鸡, 事件是蛋.
另一个槽点是, 到底是先改了属性值,然后触发了事件, 还是先触发事件再改属性值.(一般人都知道答案吧, 作者在卖萌.)
在 WPF 中, 如果有一个属性以及一个和该属性相关的事件, 通常都是修改该属性值来触发该事件. 例如对于 ListBox, 总是修改 SelectedItem 属性值, 触发了SelectionChanged 事件, Loaded 和 Initialized 事件也遵循这个模式.
对于 Loaded 事件有点特殊, 在任何元素的 Loaded 事件触发前, IsLoade 属性在整棵元素树中被设置. 也就是说, 元素树内的所有元素的 IsLoaded 值被设置为 true 之后, 所有元素的 Loaded 事件才被触发.
现在回过头来看上面 Page 中 的 Button 的例子,从 Xaml 文件中加载这个page, 你应当会看到以下的执行顺序.
- Button.IsInitialized goes true
- Button.Initialized event is raised
- Page.IsInitialized goes true
- Page.Initialized event is raised
- Page IsLoaded goes to true
- Button IsLoaded goes to true
- Page.Loaded is raised
- Button.Loaded is raised