< Day Day Up > |
A Remote Access ProblemChapter 3 introduced a fictional company named Slugger Sports. The company produces a wide variety of sports equipment for T-ball and youth baseball games. It employs dozens of regional sales representatives who work almost entirely on the road. These salespeople utilize laptops and mobile phones to communicate with the central office about sales opportunities. Because they are remote, they do not always have consistent and reliable Internet access. Marketing people and sales managers located at the central office often produce documents that may be useful to the sales representatives. The remote reps need a reliable way of accessing all this changing information as quickly as possible. In addition, new or updated sales opportunities are constantly being added to the central database. The sales representative needs a way of being notified when a change has occurred. Loading the Sales Scheduling DatabaseTo run the code accompanying this chapter, you will need to create a database in Microsoft SQL Server named SalesScheduling. You may have already done this if you ran the code for Chapter 3. This chapter will utilize the same database used in that chapter, and the SalesScheduling.mdf file can be downloaded from the book's Web site. To attach the SQL database, execute the following steps:
Loading the Sample ApplicationFigure 8.1 is a diagram representing the remote access solution presented in this chapter. The solution consists of a remote agent and a server agent. Table 8.3 is a listing of the different projects that make up the solution. To execute the sample code available for download from the book's Web site, you will need to execute the following steps:
Figure 8.1. Diagram of the remote access solution presented in this chapter. The server agent is responsible for publishing information about changing files and directories on the central server. The remote agent is responsible for retrieving desired changes from a Web service when an Internet connection is available. Files are transferred asynchronously using Microsoft's Background Intelligent Transfer Service (BITS).
The remote agent is responsible for pulling files and database updates from the central server. The sales representatives will all execute the same remote agent code on their local machines. The application will behave differently depending on the values stored in the user configuration file. The server agent is responsible for producing an XML file used by each remote agent to determine which files it needs to pull. The Web service is the method by which the remote agent will access information from the central server. It is essentially the interface to the data. Remote AgentThe remote agent (represented by the project named Agent) will execute continuously as a Windows service and thus can operate independently. If it encounters errors, it will log them in the Windows Event Log and continue processing. Because it is a Windows service, it can be configured to start automatically so that the user is not responsible for executing it specifically. The agent will periodically check to see if the laptop is connected to the Internet. The InternetGetConnectedState function is used because we do not want the sales representative to be prompted to provide connection details every time the agent polls for a connection. This function is exposed by the Windows Internet (WinInet) API. The code that uses this function to poll for a connection is encapsulated inside the InetConnection class file and is seen as follows: Public Shared Function CheckInetConnection() As Boolean Dim lngFlags As Long If InternetGetConnectedState(lngFlags, 0) Then Return True Else Return False End If End Function If CheckInetConnection returns a true value, it will begin the process of checking for updates. If it determines that specific files the user is interested in have been updated or new files added, it will initiate a BITS job. The BITS job is responsible for asynchronously transferring files over a network despite network interruptions or bandwidth restrictions. BITS is covered in more detail in the section titled "Background Intelligent Transfer Service (BITS)." The agent is customized for each sales representative through the use of an XML-based configuration file (see Figure 8.2). The configuration file, named AgentSettings.xml, should be located in the application data directory for all users. Typically, this path is C:\Documents and Settings\All Users\Application Data\Agent. Figure 8.2. Screenshot of the Services dialog on a Windows XP machine. The Background Intelligent Transfer Service listed in Services allows Windows Update to automatically download the latest updates to your machine. It will also be used by the remote agent in this chapter to download file updates.This file contains information about which documents the user is interested in. The remote agent determines which files the user wants based on certain predetermined file attributes, such as extension, author, and keywords. Thus, if the configuration file contains a node for an author named Mark Peters, the remote agent will pull all the files in which Mark Peters is the author. The sales representative is required to select an existing directory on the local hard drive in which to store all transferred documents. This value is stored in the node named LocalStartPath. This path may contain as many nested subdirectories as necessary. The configuration file will be modified by the remote agent every time the sales representative copies a new file into the local directory. This is because the agent will assume that the representative is interested in knowing about future updates to the file. A sample configuration file is shown as follows: <?xml version="1.0" encoding="utf-8" ?> <UserSettings> <LocationSettings> <ServerName>CentralServer</ServerName> <ServerStartPath>AgentServerPath</ServerStartPath> <LocalStartPath>C:\AgentLocalPath</LocalStartPath> </LocationSettings> <FileSettings> <Authors> <Author>mark peters</Author> <Author>lauren jones</Author> </Authors> <FileTypes> <FileType>.xls</FileType> <FileType>.doc</FileType> </FileTypes> <Keywords> <Keyword>2003</Keyword> <Keyword>budget</Keyword> <Keyword>spalding</Keyword> <Keyword>t-balls</Keyword> </Keywords> </FileSettings> </UserSettings> For the sample file shown, the local start path is C:\AgentLocalPath. The configuration file also specifies that the user is interested in all files with an .xls or .doc extension. Using the Windows FileSystemWatcher class supplied by .NET (covered in more detail in the section titled "Detecting File Changes"), the remote agent will look for files being added to this directory and all the subdirectories within it. As a result, each sales representative's configuration file will be unique. Thus, the agent assumes another characteristic listed in Table 8.1 personalization. Note In the sample application, the remote agent is only concerned with author, extension, and keyword. The code could be extended to include additional file attributes. It might also include an interface that allows users to specify which file attributes they are interested in and assign priorities to each attribute. The remote agent would then select files based on this weighted assignment.
Server AgentThe second agent (represented by the Windows service project named ServerAgent) is responsible for producing an XML file (named FileInfo.xml) that lists all the files and directories that have recently changed on the central server. The agent utilizes the FileSystemWatcher class to raise events every time a file or directory is added, modified, or deleted. This is covered in more detail in the section titled "Detecting File Changes." As changes are detected, the FileInfo.xml file is updated. This file should be located on the central Web server and will be exposed to the remote agent through a Web service call. A sample version of this file is seen as follows: <?xml version="1.0" encoding="utf-8" ?> <agentserverpath lastupdated="7/23/2004 12:41:23 PM"> <subdirectory1a type="dir"> <subdirectory2a type="dir"> <SR01.doc type="file"> <createddate>7/20/2004 4:20 PM</createddate> <modifieddate>7/17/2004 5:44 PM</modifieddate> <extension>.doc</extension> <author>Sara Rea</author> <keywords>2003 Sales</keywords> </SR01.doc> <licensekey.txt type="file"> <createddate>7/22/2004 3:30 PM</createddate> <modifieddate>7/21/2004 4:48 PM</modifieddate> <extension>.txt</extension> <author></author> <keywords></keywords> </licensekey.txt> </subdirectory2a> <postinfo.html type="file"> <createddate>7/23/2004 12:29 PM</createddate> <modifieddate>6/9/2003 9:17 AM</modifieddate> <extension>.html</extension> <author></author> <keywords></keywords> </postinfo.html> </subdirectory1a> <subdirectory1b type="dir"></subdirectory1b> </agentserverpath> The FileInfo.xml file contains an attribute named lastupdated. This is used by each remote agent to determine whether changes have taken place. The remaining information is used by the remote agent to determine whether new or updated files need to be updated. Detecting File ChangesAs stated earlier, file changes are detected using the FileSystemWatcher class. This class allows you to specify a directory and watch for any changes to the files and subdirectories within it. Both the server and the remote agent will utilize this class. For the remote agent, the class is used to monitor the local hard drive. Sales representatives can configure their agents by initially copying files they are interested in to a subdirectory within their local start path. Upon startup, the remote agent will instruct the FileSystemWatcher class to monitor the local start path and kick off code whenever a new file is added. The code to initialize FileSystemWatcher is seen as follows: Public Shared Sub SetLocalFileWatcher(ByVal StartPath As String) 'Create a new FileSystemWatcher object and set it's 'properties. This watcher will be set to monitor files 'being copied manually to the local start path or files 'that are created new in that start path Dim fw As New FileSystemWatcher fw.Path = StartPath fw.IncludeSubdirectories = True 'Include subdirectories fw.Filter = "" 'Watch all files fw.NotifyFilter = (NotifyFilters.LastWrite _ Or NotifyFilters.FileName) 'Add the event handlers indicating that we want to 'be notified of file creations AddHandler fw.Created, _ New FileSystemEventHandler(AddressOf LocalFileCreated) 'Tell it to start watching fw.EnableRaisingEvents = True End Sub In this code, we create a new FileSystemWatcher object and set its properties to include all subdirectories and file types. We also restrict it to look only for changes to the last write time stamp and file name. Since file changes can trigger a number of different events, this prevents the event handler from being called unnecessarily. A handler is added to indicate what method should be called when a new file is created. This handler points to a method named LocalFileCreated, which contains code to collect the file attributes and add them to the AgentSettings.xml file. The last line of code is used to initiate the monitoring process. The server agent utilizes the FileSystemWatcher class to monitor the server start path and record changes to the files and directories within. Upon startup, the agent will initiate two FileSystemWatcher objects. One is used to monitor file changes, and the other monitors directory changes. In both cases, the objects will be configured to watch not only for the creation of files and directories, but also for any changes, renamings, and deletions. Every time an event handler is executed, it will modify the FileInfo.xml file to match the change that has taken place. If someone at the central office changes a file named SalesFigures2003.doc, for example, the event handler will update the modifieddate node for that file. Each event handler utilizes the Document Object Model (DOM) XML parser that is part of Visual Studio .NET. By using this parser, we can easily modify the contents of FileInfo.xml. In each handler the xml document is loaded and the correct node located with an XPath query. For example, the code executed when a file is renamed is as follows: Private Shared Sub FileChanged(ByVal source As Object, _ ByVal e As FileSystemEventArgs) 'The last write time would have changed since this 'is what we are monitoring for we will want to 'alter the modified date entry for this file Dim ext As String = "" 'Get the attributes to determine what type of file we have Dim fi As New FileInfo(e.FullPath) If fi.Exists Then ext = fi.Extension.ToLower 'We will ignore temporary files If e.Name.IndexOf("~") < 0 And ext.ToLower < > ".tmp" Then 'Load the XML Dim doc As New XmlDocument doc.Load(Service1._FilePath) 'Split the path so we can parse it back for the query Dim arrDir As Array = e.Name.Split("\") Dim oldfile As String = arrDir(arrDir.GetUpperBound(0)) 'replace spaces with a dash so XML remains well formed oldfile = oldfile.Replace(" ", "-") 'Loop through the array and rebuild string for XPath query Dim rootpath As String = "" Dim pos As Int16 = 0 Do Until pos = arrDir.GetUpperBound(0) + 1 rootpath += arrDir(pos) + "/" pos += 1 Loop rootpath = Service1._RootDir + "/" + rootpath _ + "modifieddate" 'Do an XPath query Dim oldnode As XmlNode = _ doc.SelectSingleNode("/" + rootpath.ToLower) oldnode.InnerText = fi.LastWriteTime.ToShortDateString + _ " " + fi.LastWriteTime.ToShortTimeString 'update the lastupdated attribute of the root node Dim root As XmlNode = _ doc.SelectSingleNode("/" + Service1._RootDir.ToLower) Dim lastupdated As XmlAttribute = root.Attributes(0) lastupdated.Value = Date.Now 'Save the file doc.Save(Service1._FilePath) doc = Nothing End If fi = Nothing End If End Sub Utilizing the FileInfo.xml file eliminates the need for each remote agent to scan the entire file tree every time it looks for updates. The burden of detecting file changes has been offset to the server agent. When it detects that a change has taken place, it modifies the FileInfo.xml file and updates the lastupdated attribute. When the remote agent kicks off its processing because an Internet connection is available, it begins by using the Web service to get a copy of the FileInfo.xml file. It will examine the lastupdated date and determine whether a change has taken place since the last time it checked. Only then will it attempt to look for new files to pull. This is one example of the potential power of multiple agents. Since each agent can assume a separate responsibility, a group of associated agents is capable of accomplishing large tasks. In our remote access solution, the server and the remote agents assume separate responsibilities that together allow them to keep each sales representative up to date. Using the Background Intelligent Transfer Service (BITS)Background Intelligent Transfer Service (BITS) provides the perfect way to access files remotely. Not only does BITS transfer files even after the application that initiated it exits, but it does not force a connection. Files are transferred asynchronously between an HTTP server and a remote client. BITS will adjust the transfer rate to ensure that the machine's resources are not all consumed. Most important, if an Internet connection is lost in the middle of a transfer, all is not lost. BITS will simply pick up where it left off when the connection is reestablished. Even if a file transfer takes hours or even days to complete, the system will not be compromised. With BITS, the agent is able to function independently despite bandwidth restrictions or network interruptions. These are all-too-common problems for remote salespeople, and therefore a dependable transfer method is a must-have for remote agents. How to Access BITSIf you have ever used the Windows Update feature, you have already used BITS. Available with Windows XP, Windows Updates automatically searches the Microsoft servers for the latest updates and patches, and then checks to see whether your machine is up to date. If it is missing any updates, BITS transfers them to your machine without interrupting or otherwise detracting from your user experience. In fact, you may have noticed the engine that allows BITS to function in the Services dialog (see Figure 8.2). It's a great idea, and fortunately Microsoft exposes the functionality used to accomplish Automatic Updates through a set of API's. The bad part is that for now, the API's are not exposed through a Visual Studio .NET wrapper. Therefore a little work is required to get to the functionality. Tip BITS is utilized by one of the applications available from the Microsoft Patterns and Practices group (http://www.microsoft.com/resources/practices/default.mspx). The Updater Application Block can be used to quickly create self-updating applications. This feature can be invaluable for Windows Forms applications in which deployment is often a major hurdle. The application block is responsible for polling a central location for application updates. When one is available, it uses BITS to download the files and then updates the client. For starters, you must download the latest version of the Platform SDK from MSDN. This will give you access to the BITS.idl file. From there, you can use the Microsoft Intermediate Language (MIDL) compiler that is included with Visual Studio .NET to compile a type library. Next you will need to use the Type Library Importer (Tlbimp.exe) to convert the type definitions into a form useable by COM. The result of all these steps is a binary file (BackgroundCopyManager1_5.dll) that is available to you on the book's Web site. We can now add a reference to the binary file through References and thereby access the BITS functions. Tip Adding a reference to the BackgroundCopyManager1_5.dll file gives you access to the BITS functions directly but can be cumbersome to use. You may want to consider using a wrapper for BITS. This can be especially useful when you have an application that utilizes many of the complex features of BITS, such as concurrent foreground downloads or downloading ranges of files. To find out how to create your own BITS wrapper, refer to the article on MSDN titled "Using Windows XP Background Intelligent Transfer Service (BITS) with Visual Studio .NET." Creating a Transfer JobThe transfer job is the central object in BITS. A job can consist of one or more files, and it is used to specify how files are to be transferred. The remote agent creates a transfer job using the CreateJob function, seen below: Public Shared Sub CreateJob(ByVal FileList As ArrayList, _ ByVal RemoteURL As String, ByVal LocalStartPath As String) Dim bcm As New BITS.BackgroundCopyManager1_5 Dim job As BITS.IBackgroundCopyJob Dim jobId As BITS.GUID Dim bcc As New BackgroundCopyCallback Dim jobname As String = "" Dim username As String = "BITSUser" Dim password As String = "bitsuser" 'The job will be named the machine name along with ' a date time stamp jobname = Environment.MachineName + "-" + _ Date.Now.ToShortDateString + "-" + Date.Now.ToShortTimeString 'Create the download Job that will be added to the queue bcm.CreateJob(jobname, BITS.BG_JOB_TYPE.BG_JOB_TYPE_DOWNLOAD, _ jobId, job) 'Set the job priority to normal job.SetPriority(BITS.BG_JOB_PRIORITY.BG_JOB_PRIORITY_NORMAL) 'Associate all the files in the FileList with this job Dim strFile As [String] For Each strFile In FileList job.AddFile(RemoteURL + strFile, LocalStartPath _ + "\" + strFile) Next 'Set a reference to the BackgroundCopyCallback Interface 'This is used to receive notification about the jobs state job.SetNotifyInterface(bcc) 'Tell BITS which events we want to be notified about job.SetNotifyFlags(Convert.ToUInt32(Flags.IsTransferred _ Or Flags.IsError)) 'Set Credentials by calling out BITSCredentials wrapper Dim wrapper As New BITSWrapper Dim iunknown As IntPtr = Marshal.GetIUnknownForObject(job) wrapper.BITSSetCredentials(iunknown, username, password) Marshal.Release(iunknown) 'Activate the job in the queue job.Resume() End Sub Once the remote agent determines which files will be added to the transfer job, it calls the CreateJob function and passes it an array list containing all the files to be transferred. To make the job name unique, it is named as the machine name along with a date and time stamp. The job is created as a download type, which is the default type for new jobs. The other job types are upload and upload-reply. Both of these types are used if uploading files to a server. The upload-reply type is also used to receive a reply from the server application. For this example, we set the priority to normal, which means that all files will be marked with the same importance level. The alternative priority values are foreground, high, low, and normal. A job marked with a high priority value will transfer out of the queue before one with a low or normal value. The foreground priority is the highest value, but you should take care when using it. A job with this priority will directly compete with other applications on your machine. BITS AuthenticationEven though BITS supports secure connections over HTTPS, you will most likely want to provide additional security. You can do this by specifically setting the credentials that BITS uses to access the files on the server. BITS supports Basic, Challenge/Response, and Passport authentication schemes. To execute the sample application, you need to create a virtual directory on your Web server from which the server files will be available. You can access directory security by executing the following steps:
Once security is configured for the virtual directory, we will need to explicitly declare credentials using the SetCredentials method. BITS uses the Crypto API to protect credentials. The Crypto API is part of the core cryptography functionality in Windows and, like BITS, is available to developers through the Platform SDK. Unfortunately, the SetCredentials method is not included when the MIDL compiler compiles the BITS type library. To use this functionality, you have to perform an additional step. This involves writing a managed C++ wrapper to call the SetCredentials method from the native BITS library. A C++ wrapper is included with the agent solution file on the book's Web site. It is embedded in the BITSCredentials project. The code for the BITSSetCredentials method is seen as follows: void BITSWrapper::BITSSetCredentials(System::IntPtr ptr, String* _ userName, String* password) { HRESULT hr = S_OK; void* pv = ptr.ToPointer(); IBackgroundCopyJob2* job; BG_AUTH_CREDENTIALS creds; const wchar_t __pin* user = PtrToStringChars(userName); const wchar_t __pin* passwd = PtrToStringChars(password); creds.Scheme = BG_AUTH_SCHEME_NTLM; creds.Target = BG_AUTH_TARGET_SERVER; creds.Credentials.Basic.UserName = (LPWSTR)user; creds.Credentials.Basic.Password = (LPWSTR)passwd; hr = ((IUnknown*)pv)->QueryInterface_ (__uuidof(IBackgroundCopyJob2),(void**)&job); if (SUCCEEDED(hr)) hr = job->SetCredentials(&creds); if (FAILED(hr)) { BITSCredentials::BITSWrapperException* e = _ new BITSCredentials::BITSWrapperException(hr); throw e; } }; In this code, the BITSSetCredentials method accepts the user name and password as input parameters. It also accepts a pointer to the BITS transfer job. The method defaults to use the Windows challenge/response scheme (BG_AUTH_SCHEME_NTLM). Alternatively, we could have specified that it use basic authentication with the BG_AUTH_SCHEME_BASIC value. The drawback of this authentication method is that the user name and password are sent as clear text and therefore authentication is not as secure. Note When attempting to run the sample application available for download from the book's Web site, you may have to use basic authentication in order to successfully execute the code. Depending on where you execute the application and the login rights granted to the logon user, you may receive an authentication error. If this happens, change the Directory security to use basic authentication and modify the value in BITSSetCredentials. Register for NotificationAfter a job has been added to the transfer queue, the next step is to determine when the files were transferred. This can be done in one of two ways. The first is to create a timer and poll for the state of the job. The job will either be in a Transferred, Disabled, Error, or Notification state. The problem with this method is that it is synchronous and requires that the job be transferred or in error before it can complete the job. A sales representative may terminate an Internet connection at any time, even during the transfer of a job. A synchronous process would cause problems if the connection was dropped before the transfer was complete. The remote agent instead implements the IBackgroundCopyCallback interface to maintain asynchronous processing. This allows us to register for notification whenever the job reaches a certain state. The following code from our CreateJob function is used to set a reference to the interface and tell it which events we are interested in. 'Set a reference to the BackgroundCopyCallback Interface 'This is used to receive notification about the jobs state job.SetNotifyInterface(bcc) 'Tell BITS which events we want to be notified about job.SetNotifyFlags(Convert.ToUInt32(Flags.IsTransferred _ Or Flags.IsError)) For the sample application, we have asked to receive notification whenever a job is transferred or an error occurs. When the job has transferred, we will call the Complete method. This enables the user to see the files. Until this method is called, the files will only appear as empty temp files on the remote agent's machine. When an error occurs, the following code is executed: Sub JobError(ByVal job As BITS.IBackgroundCopyJob, _ ByVal jobError As BITS.IBackgroundCopyError) _ Implements BITS.IBackgroundCopyCallback.JobError 'The job received an error, but we need to determine what type of 'error it was Dim jobname As String = "" Dim ErrorMsg As String = "" Dim BITSError As String = "" Dim LanguageID As Integer = &H409 'Indicate language is English(US) If Not job Is Nothing Then 'Get the name of the job job.GetDisplayName(jobname) 'Get the error job.GetError(jobError) jobError.GetErrorDescription(Convert.ToUInt32(LanguageID), _ BITSError) 'Log the error using the Exception Manager application block ErrorMsg = "The following error was encountered trying to " ErrorMsg += "process the BITS job " + jobname + " : " ErrorMsg += BITSError ExceptionManager.Publish(New Exception(ErrorMsg)) 'Cancel the job job.Cancel() End If End Sub In this code we get the display name of the job, which should be the machine name and a date/time stamp (this was defined in the CreateJob function). We also get the error description and then publish the error using the Exception Management Application Block. Finally, we cancel the job so that it will no longer appear in the transfer queue. At this point, any empty temp files on the local machine should disappear. Monitoring New OpportunitiesNew leads are constantly being added to the central database. The salespeople all have access to a Web application that allows them to work these new opportunities. Unfortunately, most salespeople only check the Web application once a day. In some cases it may take several hours or days for a sales opportunity to be discovered. The remote agent contains a CheckDatabase function that is used to check for new opportunities. The function is initiated as soon as an Internet connection becomes available. CheckDatabase will make a call to the Agent Web Service and pass in the remote salesperson's e-mail address. The Web service will search the leads table for leads located in the zip code assigned to the salesperson. If the salesperson has the status of 'N' (new) and also has a notify date older than forty-eight hours, the contact information is returned. Note The Web Service featured in the sample solution is not considered secure. At a bare minimum, a production application would want to consider enabling point-to-point security using SSL (Secure Sockets Layer). To enable SSL, readers would need to request a certificate from a certification authority utility on the Web server. Readers can obtain a certificate from a recognized authority like Verisign (http://www.verisign.com), or they can generate their own using the secure certificate wizard in Internet Information Services Manager. Once a certificate is downloaded, a pending certificate request can be installed on a Web server using the IIS Manager. At this point, requests made to the Web service would have to include the HTTPS header instead of HTTP. This will ensure that any traffic between the Web service and the client is encrypted. Every time the CheckDatabase function finds new leads, it displays a message box to the user (see Figure 8.4). Thus, the remote agent exhibits one more agent characteristic the ability to communicate with the user. Figure 8.4. Screenshot of the dialog box used to notify the remote salesperson of new sales opportunities. A notification will be sent to the salesperson every forty-eight hours until the lead is worked or no longer has the status of new.
|
< Day Day Up > |