TaskVision 解决方案概述:设计与实现

时间:2022-03-15 09:06:18

TaskVision 解决方案概述:设计与实现

发布日期: 08/20/2004 | 更新日期: 08/20/2004

Vertigo Software, Inc.

适用于:
.NET Framework
Windows 窗体

摘要:本文介绍了 TaskVision 解决方案示例的设计和体系结构决策。该示例演示了如何使用 .NET Framework 的 Windows 窗体类和 XML Web 服务生成智能客户端任务管理应用程序。

注:从 2003 年 5 月 7 日起,暂不提供该应用程序的代码。我们很快会重新发布它。

下载本解决方案的代码:
TaskVision 客户端
TaskVision服务器
TaskVision源代码

TaskVision 解决方案概述:设计与实现
本页内容
TaskVision 解决方案概述:设计与实现 概述
TaskVision 解决方案概述:设计与实现 解决方案体系结构
TaskVision 解决方案概述:设计与实现 学习心得
TaskVision 解决方案概述:设计与实现 获得更多信息

概述

什么是 TaskVision 解决方案?

TaskVision 是一个示例智能客户端任务管理应用程序,它是使用 Microsoft®.NET Framework(一个至关重要的 Windows® 组件,支持运行下一代应用程序和 XML Web 服务)的 Windows 窗体类生成的。TaskVision 允许经过身份验证的用户查看、修改和添加与其他用户共享的项目和任务。它可以在多种方案中使用,从错误跟踪到管理工作程序或者客户服务请求,都可以使用。它的主要用途是为有兴趣使用 .NET Framework 生成智能客户端应用程序和 XML Web 服务的开发人员提供高质量的示例源代码。图 1 所示为 TaskVision 应用程序。

TaskVision 解决方案概述:设计与实现

图 1 TaskVision 界面

TaskVision 解决方案演示了许多由 .NET Framework 提供的技术,其中包括:

应用程序脱机和联机模型

通过 HTTP 的应用程序更新模型(无接触式部署)

控制用户对应用程序功能的访问的授权

数据冲突处理

打印和打印预览

Windows XP 主题

动态属性

本地化支持

辅助功能支持(有限)

使用用户名/密码数据库进行窗体身份验证

异步 XML Web 服务调用

使用 SQL 存储过程进行 ADO.NET 数据访问

使用 GDI+ 进行图形开发

基于 .NET Framework 的代码和 COM 应用程序 (COM interop) 之间的集成

本白皮书对 TaskVision 进行了深入的讨论,从该解决方案的开发人员的角度提供了有关其体系结构的详细信息。此外,本文档还通过分析许多主要的应用程序功能以及用以实现这些功能的技术,讲解了如何将 TaskVision 用作能够生成智能客户端应用程序的模板。有关 TaskVision 的完整信息,请访问 TaskVision 主页

TaskVision 解决方案使用 Microsoft®Visual Studio .NET® 开发,由 C# 和 Visual Basic .NET 编程语言编写。TaskVision 已经移植到 PocketPC 平台上了。有关如何生成 TaskVision 的 PocketPC 版的信息,请参阅 MSDN 上的 Creating the Pocket TaskVision Application

TaskVision 入门

实际体验 TaskVision 最简便的方法是下载并安装 TaskVision Live Client v1.0 MSI。该 Live Client 包含已编译的可执行文件,并配置为从一个公共 XML Web 服务发送和检索它所需要的数据。

与该客户端应用程序对应的服务器应用程序是 TaskVision Server v1.1 MSI。该 Server 安装程序将创建数据库,并安装 XML Web 服务和承载客户端 v1.1 更新(该更新将通过无接触式部署检索和执行)的 Web 站点(分别为“http://localhost/TaskVisionWS”和“http://localhost/TaskVisionUpdates”)。该安装程序特别适用于想在不编译代码的情况下在本地运行整个解决方案的开发人员。

对于想要亲手访问源代码的人,可以使用已发行的 TaskVision Source Code v1.1 MSI,该安装程序将创建数据库,并安装客户端应用程序 v1.1(对于 TaskVision Live Client,就是通过无接触式部署下载和执行的版本)的源代码,以及 XML Web 服务的源代码。对于没有安装该 MSI 所必需的软件的人,可以通过 TaskVision Source Code Viewer 联机查看其中一些源代码。

有关 MSI 的系统要求和安装说明,请参考下面的说明。

不可以将 TaskVision Server v1.1 MSI 与 TaskVision Source Code v1.1 MSI 安装到同一台计算机上,这是因为这两个安装使用相同的数据库和 IIS 虚拟目录名称。

TaskVision Live Client v1.0 MSI 安装

下载和安装适当的 MSI。启动该应用程序时(从 Start 菜单),将提示一个标准登录屏幕,您需要输入“jdoe”作为用户名,输入“welcome”作为密码。登录后,您就可以随意修改数据并测试应用程序功能,但是请注意,公共服务器上的所有数据在每天夜间都会重置,因此在某一天所做的更改无法保留到第二天。

最低要求:

Windows 2000/XP 或更高版本

Microsoft .NET Framework 1.0

LAN/拨号 Internet 连接

Microsoft Excel 2002(推荐;不需要 — 要实际查看 COM interop,您需要使用该应用程序将数据导出到 Excel)。

该应用程序不支持 ISA 客户端或 Web 代理。

TaskVision Server v1.1 MSI 安装

安装之前,请确保满足了下列条件:

您具有本地计算机上的管理员特权。

SQL Server 默认实例命名为“local”(默认情况下安装的实例),并且,您的 Windows 帐户具有使用集成安全性的数据库的 SQL Server 管理员特权。

ASP.NET 文件扩展名(.aspx 和 .asmx)必须注册到 Internet 信息服务 (IIS)。(如果 IIS 是在安装 .NET Framework 之后安装的,必须运行以下位置的应用程序:C:/WINDOWS/Microsoft.NET/Framework/v1.0.3705/aspnet_regiis.exe –i。)

最后,确保 SQL Server 和 IIS 服务都处于运行状态。下载和安装适当的 MSI。在安装好 XML Web 服务并更新了 Web 站点和数据库之后,请导航到包含 TaskVision 客户端应用程序(通过TaskVision Live Client v1.0 MSI 安装)的目录,并将 TaskVision.exe.config 文件编辑为指向本地服务器 URL,如下所示。(该文件位于“1.0.0.0”中,也有可能位于“1.1.0.0”子目录下。)

在下面的示例中,我们将“localhost”用作服务器。这适用于客户端和服务器应用程序运行在同一台计算机上的情形。如果服务器与客户端运行在不同的计算机上,请使用运行服务器的计算机的 URL 代替“localhost”。如果服务器与客户端运行在不同的计算机上,您需要在 TaskVision.exe.config 文件中进行相同的更改,该文件位于 TaskVision Server 目录的“TaskVisionUpdates/1.1.0.0”下。

      <appSettings>       <!-- User application and configured property settings go here.-->       <!-- Example: <add key="settingName" value="settingValue"/> -->       <add key="AppUpdater1.UpdateUrl" value="http://localhost/taskvisionupdates/updateversion.xml"/>       <add key="TaskVision.AuthWS.AuthService" value="http://localhost/taskvisionws/authservice.asmx"/>       <add key="TaskVision.DataWS.DataService" value="http://localhost/taskvisionws/dataservice.asmx"/>       </appSettings>       </configuration>

最低要求:

Windows 2000/XP 或更高版本

Microsoft .NET Framework 1.0

IIS 5.0+

SQL Server 2000

TaskVision Source Code v1.1 MSI 安装

安装之前,请确保满足了下列条件:

您必须具有本地计算机上的管理员特权。

SQL Server 默认实例命名为“local”(默认情况下安装的实例),并且,您的 Windows 帐户具有使用集成安全性的数据库的 SQL Server 管理员特权。

ASP.NET 文件扩展名(.aspx 和 .asmx)必须注册到 IIS。(如果 IIS 是在 ASP.NET 之后安装的,必须运行以下位置的应用程序:C:/WINDOWS/Microsoft.NET/Framework/v1.0.3705/aspnet_regiis.exe -i。)

最后,确保 SQL Server 和 IIS 服务都处于运行状态。

下载和安装适当的 MSI。安装好 XML Web 服务和数据库后,请使用 Visual Studio .NET 打开 TaskVision 解决方案。请注意,在没有安装 Excel 2002 的情况下,如果不删除与“导出到 Excel”相关的代码(位于 ExportExcel 类和 Main 窗体类中),或者将这些代码变为注释,该解决方案将无法编译。

编译项目和启动应用程序时,将提示一个标准登录屏幕,您需要输入“jdoe”作为用户名,输入“welcome”作为密码。

最低要求:

Windows 2000/XP 或更高版本

Visual Studio .NET 2002

IIS 5.0+

SQL Server 2000

Microsoft Excel 2002(推荐)

解决方案体系结构

我们的任务管理解决方案包含三个主要组件:数据库、XML Web 服务和使用 Windows 窗体类生成的智能客户端应用程序(见图 2)。

数据库由 XML Web 服务访问,这些 XML Web 服务仅具有在数据库上运行存储过程的权限。通过限制 XML Web 服务在数据库上所能访问的内容,可以确保只有我们的查询才能运行在数据库上。

在我们的示例解决方案中,XML Web 服务实现为在公共服务器上运行,任何应用程序都可以通过 Internet 对其进行访问。当然,可以在企业的 Intranet 上运行它们,从而将数据访问限制到内部网络。(注:虽然任何可以访问运行 XML Web 服务的服务器的应用程序都可以访问 XML Web 服务,但只有能够提供有效用户名和密码的应用程序能够使用它们。)

智能客户端应用程序通过将用户名和密码传递给身份验证 XML Web 服务来对用户进行身份验证。身份验证成功后,XML Web 服务将向智能客户端应用程序传回一个加密票,该加密票将被存储,并在将来每次请求数据时提交给数据 XML Web 服务。数据 XML Web 服务将验证该加密票并处理数据请求。(注:在您自己的解决方案中,当通过 XML Web 服务传递的凭据或加密票能够用于访问敏感信息或资源,或者敏感数据本身被来回传递时,我们建议您使用安全套接字层 (SSL)。这层加密可以防止潜在攻击者的攻击。)

TaskVision 解决方案概述:设计与实现

图 2 TaskVision 应用程序体系结构

数据库

所有的共享数据都存储在 SQL Server 数据库中。应用程序特定的数据或配置设置不包括在内。这使得开发人员能够创建自定义应用程序,每个应用程序都从单个唯一的数据存储提取数据。使用相同的服务器端功能,我们就可以以这种方式创建 .NET Compact Framework 版的 TaskVision,称为 Pocket TaskVision(将于三月发布)。本部分提供了 TaskVision 解决方案中使用的数据库的概述。

数据库架构

TaskVision 的数据库架构(如图 3 所示)相当简单,但是已经足以支持此任务管理解决方案。

TaskVision 解决方案概述:设计与实现

图 3 TaskVision 数据库架构

存储过程

TaskVision 解决方案使用存储过程封装了所有的数据库查询。存储过程提供了数据库与中间层数据访问层之间的清晰隔离。这简化了维护工作,因为对数据库架构的更改对于数据访问组件是不可见的。因为数据库中进行缓存以及某些本地处理可以减少必需的网络请求数,所以对于某些体系结构方案来说,使用存储过程还可以带来一些性能方面的好处。

XML Web 服务

XML Web 服务的组合有效地充当了主中间层,负责处理身份验证和来自于访问它们的任何客户端应用程序的数据请求。

我们选择了将 XML Web 服务功能分为两类:

身份验证 — 可以提交明文凭据以提供登录信息,也可以配置为在 SSL 下运行(虽然本示例中当前并未实现)。

数据 — 发送和接收非关键性的数据(在进行某种形式的身份验证后),而不会引起 SSL 系统开销。如果在实际的应用程序中使用此解决方案,数据 XMl Web 服务将可以在 SSL 下运行,以防止潜在的攻击者访问序列化数据。

身份验证 XML Web 服务

身份验证服务(图 4)遵循非常简单的原则:(使用一个存储过程)针对数据库验证用户名和密码,然后返回嵌套了用户 ID 的唯一加密票。如果用户名和密码失败,则不返回任何内容。

发出该票后,将在服务器上缓存其值两分钟(缓存在 Web 应用程序的静态缓存对象中)。这样,我们就可以维护一个当前发出的票的服务器端列表,在同一个应用程序域中运行的所有代码都可以访问该列表(如后面的数据服务所示)。由于只维护该列表中的票两分钟,客户端应用程序不得不经常重新进行身份验证,这有助于防止“回复攻击”(Replay Attack),在这种攻击方式中,攻击者从网络中偷取一张票,并使用它来冒充经过验证的用户。

票是使用 System.Web.Security.FormsAuthenticationTicket 类创建的,之所以选择该类,是因为它能够在票本身内嵌套数据,例如,用户 ID。

TaskVision 解决方案概述:设计与实现

图 4 TaskVision 身份验证过程

'create the ticketDim ticket As New FormsAuthenticationTicket(userID, False, 1)Dim encryptedTicket As String = FormsAuthentication.Encrypt(ticket)'get the ticket timeout in minutesDim configurationAppSettings As AppSettingsReader = New AppSettingsReader()Dim timeout As Integer = _   CInt(configurationAppSettings.GetValue("AuthenticationTicket.Timeout", _   GetType(Integer)))'cache the ticketContext.Cache.Insert(encryptedTicket, userID, Nothing, _   DateTime.Now.AddMinutes(timeout), TimeSpan.Zero)

数据 XML Web 服务

数据 XML Web 服务提供了客户端应用程序用以检索和更改数据的功能,并且提供了身份验证服务,能够验证用户的每个请求。

两种 XML Web 服务都运行在同一个应用程序域中(本例中,为同一个 IIS Web 应用程序),这使得数据服务能够访问身份验证服务用于保存有效身份验证票的副本的相同缓存内存。

数据服务所支持的每种公共 Web 方法都要求身份验证票随调用传入。在返回任何数据之前,将在缓存中检查票是否存在。如果票存在,说明在最近两分钟内对用户名和密码进行了验证;否则票将无效或过期。作为额外的安全措施,我们从票中提取了嵌套的用户 ID,并针对数据库验证该用户 ID,以确保用户帐户未被(另一个管理员)锁定,并且具有 TaskVision 管理员特权,以用于需要管理员状态的功能。

Private Function IsTicketValid(ByVal ticket As String, ByVal IsAdminCall _   As Boolean) As Boolean   If ticket Is Nothing OrElse Context.Cache(ticket) Is Nothing Then      'not authenticated      Return False   Else      'check the user authorization      Dim userID As Integer = _         CInt(FormsAuthentication.Decrypt(ticket).Name)      Dim ds As DataSet      Try         ds = SqlHelper.ExecuteDataSet(dbConn, "GetUserInfo", userID)      Finally         dbConn.Close()      End Try      Dim userInfo As New UserInformation()      With ds.Tables(0).Rows(0)         userInfo.IsAdministrator = CBool(.Item("IsAdministrator"))         userInfo.IsAccountLocked = CBool(.Item("IsAccountLocked"))      End With      If userInfo.IsAccountLocked Then         Return False      Else         'check admin status (for admin required calls)         If IsAdminCall And Not userInfo.IsAdministrator Then            Return False         End If         Return True      End If   End IfEnd Function _Public Function GetTasks(ByVal ticket As String, ByVal projectID _   As Integer) As DataSetTasks   'if the ticket is not valid, return   If Not IsTicketValid(ticket) Then Return Nothing      Dim ds As New DataSetTasks()   daTasks.SelectCommand.Parameters("@ProjectID").Value = projectID   daTasks.Fill(ds, "Tasks")   Return dsEnd Function

Windows 窗体智能客户端

智能客户端应用程序在本解决方案中是最显眼的部分,这是因为它是终端用户用来管理项目和任务的工具。如前所述,TaskVision 用于演示一些关键的智能客户端技术和方案。下面,我们将对其中的一些技术和方案进行逐个演示。对于这一部分,有一份很有价值的补充材料:TaskVision Source Code Viewer,它提供了对源代码中许多更有趣部分的详细分析。

用户界面窗体

在深入研究核心技术及其使用方法之前,对应用程序中的各个部分 — 各种窗体及其用途 — 进行简短的说明,可能很有必要。

Login 窗体(图 5)使用户能够通过输入他们的 TaskVision 凭据对自己进行身份验证。(同样,用于访问此应用程序的默认登录凭据为“jdoe”和“welcome”,分别代表用户名和密码。)

TaskVision 解决方案概述:设计与实现

图 5 TaskVision 登录窗体

Main 窗体(图 6)显示了从数据 XML Web 服务(或从脱机文件)检索的数据。Main 窗体充当了我们的事件驱动的应用程序的基础,并且是用户体验的核心场所。该窗体本身主要包括一个主菜单、一个带按钮的工具栏、几个带有 ComboBox、图表和链接的自定义面板、一个 DataGrid、预览窗格的另一个自定义面板、两个分隔条和一个用于显示活动信息(例如,项的数量和联机状态)的状态栏。

在占据窗体大部分空间的 DataGrid 中,可以看到数据库中为当前选定的项目列出的任务的摘要。在 DataGrid 下,显示了选定任务的更为详细的信息。DataGrid 左侧是用于选择其他项目和筛选所显示的任务的控件。这些控件下方有两个 GDI+ 图表,显示有关任务的信息。在它们下方,有一个显示选定任务的历史记录(有关其创建与任何修改的信息)的控件,在本屏幕快照中,该控件不可见。在窗体顶部,有一些菜单和按钮,用于管理 TaskVision 用户、切换语言、创建新项目和任务、将 DataGrid 中的信息导出到 Excel,以及脱机工作(或者在应用程序当前处于脱机状态时,联机工作)。

TaskVision 解决方案概述:设计与实现

图 6 TaskVision Main 窗体

双击 Main 窗体的 DataGrid 中的任务,将显示 Edit Task 窗体(图 7)。通过可由用户编辑的控件,该窗体使用户能够修改任务 — 它的截止日期、负责它的工作人员以及任务的优先级、摘要、说明和当前完成任务的进度。此外,这一双功能窗体还可以在 History 选项卡(图 8)上显示给定任务的任何关联历史记录。应当注意,当用户单击 Main 窗体上的 New Task 按钮或者单击 File 菜单下的 New Task 项时,将启动相同的窗体来定义新任务。

TaskVision 解决方案概述:设计与实现

图 7 Edit task

TaskVision 解决方案概述:设计与实现

图8 Edit Task 的 History 选项卡

Manage Users 窗体(图 9)是通过 Main 窗体顶部的 Manage 菜单中的 Users 项启动的。它列出了应用程序的所有用户。Edit 按钮将启动 Edit User 窗体(图 10),该窗体用于对用户进行更改。请注意,仅当您作为 TaskVision 管理员登录时,Manage 菜单项才可用。

TaskVision 解决方案概述:设计与实现

图 9 Manage Users 窗体

TaskVision 解决方案概述:设计与实现

图 10 Edit User 窗体

Change Password 窗体(图 11)通过 File 菜单中的 Change Password 项启动,用于更改当前用户的密码,不要求 TaskVision 管理员特权。仅当应用程序处于联机模式时,该功能才可用。

TaskVision 解决方案概述:设计与实现

图 11 Change Password 窗体

Search 窗体(图 12)通过 View 菜单中的 Search 项或通过单击 Main 窗体顶部的 Search 按钮启动,用于在当前项目内的所有任务中执行简单的子字符串搜索。

TaskVision 解决方案概述:设计与实现

图 12 Search 窗体

Customize Columns 窗体(图 13)通过 View 菜单中的 Customize Columns 项启动,用于操作 DataGridTableStyle,使用户能够控制列布局的外观和感觉。

TaskVision 解决方案概述:设计与实现

图 13 Customize Columns 窗体

Add Project 窗体(图 14)通过 Manage 菜单中的 Add Project 项启动,可供 TaskVision 管理员向远程数据库添加新项目。

TaskVision 解决方案概述:设计与实现

图 14 Add Project 窗体

数据层组件

DataLayer 类是 XML Web 服务包装,并且是客户端应用程序的数据管理器。

对于应用程序本身来说,有一个与数据处理有关的可见结构和设计模式。图 15 显示了与 DataLayer 类和窗体类有关的对象所有者关系。

当 Main 窗体处理事件(例如,打开 Search 窗体)时,DataLayer 对象将传递给新的窗体,从而提供对 Main 窗体有权访问的数据的访问权。

TaskVision 解决方案概述:设计与实现

图 15 TaskVision 类层次结构中的对象所有者关系

项目信息、任务信息、用户信息以及从 XML Web 服务检索的所有其他信息都归 DataLayer 类所有。可以通过 DataLayer 类的公共成员访问这些数据,各种 UI 窗体可以*读取和更改这些本地数据。从 XML Web 服务更新或检索数据的操作仅可以通过使用 DataLayer 类中的公共方法来完成。这些公共方法包括:GetProjectsGetTasksUpdateTasks

DataLayer 类设计为用于单线程环境中,通过在主线程中调用这些方法,可以确保从 XML Web 服务调用检索的信息能够正确地同步合并到本地数据中,并且,数据绑定 UI 控件不会在后台线程上刷新它们的图形。

这些功能方法中的大多数(如下面的代码)都具有类似的设计:使用当前的身份验证票向数据 XML Web 服务请求数据(或将数据发送到数据 XML Web 服务),在必要时重新进行身份验证并处理任何异常,合并返回的任何数据,然后为调用代码返回一个 DataLayerResult,以指示操作是成功还是失败。

Public Function GetProjects() As DataLayerResult   'this is the ds that gets returned from the ws   Dim ds As DataSetProjects   Try      'request the ds and pass the ticket       ds = m_WsData.GetProjects(m_Ticket)       'all TaskVision web services return nothing       '(or -1 for integer requests) to indicate an expired ticket       If ds Is Nothing Then          'get a new ticket and try the call again          Dim ticketResult As DataLayerResult = GetAuthorizationTicket()         'if the ticket failed return its error as our own          If ticketResult <> DataLayerResult.Success Then             Return ticketResult          End If         'try the call again          ds = m_WsData.GetProjects(m_Ticket)          'this next block should never happen.         'it means the ws ticket expired too quickly          If ds Is Nothing Then             Return DataLayerResult.AuthenticationFailure          End If       End If   Catch ex As Exception      Return HandleException(ex)    End Try    DsProjects.Clear()    DsProjects.Merge(ds)    Return DataLayerResult.SuccessEnd FunctionPublic Enum DataLayerResult    None = 0    Success = 1     ServiceFailure = 2    UnknownFailure = 3    ConnectionFailure = 4    AuthenticationFailure = 5End Enum

对于上面的 Enum,解释如下:

DataLayerResult.Success 意味着公共方法成功实现了其目的。

DataLayerResult.ServiceFailure 意味着在 XML Web 服务和 XML Web 服务的代码本身中发生了异常。

DataLayerResult.ConnectionFailure 意味着连接到 XML Web 服务时发生问题(问题出在本地 Internet 连接或者 XML Web 服务的响应)。

当前用户名和密码(由 Login 窗体设置)不再有效时,将使用 DataLayerResult.AuthenticationFailure

到目前为止,我所介绍的全部 XML Web 服务调用都是在应用程序的主线程中同步执行的。在应用程序中,有两处实现异步 XML Web 服务调用的实例(即,调用在主应用程序线程以外的其他线程中执行)。这使得应用程序能够在等待异步 XML Web 服务在后台完成其任务时正常工作。

第一种方案是检索项目历史记录 — 对项目中所有任务进行的所有更改的列表。不难想象,这很容易产生大量数据并导致下载时间过长。因此,最好在后台检索这些只读数据,以避免对应用程序造成妨碍。

我们的 Main 窗体中包含一个计时器,它会定期更新历史记录信息。调用 BeginGetProjectHistory 方法将返回一个 IAsyncResult 对象,在未来的计时器计时事件中,将对该对象进行检查,直到调用完成。完成后,调用 EndGetProjectHistory 方法将完成合并数据的过程,这类似于前面讨论过的同步方法。

Public Function BeginGetProjectHistory(ByVal projectID As Integer) As IAsyncResult   Try      'note: there is an assumption here that our ticket is always valid      'because this method is called immediately after a project or task request.      'start an async call for the      Return m_WsData.BeginGetProjectHistory(m_Ticket, projectID, _         Nothing, New Object() {projectID})   Catch ex As Exception      LogError.Write(ex.Message & vbNewLine & ex.StackTrace)      Return Nothing   End TryEnd FunctionPublic Function EndGetProjectHistory(ByVal ar As IAsyncResult) As DataLayerResult   Dim ds As DataSetProjectHistory   Try      'grab the new DataSet      ds = m_WsData.EndGetProjectHistory(ar)      If ds Is Nothing Then         Return DataLayerResult.AuthenticationFailure      End If   Catch ex As Exception      Return HandleException(ex)   End Try   DsProjectHistory.ProjectHistory.Clear()   DsProjectHistory.Merge(ds)   Return DataLayerResult.SuccessEnd Function

TaskVision 中的第二处异步 XML Web 服务调用发生在更新 Main 窗体用于填充 DataGrid 的任务 DataSet 时。

Main 窗体中包含另一个计时器,用于更新任务 DataSet。这里,真正的区别在于何时 真正进行异步调用。与定期或强制 XML Web 服务调用不同,此计时器在用户修改数据时会频繁停止和重置。如果应用程序空闲时间长到足以使计时器进行计时,则对于计时器此后的每次计时,都会启动异步请求,并检查请求是否完成。如果请求已完成,将把数据合并到本地数据中。如果用户在异步请求执行时进行了任何更改(通过交互有效地更新了数据),将放弃请求,原因是它已过期。

数据冲突

处理数据冲突的方法很多。常见的情况是客户端尝试更新或删除数据库中的数据,而这些数据自该客户端上次访问它们以来已被更改,或者根本不存在。通常这可以通过引发错误或者简单地使用客户端版的记录重写数据库中的任何内容来处理。第一种方案会导致客户端的工作无效。第二种方案带来的风险是忽略和删除自从客户端上一次检查数据库以来输入的重要数据。TaskVision 对此问题引入了一个简单的解决方案,主要依靠 .NET Framework 中 ADO.NET 库的 DataSet 对象中的功能。(DataSet 是一个包含从数据库检索的数据的缓存的对象。)

为了管理 TaskVision 任务的 DataSet,我们选择了使用 System.Data.SqlClient 命名空间中的 System.Data.SqlClient 类,该类可用于将选择、更新、插入和删除功能封装到一个对象中。DataAdapter 的 Update 方法将检查 DataSet 内每个 DataRowRowState,以确定 DataRow 是新的、已删除,还是已更改,然后执行适当的存储过程。然后,DataAdapter 将确保数据库中受影响的行的计数大于零。(对于更新和删除操作,不大于零在逻辑上意味着存储过程未能找到目标数据。)

请务必注意,Update 存储过程仅在能够验证数据库中记录自从其副本存储在客户端的 DataSet 中以来未被更改(即不存在数据冲突)的情况下才会对记录进行更新。该存储过程如下所示:

CREATE PROCEDURE [UpdateTask](   @TaskID int,   @ProjectID int,   @ModifiedBy int,   @AssignedTo int,   @TaskSummary varchar(70),   @TaskDescription varchar(500),   @PriorityID int,   @StatusID int,   @Progress int,   @IsDeleted bit,   @DateDue datetime,   @DateModified datetime,   @DateCreated datetime,   @Original_ProjectID int,   @Original_ModifiedBy int,   @Original_AssignedTo int,   @Original_TaskSummary varchar(70),   @Original_TaskDescription varchar(500),   @Original_PriorityID int,   @Original_StatusID int,   @Original_Progress int,   @Original_IsDeleted bit,   @Original_DateDue datetime,   @Original_DateModified datetime,   @Original_DateCreated datetime)ASSET NOCOUNT OFF;--note we are using convert to varchar on the date comparison so that the pocket pc app can use this sproc the pocket pc app stores offline data which only supports a 4 byte datetime.UPDATE Tasks SET ProjectID = @ProjectID, ModifiedBy = @ModifiedBy, AssignedTo = @AssignedTo, TaskSummary = @TaskSummary, TaskDescription = @TaskDescription, PriorityID = @PriorityID, StatusID = @StatusID, Progress = @Progress, IsDeleted = @IsDeleted, DateDue = @DateDue, DateModified = @DateModified WHERE (TaskID = @TaskID) AND (ProjectID = @Original_ProjectID) AND (ModifiedBy = @Original_ModifiedBy) AND (AssignedTo = @Original_AssignedTo) AND (TaskSummary = @Original_TaskSummary) AND (TaskDescription = @Original_TaskDescription) AND (ProjectID = @Original_ProjectID) AND (StatusID = @Original_StatusID) AND (Progress = @Original_Progress) AND (IsDeleted = @Original_IsDeleted) AND (convert(varchar(20), DateDue) = convert(varchar(20), @Original_DateDue)) AND (convert(varchar(20), DateModified) = convert(varchar(20), @Original_DateModified)) AND (convert(varchar(20), DateCreated) = convert(varchar(20), @Original_DateCreated)) AND (PriorityID = @Original_PriorityID);SELECT TaskID, ProjectID, ModifiedBy, AssignedTo, TaskSummary, TaskDescription, PriorityID, StatusID, Progress, IsDeleted, DateDue, DateModified, DateCreated FROM Tasks WHERE (TaskID = @TaskID)GO

如上所示,WHERE 子句非常彻底地检查了数据冲突。您可能不是很清楚 WHERE 子句中用于检查数据冲突的“original”值来自何方。默认情况下,DataSet 中的每个 DataRow 都会跟踪从数据库返回的原始值(当最初创建 DataSet 时)以及用户正在更新的当前值。

在我们的 SQLDataAdapter 实现中,当 DataRow 返回零个受影响的行并引发 DBConcurrency Exception 时,它将停止更新。我们正是通过这个异常处理数据冲突的。

通过参考下面的代码块,可以看到我们进入了一个循环(稍后将介绍),而且,如果没有任何异常,将退出循环。如果捕捉到 DBConcurrency Exception,我们将首先尝试检索任务记录(通过 TaskID),并确定数据库记录是仍然存在,还是实际上已被删除。删除相对容易处理,原因是数据库记录不是由我们的客户端应用程序物理删除的,而是仅可以由系统管理员删除。我们认定结果为删除,这样,我们的客户端将丢失记录和任何待定的更改。进行初始循环纯粹是为了确保在没有退出方法调用并返回客户端的情况下删除了 DataRow 并重新启动了更新过程(原因是这些代码是在 XML Web 服务中执行的)。如果记录仍存在,说明它不匹配 WHERE 子句,我们应当允许用户作出有关记录的决定。当前的任务是将待定的更改和数据库中的新值返回给用户。

不再需要用户原以为自己正在更改的原始值,这是因为已经使用由另一个客户端提供的新值更新了数据库记录。了解了这一点以及 DataRow 能够保存两组值的情况后,我们制作了待定更改的一个副本,应用了新值,并将待定的更改重新复制回 DataRowDataRow 现在包含了最新的数据库项和用户的待定更改。原始值被有效复制了。完成了这些工作后,我们从 XML Web 服务将 DataSet 返回给 TaskVision 智能客户端应用程序,以便它能够为用户显示错误(图 16)。我们的应用程序在一个窗体(Collision 窗体)中显示了两组值,允许用户决定继续进行更改,还是取消操作,保留数据库中的当前值。

TaskVision 解决方案概述:设计与实现

图 16 冲突解决窗体

'we're doing a loop on the update function and breaking out on a successful update'or returning prematurely from a data collision, this allows us to handle'missing data without returning to the client.Do   Try      'try to update the db      daTasks.Update(dsTasks, "Tasks")      Exit Do 'this is the most common path   Catch dbEx As DBConcurrencyException      'we're here because either the row was changed by someone else      'or deleted by the dba, let's try get the updated row      Dim ds As New DataSet()      Dim cmd As New SqlCommand("GetOneTask", dbConn)      cmd.CommandType = CommandType.StoredProcedure         'get the updated row      Dim da As New SqlDataAdapter(cmd)      da.SelectCommand.Parameters.Add("@TaskID", dbEx.Row.Item("TaskID"))      da.Fill(ds)         'if the row still exists      If ds.Tables(0).Rows.Count > 0 Then         Dim proposedRow As DataRow = dbEx.Row.Table.NewRow()         Dim databaseRow As DataRow = ds.Tables(0).Rows(0)            'copy the attempted changes         proposedRow.ItemArray = dbEx.Row.ItemArray            'set the row with what's in the database and then re-apply         'the proposed changes         With dbEx.Row            .Table.Columns("TaskID").ReadOnly = False            .ItemArray = databaseRow.ItemArray            .AcceptChanges()            .ItemArray = proposedRow.ItemArray            .Table.Columns("TaskID").ReadOnly = True         End With            'note: because this row triggered an ADO.NET exception, the row         'was tagged with a rowerror property which we'll leave for the          'client app         Return dsTasks      Else         'row was deleted from underneath user, deletion always wins         dbEx.Row.Delete()         dbEx.Row.AcceptChanges()      End If   End TryLoop

脱机 — 联机数据模型

处于联机模式时,TaskVision 客户端应用程序将管理内存中的所有数据,并依靠 XML Web 服务调用在终端用户每次进行更改时验证数据更改。但是,TaskVision 客户端应用程序还支持脱机模式。

脱机模式是由终端用户手动调用的(通过单击脱机工具栏按钮)。执行此操作时,会发生若干事件:

首先,将 DataSet 作为 XML 保存到本地硬盘驱动器。请务必注意,此时,数据表示数据库的上一次已知状态。

接着,将一个全局 Boolean 对象设置为 false,以防止以后应用程序将更改发送给 XML Web 服务(这是因为在用户尝试恢复联机之前,数据在本地维护)。

最后,更新 GUI 以反映其脱机状态。

当应用程序以脱机模式运行时,更改将保存到 DataSet 中,受影响的 DataRow 将被标记为“Changed”(已更改)。

如果用户在脱机模式下退出应用程序,已更改的 DataRow 将作为一个单独的 XML 文件保存到磁盘(请记住,ProjectsTasksLookupTables 这三个主要的 DataSet 已经保存,如前所述)。

如果存在脱机数据(当应用程序再次加载时),将假定上一个状态是脱机模式。这些 XML 文件将用于填充主 DataSet,然后,应用程序将检查更改文件(请注意为什么没有为该 XML 文件调用 AcceptChanges 方法)。通过不调用 AcceptChanges 方法,这些合并的行将继续被标记为“Changed”,从而向应用程序提供它以前(在用户退出应用程序之前)所具有的相同的值。

Try    'check for the offline files    If File.Exists(m_MyDocumentsPath & c_OfflineTasksFile) AndAlso _      File.Exists(m_MyDocumentsPath & c_OfflineProjectsFile) AndAlso _      File.Exists(m_MyDocumentsPath & c_OfflineLookUpTablesFile) Then        Try            'engage offline mode            ChangeOnlineStatus(False)            'try to read the offline data            m_DataLayer.DsProjects.ReadXml(m_MyDocumentsPath & _         c_OfflineProjectsFile, XmlReadMode.ReadSchema)            m_DataLayer.DsTasks.ReadXml(m_MyDocumentsPath & _         c_OfflineTasksFile, XmlReadMode.ReadSchema)            m_DataLayer.DsLookupTables.ReadXml(m_MyDocumentsPath & _         c_OfflineLookUpTablesFile, XmlReadMode.ReadSchema)            'workaround: scheme doesn't include autoincrement            m_DataLayer.DsTasks.Tasks.Columns("TaskID").AutoIncrement = True            'we now have the exact data when the user went offline            m_DataLayer.DsTasks.AcceptChanges()            'if we have any changes then read them in            If File.Exists(m_MyDocumentsPath & c_OfflineTaskChangesFile) Then                m_DataLayer.DsTasks.ReadXml(m_MyDocumentsPath & _         c_OfflineTaskChangesFile, XmlReadMode.DiffGram)            End If            'because our project could have come from the registry let's verify it            'otherwise choose the first project id            If m_DataLayer.DsProjects.Projects.Rows.Find(m_ProjectID) _            Is Nothing Then                m_ProjectID = _               CType(m_DataLayer.DsProjects.Projects.Rows(0)("ProjectID"), _               Integer)            End If        Catch ex As Exception            LogError.Write(ex.Message & vbNewLine & ex.StackTrace)            'we don't care what the error is, lets dump it and move on            Dim mbResult As DialogResult = _            MessageBox.Show(m_ResourceManager.GetString( _            "MessageBox.Show(There_was_an_error_reading_theoffline_files)") _            & vbNewLine & vbNewLine & _            m_ResourceManager.GetString("Do_you_want_to_go_online"), _            "", MessageBoxButtons.YesNo, MessageBoxIcon.Error, _            MessageBoxDefaultButton.Button1, _            MessageBoxOptions.DefaultDesktopOnly)            Me.Refresh()            If mbResult = DialogResult.Yes Then                'user choose to go online                ChangeOnlineStatus(True)                DeleteOfflineFiles()                m_DataLayer.DsProjects.Clear()                m_DataLayer.DsTasks.Clear()                m_DataLayer.DsLookupTables.Clear()            Else                Throw New ExitException()            End If        End Try    End If

此后,如果终端用户选择恢复联机状态,将把任务 DataSet 发送给数据 XML Web 服务,并正常处理各个更改(最后将结果发送给客户端应用程序)。如果 XML Web 服务连接成功,应用程序将把全局 Boolean 对象重新设置为 true,并且不会禁止未来的 XML Web 服务请求。

.NET Updater 组件

.NET Application Updater 组件使得 .NET Framework 智能客户端应用程序能够自动更新自己,方法是在远程 Web 服务器上具有更新的版本后下载该版本。

这个过程实际上包含两部分:一个 stub(或帮助器)可执行文件和一个内置到智能客户端应用程序本身中的组件。

stub 可执行文件 AppStart.exe 负责启动 TaskVision 应用程序的适当版本。(请注意,从客户端 MSI 安装的快捷方式图标实际上指向 AppStart.exe 文件,而不是 TaskVision.exe 文件。)

该 stub 可执行文件的工作方式是:读取一个本地配置文件 AppStart.config 来确定智能客户端应用程序的最新版本的位置。然后,启动一个新的进程以运行位于在配置文件中命名的目录中的 TaskVision.exe。在新的进程中启动 TaskVision.exe 后,它不会执行任何操作,只是等待该进程关闭。

当 TaskVision 智能客户端应用程序处于运行状态时,前面提及的组件将在后台工作,检查是否有适用于应用程序的更新,下载该更新并重定向 stub 可执行文件,以使其启动更新后的版本,而不是原来的版本。

该组件是通过轮询位于服务器上的一个 XML 文件 UpdateVersion.xml 来完成这一任务的。

如果该文件中列出的版本号大于本地 TaskVision 应用程序的版本,该组件将按照 UpdateVersion.xml 文件中的路径找到新版的文件,创建一个新的本地目录,然后将新版的文件下载到该目录中。下载完成后,该组件将编辑本地配置文件,将 stub 可执行文件重定向为更新版本的新本地目录(这样,下一次运行该 stub 可执行文件时,将启动最新的版本)。

我们将该组件配置为在下载了新的版本后提示用户,让用户选择重新启动应用程序并加载更新,或者继续应用程序会话,从而在下一次启动可执行文件 stub 时可以看到更新后的版本。

DataGrid 列样式

Windows 窗体库中的 DataGrid 类是一个现成的控件,用于显示类似于基本电子表格的信息(图 17 所示为其最基本的形式)。

TaskVision 解决方案概述:设计与实现

图 17 DataGrid 类

通过应用 DataGrid 表样式,开发人员可以自定义各个列的外观和功能。我们创建了三个自定义列类,并将它们应用到了我们的 TableStyle,以处理我们的 UI 需求。

第一个目标是重新编写 DataGridTextBoxColumn 类的功能,该类包含在 .NET Framework 中,用于处理标准文本列。默认情况下,DataGridTextBoxColumn 当用户单击单元格时会突出显示单元格内的文本,使用户能够复制该文本。

要避免这种情况,我们不得不重写 DataGridTextBoxColumn 类(首先从 Framework DataGridTextBoxColumn 类派生或继承)的其中一个基类 Edit 方法,使其不进行任何操作,从而防止了单元格获得焦点,并使得文本无法被复制。

Protected Overloads Overrides Sub Edit(ByVal source As _   System.Windows.Forms.CurrencyManager, ByVal rowNum As Integer, _   ByVal bounds As System.Drawing.Rectangle, ByVal isReadOnly As _   Boolean, ByVal instantText As String, ByVal cellIsVisible As Boolean)      'Do NothingEnd Sub

接下来是 DataGridPriorityColumn 类,该类在 DataGrid 内显示优先级图像。DataGridPriorityColumn 类假定所提供的值是要显示的 .gif 图像的文件名,并且,该文件名应当位于应用程序的图像目录中。

Protected Overloads Overrides Sub Paint(ByVal g As System.Drawing.Graphics, _   ByVal bounds As System.Drawing.Rectangle, ByVal source As _   System.Windows.Forms.CurrencyManager, ByVal rowNum As Integer, _   ByVal backBrush As System.Drawing.Brush, ByVal foreBrush As _   System.Drawing.Brush, ByVal alignToRight As Boolean)   Dim bVal As Object = GetColumnValueAtRow(source, rowNum)   Dim imageToDraw As Image   'we're caching the image in a hashtable   If m_HtImages.ContainsKey(bVal) Then      imageToDraw = CType(m_HtImages(bVal), System.Drawing.Image)   Else      'get the image from disk and cache it      Try         imageToDraw = Image.FromFile(c_PriorityImagesPath & _            CType(bVal, String) & ".gif")         m_HtImages.Add(bVal, imageToDraw)      Catch            'display error msg         Return      End Try   End If   'if the current row is this row, draw the selection back color   If Me.DataGridTableStyle.DataGrid.CurrentRowIndex = rowNum Then      g.FillRectangle(New    SolidBrush(Me.DataGridTableStyle.SelectionBackColor), _         bounds)   Else      g.FillRectangle(backBrush, bounds)   End If   'now draw the image   g.DrawImage(imageToDraw, New Point(bounds.X, bounds.Y))End Sub

最后是 DataGridProgressBarColumn 类,该类显示表示各个任务的进度的进度栏(映射为 DataTable 中的 Progress 列)。为此,我们在 Paint 方法中使用了 Graphics 对象,以根据所提供的值和该值的字符串表示形式(例如,“75%”)绘制一个彩色方框。

Protected Overloads Overrides Sub Paint(ByVal g As System.Drawing.Graphics, _   ByVal bounds As System.Drawing.Rectangle, ByVal source As _   System.Windows.Forms.CurrencyManager, ByVal rowNum As Integer, _   ByVal backBrush As System.Drawing.Brush, ByVal foreBrush As _   System.Drawing.Brush, ByVal alignToRight As Boolean)   Dim progressVal As Integer = CType(GetColumnValueAtRow(source, rowNum), Integer)   Dim percentage As Single = CType((progressVal / 100), Single)   'if the current row is this row, draw the selection back color   If Me.DataGridTableStyle.DataGrid.CurrentRowIndex = rowNum Then       g.FillRectangle(New SolidBrush(Me.DataGridTableStyle.SelectionBackColor), _       bounds)   Else       g.FillRectangle(backBrush, bounds)   End If   If percentage > 0.0 Then      'draw the progress bar and the text      g.FillRectangle(New SolidBrush(Color.FromArgb(163, 189, 242)), _         bounds.X + 2, bounds.Y + 2, Convert.ToInt32((percentage * _         bounds.Width - 4)), bounds.Height - 4)      g.DrawString(progressVal.ToString() & "%", _      Me.DataGridTableStyle.DataGrid.Font, foreBrush, bounds.X + 6, _         bounds.Y + 2)   Else      'draw the text      If Me.DataGridTableStyle.DataGrid.CurrentRowIndex = rowNum Then         g.DrawString(progressVal.ToString() & "%", _            Me.DataGridTableStyle.DataGrid.Font, New _            SolidBrush(Me.DataGridTableStyle.SelectionForeColor), _            bounds.X + 6, bounds.Y + 2)      Else         g.DrawString(progressVal.ToString() & "%", _            Me.DataGridTableStyle.DataGrid.Font, foreBrush, _            bounds.X + 6, bounds.Y + 2)      End IfEnd IfEnd Sub

打印和打印预览

在 .NET Framework 中,创建一个文档来打印是很简单的。我们需要熟悉三个类:PrintDialog 类、PrintPreviewDialog 类和 PrintDocument 类。

PrintDialog 类提供了一个打印提示,并可用于访问打印机设置,PrintPreviewDialog 类将文档打印到屏幕,在将任何数据发送给打印机之前供用户查看,PrintDocument 类则包含实际的打印输出,并能够启动打印过程。

显示 PrintDialog 非常简单:将文档属性设置为引用我们的 PrintDocument 并调用 ShowDialog 方法。PrintDialog 类不会自动打印输出;相反,我们需要检查 DialogResult,并调用 PrintDocument 的 Print 方法。显示 PrintPreviewDialog 同样简单,不同之处是没有可检查的 DialogResult。如果用户想从该对话框打印,该对话框将调用 Print 方法。

Dim pDialogResult As DialogResult = PrintDialog1.ShowDialog()
If pDialogResult = DialogResult.OK Then PrintDocument1.Print()

现在,我们已经了解了如何打印,接下来,让我们看看 TaskVision 如何创建要打印的实际输出。通常,可以创建 PrintDocument 类的一个实例,设置描述如何打印的属性,并调用 Print 方法来启动打印过程。然后,可以处理 PrintPage 事件,指定要打印的输出(通过使用 PrintPageEventArgs 中包含的 Graphics 对象)。TaskVision 将处理 PrintPage 事件并将 Graphics 对象传递给我们创建的名为 DataGridPrinter 的类,以便演示如何打印 DataGrid 中显示的信息。

DataGridPrinter 类将绘制输出的任务分解为两个部分,首先是页眉(列名),然后是所有包含数据的行。

为了绘制页眉(见下面的代码),我们创建了一个方框,并使用 Graphics 对象用灰色背景绘制了该方框。然后,循环通过 DataGrid 列以查找当前显示的列 (width > 0)。对于每个显示的列,我们创建了一个矩形以显示绘制的位置,然后使用 Graphics 对象实际在该方框内绘制列名。(请注意,不会绘制这些矩形,它们只是确定了绘制的边界。)

Private Sub DrawPageHeader(ByVal g As Graphics)   'create the header rectangle   Dim headerBounds As New RectangleF(c_LeftMargin, c_TopMargin, _      m_PageWidthMinusMargins, m_DataGrid.HeaderFont.SizeInPoints + _      c_VerticalCellLeeway)   'draw the header rectangle   g.FillRectangle(New SolidBrush(m_DataGrid.HeaderBackColor), headerBounds)   Dim xPosition As Single = c_LeftMargin + 12 ' +12 for some padding   'use this format when drawing later   Dim cellFormat As New StringFormat()   cellFormat.Trimming = StringTrimming.Word   cellFormat.FormatFlags = StringFormatFlags.NoWrap Or _   StringFormatFlags.LineLimit   'find the column names from the TableStyle   Dim cs As DataGridColumnStyle   For Each cs In m_DataGrid.TableStyles(0).GridColumnStyles      If cs.Width > 0 Then         'temp width to draw this column         Dim columnWidth As Integer = cs.Width         'scale the summary column width         'note: just a quick way to fit the text to the page width         'this is not the best way to do this but it handles the most         'common ui path for this demo app         If cs.MappingName = "TaskSummary" And m_IsTooWide Then            columnWidth -= m_AdjColumnBy         ElseIf cs.MappingName = "TaskSummary" Then            columnWidth += m_AdjColumnBy         End If         'create a layout rectangle to draw within.         Dim cellBounds As New RectangleF(xPosition, c_TopMargin, columnWidth, _            m_DataGrid.HeaderFont.SizeInPoints + c_VerticalCellLeeway)         'draw the column name         g.DrawString(cs.HeaderText, m_DataGrid.HeaderFont, New SolidBrush(m_DataGrid.HeaderForeColor), cellBounds, cellFormat)         'adjust the next X Pos         xPosition += columnWidth      End If   NextEnd Sub

绘制行类似于绘制页眉,不同之处在于后者首先循环 DataTable,并且对于每个 DataRow,都循环通过所有的列以确定是否应当显示该单元格中的值 (width > 0)。然后,检查列名以确定是应当直接打印文本值,还是打印对应于值的图像。

Expander 控件和 Expander 列表控件

Expander 类是包含 Priority 和 Overall Progress 图表与 Task History 面板的实际控件。ExpanderList 类是作为 Expander 对象的容器创建的。将任何控件添加到 ExpanderList 控件容器时,将执行类型检查。如果所添加的控件类型为 ExpanderExpanderList 对象将预订所添加的 Expander 对象的 ControlCollapsedControlExpanded 事件。事件处理程序将以编程方式调整所有 Expander 控件的 Location 属性,以便根据需要调整它们的上下位置。此外,ExpanderList 控件包含将拖放的 Expander 控件自动居中和定位的设计时支持。

    Public Sub ControlExpanded(ByVal x As XPander)        Dim ctl As Control        Dim enumerator As IDictionaryEnumerator = m_ControlList.GetEnumerator()        While enumerator.MoveNext            ctl = CType(enumerator.Value, Control)            If ctl.Top > x.Top Then                ctl.Top += x.ExpandedHeight - x.CaptionHeight            End If        End While    End Sub

虽然 ExpanderList 类和 Expander 类都是作为已编译的库包括在内的,我们所要演示的是如何使用 GDI+ 绘制 Expander 控件的蓝色倾斜顶边。注意:LinearGradientBrush 接受起始颜色 (Color.White) 和结束颜色(CaptionColor 表示 Color.FromArgb(198, 210, 248))。

Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)   Dim rc As New Rectangle(0, 0, Me.Width, CaptionHeight)   Dim b As New LinearGradientBrush(rc, Color.White, CaptionColor, _      LinearGradientMode.Horizontal)   'now draw the caption area at the top   e.Graphics.FillRectangle(b, rc)

Custom Chart 控件

CustomChartControl 类是作为已编译的库包括在内的,是通过设置 DataTableDataMember(列名)实现的,使用这些对象,该控件将绘制饼形图表扇区,这些扇区分别表示列的值的各个分解部分。在我们的示例中,该图表是使用 GDI+ 绘制的,用于显示当前项目的优先级分解图。

chartPriority.DataTable = m_DataLayer.DsTasks.TaskschartPriority.DataMember = "PriorityText"

Progress Chart 控件

与 Custom Chart 控件类似,Progress Chart 控件同样是作为已编译的库包括在内的,用于显示一个使用 GDI+ 绘制的矩形图表,以表示当前项目的平均进度。

chartProgress.DataTable = m_DataLayer.DsTasks.TaskschartProgress.DataMember = "Progress"

History Panel 控件

TaskHistoryPanel 类是一个简单的控件,它迭代通过任务历史记录行的 DataView,并为每个与当前 TaskID 匹配的行以编程方式向 UI 中添加一个可单击的 LinkLabel

向控件中添加每个 LinkLabel 时,将使用继承的 Control.Tag 来存储一个表示各个记录的整数,并预订代码中处理的 LinkLabel 单击事件。

'create a linklabelDim newLinkLabel As New LinkLabel()newLinkLabel.Text = datePrefix & CType(row.Item(m_DisplayMember), String)newLinkLabel.Location = New System.Drawing.Point(c_LinkLabelStartingX, _   (c_LinkLabelStartingY + (numLinks * c_LinkLabelHeight)))newLinkLabel.Size = New System.Drawing.Size((Me.Width - _   (c_LinkLabelStartingX * 2)), c_LinkLabelHeight)newLinkLabel.Name = "LinkLabel" & numLinksnewLinkLabel.Tag = (numLinks)newLinkLabel.TabStop = TruenewLinkLabel.FlatStyle = FlatStyle.System'add the link labelMe.Controls.Add(newLinkLabel)AddHandler newLinkLabel.Click, AddressOf LinkLabel_Click'increment the number of matching linksnumLinks += 1

在我们的单击事件处理程序中,我们向 Main 窗体引发了一个自定义事件,其中包括一些用以帮助确定实际单击了哪个历史记录的值,并最终将该历史记录显示给用户。

    Private Sub LinkLabel_Click(ByVal sender As Object, ByVal e As _        System.EventArgs)        're-raise the click event with our own parameters        Dim link As LinkLabel = CType(sender, LinkLabel)        RaiseEvent HistoryLinkClicked(m_SelectedTaskID, _            CType(link.Tag, Integer))    End Sub

DataProtection 类

由于客户端应用程序在注册表中存储密码信息,我们将需要一种防止潜在攻击者获取其他用户的密码的方法。完成这一任务的方法有许多种,但是我们选择了使用 Windows 2000/XP Data Protection API (DPAPI) 函数 CryptProtectDataCryptUnprotectData,这样,我们就能够在无需直接管理秘钥的情况下保护秘密信息。

我们项目中的 DataProtection 类实际上只是一个用以访问 DPAPI 函数的包装。下面提供了设置注册表项和检索未加密的文本的方法。

有关 DPAPI 的详细信息,请参阅 MSDN 上的 Windows Data Protection

'set the registry key value with the encrypted textDim regKey As RegistryKey = Registry.CurrentUser.CreateSubKey(c_RegistryKey)regKey.SetValue("Password", _   DataProtection.ProtectData(txtPassword.Text, "TaskVisionPassword"))'set the string value to decrypted registry key textDim password As String = String.Emptypassword = DataProtection.UnprotectData(CType(regKey.GetValue("Password"), _      String))

支持的功能

本地化支持 — 本地化是一个将应用程序的资源翻译为该应用程序将要支持的各个国家/地区文化(即语言和历法差异)的本地化版本的过程。.NET Framework 主要使用了资源管理器的概念、资源文件和附属程序集来提供应用程序本地化的体系结构。Visual Studio .NET 通过在 Windows 窗体设计器中设置若干属性,简化了创建这些资源文件和程序集的过程。TaskVision 版本 1.1 实现了德语的本地化,并包含一个附属程序集,每种本地化形式都可以从该程序提取资源和属性值。简而言之,每种形式都有一个默认的资源文件,其中包含适用于默认文化的属性和图像。此外,每种形式都有一个“<formname>.de.resx”文件,该文件包含适用于德国文化的属性和图像。这些文件通常由 Visual Studio .NET 维护,并且仅存储特定于各个形式的数据。而且,如果开发人员要存储自定义本地化字符串(例如,自定义的异常消息),就需要创建附加的资源文件。TaskVision 有两个这样的文件,它们是“localize.resx”和“localize.de.resx”,用于存储自定义字符串。有关本地化文件的详细信息,请参阅 MSDN 上的 Introduction to Resources and Localization

COM Interop — 与 COM 的互操作性,即 COM interop,使您能够使用现有的 COM 对象,并按照自己的节奏转换到 .NET 平台。TaskVision 演示了 COM Interop,方法是访问 Excel 10.0 类型库,然后实际上使 MS Excel 自动创建一个电子表格并使用 TaskVision 数据填充该表格。使用 COM interop 时,有两方面的事项需要注意。首先,必须在开发人员的计算机上安装软件(例如,本例中的 Excel)才可以引用 COM 对象。其次,开发人员应当预期到他们的软件的终端用户可能没有安装该软件(或必要的 COM 对象)的情况。有关 COM Interop 的详细信息,请参阅 MSDN 上的 Introduction to COM Interop

辅助功能支持 — 为了演示 Visual Studio.NET 中支持的一种主要辅助功能,我们浏览并设置了所有 UI 控件的 AccessibleDescriptionAccessibleName 属性。这些属性在诸如 Microsoft Narrator 等辅助功能应用程序(能够在运行时访问 UI 控件并在用户在应用程序中导航时准确地为用户读出说明)中扮演着重要角色。

动态属性 — 动态属性使您能够配置自己的应用程序,以便将其中一些属性值或全部属性值存储到一个外部配置文件中,而不是存储到已编译的代码中。通过向管理员提供对可能需要在未来进行更改的属性进行更新的方法,可以降低在部署应用程序后对应用程序进行维护的成本。例如,假定您要生成一个在开发过程中使用测试数据库的应用程序,并且,您需要在部署该应用程序时将其切换为产品数据库。如果将属性值存储到应用程序内部,您将需要在部署之前手动更改所有的数据库设置,然后重新编译源代码。如果在外部存储这些值,您只需在外部 XML 文件中进行一处更改,应用程序在下一次运行时即会加载新的值。TaskVision 演示了动态属性:它将 Updater Component Update URL 以及身份验证和数据 Web 服务这两者的 URL 存储到 TaskVision.exe.config 文件中。

Windows XP 主题 — Windows XP 中包含新版本的 Shell Common Controls 库(COMCTL32.DLL 版本 6.0)。该库中包含新的经过改进的彩色控件,例如,按钮和选项卡(请参考下面的图像)。要使用新版的公共控件,应用程序必须通过提供应用程序清单显式请求新的版本。该应用程序清单可以作为一个单独的文件或附加到可执行文件的资源来提供给 Windows XP。作为单独的文件提供时,该清单文件必须位于与可执行文件相同的目录中,并且必须具有与可执行文件相同的名称,名称后跟“.manifest”。例如,TaskVision.exe 的清单文件应当是 TaskVision.exe.manifest。除提供清单外,许多控件还要求将 FlatStyle 属性设置为 System,以便使用公共控件。

TaskVision 解决方案概述:设计与实现

图 18 不带清单文件的应用程序

TaskVision 解决方案概述:设计与实现

图 19 带有清单文件的应用程序

学习心得

TaskVision 是一个示例解决方案,旨在演示使用 .NET Framework 生成的智能客户端应用程序的众多强大功能。

像许多项目一样,在开发阶段,TaskVision 同样经历了一定的发展变化。下面,我们列出了如果现在重头开始开发的话,两处可能本应以不同方式实现的地方。

数据层组件

随着您对客户端应用程序的不断熟悉,您可能会发现我们的数据体系结构会导致一些轻微的负面 UI 效应。Windows 窗体控件支持数据绑定,提供了一种使控件使用来自数据源的值自动填充(并维护)自身的方式。由于我们使用的是 XML Web 服务,因此无法真正发送对象,修改它,然后自动更新所有引用它的控件。与此相对,我们的两个选择是:在每次 XML Web 服务调用后将返回的、已更新的 DataSet 与当前的 DataSet 合并,或者在每次 XML Web 服务调用后更新相关控件的数据绑定。我们选择了合并已更新和现有的 DataSet,原因是这可以保留数据绑定。举个例子,您可能会注意到由此产生的一些轻微的副作用 — DataGrid 中的当前记录在更新后会失去焦点。如果有机会重头开始,我们可能会选择在每次 XML Web 服务调用后更新数据绑定来更新数据,以避免这些副作用。

XML Web 服务安全性

很值得一提的是 Web Services Enhancements (WSE) 1.0 for Microsoft .NET,它是一个工具包,向 Visual Studio .NET 和 .NET Framework 开发人员提供了对最近提出的一些 XML Web 服务规范(包括 WS-Security、WS-Routing、WS-Attachments 和 DIME)的支持。但是,在开发时 WSE 尚未面市。因此我们未能演示某些对 WS-Security 的组件式支持如何能够在保护 XML Web 服务方面向开发人员提供更多灵活性和控制。我们计划在将于本年度后期发行的第二个重要示例应用程序中演示该功能。您可以在 MSDN 上的 WSE 主页中了解有关 WSE 的详细信息。

获得更多信息

完整的 TaskVision 文档和源代码
TaskVision Source Code Viewer
TaskVision 研讨论坛
Web Services Enhancements 1.0 for Microsoft .NET
本地化和全球化信息
.NET 开发人员中心
.NET Framework 产品站点
Visual Studio .NET 产品站点