为 Microsoft .NET Framework 精简版创建多窗体应用程序框架

摘要:学习如何为基于 .NET Framework 精简版的应用程序创建有效的用户界面引擎。

下载 FormStack.msi(英文)。

目录

简介

在进行绝大多数应用程序设计时,首先要考虑的设计问题之一是如何实现图形用户界面 (GUI) 导航,即如何向用户显示新的信息屏幕。在传统的 PC 环境中,应用程序只为每个信息屏幕创建一个新窗体(或对话框)。

在开发人员使用 Microsoft® eMbedded Visual Basic® (eVB) 之前,这种方法一直很有效。使用 Microsoft® eMbedded Visual Basic® (eVB) 后,如果应用程序中包含很多窗体,则很快会将宝贵的内存资源用尽;如果在所有窗体中都需要使用菜单,则最终不得不复制大段代码以保持行为的一致性。

为解决这一问题,许多 eVB 开发人员选择在单个窗体中使用多个框架。开发人员要使用 Microsoft® .NET Framework 精简版,应再次检查生成有效用户界面 (UI) 引擎(未与使用此引擎的应用程序紧密耦合)的方法。

限制资源的使用

使用现今的计算机,桌面应用程序开发人员几乎无须顾虑内存的使用情况。而智能设备开发人员则不得不考虑这个问题。要尽量减少内存的使用,方法之一是尽可能重复使用类,尤其是 Forms 类。

例如,让我们来看看可以在当地录像带出租商店找到的电影查询应用程序。我们假设此应用程序启动时将显示所有电影的列表。不可否认,这将是一个庞大的列表,而且是一个很糟糕的应用程序设计,但出于演示目的我们假设它是合理的。

现在,如果某位用户选择了一部电影的名称,应用程序将显示另一个窗体,其中显示这部电影的详细信息(包括演员)和另一个类似的推荐电影列表(“如果您喜欢这部电影,那么可能也会喜欢  . . . ”)。

图 1:屏幕流程图

在流程图中,我们假设用户既可以从新列表中选择电影,仅将有关这部电影的新信息重新填充到当前窗体中;也可以选择演员。如果选择演员,可以立即显示演员的信息,包括合演的明星以及另一个电影列表,这次将显示列出此演员出演的电影列表。

很显然,如果查看的每个窗体都是带有所有关联数据的新窗体实例,则应用程序将很快占用大量内存。而且,Windows CE 设备的处理器性能通常都赶不上台式计算机的性能,使用新数据加载每个窗体将生成一个缓慢、响应迟钝且用户不友好的应用程序。

堆栈和缓存

现在,我们将深入了解另一个对用户有益的功能,即查看上一个窗体,它与浏览器中的“后退”按钮功能类似。这就意味着,应用程序必须“记住”用户的位置。

在某些应用程序中实现此操作比较简单,因为窗体的“目标”和“起始”路径很有限,可通过硬编码来实现。对于我们的应用程序,您可以看到这些窗体显示给用户的顺序几乎是随机的(参见图 1)。

因此,我们现在需要了解基本要求。首先,我们希望尽量减少内存中保留的窗体数;其次,我们希望记录用户查看过的窗体以及查看顺序。事实上,我们可能还希望跟踪在每个窗体中查看的数据,但是完成架构后,它只是一个简单的新增部分,请读者自己练习。

那么,到底什么解决方案能够满足这些要求呢?要尽量减少窗体数,可以使用所谓的缓存。缓存只是一种存储机制,可用于保存已加载窗体的单个副本,并在可用时重复使用缓存的窗体。这就意味着当用户要查看窗体时,应首先检查它是否位于缓存中。如果不在缓存中,则按正常方式加载。但如果不是简单地显示缓存的窗体,而是要在内存中保存一个新副本,则还需要考虑实际创建窗体的时间。

可以使用堆栈跟踪已查看的窗体的历史记录。假设查看的每个屏幕都可以表示为一张卡片,而这些卡片可以插入到堆栈中。用户查看每个屏幕时,可以将卡片插入(压入)堆栈中。也就是说,位于顶部的卡片是最后查看的窗体;要查看历史记录,只需从堆栈中删除(或弹出)卡片即可。使用这种方法,应用程序可以“撤消”由用户执行的任意数量、任何顺序的浏览操作。

设计 FormStack

我们已经了解了需要满足的要求和基本方法,现在让我们看看如何实现它们。首先,让我们仔细考虑完成这项工作需要执行的操作。

我们将需要 FormStack,它可以作为类来实现。它需要一种压入和弹出窗体的机制。还需要一个用于存放实际窗体的缓存和一个堆栈,鉴于前面介绍的资源问题,缓存不应该保存窗体的实际副本,而应该只保存一个标识符。我们将窗体类的类型用做字符串。

加载窗体并非瞬间即可完成,尤其是当窗体包含许多控件时。必须创建窗体的所有控件,通常可能需要为列表和下拉列表提取数据。提取数据后,则需要填充控件。如果在应用程序进程的主线程中执行以上所有操作,则在加载窗体的过程中,用户必须等待很长时间。

为了减少等待时间,需要添加两个功能。首先,应该使用辅助线程提取所有需要的数据;然后,添加一个方法,允许应用程序在缓存中“预先加载”一个窗体。这就使应用程序能够在后台或在空闲时(例如启动画面过程中)加载“重型”窗体。

实现缓存和堆栈时还需要考虑其他一些问题,但是我们先定义所需的核心内容。由 FormStack 类完成的实际操作几乎都会在“压入”或“预先加载”过程中启动,因此,我们先看一下其中的逻辑。

图 2:执行压入

图 2 显示了“压入”的执行流程图。正如您看到的,我们只检查缓存是否存在,如果不存在,则调用“预先加载”;否则,只显示窗体并将其添加到堆栈中。

图 3:执行预先加载

尽管预先加载(参见图 3)执行的步骤略多一些,但还是比较直观。首先,必须创建 Form 类。在调用标准 InitializeComponent 方法创建窗体的所有控件之前,启动数据查询线程。

一个重要的步骤是,创建所有控件后,必须等待数据查询线程完成才能填充控件,因此需要处理一些基本的线程同步。

填充控件后,最后一步是将控件添加到缓存中。

StackForm 类

FormStack 的实际代码实现与这些流程图基本相同。首先,我创建了所有窗体必须继承的 StackForm 基类。这样,即使窗体不使用基本架构,也可以确保其存在(例如数据线程和控件填充方法)。

StackForm 执行以下任务:

  • 添加一个 EventHandler 来截取窗体中的 Closing 事件,这样当窗体关闭而不是实际卸载时可以调用“弹出”。
  • 提供 LoadData 辅助线程函数和 Populate 函数,并在辅助线程完成后调用它们。
  • 为 InitializeComponent 提供一个公共的抽象方法签名。这很重要,因为 Form Designer(窗体设计器)默认情况下创建 InitializeComponent Private,而 FormStack 类又需要调用它。通过提供一个抽象实现,编译器会确认我们已进行了此更改。

遗憾的是,Visual Studio .NET 2003 中的 Form Designer(窗体设计器)只能使用从 Form 类继承的内容。我执行的操作是:当窗体继承 Form 时使用设计器设计的窗体,然后在生成期间将基类更改为 StackForm。尽管这有点不方便,但要比手动对窗体进行编码好一些。

FormStack 类

然后,我创建了 FormStack 类。FormStack 实现“压入”和“预先加载”的方法与流程图相同。堆栈本身是使用 ArrayList 实现的,因此便于添加或删除项目。窗体 Cache 是通过从 CollectionBase 继承 FormStack 类来实现的。它提供了一个 List 成员集合,大大简化了 FormStack 对象的存储过程。

除了“压入”和“预先加载”,我还实现了“弹出”(只需删除堆栈中的弹出项目并向下显示下一个 StackForm)。它并未从缓存中删除 StackForm,我也未实现完成该操作的函数。尽管在某些情况下,从缓存中实际释放窗体的方法可能更有用,但它过于复杂,已超出本文的范围。

实际上该体系结构较大的障碍之一是运行应用程序。.NET Framework 精简版提供了入口点 Application.Run,它加载一个窗体并提供一个消息泵。使用 Run 在体系结构中无法正常运行,因为它需要一个实例化的窗体,应用程序中的每个其他窗体必须在该初始窗体中运行。我们需要提供自己的消息泵,以调度系统消息,并为应用程序实际结束提供一个简单方法。

虽然这看似复杂,但实际上却非常简单。我使用一个紧密的循环在 FormStack.Run 中实现了消息泵,只要缓存中包含窗体,该循环即调用 Application.DoEvents。这就意味着在调用 Run 之前必须先将 StackForm 压入堆栈。我还实现了只清除缓存的互补方法 Stop,该方法能够使消息泵退出,并运行到完成。

最后,我重写了 StackForm.ToString。这只是提供了一个极好的字符串,允许您了解缓存中 StackForms 的数量和堆栈的精确内容。

继承 StackForm 类

最后需要考虑的是如何真正实现 StackForm。由于 StackForm 是一个抽象类,因此无法将其实例化。而必须在自行派生的类中继承 StackForm。继承性是面向对象编程的优点之一,可以变得非常复杂,因此我假设您已经完全理解它,只介绍从 StackForm 中继承时需要牢记的特殊事项。

首先,构造函数必须调用 StackForm 基本构造函数。这是因为基本构造函数围绕 Closing 事件。

接下来,默认构造函数调用 InitializeComponent。在我们的实现中,FormStack 调用 InitializeComponent,因此我们不希望构造函数真正调用它,但是由于我们确实希望设计者能够呈现窗体,因此需要在此处进行调用。.NET Framework 精简版的一大优点是具有一个编译器指令,允许我们按照以下方法排除调用:

#if NETCFDESIGNTIME
InitializeComponent();
#endif

最后,我们还可以选择实现 DataThread 和 Populate 方法(如果需要)。

同样,DataThread 在单独的线程中执行,并应该用于提取数据。请记住,在 DataThread 完成其工作之前,StackForm 不会被完全压入和显示,因此不要执行可能中断执行的任务(例如调用 Sleep)。如果选择实现 DataThread,则需要做的最后一件事(并且必须实现以发出线程完成信号)是调用 base.DataThread。

最后,InitializeComponent 和 DataThread 方法完成后,将调用 Populate,以便在 StackForm 可见之前有机会填充列表、下拉列表或其他控件。

请记住,由于这些 StackForms 已被缓存,因此 DataThread 和 Populate 只在 StackForm 加载到缓存时被调用,而不是每次显示时都调用。

小结

正如我在本文开头提到的,在开发较为复杂的应用程序时,您将发现 FormStack 缺少一些功能,例如根据显示内容刷新数据的功能。请记住,堆栈变量本身不仅易于保存窗体类型,而且可以保存数据库中的实体 ID 值并允许快速重写窗体。

编程的优点是随时可以添加其他功能,但应始终记住智能设备编程与桌面应用程序编程有着本质上的区别。设备资源通常都很有限,处理器的速度也往往慢很多,因此,作为应用程序编程人员,要想开发成功的应用程序,必须记住以上两点。FormStack 这个工具可以帮助开发人员尽量减少对应用程序资源的影响,同时可以管理 GUI 引擎的复杂功能,这样可以通过缓存快速更改窗体并通过堆栈轻松导航。

原文地址:https://www.cnblogs.com/luqingfei/p/650121.html