TaskVision 解決方案概述:設計與實現

TaskVision 解決方案概述:設計與實現

*
本頁內容
概述 概述
解決方案體系結構 解決方案體系結構
學習心得 學習心得
獲得更多信息 獲得更多信息

概述

什麼是 TaskVision 解決方案?

TaskVision 是一個示例智能客戶端任務管理應用程序,它是使用 Microsoft®.NET Framework(一個至關重要的 Windows® 組件,支持運行下一代應用程序和 XML Web 服務)的 Windows 窗體類生成的。TaskVision 允許經過身份驗證的用戶查看、修改和添加與其他用戶共享的項目和任務。它可以在多種方案中使用,從錯誤跟蹤到管理工作程序或者客戶服務請求,都可以使用。它的主要用途是爲有興趣使用 .NET Framework 生成智能客戶端應用程序和 XML Web 服務的開發人員提供高質量的示例源代碼。圖 1 所示爲 TaskVision 應用程序。

wnf_TaskVision_01_thumb

圖 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)。這層加密可以防止潛在攻擊者的攻擊。)

wnf_taskvision_02

圖 2 TaskVision 應用程序體系結構

數據庫

所有的共享數據都存儲在 SQL Server 數據庫中。應用程序特定的數據或配置設置不包括在內。這使得開發人員能夠創建自定義應用程序,每個應用程序都從單個唯一的數據存儲提取數據。使用相同的服務器端功能,我們就可以以這種方式創建 .NET Compact Framework 版的 TaskVision,稱爲 Pocket TaskVision(將於三月發佈)。本部分提供了 TaskVision 解決方案中使用的數據庫的概述。

數據庫架構

TaskVision 的數據庫架構(如圖 3 所示)相當簡單,但是已經足以支持此任務管理解決方案。

wnf_taskvision_03

圖 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。

wnf_taskvision_04

圖 4 TaskVision 身份驗證過程

'create the ticket
Dim ticket As New FormsAuthenticationTicket(userID, False, 1)
Dim encryptedTicket As String = FormsAuthentication.Encrypt(ticket)

'get the ticket timeout in minutes
Dim configurationAppSettings As AppSettingsReader = New AppSettingsReader()
Dim timeout As Integer = _
   CInt(configurationAppSettings.GetValue("AuthenticationTicket.Timeout", _
   GetType(Integer)))

'cache the ticket
Context.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 If
End 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 ds
End Function

Windows 窗體智能客戶端

智能客戶端應用程序在本解決方案中是最顯眼的部分,這是因爲它是終端用戶用來管理項目和任務的工具。如前所述,TaskVision 用於演示一些關鍵的智能客戶端技術和方案。下面,我們將對其中的一些技術和方案進行逐個演示。對於這一部分,有一份很有價值的補充材料:TaskVision Source Code Viewer,它提供了對源代碼中許多更有趣部分的詳細分析。

用戶界面窗體

在深入研究核心技術及其使用方法之前,對應用程序中的各個部分 — 各種窗體及其用途 — 進行簡短的說明,可能很有必要。

Login 窗體(圖 5)使用戶能夠通過輸入他們的 TaskVision 憑據對自己進行身份驗證。(同樣,用於訪問此應用程序的默認登錄憑據爲“jdoe”和“welcome”,分別代表用戶名和密碼。)

wnf_taskvision_05

圖 5 TaskVision 登錄窗體

Main 窗體(圖 6)顯示了從數據 XML Web 服務(或從脫機文件)檢索的數據。Main 窗體充當了我們的事件驅動的應用程序的基礎,並且是用戶體驗的核心場所。該窗體本身主要包括一個主菜單、一個帶按鈕的工具欄、幾個帶有 ComboBox、圖表和鏈接的自定義面板、一個 DataGrid、預覽窗格的另一個自定義面板、兩個分隔條和一個用於顯示活動信息(例如,項的數量和聯機狀態)的狀態欄。

在佔據窗體大部分空間的 DataGrid 中,可以看到數據庫中爲當前選定的項目列出的任務的摘要。在 DataGrid 下,顯示了選定任務的更爲詳細的信息。DataGrid 左側是用於選擇其他項目和篩選所顯示的任務的控件。這些控件下方有兩個 GDI+ 圖表,顯示有關任務的信息。在它們下方,有一個顯示選定任務的歷史記錄(有關其創建與任何修改的信息)的控件,在本屏幕快照中,該控件不可見。在窗體頂部,有一些菜單和按鈕,用於管理 TaskVision 用戶、切換語言、創建新項目和任務、將 DataGrid 中的信息導出到 Excel,以及脫機工作(或者在應用程序當前處於脫機狀態時,聯機工作)。

wnf_TaskVision_06_thumb

圖 6 TaskVision Main 窗體

雙擊 Main 窗體的 DataGrid 中的任務,將顯示 Edit Task 窗體(圖 7)。通過可由用戶編輯的控件,該窗體使用戶能夠修改任務 — 它的截止日期、負責它的工作人員以及任務的優先級、摘要、說明和當前完成任務的進度。此外,這一雙功能窗體還可以在 History 選項卡(圖 8)上顯示給定任務的任何關聯歷史記錄。應當注意,當用戶單擊 Main 窗體上的 New Task 按鈕或者單擊 File 菜單下的 New Task 項時,將啓動相同的窗體來定義新任務。

wnf_taskvision_07

圖 7 Edit task

wnf_taskvision_08

圖8 Edit Task 的 History 選項卡

Manage Users 窗體(圖 9)是通過 Main 窗體頂部的 Manage 菜單中的 Users 項啓動的。它列出了應用程序的所有用戶。Edit 按鈕將啓動 Edit User 窗體(圖 10),該窗體用於對用戶進行更改。請注意,僅當您作爲 TaskVision 管理員登錄時,Manage 菜單項纔可用。

wnf_taskvision_09

圖 9 Manage Users 窗體

wnf_taskvision_10

圖 10 Edit User 窗體

Change Password 窗體(圖 11)通過 File 菜單中的 Change Password 項啓動,用於更改當前用戶的密碼,不要求 TaskVision 管理員特權。僅當應用程序處於聯機模式時,該功能纔可用。

wnf_taskvision_11

圖 11 Change Password 窗體

Search 窗體(圖 12)通過 View 菜單中的 Search 項或通過單擊 Main 窗體頂部的 Search 按鈕啓動,用於在當前項目內的所有任務中執行簡單的子字符串搜索。

wnf_taskvision_12

圖 12 Search 窗體

Customize Columns 窗體(圖 13)通過 View 菜單中的 Customize Columns 項啓動,用於操作 DataGridTableStyle,使用戶能夠控制列布局的外觀和感覺。

wnf_taskvision_13

圖 13 Customize Columns 窗體

Add Project 窗體(圖 14)通過 Manage 菜單中的 Add Project 項啓動,可供 TaskVision 管理員向遠程數據庫添加新項目。

wnf_taskvision_14

圖 14 Add Project 窗體

數據層組件

DataLayer 類是 XML Web 服務包裝,並且是客戶端應用程序的數據管理器。

對於應用程序本身來說,有一個與數據處理有關的可見結構和設計模式。圖 15 顯示了與 DataLayer 類和窗體類有關的對象所有者關係。

當 Main 窗體處理事件(例如,打開 Search 窗體)時,DataLayer 對象將傳遞給新的窗體,從而提供對 Main 窗體有權訪問的數據的訪問權。

wnf_TaskVision_15_thumb

圖 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.Success
End Function
Public Enum DataLayerResult
    None = 0
    Success = 1 
    ServiceFailure = 2
    UnknownFailure = 3
    ConnectionFailure = 4
    AuthenticationFailure = 5
End 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 Try
End Function

Public 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.Success
End 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
)
AS
SET 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 窗體)中顯示了兩組值,允許用戶決定繼續進行更改,還是取消操作,保留數據庫中的當前值。

wnf_taskvision_16

圖 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 Try
Loop

脫機 — 聯機數據模型

處於聯機模式時,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 所示爲其最基本的形式)。

wnf_taskvision_17

圖 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 Nothing
End 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 If
End If
End 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
   Next
End 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.Tasks
chartPriority.DataMember = "PriorityText"

Progress Chart 控件

與 Custom Chart 控件類似,Progress Chart 控件同樣是作爲已編譯的庫包括在內的,用於顯示一個使用 GDI+ 繪製的矩形圖表,以表示當前項目的平均進度。

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

History Panel 控件

TaskHistoryPanel 類是一個簡單的控件,它迭代通過任務歷史記錄行的 DataView,併爲每個與當前 TaskID 匹配的行以編程方式向 UI 中添加一個可單擊的 LinkLabel

向控件中添加每個 LinkLabel 時,將使用繼承的 Control.Tag 來存儲一個表示各個記錄的整數,並預訂代碼中處理的 LinkLabel 單擊事件。

'create a linklabel
Dim 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" & numLinks
newLinkLabel.Tag = (numLinks)
newLinkLabel.TabStop = True
newLinkLabel.FlatStyle = FlatStyle.System

'add the link label
Me.Controls.Add(newLinkLabel)
AddHandler newLinkLabel.Click, AddressOf LinkLabel_Click

'increment the number of matching links
numLinks += 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 text
Dim regKey As RegistryKey = Registry.CurrentUser.CreateSubKey(c_RegistryKey)
regKey.SetValue("Password", _
   DataProtection.ProtectData(txtPassword.Text, "TaskVisionPassword"))

'set the string value to decrypted registry key text
Dim password As String = String.Empty
password = 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,以便使用公共控件。

wnf_taskvision_18

圖 18 不帶清單文件的應用程序

wnf_taskvision_19

圖 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 產品站點

轉到原英文頁面


<?xml version="1.0" encoding="utf-16"?>
©2004 Microsoft Corporation. 版權所有.  保留所有權利 |商標 |隱私權聲明
Microsoft
'); top.document.title = self.document.title;
trans_pixel.asp?source=www&TYPE=PV&p=china_MSDN_library_enterprisedevelopment_softwaredev&r=%2fchina%2fMSDN%2flibrary%2fenterprisedevelopment%2fsoftwaredev%2fSCdnwinformswnftaskvision.mspx
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章