Access 开发框架(翻译+改编)系列之八——从属对象

框架的最终目的,是提供一个明确定义的地方,将对象的属性和行为可以放置在这里,用于将来的项目开发。控件是一个主要的例子,因为它们没有内建类(built-in class),而我们想扩展它们的功能。于是,我们为每个控件类型创建了类,这样我们就能扩展这个控件的功能。

为了这个目的,在本系列中,我们将为已创建的组合框控件类添加一些新的功能。这个新功能就是从属对象重新查询(dependent object re-query)

从属对象重新查询的意思就是,我们某个类对象创建一个方法,当该类对象的值有变化时,任何从属于该类对象的对象的值,都会执行重新查询,获得更新。在本系列中,这个类对象就是组合框控件类。当用户在组合框控件中选择了一个新的值,任何从属于这个组合框控件的对象会自动的重新查询,基于组合框控件的值,更新自己的筛选数据。

这样的行为通常是非常有用的,同时也能向我们展示,如果我们想让框架做更多的事情的话,我们应该如何在其中添加新的事物,从而扩展我们的框架。

从属对象

从属对象在Access应用程序中是很普遍的。举个例子,有一个窗体,上面有2个组合框,名字分别为cboCompany和cboEmployees。当用户选择cboCompany的值后,cboEmployees需要重新查询自己的可选数据,只显示公司为组合框控件cboCompany中的值的员工数据。

从属对象的功能在通常情况下是怎样实现的呢?一般是为从属对象创建一个查询,这个查询为该从属对象提供数据,在该查询中,有一个字段,该字段引用了被从属的对象的值。换句话说,cboEmployees的“row source”属性(译者注:我用的英文版本Access,该属性对应的中文名称可能是“行来源”)中的查询会设置类似于“Like form!frmMyForm!cboCompany”这样的筛选条件。控件cboEmployees是从属于(依赖于)控件cboCompany的。仅仅这样做好设置以后,当你修改cboCompany的值以后,你会发现cboEmployee的值并未立即发生更新,你必须手动刷新(或通过代码)窗体,从属的组合框控件才会更新。

为了在我们的框架中实现这种功能。我们可以简单的直接在控件类中添加一个从属对象集,然后为每一个控件类添加方法,以便可被添加到这个集合中,当控件类对象销毁之前,将其从集合中移除。当然我们也会添加一个重新查询的方法,遍历集合中的所有对象,调用每个对象的重新查询方法。注意,这隐含着一个意思,每个可从属于其他控件的控件必须有一个公用的重新查询方法。

本章相关代码示例文件8.1 从属对象

从属对象代码

从属对象的代码大概是这个样子的:

在每个控件类的开始部分(为了方便阅读,剥离了所有的错误处理代码)

在每个控件类初始化事件中,将集合对象实例化。

在每个控件类的Term()方法中写入清除代码

最后,公用函数或方法来处理从属对象的相关事情:

用这种方法,当然,不会有任何问题。但是,如果我们从类的角度,来思考这个问题的话,我们完全有更好的方案。我们可以创建一个从属对象类,将相关的代码都放置其中,然后我们只需要在每个控件类中添加一个从属类对象。

使用一个专用的类来处理这个问题的最大的好处,就是我们可以把所有类似的代码都放在一个地方——类中,然后在我们需要的地方,定义一个clsDepObj对象,然后调用类的方法。我们不再需要在每个控件中都重复去维护那一堆相似的代码。

每个控件类代码

创建了从属对象类clsDepObj,在控件类中,我们不再需要上述代码了,我们只需要这些:

在每个控件类的开始部分(为了方便阅读,剥离了所有的错误处理代码)

在每个控件类初始化事件中,将从属类对象实例化。

在每个控件类的Term()方法中写入清除代码

然后创建一个属性来获取从属类对象的指针(句柄/引用)

你不得不承认,相比将整个从属类的代码都添加在到每一个控件类中来说,在每个控件类中只用添加这些行,要来的更加清晰和容易。更不必说,如果我们想添加一些其他的从属对象功能到框架中来的话,我们只需要在一个地方添加即可,而不必在每个控件类中都做一遍。

有一件事情我们必须要记住,在每个包含从属对象的控件类中,要添加一个重新查询的方法。显而易见,如果这个控件类有一个数据源,那么我们需要重新查询这个控件,也需要调用从属对象类的重新查询方法。

最后,当组合框的AfterUpdate(更新后)事件被触发后,我们想重新查询新类的从属对象集合中所有的对象。我们通过调用mclsDepObj的重新查询方法就可以了。WithEvents!你将会爱上它。

现在,我们有了这个新的类和它的功能,我们怎样告诉框架来使用它呢?答案就在窗体类的初始化的地方——窗体的Open事件。

窗体的内建类

我修改了frmPeople11窗体,添加了2个组合框控件在窗体的头部,cboCompany和cboEmployee,分别包含公司和和这些公司的员工。窗体的代码大概是这样子的:

我们的自定义窗体类dclsFrm

以及窗体的打开事件

我知道这看起来非常复杂,但是我们可以一步一步来看,我会告诉你每一步是在干什么。Set语句和Init语句跟以前是一样的。我们将指向窗体内建类的指针(Me)和指向窗体本身的指针(Me)传递给fdclsFrm对象。非常凑巧的是,对窗体来讲,Me既代表了窗体内建类,也代表了物理窗体,所以出现2个Me,看起来有点奇怪。

接下来,我们用一个With语句来加快我们获取dclsFrm对象内部内容的速度

dclsFrm有一个Children属性,可以返回类的集合指针。有了这个,我们现在可以引用集合的属性和方法。我们使用.Item()方法从集合中返回一些东西。

还记得吗?在dclsFrm类中,我们有一个控件扫描方法,可以发现窗体上的所有控件,然后为每个控件创建一个对象,然后将该对象的指针保存在集合colChildren中。还记得我们使用什么作为主键吗?是的,我们使用了控件的名字。这样一来,.Item(“cboCompany”)告诉类对象在集合colChildren中返回名叫“cboCompany”的对象,换句话说,就是控件cboCompany的控件类对象。

这样的话,.Item(“CboCompany”)就是指向组合框类对象的指针,与下述语句是一样的:

既然我们有了指向组合框控件类对象的指针,我们就可以调用这个类对象的任何方法或属性了,这就有了下面的语句:

代码的意思就是,对组合框控件cboCompany的控件类对象来讲,调用它的从属类方法clsDepObj,返回指向它的从属类对象的指针,然后调用从属类对象的Add方法,准备添加一个从属类对象。

记住,句点符号指示那个类的属性或方法,这样一层一层的引用,有点像魔术师的盒子,大盒子放着中盒子,中盒子中放着小盒子,一直到我们最终要引用的对象。

fdclsFrm.Children.Item(“class name”).clsDepObj.Add

顺便说一下,我们可以在这行上面设置一个断点,然后单步调试这段代码,观察各种类的方法的调用情况,返回的指针指向下一个对象,然后调用下一个对象的方法,返回下下一个对象等,一直到最后,我们钻入到最后的Add方法。

这非常有趣!

好了,让我们回到主题。

我们已经告诉dclsFrm要添加一些东西到clsDepObj。“一些东西”就是组合框控件cboEmployee的类对象。

类似的,我让窗体本身也从属与组合框控件cboCompany,所以我们也需要告诉cboCompany,dclsFrm也是一个从属对象。

最后,我们还需要重新查询cboCompany,以便它能重新查询它的从属对象。

引用窗体的Children集合对象,找到为cboCompany创建的对象,适用其clsDepObj的方法获取从属对象的引用,使用从属对象的Add方法,添加一个从属对象到从属对象集中。被添加的对象是cboEmployee的类对象和窗体本身,fdclsFrm。一旦我们将这两个项目添加到从属对象集中,重新查询整个结构。

我非常清楚,当你第一看到这些东西的时候,你肯定是眼花缭乱的。但是,请相信我,一旦你使用了类,方法,属性和集合一段时间后,这会变成你的第二天性。

总结

通过添加一个新的,只有一个集合变量,和几个公用方法的小的类,我们创建了一个操控从属对象的方法。一旦类创建好,每个需要从属对象功能的控件类,只需要添加几行代码就可以了。很有可能,最大的难点,是在窗体打开事件中,需要找到正确的控件,引用到它的从属对象,然后添加该控件的从属对象。这两个从属对象分别是cboEmployee和窗体本身。

最后的结果就是,当窗体被打开以后,没有任何记录显示出来,因为公司还没有选择,而窗体的绑定数据源中,公司作为了筛选条件。一旦我们选择了一个公司,公司组合框控件就有了值,这个值就可以用来筛选窗体和组合框控件cboEmployee。因为cboCompany会调用它的从属对象clsDepObj的重新查询方法Requery,窗体的类会重新查询窗体本身,cboEmployee的控件类会重新查询cboEmployee。顺便说一下,如果cboEmployee也被编码了,拥有自己的从属对象,cboEmployee的类对象也会重新查询它所有的从属对象,整个过程,就想波纹一样,自动的一环套一环的执行下去。

类是非常强大的工具,允许我们将功能封装在一个地方,将所有的实现必要的行为相关的变量和代码封装在一个位置,如果有必要的话,我们只需要到这个地方修改或添加新的功能。你可以将这个独立的类放到你自己的框架中,只需要相对少的功夫,就能为你自己的组合框和其他控件添加从属对象功能。从属对象处理是非常小而琐碎的功能,但是想象一下,如果类拥有十几个属性和方法会有会怎样?封装性和可移植行成为了学习使用类的首要原因。

译者注:本章介绍的从属对象非常不好理解,要搞清楚三点,一是,谁是从属对象(cboEmployee,fdclsFrm),谁是被从属对象(cboComanpy),被从属对象的变化会导致从属对象的变化。第二点是要知道如何设置窗体的绑定数据,以及组合框控件的绑定数据源,当然Access比较基础的知识。最后一点是,要了解,如果没有本章介绍的从属对象,那些在绑定的数据源中引用了被从属控件值的窗体,在被从属控件值更新后,会自动获得更新吗?答案当然是否定的。

最后,我做了这个示例文件8.1 从属对象,在该示例文件中,有3个窗体:frmPeople10,frmPeople11,frmPeople12,分别演示了未应用任何从属对象类的情况,为cboCompany添加了从属对象,为cboCompany和cboEmployee都添加了从属对象的情况。读者可以分别测试一下,看看效果差别在哪里。

后记:翻译到此章的时候,一直萦绕在我脑中的一个疑问有了一个答案。还是那个比较恼人的对象卸载问题。按照Colby的类模板的要求,对象在卸载自己的时候,首先卸载自己colChildren中的子对象,卸载子对象前,先执行子对象的Term()方法。问题来了:很显然,本章介绍的clsDepObj中的集合mcolDepObj中的对象,肯定都不是由其父对象dclsCtlComboBox所实例化,当卸载clsDepObj的时候,也需要执行它的集合中的子对象的Term()方法吗?答案当然是否定的。那么什么时候执行子对象的Term()方法?什么时候不用执行呢?

大家可以注意到,我在类模板的最后,添加了下面一个私有方法。什么时候执行Term(),由子对象的父对象是否是当前对象来决定。因为每个对象的父对象只有一个,就是实例化自己的对象。

相应的,重写后的每个类中,都有类似的代码。这样就解决了,什么时候该释放类,什么时候只是清除其指针的问题。

Colby自始至终都未曾论述过这个问题,相反,他创建了一个叫做实例堆的类clsInstanceStack(后续系列中会谈到),这个类用通俗的话来讲,就是对象的生死簿。主要用来做故障查询和除错用的。个人觉得,如果不把问题的根源找到,而只是在出现问题的时候,再来调试查错,还是于事无补的。

对象由谁实例化的,谁就是这个对象的父对象,这个对象的销毁也必须由其父对象来完成,其他的类对象,如果在其子集中引用了该类对象,那么只能在其子集中清除引用指针就好了。总结成一句话,谁创造的,谁才能毁灭

《Access 开发框架(翻译+改编)系列之八——从属对象》有9个想法

  1. 群主你好,
    如你所说,access报表的自带示例 或 书籍\网络的应用示例,其功能和界面都太过简单.
    1:无从顺藤摸瓜,了解掌握报表有什么属性,方法,事件及其组合应用(窗体则有充分的资料和示例,窗体本身也更直观);
    2:甚至不能知道,access报表能否接受非本地/本文件的数据源(服务器端的recordset,群主能否介绍一些资料?);
    3:access报表即使能接受非本地的,复杂的数据源,又如何输出呈现为:
    a:类似于excel透视表的样式,数个单元格/方框可以合并居中;
    b:可以对任意一个方框内的全部/部分字符串,任意设定其:字体/大小/样色/背景色;
    c:更进一步,可以插入总结文字\图片\附件链接等,外加右键弹窗;

    上述第3点的功能需求,几百万的erp未必能有,但也不超过excel的vba功能.access是”在其擅长的领域能做到最好”的,上述功能需求,就应该可以用access报表直接实现,而不是借力与excel 或 word.

    1. 你对access报表功能的想象有点局限于Excel的经验了,既然你希望实现像Excel那样的报表功能,为什么不直接使用Excel呢?Excel现在有一组Power BI免费插件:Power Query(解决数据源问题) / Power Pivot(解决数据之间关联问题,或者称之为‘数据模型’,其实就是Access中的‘表之间关系’) / Power View(解决数据呈现问题) / Power Map(解决数据地图问题)应该更符合你的需求。
      Access适合解决一些小规模的多用户事务处理问题,类似于’ERP系统’,当然因为它有数据库引擎的原因,也适合用来做中等规模的数据加工整理分析工作。纯粹的基于某个数据源的报表,可以使用Power BI或者Tableau。如果一定要使用Access,多半是因为看重了它的自动化功能。

    1. 高度复杂的报表?描述得不够具体,好难回答啊。不过以我最大的想象力来看,如果Access自带的报表模块不能满足你的复杂性的话,你还可以将数据写入到Excel和Word中,运用Excel和Word的特有功能,满足你高度复杂的报表需求。
      关于Access中控制Excel,以前做市场数据分析预测的时候,开发过很多类似应用,比如数据源是某个大型数据库,Oracle/Teradata等,使用ODBC将Access链接到数据仓库后,写一些SQL语句抓取其中的数据保存到本地Access中,然后运行一些计算逻辑,商业规则等,最后将结果输出到Excel模板中的报表数据区,再利用Excel自带的公式图表功能,形成最终的报表。财务数据分析中也能找到很多类似的应用场景。
      关于Access中控制Word,现在在人事部门有很多此类应用。比如合同自动化,Offer letter自动化等。很多合同条款针对不同的城市,不同的岗位,不同的部门,都有不同的表述。既可以在Access中判断这些逻辑,然后写入到Word模板,也可以利用Word自带的域(英文叫Field,有点类似Excel中的公式)来进行自动化。

      1. 谢谢指导!
        如您所说,access自带报表 或者一般参考书籍\网络的示例报表,都太过简单:
        1:不能借此获知报表到底有哪些对象,属性,方法和事件,(窗体则有充分的资料和示例);
        2:甚至不知道access报表能否接受:非本地的数据源(服务器端的recordset);
        3:access报表如\能接受非本地的,复杂的,数据源,能否至少将其输出呈现为excel透视表的格式,且在任意一个方框(同一长字符串)内部,可以任意设定字体大小-颜色-下划线-背景色,进而有右键弹窗,回复保存等功能;

        —-以上功能,几百万定制erp报表未必会有,但也不超过excel的vba功能. 既然是access能在”开其擅长领域能做到最好”.我想用access直接实现以上报表功能和需求(而非excel或word).

发表评论

电子邮件地址不会被公开。