TaskVision Solution Overview: Design and Implementation

 

Vertigo Software, Inc.

February 2003

Applies to:
   .NET Framework
   Windows Forms

Summary: This article describes the design and architecture decisions for the TaskVision solution sample, which demonstrates the use of .NET Framework's Windows Forms classes with XML Web Services to build a smart client task management application. (39 pages)

Note: As of 5/7/ 2003, code for this application is temporarily unavailable. We will repost it soon.

Download the code for this solution:
   TaskVision Client
   TaskVision Server
   TaskVision Source

Contents

Overview
Solution Architecture
Lessons Learned
For More Information

Overview

What is the TaskVision Solution?

TaskVision is a sample smart client task management application built using the Windows Forms classes of the Microsoft® .NET Framework—an integral Windows® component that supports building and running the next generation of applications and XML Web services. TaskVision allows authenticated users to view, modify, and add projects and tasks that are shared with other users. It may be used in any number of scenarios, from bug tracking to managing work orders or customer service requests. Its primary purpose is to provide quality, sample source code to developers interested in building smart client applications and XML Web services using the .NET Framework. Figure 1 shows the TaskVision application.

Click here for larger image

Figure 1   TaskVision interface

The TaskVision solution demonstrates many technologies provided by the .NET Framework including:

  • Application offline and online model
  • Application update model via HTTP (no-touch deployment)
  • Authorization to control user access to application features
  • Data collision handling
  • Printing and Print Preview
  • Windows XP Themes
  • Dynamic properties
  • Localization support
  • Accessibility support (limited)
  • Forms authentication using a database for user names/passwords
  • Asynchronous XML Web service calls
  • ADO.NET data access using SQL stored procedures
  • Graphics development using GDI+
  • Integration between .NET Framework-based code and COM applications (COM interop)

This white paper discusses TaskVision in depth and provides insight into its architecture from the perspective of those who developed the solution. In addition, this document explains how TaskVision can be used as a template for building smart client applications by examining many of the key application features and the technology used to implement them. Complete info about TaskVision can be found at the TaskVision home page.

The TaskVision Solution was developed using Microsoft® Visual Studio .NET™ and is written using the C# and Visual Basic .NET programming languages. TaskVision has also been ported to the PocketPC platform. For information about how the PocketPC version of TaskVision was built, see Creating the Pocket TaskVision Application on MSDN.

Getting Started with TaskVision

The easiest way to see TaskVision in action is to download and install the TaskVision Live Client v1.0 MSI. The Live Client contains the compiled executable and is configured to send and retrieve the data it needs from a public XML Web service.

The counterpart to the client application is the TaskVision Server v1.1 MSI. The Server installation creates the database and installs the XML Web services and the Web site hosting the client v1.1 update which will be retrieved and executed via no-touch deployment ("https://localhost/TaskVisionWS" and "https://localhost/TaskVisionUpdates," respectively). This installation is ideal for those wishing run the entire solution locally without compiling code.

For those desiring hands-on access to the source code we've released the TaskVision Source Code v1.1 MSI, which creates the database and installs the source code for v1.1 of the client application (the version that is downloaded and executed via no-touch deployment in the case of the TaskVision Live Client) as well as the source code of the the XML Web services. Those not having the software necessary to install this MSI may view some of the source code online via the TaskVision Source Code Viewer.

For MSI system requirements and installation instructions please refer to the instructions below.

Note   The TaskVision Server v1.1 MSI cannot be installed on the same computer as the TaskVision Source Code v1.1 MSI, because both installations utilize the same database and IIS virtual directory names.

TaskVision Live Client v1.0 MSI Setup

Download and install the appropriate MSI. Upon starting the application (from the Start Menu), you'll be prompted with a standard login screen where you'll enter "jdoe" for the user name and "welcome" for the password. After logging in, you're free to modify data and test application functionality but please note that all data on the public server is reset nightly, so changes made on one day will not appear on the next day.

Minimum Requirements:

  • Windows 2000/XP or above
  • Microsoft .NET Framework 1.0
  • LAN/Dialup Internet Connection
  • Microsoft Excel 2002 (recommended; not required—you'll need this to export data to Excel to see COM interop in action)

This application does not support ISA clients or Web proxies.

TaskVision Server v1.1 MSI Setup

Before installation, ensure that the following conditions have been met:

  • You have administrator privileges on the local computer.

  • The SQL Server default instance is named "local" (this is installed by default) and that your Windows account has SQL Server administrator privileges for the database using integrated security.

  • The ASP.NET file extensions (.aspx and .asmx) must be registered with Internet Information Services (IIS). (In the case that IIS was installed after the .NET Framework was installed, you must run the following application "C:\WINDOWS\Microsoft.NET\Framework\v1.0.3705\aspnet_regiis.exe –i".)

  • Finally, ensure that both the SQL Server and IIS services are running. Download and install the appropriate MSI. Once this installs the XML Web services, update Web site, and database, please navigate to the directory containing the TaskVision client application (installed via the TaskVision Live Client v1.0 MSI) and edit the TaskVision.exe.config file to point to the local server URLs, as shown below. (This file can be found in the "1.0.0.0" and, if applicable, "1.1.0.0" sub-directories.)

    Note   In the example below, we use "localhost" as the server. This is appropriate if both the client and server applications run on the same machine. If the server is running on the separate machine from the client, use that machine's URL in place of "localhost." If the server is running on a separate machine from the client, you'll need to make this same change on the TaskVision.exe.config file located in the TaskVision Server directory, under "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="https://localhost/taskvisionupdates/updateversion.xml"/>
    <add key="TaskVision.AuthWS.AuthService" value="https://localhost/taskvisionws/authservice.asmx"/>
    <add key="TaskVision.DataWS.DataService" value="https://localhost/taskvisionws/dataservice.asmx"/>
    </appSettings>
    </configuration>
    

Minimum Requirements:

  • Windows 2000/XP or above
  • Microsoft .NET Framework 1.0
  • IIS 5.0+
  • SQL Server 2000

TaskVision Source Code v1.1 MSI Setup

Before installation, ensure that the following conditions have been met:

  • You must have administrator privileges on the local computer.
  • The SQL Server default instance is named "local" (this is installed by default) and that your Windows account has SQL Server administrator privileges for the database using integrated security.
  • The ASP.NET file extensions (.aspx and .asmx) must be registered with IIS. (In the case that IIS was installed after ASP.NET, you must run the following application "C:\WINDOWS\Microsoft.NET\Framework\v1.0.3705\aspnet_regiis.exe –i".)
  • Finally, ensure that both the SQL Server and IIS services are running.

Download and install the appropriate MSI. Once this installs the XML Web services and database, use Visual Studio .NET to open the TaskVision solution. Please note that if Excel 2002 is not installed, the solution will not compile until the "Export to Excel" related code (in the ExportExcel class & Main form class) has been removed or commented out.

Upon compiling the project and starting the application, you'll be prompted with a standard login screen where you'll enter "jdoe" for the user name and "welcome" for the password.

Minimum Requirements:

  • Windows 2000/XP or above
  • Visual Studio .NET 2002
  • IIS 5.0+
  • SQL Server 2000
  • Microsoft Excel 2002 (recommended)

Solution Architecture

There are three primary components to our task management solution: the database, the XML Web services, and the smart client application built using the Windows Forms classes (see Figure 2).

The database is accessed by the XML Web services which only have permissions to run stored procedures on the database. By limiting what the XML Web services can access on the database we ensure that only our queries are run on the database.

The XML Web services in our sample solution were implemented to run on a public server and are accessible to any application via the Internet. They can, of course, be run on a company intranet, thus confining data access to an internal network. (Note: while the XML Web services are accessible to any application with access to their server, they may only be used by those that can supply a valid user name and password.)

The smart client application authenticates the user by passing the username and password to the Authentication XML Web service. Upon successful authentication, the XML Web service passes back an encrypted ticket to the smart client application which is stored and then submitted to the data XML Web service with each future request for data. The Data XML Web service validates the encrypted ticket and processes the data request. (Note: In your own solutions, in situations where the credentials or an encrypted ticket being passed via XML Web services can enable access to sensitive information or resources or in situations where sensitive data itself is being passed back and forth, we recommend you use Secure Sockets Layer (SSL). This layer of encryption guards against would-be attackers.)

Figure 2   TaskVision application architecture

Database

All of the shared data is stored in a SQL Server database. This does not include application specific data or configuration settings. This allows developers to create custom applications, each pulling from a single unique data store. This is how we were able to create a .NET Compact Framework version of TaskVision, called Pocket TaskVision (which will be released in March), using the same server-side functionality. This section provides an overview of the database used in the TaskVision solution.

Database Schema

The TaskVision database schema, shown in Figure 3, is fairly simple, yet ample enough to support this task management solution.

Figure 3   TaskVision database schema

Stored Procedures

The TaskVision solution uses stored procedures to encapsulate all of the database queries. Stored procedures provide a clean separation between the database and the middle-tier data access layer. This in turn provides easier maintenance, since changes to the database schema will be invisible to the data access components. Using stored procedures can also provide some performance benefits in certain architectural scenarios thanks to caching in the database and the fact that doing some of the processing locally in the database can reduce the number of network requests necessitated.

XML Web Services

Effectively acting as the primary middle tier, the combined XML Web services handle authentication and data requests from any client application that accesses them.

We chose to separate the XML Web service features in to two categories:

  • Authentication—where clear text credentials would be submitted to provide login information and could be configured to run under SSL (although not currently implemented in this sample).
  • Data—where non-critical data could be sent and received (after some form of authentication) without the overhead of SSL. If this solution was used in a real world application, the Data XMl Web services could be run under SSL to prevent potential attackers from accessing the serialized data.

Authentication XML Web Service

The authentication service (Figure 4) works on very simple principle: validate the user name and password against the database (using a stored procedure), and then return a unique encrypted ticket with the user ID embedded. If the user name and password fail then nothing is returned.

The value of the ticket is cached (in the Web application's static cache object) for two minutes on the server after it is issued. This allows us to maintain a server-side list of recently issued tickets that can be accessed by any code running in the same application domain (as demonstrated later by the data service). Because tickets are only maintained in this list for two minutes, client applications are forced to re-authenticate often, which helps to prevent "replay attacks"—situations in which an attacker sniffs a ticket off the network and uses it impersonate the validated user.

The ticket is created by using the System.Web.Security.FormsAuthenticationTicket class that we chose because of its ability to embed data, such as the user ID, within the ticket itself.

Figure 4   TaskVision authentication process

'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)

Data XML Web Service

The Data XML Web service provides the functionality for client applications to retrieve and change data, and, with the help of the authentication service, is able to validate each request back to a user.

Both XML Web services are run within the same application domain (in this case, the same IIS Web application) which allows the Data service to access the same cache memory that Authentication service used to save copies of the valid authentication tickets.

Every public Web method supported by the Data service requires the authentication ticket to be passed in with the call. Before any data is returned, the ticket is checked for its existence in the cache. If the ticket exists, we know that the user name and password were validated within the last two minutes; otherwise the ticket is invalid or expired. As an extra security measure, we extract the embedded user ID from the ticket and validate the user ID against the database to ensure that the user account was not locked out (by another administrator) and that it has TaskVision administrator privileges for functionality requiring administrator status.

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

<WebMethod()> _
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 Form Smart Client

The smart client application is the most visible piece in this solution because it is the tool through which end users manage projects and tasks. As mentioned earlier, TaskVision is intended to showcase a number of key smart client technologies and scenarios. We'll walk through many of these below. A valuable resource complementing this section is the TaskVision Source Code Viewer, which provides detailed analysis of many of the more interesting sections of the source code.

User Interface Forms

Before diving into the core technologies and how they were used, it may be useful to briefly walk through the application—the various forms presented and the purpose of each.

The Login form (Figure 5) allows users to authenticate themselves by entering their TaskVision credentials. (Again, the default login credentials to access this application are "jdoe" and "welcome," representing the user name and password, respectively.)

Figure 5   TaskVision login form

The Main form (Figure 6) displays the data retrieved from the Data XML Web service (or from the offline files). The Main form sets the foundation for our event driven application and is the core of the user experience. The form itself consists primarily of a main menu, a toolbar with buttons, several custom panels with ComboBoxes, charts, and links, a DataGrid, another custom panel for the preview pane, two Splitters, and a status bar to display active information, such as number of items and online status.

In the DataGrid occupying the bulk of the form, you'll see a summary of the tasks listed in the database for the currently selected project. Below the DataGrid is a more detailed display of the selected task. To the left of the DataGrid are controls for selecting other projects and filtering the tasks displayed. Below that are two GDI+ charts displaying information about the tasks. Beneath those, not visible in this screenshot, is a control displaying the selected task's history—information about its creation and any modifications. At the top of the form, are menus and buttons to manage TaskVision users, switch languages, create new projects and tasks, export the information in the DataGrid to Excel, and work offline or, if the application is currently offline, work online.

Click here for larger image

Figure 6   TaskVision main form

Double-clicking a task in the Main form's DataGrid will bring up the Edit Task form (Figure 7). Through user editable controls, this form enables users to modify a task—its due date, the worker responsible for it, as well as the task's priority, summary, description, and progress thus far in completing the task. Additionally, this dual-purpose form displays any associated history for a given task on the History tab (Figure 8). It should be noted that this same form is launched to define a new task when the user clicks the New Task button on the Main form or clicks the New Task entry under the File menu.

Figure 7   Edit Task

Figure 8   Edit Task, History tab

The Manage Users form (Figure 9) is launched via the Users entry in the Manage menu atop the Main form. It lists all users of the application. The Edit button raises the Edit User form (Figure 10), which allows changes to the users. Note that the Manage menu item is only available while logged in as a TaskVision administrator.

Figure 9   Manage Users form

Figure 10   Edit User form

The Change Password form (Figure 11), launched via the Change Password entry in the File menu, is used to change the password of the current user and does not require TaskVision administrator privileges. The feature is only available while the application is in online mode.

Figure 11   Change Password form

The Search form (Figure 12), launched via the Search entry in the View menu or by clicking the Search button atop the Main form, performs simple substring searches through all the tasks within the current project and displays the results back to the user.

Figure 12   Search form

The Customize Columns form (Figure 13), launched via the Customize Columns entry in the View menu, manipulates the TableStyle of the DataGrid allowing users to control the look and feel of the column layout.

Figure 13   Customize Columns form

The Add Project form (Figure 14), launched via the Add Project entry in the Manage menu, allows TaskVision administrators to add new projects to the remote database.

Figure 14   Add Project form

Data Layer Component

The DataLayer class is the XML Web services wrapper and the data manager for our client application.

In terms of the application itself, there is a visible structure and pattern of design pertaining to the handling of data. Figure 15 shows the object-owner relationship in regard to the DataLayer class and the form classes.

As the Main form handles events such as opening the Search form, the DataLayer object is passed to the new form, providing access to the same data that the Main form is privileged to.

Click here for larger image

Figure 15   Object-owner relationships in the TaskVision class hierarchy

Project information, task information, user information, and all other information retrieved from the XML Web services are owned by the DataLayer class. The data is accessible through public members of the DataLayer class and the various UI forms are free to read and change this local data. The act of updating or retrieving data from the XML Web services can only be accomplished by using public methods in the DataLayer class. Such public methods include: GetProjects, GetTasks, and UpdateTasks.

The DataLayer class was designed to be used in a single threaded environment, and by calling these methods on the main thread, we're able to ensure that information retrieved from the XML Web service calls is properly merged into our local data synchronously and that our data bound UI controls do not refresh their graphics on a background thread.

Most of the public methods (such as the code below) follow a similar design: request (or send) the data with the current authentication ticket from (or to) the Data XML Web service, re-authenticate and handle any exceptions if necessary, merge any returned data, and then return a DataLayerResult back to the calling code to indicate the success or failure of the operation.

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

The Enum above is explained as follows:

  • DataLayerResult.Success means that the public method was successful in its purpose.
  • DataLayerResult.ServiceFailure means that an exception has occurred at the XML Web service and within the XML Web service's code itself.
  • DataLayerResult.ConnectionFailure means that a problem connecting to the XML Web service has occurred (it could be the local Internet connection or the responsiveness of the XML Web service).
  • DataLayerResult.AuthenticationFailure is used when the current user name and password (which are set by the Login form) are no longer valid.

Up until this point, all the XML Web service calls described are performed synchronously in the application's main thread. There are two instances in the application where we implemented asynchronous XML Web service calls (i.e., the calls were performed on a separate thread from the main application thread). This enables the application to function as normal while it waits for the asynchronous XML Web service to complete its work in the background.

The first scenario is retrieving the project history—the list of all changes made to all tasks in that project. As you might imagine, this can easily amount to a lot of data and substantial download time. Therefore, it is desirable to retrieve this read-only data in the background without hindering the application.

Our Main form contains a timer that periodically updates the history information. Calling the BeginGetProjectHistory method returns an IAsyncResult object that's checked on future timer tick events until the call is complete. Once complete, calling the EndGetProjectHistory method will finish the process of merging our data, similar to the previous synchronous methods discussed.

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

The second asynchronous XML Web service call in TaskVision comes into play in updating the tasks DataSet used by the Main form to populate the DataGrid.

The Main form contains another timer that we use to update our tasks DataSet. The only real difference here is when the asynchronous call is actually made. Rather than a periodic or forced XML Web service call, this timer is constantly being stopped and reset while the user is modifying data. Should the application sit idle long enough for the timer to tick, the asynchronous request is started and with each future timer tick, the request is checked for completion. If the request is complete, the data is merged with the local data. If the user should make any changes (effectively updating the data through interaction) while the asynchronous request is in action, the request is discarded because it is now outdated.

Data Collisions

There are numerous ways to handle data collisions. These common scenarios occur when a client attempts to update or delete data in a database that have been changed since the last time the client accessed them or which simply doesn't exist. Oftentimes this is handled by raising an error or alternately, by simply overwriting whatever is in the database with the client's version of the record. The first scenario invalidates the client's work. The latter scenario runs the risk of ignoring and deleting important data entered since the client last checked the database. TaskVision introduces a simple solution to the problem, relying heavily on functionality in the DataSet objects of the ADO.NET library in the .NET Framework. (A DataSet is an object containing a cache of the data retrieved from the database.)

To manage the DataSet of tasks for TaskVision, we chose to use the SQLDataAdapter class in the System.Data.SqlClient namespace that allows us to encapsulate the select, update, insert, and delete functionality into one object. The Update method of DataAdapter will inspect the RowState of each DataRow within the DataSet, determine if the DataRow is new, deleted, or changed, and then execute the appropriate stored procedure. The DataAdapter then ensures that the count of affected rows in the database is greater than zero. (For update and delete operations, not being greater than zero logically suggests that the stored procedure was unsuccessful in locating the target data.)

It's important to realize that the Update stored procedure will only update the record if it can verify that the record in the database has not been altered since a copy of it was stored in the client's DataSet—i.e., that no data collisions exist here. The stored procedure is as follows:

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

As you can see above, the WHERE clause is quite thorough in its check for data collisions. You may be wondering now where the "original" values used to check for data collisions in the WHERE clause come from. By default, each DataRow in a DataSet will keep track of both the original value returned from the database—when the DataSet was originally created—as well as the current value being updated by the user.

In our implementation of the SQLDataAdapter, it stops updating when a DataRow returns zero affected rows and throws a DBConcurrency Exception. It is at this exception that we handle a data collision.

Referring to the code block below, you can see that we enter a loop (which is explained shortly), and if there aren't any exceptions, we exit the loop. Should we catch a DBConcurrency Exception, we first try to retrieve the task record (by TaskID) and determine if the database record still exists or whether it was actually deleted. Deletions are fairly simple to deal with, since database records are not physically deleted by our client application and can only be deleted by the system administrator, we rule that deletion wins and thus our client loses the record and any pending changes. The reason for the initial loop is purely to ensure that we delete the DataRow and restart the update process without exiting the method call and returning to the client (since this code is executed in the XML Web service). If the record still exists, we know that it didn't match the WHERE clause and we should allow the user to make a decision about the record. The task at hand now is to return the pending changes and the new values in the database back to the user.

The original values that the user thought they were changing are no longer needed because the database record is already updated with new values made by another client. With this in mind, and the fact that the DataRow can hold two sets of values, we make a copy of the pending changes, apply the new values, and re-copy the pending changes back to the DataRow. The DataRow now contains the latest database entry as well as the user's pending changes. The original values are effectively copied over. Having done this, we return the DataSet back to the TaskVision smart client application from the XML Web services so it can display an error to the user (Figure 16). Our application displays both sets of values in a form (the Collision form), allowing the user to decide to go ahead with the change or cancel the action in favor of the values currently in the database.

Figure 16   Conflict resolution form

'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

Offline-Online Data Model

While in the online mode, the TaskVision client application manages all data in memory and relies on XML Web service calls to validate data changes as each change is made by the end user. However, the TaskVision client application also supports the idea of an offline mode.

The offline mode is manually invoked by the end user (by clicking the offline tool bar button). A few things occur when this action is a taken:

  • First, the DataSets are persisted as XML to the local hard drive. It is important to realize that at this point the data represents the last known state of the database.
  • Second, a global Boolean object is set to false which later prevents the application from sending changes to the XML Web services, (because the data is maintained locally until the user attempts to go back online).
  • Lastly the GUI is updated to reflect its offline state.

While the application is running in the offline mode, changes are saved to the DataSets and affected DataRows are marked as "Changed."

If the user exits the application while in offline mode, the changed DataRows are persisted to disk in a separate XML file (remembering that the three primary DataSets (Projects, Tasks, and LookupTables) were already saved as noted above).

If offline data exists (when the application is loaded again) it is assumed that the last state was offline mode. The XML files are used to populate the primary DataSets, and then the application checks for the change file (note how the AcceptChanges method is not called for this XML file). By not calling the AcceptChanges method, these merged rows remain marked as "Changed," giving the application the same data values it had previously (before the user exited the application).

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

Later should the end user choose to go back online, the tasks DataSet will be sent the Data XML Web service and each change will be processed normally (ending in a result back to the client application). Providing the XML Web service connection was successful, the application will set the global Boolean object back to true and not prevent future XML Web service requests.

.NET Updater Component

The .NET Application Updater Component enables a .NET Framework smart client application to update itself automatically by downloading a newer version once it becomes available on a remote Web server.

There are actually two pieces to work this magic: a stub (or helper) executable and a component that is built into the smart client application itself.

The stub executable, AppStart.exe, is responsible for starting the proper version of TaskVision application. (Note that the shortcut icon installed from the client MSI actually points to the AppStart.exe file and not the TaskVision.exe file.)

The stub executable works by reading a local configuration file, AppStart.config, to determine the location of latest version of the smart client application. It then starts a new process to run TaskVision.exe located in the directory named in the config file. Once it has launched TaskVision.exe in this new process, it simply sits back and waits for the process to close.

While the TaskVision smart client application is running, the component mentioned earlier works behind the scenes, to find out if an update is available for the application and to download that update and redirect the stub executable so that it launches the updated version instead of the original version.

The component accomplishes this by polling an XML file, UpdateVersion.xml, located on the server.

If the version number listed in this file is greater than the local TaskVision application version, then the component will follow the path in the UpdateVersion.xml file to the new version files, create a new local directory, and download those new version files into it. After the download is complete, the component will edit the local configuration file to redirect the stub executable to this new local directory with the updated version (so the next time the stub executable is run, it will start up the latest version).

We have configured this component to alert the user when a new version has been downloaded, offering the opportunity to restart the application and load the update or simply continue with the application session, seeing the updated version the next time the executable stub is launched.

DataGrid Column Styles

The DataGrid class in the Windows Forms library is a ready-to-use control that displays information similar to a basic spreadsheet (as seen in Figure 17 in its most basic form).

Figure 17   The DataGrid class

By applying DataGrid table styles, developers can customize the look and functionality of each column. We created three custom column classes and applied them to our TableStyle to handle our UI needs.

The first goal was to re-write the functionality of the DataGridTextBoxColumn class that's included in the .NET Framework to handle regular text columns. By default, the DataGridTextBoxColumn highlights the text within a cell, when the cell is clicked on, allowing the user to copy the text.

To prevent this, our DataGridTextBoxColumn class (which is first derived, i.e. inherited, from the Framework DataGridTextBoxColumn class) had to override one of the base class Edit methods and by doing nothing we preempted the cell from gaining focus and the text from being copied.

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

Next was the DataGridPriorityColumn class, which displays the priority images within our DataGrid. The DataGridPriorityColumn class assumes that the value provided is the file name of the .gif image that should be displayed, and that it should exist in the application's image directory.

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

Last is the DataGridProgressBarColumn class that displays the progress bar representing the progress of each task (mapped to our Progress column in the DataTable). For this, we used the Graphics object in the Paint method to draw a colored rectangle based on the value provided and the string representation of the value (e.g., "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

Printing and Print Preview

Creating a document to print is fairly straightforward in the .NET Framework. There are three classes that we should get familiar with: the PrintDialog class, the PrintPreviewDialog class, and the PrintDocument class.

The PrintDialog class provides us with a prompt to print and also grants access to the printer settings, the PrintPreviewDialog class prints the document to the screen for the user to view before sending anything to the printer, and the PrintDocument class contains the actual output to print as well as the ability to start the printing process.

Displaying a PrintDialog is simple: set the document property to reference our PrintDocument and call the ShowDialog method. The PrintDialog class doesn't automatically print the output; rather we check the DialogResult and call the Print method of the PrintDocument. Displaying a PrintPreviewDialog is just as simple, except there isn't a DialogResult to check. If the user wishes to print from within the dialog, the dialog will call the Print method for us.

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

Now that we know how to print, let's examine how TaskVision creates the actual output to print. Typically, you can create an instance of the PrintDocument class, set the properties that describe how to print, and call the Print method to start the printing process. Then you can handle the PrintPage event, where you specify the output to print, by using the Graphics object included in the PrintPageEventArgs. TaskVision handles the PrintPage event and passes the Graphics object to a class we created called DataGridPrinter, as we want to demonstrate how to print the information displayed in our DataGrid.

The DataGridPrinter class breaks the task of the drawing the output into two pieces, first the header of the page (the column names) and then all of the rows containing data.

To draw the page header (see code below), we created a rectangle and used the Graphics object to draw the rectangle with a gray background. Then we looped through the DataGrid columns to find the columns currently displayed (width > 0). For each displayed column, we created a rectangle to show us where to draw, and then we used the Graphics object to actually draw the column name within the rectangle. (Note, these rectangles don't get drawn, rather they only establish the bounds within which we will draw.)

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

Drawing the rows is similar to drawing the page header with the exception that we started by looping the DataTable and for each DataRow; we looped the columns to determine whether the value in that cell should be displayed (width > 0). From there, we examined the column name to determine if we should simply print the text value, or perhaps print an image that corresponds to the value.

Expander Control and Expander List Control

The Expander class is the actual control that contains the Priority and Overall Progress charts and Task History panel. The ExpanderList class was created as a container for Expander objects. When any control is added to the ExpanderList control container, a type check is performed. If the added control is of type Expander, the ExpanderList object will subscribe to the ControlCollapsed and ControlExpanded events of the added Expander object. The event handlers will programmatically adjust the Location properties of all Expander controls in order to slide them up and down as needed. Additionally, the ExpanderList control contains design time support to automatically center and position Expander controls that are dragged and dropped.

    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

Although both the ExpanderList class and the Expander class are included as compiled libraries, we wanted to show how to draw the gradient blue top of the Expander control using GDI+. Note: LinearGradientBrush accepts the starting color (Color.White) and the ending color (CaptionColor represents 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 Control

The CustomChartControl class is included as a compiled library and is implemented by setting a DataTable and DataMember (column name) from which the control will draw pie chart slices representing the proportion breakdown of values for that column. In our case, this chart, drawn using GDI+, is used to display the priority breakdown of the current project.

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

Progress Chart Control

Similar to the Custom Chart Control, the Progress Chart Control is included as a compiled library and displays a rectangular chart, drawn using GDI+, representing the mean progress of the current project.

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

History Panel Control

The TaskHistoryPanel class is a simple control that iterates through a DataView of task history rows and programmatically adds a clickable LinkLabel to the UI for every row that matches the current TaskID.

As we add each LinkLabel to the control, we use the inherited Control.Tag property to store an integer representing the individual record and subscribe to the LinkLabel click event that is handled in our code.

'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

In our click event handler, we raise a custom event back to the Main form with some values to help determine which history record was actually clicked and ultimately display that to the user.

    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 Class

Because the client application stores password information in the registry, we needed a way to prevent prying eyes from obtaining another user's password. While there are a number of ways to accomplish this task, we chose to use the Windows 2000/XP Data Protection API (DPAPI) functions CryptProtectData and CryptUnprotectData, which provide us with the ability to protect secrets without having to directly manage keys.

The DataProtection class in our project is effectively just a wrapper to access the DPAPI functions. Below you can see how a registry key is set and how the unencrypted text is retrieved.

For more information about the DPAPI, see Windows Data Protection on MSDN.

'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))

Supported Features

  • Localization Support—Localization is the process of translating an application's resources into localized versions for each regional culture (i.e. language and calendar differences) that the application will support. The .NET Framework primarily uses the concept of a resource manager, resource files, and satellite assemblies to provide the architecture for application localization. Visual Studio .NET makes it easy to create these resource files and assemblies by setting a few properties in the Windows Forms designer. TaskVision version 1.1 implements localization for the German language and contains a satellite assembly from which each respective form can extract resources and property values. As a basic explanation, each form has a default resource file containing the properties and images for the default culture. In addition, each form has a "<formname>.de.resx" file that contains the properties and images for the German culture. These files are generally maintained by Visual Studio .NET and only store data specific to the respective form. Additionally, if a developer wished to store custom localized strings, for example custom exception messages, it's necessary to create additional resource files. TaskVision has two such files, "localize.resx" and "localize.de.resx", which are used to store custom strings. For more information about Localization files see Introduction to Resources and Localization on MSDN.

  • COM Interop—Interoperability with COM, or COM interop, enables you to use existing COM objects while transitioning to the .NET Platform at your own pace. TaskVision demonstrates COM Interop by accessing the Excel 10.0 Type Library and essentially automating MS Excel to create and fill a spreadsheet with TaskVision data. There are two considerations that should be taken in to account when using COM interop. First, the software (such as Excel in this case) must be installed on the developer's computer in order to reference the COM object. Second, developers should anticipate that end users of their software may not have the software (or necessary COM objects) installed. For more information about COM Interop see Introduction to COM Interop on MSDN.

  • Accessibility Support—To demonstrate one of the main accessibility features supported in Visual Studio.NET, we've gone through and set the AccessibleDescription and AccessibleName properties of all UI controls. These properties play an important role for accessibility applications such as Microsoft Narrator, which are able to access UI controls at runtime and literally read the descriptions to the user as they navigate through an application.

  • Dynamic Properties—Dynamic properties allow you to configure your application so that some or all of its property values are stored in an external configuration file rather than in the compiled code. Providing administrators with the means to update property values that may need to change over time can reduce the cost of maintaining an application after the application has been deployed. For example, suppose you are building an application that uses a test database during the development process, and you need to switch it to a production database when you deploy it. If you store the property values inside the application, you have to manually change all of the database settings before you deploy, and then recompile the source code. If you store these values externally, you can make a single change in the external XML file and the application will pick up the new values the next time it runs. TaskVision demonstrates dynamic properties by storing the Updater Component Update URL and both URLs for the authentication and data Web services in the TaskVision.exe.config file.

  • Windows XP Themes—Windows XP includes a new version of the Shell Common Controls library (COMCTL32.DLL Version 6.0). Included in this library are new colorful and rounded controls such as buttons and tabs (refer to the images below). To use the new version of the common controls, an application must explicitly request the new version by providing an application manifest. The application manifest can be provided to Windows XP as a discrete file or as a resource attached to the executable. When provided as a discrete file, the manifest file must be located in the same directory as the executable and must have the same name as the executable with ".manifest" appended to the name. For example, the manifest file for TaskVision.exe would be TaskVision.exe.manifest. In addition to supplying a manifest, many controls will require that the FlatStyle property be set to System in order to use the common controls.

    Figure 18   Application without Manifest File

    Figure 19   Application with Manifest File

Lessons Learned

TaskVision is a sample solution intended to demonstrate many of the powerful capabilities of smart client applications built using the .NET Framework.

Like many projects TaskVision had its share of growth and changes throughout the development phase. Below we have listed two things we would have done differently were we to start development all over again now.

Data Layer Component

As you become more familiar with the client application, you may notice a few minor, negative UI side effects resulting from our data architecture. The Windows Forms controls support the idea of data binding, which provides a way for a control to automatically populate (and maintain) itself with values from a data source. Because we are using XML Web services, there is no way to truly send an object, modify it, and have all the controls referencing it be automatically updated. Instead, our two choices were: merge the returned, updated DataSet with the current DataSet after each XML Web service call, or update the data bindings of relevant controls after each XML Web service call. We chose to merge the updated and existing DataSets because it allowed us to maintain the idea of data binding. Because of this, you may notice some slight side effects—the fact that the current record in the DataGrid loses focus after an update, for example. If given the opportunity to start from scratch, we would have chosen to update the data bindings after each XML Web service call to update the data to avoid these unwanted side affects.

XML Web Services Security

Worth mentioning is the Web Services Enhancements (WSE) 1.0 for Microsoft .NET, a toolkit which provides Visual Studio .NET and .NET Framework developers with support for some of the latest proposed XML Web services specs, including WS-Security, WS-Routing, WS-Attachments, and DIME. Unfortunately, WSE was not available at the time of development. Therefore, we were not able to showcase how certain components like support for WS-Security gives developers more flexibility and control over securing XML Web services. We plan to showcase this functionality in a second major sample application due for release later this year. You can learn more about WSE at the WSE home page on MSDN.

For More Information

Complete TaskVision documentation and source code
TaskVision Source Code Viewer
TaskVision Discussion Forum
Web Services Enhancements 1.0 for Microsoft .NET
Localization and Globalization information
.NET Developer Center
.NET Framework product site
Visual Studio .NET product site