在WPF中,DataSourceProvider是一个较为特别的类型。其提供了对复杂数据获取逻辑的支持。例如该类的派生类XmlDataProvider提供了对XML数据进行分析的支持,而ObjectDataProvider则允许软件开发人员调用创建数据实例的函数。
也正是由于这些类型的使用较为生僻,因此许多软件开发人员并不能很好地使用它们。实际上,DataSourceProvider是一个非常强大的数据源框架,允许您通过继承的方式为不同的数据源,如SQL Server,提供绑定支持。
在下面的介绍中,本文主要分析DataSourceProvider作为数据源框架所提供的功能以及如何在各个扩展点中使用这些功能,并通过示例展示了这些功能的使用。
一.DataSourceProvider
为了更好地在派生类中使用基类DataSourceProvider所提供的功能,您有必要首先了解一下该类型所提供的绑定源框架。
DataSourceProvider类所提供的最重要的扩展点就是BeginQuery()函数。
BeginQuery()函数的语义特征非常明显:执行对数据的查询,以完成对参与绑定的数据源的刷新。例如XmlDataProvider.BeginQuery()的定义如下:
1 protected override void BeginQuery() 2 { 3 if (this._source != null) 4 { // 放弃内嵌XML的设置而加载Source属性所指定的XML 5 this.DiscardInline(); 6 this.LoadFromSource(); 7 } 8 else 9 { 10 XmlDocument state = null; 11 if (this._domSetDocument != null) 12 { // 放弃内嵌XML的设置而加载Document属性所指定的XML 13 this.DiscardInline(); 14 state = this._domSetDocument; 15 } 16 else 17 { 18 if (this._inEndInit) 19 return; 20 state = this._savedDocument; 21 } 22 // 创建XML树状结构 23 if (this.IsAsynchronous && (state != null)) 24 ThreadPool.QueueUserWorkItem(new 25 WaitCallback(this.BuildNodeCollectionAsynch), state); 26 else if ((state != null) || (base.Data != null)) 27 this.BuildNodeCollection(state); 28 } 29 }
这段代码展示了XmlDataProvider是如何执行对XML文档的解析:首先考虑Source属性所指定的XML文件,接下来则是Document属性所表示的XML,而最后则是内嵌的XML。当然,XmlDataProvider的XML文档解析顺序并不属于本文的范畴,因此在这里我们跳过这个话题。
那么什么时候BeginQuery()函数会被调用呢?答案是Refresh()函数。该函数是一个公有函数,以允许被用户代码显式调用。同时在DataSourceProvider的各个派生类实现中,如果对一个属性的更改会导致DataSourceProvider所生成的数据发生变化,那么Refresh()函数也应被调用。例如XmlDataProvider的Document属性就在发生更改时调用了Refresh()函数:
1 public XmlDocument Document 2 { 3 get 4 { 5 return this._document; 6 } 7 set 8 { 9 if (((this._domSetDocument == null) || (this._source != null)) 10 || (this._document != value)) 11 { 12 this._domSetDocument = value; 13 this._source = null; 14 this.OnPropertyChanged(new PropertyChangedEventArgs("Source")); 15 this.ChangeDocument(value); 16 if (!base.IsRefreshDeferred) 17 base.Refresh(); 18 } 19 } 20 }
接下来看看该类型所实现的接口。DataSourceProvider实现了INotifyPropertyChanged以及ISupportInitialize接口。接口INotifyPropertyChanged用来提供对绑定的支持。对该接口的详细介绍请见一文,这里不再赘述。而ISupportInitialize接口则用来完成对批量初始化的支持:在加载XAML的时候,程序将创建XAML元素所对应的类型实例。如果该类型实现了ISupportInitialize接口,那么在设置该实例的各个属性值之前,BeginInit()函数将被调用;在完成了对所有属性的设置之后,EndInit()函数将被调用。通过这种方法可以解决的一个问题是:对于某些具有相互关联关系的属性,对它们的使用可以置于EndInit()函数之中,即对属性进行设置这一步骤之后,以保证执行逻辑在执行时所需属性均已设置完毕。
就以展开后的XmlDataProvider.EndInit()函数为例:
1 protected virtual void EndInit() 2 { 3 _deferLevel--; 4 if (_deferLevel == 0) 5 { 6 _initialLoadCalled = true; 7 BeginQuery(); 8 } 9 }
其中BeginQuery()函数负责对数据的初始化及刷新。从前面的讲解中已经看到,在BeginQuery()函数中,XmlDataProvider会根据Source,Document以及内嵌XML属性的设置决定需要分析的XML文档。由于这三个属性具有一定的优先级,因此对一个属性的设置可能导致令一个属性设置失效。在属性没有完全设置完毕之前,我们并不能决定到底哪个属性最终会作为数据源使用。因此对BeginQuery()函数进行调用的合适时间点并不是对这三个属性进行设置的时候,而是在EndInit()函数中。
OK,到这里您应该清楚DataSourceProvider实现ISupportInitialize接口的原因了:为了处理数据源所可能具有的属性关联情况,以将对数据的求解置于所有相互关联属性被设置完毕之后。在通过继承DataSourceProvider实现数据源时,EndInit()常常是需要重写的一个成员函数。
DataSourceProvider将BeginInit()以及EndInit()函数实现为虚函数,因此它们是DataSourceProvider所提供的扩展点。
另外,DataSourceProvider内部还完成了线程相关性的支持。这是因为某些数据源可能具有与WPF不同的线程管理策略,如SQL Server。DataSourceProvider使用下面的方式兼容这些线程管理策略:DataSourceProvider类会认为创建它的线程是UI线程,并记录在保护成员Dispatcher中。在一个函数被调用时,如果当前线程并不是该创建线程,那么DataSourceProvider会通过Dispatcher.BeginInvoke()函数插入一个异步调用。这种记录创建实例的线程并用BeginInvoke()将需要执行逻辑发送到相应Dispatcher的方法是.net框架中较为常用的多线程处理方法。同时DataSourceProvider的一些成员函数也通过这种方法提供了对异步进行支持的重载,如OnQueryFinished()函数。这些函数常常是在工作线程中调用的。其内部同样通过记录创建实例的线程并调用BeginInvoke()的方法完成了线程相关性的支持。
除此之外,IsInitialLoadEnabled属性则是另一个较为重要的属性。该属性控制着是否需要在设置影响数据结果的属性时就对其进行查询。该属性的值会对程序的启动性能有较大的影响。例如XmlDataProvider的Document属性就提供了对IsInitialLoadEnabled属性的支持:
1 public XmlDocument Document 2 { 3 …… 4 set 5 { 6 if (((this._domSetDocument == null) || (this._source != null)) 7 || (this._document != value)) 8 { 9 this._domSetDocument = value; 10 this._source = null; 11 this.ChangeDocument(value); 12 if (!base.IsRefreshDeferred) // 内部使用IsInitialLoadEnabled 13 { 14 base.Refresh(); 15 } 16 } 17 } 18 }
另外,DataSourceProvider的派生类XmlDataProvider及ObjectDataProvider所同时提供的IsAsynchronous则是不得不提的一个属性。该属性提供了对异步的支持。对于未定规模的运算而言,IsAsynchronous属性的确是一种非常务实的考虑:XmlDataProvider所使用的Xml以及ObjectDataProvider所调用的函数都是不可控制规模的用户组成。对这些组成的处理可能非常耗时,如Xml非常大或函数的执行需要非常长时间的情况。
而在WPF源码中,对IsAsynchronous属性的支持仅仅是通过ThreadPool.QueueUserWorkItem()函数向线程池插入工作项来完成的。例如XmlDataProvider的ParseInline()函数就使用了这种方法:
1 private void ParseInline(XmlReader xmlReader) 2 { 3 if (((this._source == null) && (this._domSetDocument == null)) && this._tryInlineDoc) 4 { 5 if (this.IsAsynchronous) 6 { 7 this._waitForInlineDoc = new ManualResetEvent(false); 8 ThreadPool.QueueUserWorkItem(new 9 WaitCallback(this.CreateDocFromInlineXmlAsync), xmlReader); 10 } 11 else 12 this.CreateDocFromInlineXml(xmlReader); 13 } 14 }
现在让我们查看一下DataSourceProvider的启动流程和数据刷新流程。在程序启动时,XAML中的DataSourceProvider元素将被分析,从而导致BeginInit()以及EndInit()函数的执行。同时绑定或与之关联的CollectionView会探测是否IsInitialLoadEnabled属性设置为true,并在该属性为true的情况下执行对数据的请求。如果程序调用了Refresh()函数,那么BeginQuery()函数将最终执行,以请求对数据的刷新。
在数据计算执行完毕后,OnQueryFinished()函数将被调用。该函数的内部实现首先判断当前线程是否为DataSourceProvider实例的创建线程。如果是,那么表示请求任务完成的回调函数将被同步调用,并通过更改Data属性以更新使用DataSourceProvider作为绑定源的绑定。如果当前线程不是DataSourceProvider实例的创建线程,那么回调将通过Dispatcher.BeginInvoke()函数将回调执行逻辑分派到创建线程。该函数的调用也常常是由派生类完成的。
好了,我们现在已经做了足够多的准备工作。下面我们就将开始尝试创建自定义DataSourceProvider。
二.自定义DataSourceProvider
在本节中,我们将以一个用于读取RSS内容的RssSourceProvider为例讲解如何创建一个自定义DataSourceProvider。
在从一个基类派生实现自定义类型的时候,我们常常需要关注两方面内容:派生类对各个扩展点的使用流程是怎样的?它的其它派生类是如何实现的?通过对扩展点使用流程的思考,我们可以更清晰地写出派生类的实现逻辑。而通过对其它派生类的研究,我们可以借鉴其它派生类中的使用方法。创建DataSourceProvider的派生类也是如此。
首先要考虑的问题就是读取RSS的DataSourceProvider如何进行初始化。DataSourceProvider的初始化与初始化相关的是BeginInit()函数以及EndInit()函数。查看这两个函数在DataSourceProvider以及它的派生类中的实现可以得知,它们主要通过计数方式控制对数据的刷新。在计数器减为0时,对数据的刷新将被启动。使用该计数的函数有BeginInit()函数、EndInit()函数以及DeferRefresh()函数。对于RssSourceProvider而言,该默认实现已经能够满足需求。如果需要在派生类中修改该运行逻辑,那么软件开发人员应记得调用基类所提供的BeginInit()函数以及EndInit()函数,以维护内部计数;如果在函数重载中没有调用基类所提供的BeginInit()函数以及EndInit()函数,那么我们同时需要提供对DeferRefresh()函数的重写。WPF源码中,XmlDataProvider以及ObjectDataProvider都对引用计数的维护方式进行了很好的展示。
接下来要考虑的则是对BeginQuery()的重写。该函数中应添加实际的RSS读取逻辑:
1 protected override void BeginQuery() 2 { 3 if (IsAsynchronous) 4 ThreadPool.QueueUserWorkItem(new WaitCallback(QueryWorker), mRssUri); 5 else 6 QueryWorker(mRssUri); 7 }
该函数的实现会根据属性IsAsynchronous的值决定是否需要以异步方式调用实际执行逻辑QueryWorker()函数以获取地址mRssUri所指向的RSS项。该函数的执行逻辑定义如下:
1 private void QueryWorker(object obj) 2 { 3 string rssUri = obj as string; 4 if (string.IsNullOrEmpty(rssUri)) 5 return; 6 7 ListrssItems = new List (); 8 try 9 { 10 XmlReader reader = XmlReader.Create(rssUri); 11 SyndicationFeed feed = SyndicationFeed.Load(reader); 12 13 foreach (SyndicationItem item in feed.Items) 14 { 15 string link = string.Empty; 16 if (item.Links.Count != 0) 17 link = item.Links[0].Uri.AbsoluteUri; 18 19 DateTime createTime = item.PublishDate.DateTime; 20 DateTime updateTime = item.LastUpdatedTime.DateTime < createTime ? 21 createTime : item.LastUpdatedTime.DateTime; 22 RssItem rssItem = new RssItem(createTime, updateTime, item.Title.Text, link); 23 rssItems.Add(rssItem); 24 } 25 OnQueryFinished(rssItems, null, null, null); 26 } 27 catch (Exception) { } 28 }
该函数主要通过SyndicationFeed类从指定RSS位置读取信息,并将需要使用的信息存储在自定义数据类型RssItem中。在所有RSS信息都处理完毕以后,OnQueryFinished()函数将被调用,以设置DataSourceProvider的Data属性。该函数内部会自动将函数调用转发为DataSourceProvider的创建线程上的调用。
三.How-Why-When
标题所列出的三个单词,是个人认为作为软件开发工程师所具有的必备素质。在工程开发过程中,我们常常需要参考一些资料后为特定功能提供相应实现,如如何实现自定义DataSourceProvider,这也便是How;而在实现该功能,即完成当天工作之后,我们就需要仔细地考虑别人为什么不采用与该解决方案相似的实现,如自定义Markup Extension,以更清晰地区分这些不同方案之间的优缺点以及各自适应的情况;最后在明确地知道了优缺点的情况下,我们就可以在下一次遇到问题时选择更合适的方法。
在前面的讲解中,我们已经知道了如何实现自定义的DataSourceProvider。现在需要和它进行比较的则是一个具有类似功能的解决方案:自定义XAML标记扩展。首先来看看自定义标记扩展是如何在XAML中使用的。XAML以一对花括号作为XAML标记扩展的开始及结束。在XAML编译器遇到XAML标记扩展时,该标记扩展所对应类型的ProvideValue()函数会被调用。在该函数中,软件开发人员可以做两种事情:返回需要构造的类型实例,或侦听某个事件,并在事件发生时再执行特定代码。XAML标记扩展直接返回构造的类型将无法允许对绑定功能的使用。相较而言,DataSourceProvider则使用其Data属性表示数据,并通过该属性提供了对绑定的支持。同时,XAML标记扩展所使用的通过侦听某个事件并在事件发生时对目标实例进行更改的方式并无法提供一个公有接口。相反地,DataSourceProvider则提供了Refresh()函数,以允许软件开发人员手动刷新数据。另外,DataSourceProvider还提供了内建的多线程支持。对多线程的支持允许软件开发人员在自定义DataSourceProvider中执行较为耗时的操作,如远程调用等。
源码地址:
转载请注明原文地址:
商业转载请事先与我联系: