WPF - BeginInvoke编程模型
我们都知道,WPF是一个属性驱动的编程框架。在使用WPF编程的时候,您可以以任意顺序设置这些属性。相应地,WPF则会自动根据这些属性变化执行外观的变更。
这里就存在一个问题:WPF的依赖项属性在发生更改时常常需要执行某个特定功能,如在更改width属性时更新控件的布局。而在一段代码中,我们可以多次对这种具有回调功能的WPF依赖项属性进行设置。那么问题来了:这些回调功能是否会执行多次?这些执行会不会影响WPF的执行效率呢?答案是不会。WPF内部使用了一种模型避免了由于设置多个属性而导致的某一机制重复执行。由于微软并没有为该模型提供一个官方命名,因此我们在这里为其命名为BeginInvoke()编程模型。
一. BeginInvoke()函数
在讲解该模型之前,我想我们首先需要了解一下BeginInvoke()函数的定义:
1 public DispatcherOperation BeginInvoke(DispatcherPriority priority, Delegate method, Object arg, params Object[] args)
该函数用来向WPF系统中插入一个由method参数所传入的工作项。这些插入到WPF系统中的工作项将根据priority参数所指定的优先级进行排列。其中该函数的第三个参数arg以及第四个参数args都将被作为参数传递进method所表示的工作项中。
该函数的返回值是一个DispatcherOperation类型的实例。通过该实例,软件开发人员可以与插入WPF系统中的工作项进行交互:DispatcherOperation类实例的Priority属性用来操作插入工作项的优先级。一个工作项的优先级越高,其就会被越早执行。同时在代码中,软件开发人员可以通过DispatcherOperation类的Status属性来访问当前工作项的状态。在工作项执行完毕以后,软件开发人员更可以通过Result属性得到工作项的运行结果。
现在让我们来看看BeginInvoke()函数的内部实现。在每次调用BeginInvoke()函数的时候,其内部都会调用BeginInvokeImp()函数:
1 public DispatcherOperation BeginInvoke(DispatcherPriority priority, Delegate method, object arg, params object[] args) 2 { 3 return this.BeginInvokeImpl(priority, method, this.CombineParameters(arg, args), false); 4 }
从上面的代码中您可以看到,在调用BeginInvokeImpl()函数之前,WPF内部将通过CombineParameters()函数将传入的参数arg与args合并。因此在调用BeginInvoke()函数的时候,您并不需要担心参数arg与args之间的区别。
而BeginInvokeImpl()函数的定义则略为繁琐:
internal DispatcherOperation BeginInvokeImpl(DispatcherPriority priority, Delegate method, object args, bool isSingleParameter) { …… DispatcherOperation data = new DispatcherOperation(this, method, priority, args, isSingleParameter) { _item = this._queue.Enqueue(priority, data) }; this.RequestProcessing(); …… return data; }
上面的代码列出了BeginInvokeImpl()函数的主干。在该函数中,WPF将首先创建一个DispatcherOperation类的实例,以记录当前所请求执行的任务的信息。接下来,其将该实例记录在成员_queue中。_queue会根据当前传入任务的优先级priority在内部对各个任务进行排列。接下来,WPF就调用了RequestProcessing()函数请求异步执行该任务。
在这里,我们知道了WPF如何支持所插入各个任务的优先级了:成员_queue的类型为PriorityQueue。其将首先按照插入任务的优先级进行排序,然后才是插入任务的先后顺序。但是函数RequestProcessing()函数则是如何完成对异步执行任务的支持呢?
让我们继续查看.net源码:
1 private bool RequestProcessing() 2 { 3 DispatcherPriority maxPriority = this._queue.MaxPriority; 4 switch (maxPriority) 5 { 6 case DispatcherPriority.Invalid: 7 case DispatcherPriority.Inactive: 8 return true; 9 } 10 if (_foregroundPriorityRange.Contains(maxPriority)) 11 { 12 return this.RequestForegroundProcessing(); 13 } 14 return this.RequestBackgroundProcessing(); 15 }
这里的代码则讲述了WPF对具有特殊优先级的各个工作项的特殊处理。如果当前任务的优先级为Invalid或Inactive,那么直接返回true,而不去向WPF属性系统请求对它的异步调用。接下来,WPF会根据优先级是否存在于_foregroundPriorityRange来决定需要调用的是RequestForegroundProcessing()还是RequestBackgroundProcessing()。但是不管怎样,WPF都将最终调用RequestForegroundProcessing()函数。看到这里时,我们已经接近最后的答案了:
1 private bool RequestForegroundProcessing() 2 { 3 …… 4 return MS.Win32.UnsafeNativeMethods.TryPostMessage(new HandleRef(this, this._window.Value.Handle), _msgProcessQueue, IntPtr.Zero, IntPtr.Zero); 5 }
OK,这就是我们想要的。BeginInvoke()函数的执行逻辑实际上非常简单:在调用BeginInvoke()函数时,WPF将在成员_queue中记录该异步调用的相关信息。接下来,其将向Windows系统发送一个_msgProcessQueue消息。那我们可以大胆猜想:对BeginInvoke()函数所传入的回调函数的调用就是在对_msgProcessQueue消息的处理中完成的。
在.net源码中搜索对_msgProcessQueue消息的使用后可以看到。当_msgProcessQueue消息到达时,ProcessQueue()函数将被执行:
1 private IntPtr WndProcHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) 2 { 3 …… 4 else if (msg == _msgProcessQueue) 5 { 6 this.ProcessQueue(); 7 } 8 …… 9 }
这里的WndProcHook就是WPF向当前窗口中添加的消息处理函数钩子。而ProcessQueue()函数则调用了BeginInvoke()函数所插入到成员_queue中的回调函数:
1 private void ProcessQueue() 2 { 3 …… 4 operation = this._queue.Dequeue(); 5 …… 6 if (operation != null) 7 { 8 …… 9 operation.Invoke(); 10 …… 11 } 12 }
二. 标志位模型
首先让我们来提出问题。在软件开发过程中,我们常常需要在更改了一个数据之后完成一定的回调逻辑,例如在更改了控件的大小后需要刷新控件的外观。这些可能导致外观刷新的更改可能有很多:修改了控件的背景色,修改了控件的大小,修改了控件的Padding等等。那这么多更改所产生的回调将导致该回调逻辑被重复调用。这些调用之中,实际上只有最后一次调用才是真实有效的。
理想状况下,对该函数的调用只需要让该回调函数调用一次,而且调用的时间处于最后一个数据修改完毕后进行即可。
在我们的日常编程中,最简单而且最常用的做法就是添加一个标志位,记录当前是否需要对某个功能进行调用,如果是,那么调用回调逻辑,以完成对代码的更新。
1 public void switchSize(int width, int height, Brush background) { 2 bool needRefersh = false; 3 if (this.Width != width) 4 { 5 this.Width = width; 6 needRefersh = true; 7 } 8 9 if (this.Height != height) 10 { 11 this.Height = height; 12 needRefersh = true; 13 } 14 15 if (this.Background != background) 16 { 17 this.Background = background; 18 needRefersh = true; 19 } 20 21 if (needRefersh) 22 { 23 refreshVisual(); 24 } 25 }
三. BeginInvoke模型
这样做的确解决了问题,但并不是一个很好的解决方案:首先,对needRefresh标志位的设置需要在每次更改相关属性的时候就被单独地设置。另外一个问题则是在switchSize()之外的对这些属性的更改同样需要执行refreshVisual()回调函数。而这并不是switchSize()函数之内可以判断出来的。如果说,我们能找到一种解决方案自动地将标志设置完毕,并且在其后让WPF自动调用一个函数,以令其根据之前所设置的标志自动完成对功能的调用,那么一切问题就解决了。
3.1 标志的自动设置
对于第一步,我们能想到的就是依赖项属性的回调函数。由于对特定依赖项属性的更改实际上都会导致其所对应的回调函数被执行,如对Background属性的更改会请求界面重绘,因此我们完全可以利用属性回调函数这一功能将Background属性的更改与特定的执行逻辑关联起来。首先,我们可以通过OverrideMetadata()函数将一个函数注册为与Background属性的PropertyChangedCallback回调,并在其中设置标志位:
1 public static MainWindow() 2 { 3 Control.BackgroundProperty.OverrideMetadata(typeof(MainWindow), 4 new FrameworkPropertyMetadata(new PropertyChangedCallback(BackgroundChanged))); 5 }
在这种情况下,软件开发人员就可以直接设置依赖项属性,而不必关心对标志位的设置了:
1 public void switchSize(int width, int height, Brush background) { 2 if (this.Width != width) 3 { 4 this.Width = width; 5 } 6 7 if (this.Height != height) 8 { 9 this.Height = height; 10 } 11 12 if (this.Background != background) 13 { 14 this.Background = background; 15 } 16 …… 17 }
第一个问题还有另一种解决方案,那就是扩展元数据。而且这种方法就是WPF内部所使用的方法。在定义或重写一个依赖项属性的时候,WPF会为依赖项属性的元数据指定一系列标志位,如AffectsMeasure等。接下来,用户可以重写DependencyObject的OnPropertyChanged()函数。该重写的逻辑可以包含对元数据标志位的检测,并在探测到依赖项属性的元数据中包含特定标志位时执行特定的逻辑。就以FrameworkElement类对OnPropertyChanged()函数的重写为例:
1 protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) 2 { 3 DependencyProperty dp = e.Property; 4 base.OnPropertyChanged(e); 5 …… 6 FrameworkPropertyMetadata metadata = e.Metadata as FrameworkPropertyMetadata; 7 if (metadata != null) 8 { 9 …… 10 if (metadata.AffectsMeasure) 11 { 12 base.InvalidateMeasure(); 13 } 14 if (metadata.AffectsArrange) 15 { 16 base.InvalidateArrange(); 17 } 18 …… 19 } 20 }
上面的代码是FrameworkElement类中OnPropertyChanged()函数的略缩版。在该略缩版的函数定义中,我们可以看到WPF内部是如何对标志位进行使用的:在属性值发生变化时,WPF将首先探测当前元数据是否拥有特定标志位。如果有,那么WPF系统再对相应的执行逻辑进行调用。
相较于使用属性更改回调,使用标志位的确具有一些优点:标志位可以被多个依赖项属性重用,并且可以以位操作的方式将多个标志位合并到一起,从而使一个依赖项属性可以支持多个功能。而支持各个标志位的类型就可以将所有的对该标志位进行支持的代码单独地进行处理,就如上面代码所示的对AffectsMeasure和AffectsArrange标志位的支持。这样组织的代码更便于维护。但反过来,如果一个需要使用该模型的功能实现仅仅是局限于较小范围内的特定逻辑,那么使用属性更改回调则是一种更好的方法。
那么如何对元数据进行扩展呢?最直观的方法就是从FrameworkPropertyMetadata类派生,并添加自定义标志位的支持。在支持该标志位的类型中,软件开发人员通过重写OnPropertyChanged()函数来侦听依赖项属性的更改,并在该重写中探测依赖项属性的元数据是否是特定的FrameworkPropertyMetadata类的派生类。如果是,那么再探测特定标志位是否被设置。如果仍然是,那么对该标志位所对应功能的处理将被启用。
3.2 异步调用
OK。现在我们已经能让对属性的设置直接触发属性更改逻辑了。那么下一步要做的事情就是能让多个属性更改只触发一次处理逻辑,并在所有属性设置完毕后再执行。让我们回忆一下在本文开始时所介绍的BeginInvoke()函数的执行流程:
- 在当前消息处理过程中注册一个工作项,并向Windows系统发送一个自定义消息。
- 当前消息处理完毕以后,Win32自定义消息将被处理,从而使注册的工作项得以执行。
可以说,这就是我们所需要的在所有属性设置完毕后执行的特性。但是如何把这些请求合并到一起呢?对该功能的实现同样非常简单:在每次插入工作项之前都检查一遍是否已经拥有特定工作项,如果是,那么该工作项将不再被插入。InvalidateMeasure()函数实际上就是一个很好的例子。该函数最终会调用ContextLayoutManager的NeedsRecalc()函数。而NeedsRecalc()函数函数的执行逻辑大致如下所示:
1 private void NeedsRecalc() 2 { 3 if (!this._layoutRequestPosted && !this._isUpdating) 4 { 5 MediaContext.From(base.Dispatcher).BeginInvokeOnRender(_updateCallback, this); 6 this._layoutRequestPosted = true; 7 } 8 }
从上面所示的函数实现中可以看到,WPF内部同样使用标志位_layoutRequestPosted记录当前是否已经有一个请求被发出。如果没有,那么WPF将调用BeginInvokeOnRender()函数,并在该函数内部通过调用BeginInvoke()函数向WPF属性系统添加一个消息。
四. BeginInvoke模式示例
在这里,我们给出了一个最简单的BeginInvoke模式的示例。该示例的全部代码如下:
1 // 自定义元数据类型会使用的标志位 2 public enum SampleMetadataOptions 3 {
None, 4 SampleOption 5 } 6 7 // 自定义元数据。记录在该元数据中的各个标志位可以用来表示各个依赖项属性在更改时所需要 8 // 触发的执行逻辑 9 public class SampleMetadata : FrameworkPropertyMetadata 10 { 11 SampleMetadataOptions _options; 12 13 // 简化版构造函数。请根据自身需求创建构造函数 14 public SampleMetadata(Object defaultValue, SampleMetadataOptions options) 15 : base(defaultValue) 16 { 17 _options = options; 18 } 19 20 public SampleMetadataOptions Options 21 { 22 get { return _options; } 23 } 24 } 25 26 public delegate void SampleOptionChangedDelegate(); 27 28 public class SampleObject : FrameworkElement 29 { 30 private bool _sampleStatusDirty = false; 31 32 static SampleObject() 33 { 34 // 重写Width和Height依赖项属性。为它们添加SampleOption标志位,以在其发生更改 35 // 时触发相应的刷新逻辑 36 FrameworkElement.WidthProperty.OverrideMetadata(typeof(SampleObject), new SampleMetadata(10.0, SampleMetadataOptions.SampleOption)); 37 FrameworkElement.HeightProperty.OverrideMetadata(typeof(SampleObject), new SampleMetadata(10.0, SampleMetadataOptions.SampleOption)); 38 } 39 40 protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e) 41 { 42 base.OnPropertyChanged(e); 43 44 Console.WriteLine("Property changed: " + e.Property.Name); 45 46 SampleMetadata metadata = e.Property.GetMetadata(typeof(SampleObject)) as SampleMetadata; 47 if (metadata != null) 48 { 49 // 检测当前依赖项属性是否拥有SampleOption标志,并且当前是否已经请求过刷新 50 if (metadata.Options.HasFlag(SampleMetadataOptions.SampleOption) && !_sampleStatusDirty) 51 { 52 _sampleStatusDirty = true; 53 this.Dispatcher.BeginInvoke(new SampleOptionChangedDelegate(ProcessSampleStatus)); 54 } 55 } 56 } 57 58 protected void ProcessSampleStatus() 59 { 60 _sampleStatusDirty = false; 61 Console.WriteLine("Sample status has been processed!"); 62 } 63 }
在任意WPF中创建SampleObject类型实例,并设置它的Width和Height属性就能看到效果:
1 private SampleObject _object = new SampleObject(); 2 3 public MainWindow() 4 { 5 InitializeComponent(); 6 7 _object.Width = 100; 8 _object.Height = 100; 9 }
在运行时,Visual Studio中的控制台会依次打印出三条消息:
Property changed: Width
Property changed: Height
Sample status has been processed!
可以看出,该模型满足了我们的要求:所有的属性更改只会触发一次相应逻辑的执行,同时该逻辑的执行会在所有属性设置之后进行。
转载请注明原文地址:http://www.cnblogs.com/loveis715/archive/2013/01/03/2842636.html
商业转载请事先与我联系:silverfox715@sina.com,我只会要求添加作者名称以及博客首页链接。
您可以通过微博及时得到博客更新信息:http://weibo.com/u/1463936917