Advanced Basics
Digital Grandma
Duncan Mackenzie
Code download available at:AdvancedBasics0411.exe(183 KB)
Contents
The Manifest Generator and Manifest Files
Creating the UI
Populating the Manifest
Serializing the Manifest to Disk
Application Settings and the Options Form
Checking for New Images
Conclusion
As a parent of a young child, I take a lot of pictures—many more than anyone would ever be interested in seeing. Well, anyone except my mother. This is her first grandchild and the one or two pictures I send to her each week only brush the surface of her grandmotherly needs. Unfortunately, it has been difficult for me to send her any more than a few photos at a time. When she had only a dial-up account and very little room in her Inbox, full-size photos would leave her phone line tied up for hours, so I decided that I should set her up with a better way to get her grandson fix. A digital photo frame, a system that downloads photos automatically from a Web site and displays them on a small LCD screen, would be a good solution, but I didn't like the idea of paying a monthly fee indefinitely. (My mother lives in Canada where there were no toll-free dial-up numbers for digital devices, and she has a perfectly good computer and monitor right on her desk.)
To address these problems, I considered a system that would automatically download any new photos that I have put on a Web server whenever she is connected to the Internet. By default this system would save photos into her "My Pictures" folder. She is running the "My Pictures Slideshow" screensaver, so new photos will automatically appear on her screensaver, just like magic! Even if e-mail storage and connection speed weren't issues, the idea of new photos simply arriving on her machine and appearing on her monitor was enough to make me want to build this system.
Fortunately, since I last visited, technology has improved in the Manitoba town where my mother lives and her dial-up has been replaced with broadband. Now, a standard retrieval from the Web will be even easier. My original plan included using some form of file transfer that would work with minimum bandwidth so I would not interfere with her Web surfing or regular e-mail retrieval, and downloads would have to be able to pause and resume automatically as the connection status changed.
If I did end up having to build that system, the Background Intelligent Transfer System (BITS), which works well for copying files over slow or intermittent connections and which supports restarting file transfers even after a machine restarts, would have been my answer. And I would have been able to take advantage of the COM interface I wrapped in a .NET library (available at Using Windows XP Background Intelligent Transfer Service (BITS) with Visual Studio .NET). Perhaps I'll add BITS as an option to this system at some point in the future, but for the moment I've decided to build an application that is really best suited for use over a broadband connection.
Figure 1** Photo/File System Architecture **
Although I was focused on a very specific goal, I tried to design a fairly generic system that could be used for more than just photos (see Figure 1). Here's how it works:
- The client machine (user) has a small application installed and running constantly.
- The client application is configured to retrieve a listing of available files (the manifest) from a specific URL and to download those files into a specific local directory.
- Whenever new files are ready to be sent to the client, the files are made available on an HTTP server, and the server's manifest file is then updated to reflect the currently available set of files.
- The client pulls down the manifest at regular intervals and then begins downloads for any files that it doesn't already have. Along with the path to each file, the manifest contains a hash of the file so that the client application can verify that it has downloaded the right file. The manifest also defines an action—either Notify, Execute, or None in this sample—that should be taken on the file after it is downloaded.
- As downloads complete, a local history is kept of the files that have been downloaded and the ones already in progress, so as to avoid duplication.
The Manifest Generator and Manifest Files
I created two key systems—the client application and a manifest file generating a utility for use on the server. I could have created an initial manifest file by hand and then built the client system using that as input, but I decided to build the real manifest generator first. Once I had the ability to create manifest files, I could build code into my client application to read that data back into memory. The manifest generator's task is simple: take a set of files from the local machine (or a network share) and generate a manifest file complete with checksums, Web paths (you have to enter those based on the server to which you'll upload these files), and the action that should be taken at the client (Notify, Execute, None).
I used XML serialization to produce and consume the XML for the manifest file, so the first step in each case was to define the classes that I wanted to use. These classes are very simple, containing almost no code; they are just places to store data (see Figure 2).
Figure 2** The Class View **
I created the collections using CodeSmith, a code-generation tool (see Ten Must-Have Tools Every Developer Should Download Now for more information), but I customized the resulting classes by adding some Shared methods onto the collections to make it easier to save and restore the entire collection to disk (see Figure 3).
Figure 3 Adding Shared Methods
Public Shared Sub SaveManifest( ByVal fileToSaveTo As IO.StreamWriter, _ ByVal m As Manifest) Dim xw As New XmlTextWriter(fileToSaveTo) Dim xs As New XmlSerializer(GetType(Manifest)) xs.Serialize(xw, m) fileToSaveTo.Close() End Sub Public Shared Function OpenManifest( _ ByVal fileToReadFrom As IO.StreamReader) As Manifest Dim xr As New XmlTextReader(fileToReadFrom) Dim xs As New XmlSerializer(GetType(Manifest)) Dim result As Manifest = DirectCast(xs.Deserialize(xr), Manifest) fileToReadFrom.Close() Return result End Function
Note that I choose to pass in a StreamWriter and StreamReader to these methods instead of a file path. To access a local file using its path requires certain security permissions, and is usually not possible at all in an application running with restricted permissions, but in that situation you can still obtain a Stream through the Open and Save dialog controls or by opening a file in Isolated Storage. By accepting a Stream into my functions, I left my options open and didn't force full-trust requirements onto my code. I tried to do this as much as possible, even though I expected it to be installed locally where it could be run with Full Trust.
Creating the UI
The UI for the manifest generator, which I built with Windows® Forms, consists of a DataGrid to hold the list of files, a couple of textboxes to provide the destination address of the manifest file and of the images, and the following four buttons:
- newManifest (to clear the current manifest and start a new one)
- openManifest (to open an existing manifest as a starting point)
- populateManifest (to pick the folder where the images are stored locally)
- generateManifest (to generate the file out to disk)
If you choose to open an existing file, it is deserialized back into an instance of the Manifest class (see Figure 4).
Figure 4 Deserializing to the Manifest Class
Private Sub openManifest_Click( ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles openManifest.Click If Me.pickManifestFileToOpen.ShowDialog = DialogResult.OK Then Me.currentManifest = Manifest.OpenManifest( _ New IO.StreamReader( _ Me.pickManifestFileToOpen.OpenFile)) setupDataBinding() End If End Sub Private Sub saveManifest_Click( ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles saveManifest.Click If Not Me.currentManifest Is Nothing Then If Me.pickSaveLocation.ShowDialog = DialogResult.OK Then Manifest.SaveManifest( New IO.StreamWriter( _ Me.pickSaveLocation.OpenFile), Me.currentManifest) End If End If End Sub
I also added a Save Dialog, Open Dialog, and Folder Browser dialog to the form to support the rest of the code.
Populating the Manifest
Clicking the Populate button allows you to pick a directory from which to load your images, and then it loads all the files (matching a wildcard-based search pattern) from that folder into the current manifest file. For each file, a GUID is assigned (this GUID will be used by the client to track which files it has already downloaded), and then a hash is created using the MD5 classes in the Microsoft® .NET Framework (see Figure 5). The CreateChecksumString method converts the hash value from a byte array to a string of hexadecimal.
Figure 5 Populating the Manifest
Imports System.Security.Cryptography ... Private Sub AddFileToManifest(ByVal filePath As String) Dim hasher As New MD5CryptoServiceProvider Dim fi As New IO.FileInfo(filePath) Dim imageStream As IO.FileStream = fi.OpenRead Dim myFile As New File With myFile .action = "None" .fileSize = fi.Length .fileURL = fi.Name .checkSum = File.CreateChecksumString( hasher.ComputeHash(imageStream)) End With Me.currentManifest.includedFiles.Add(myFile) End Sub
Serializing the Manifest to Disk
The generateManifest button asks you to pick a file name and then takes the currently loaded manifest file and serializes it out to that location. With the file created, your next step is to copy the manifest and all of the images that it references up to your Web server. Make sure that wherever you put the files, the path for each of the images matches the value you entered when creating the manifest. Now, with the manifest and the files all uploaded to the Web server, I'll explain the client side of this system.
The client application is run in the user's context, as a regular application (not a Windows service), and polls a single Web location for updates to the manifest file. Whenever that file is modified, it is pulled down and any new images are downloaded to a local directory.
The whole point of this system is to automate the delivery of these photos to the grandmother, so the client system should be unobtrusive. Although I am loath to add yet another system tray icon to anyone's machine, it seems to be the best way to make the application available for configuration but not too visible to the user. I decided to create a Windows Forms application that, by default, does not display any UI other than in the system tray. If needed, the user can open up an options dialog (see Figure 6) from that icon, but otherwise it will just quietly go about its business.
Figure 6** The Options Dialog **
To produce a Windows Forms application that doesn't (initially) display any forms, I created a Sub Main for my application and within that routine I set up the system tray icon, a timer (to check for updates), and a context menu for the icon. The context menu has only two choices: exit the application or open the Options dialog. I've made the Options choice bold to indicate that it's the default action that would occur if you double-clicked the tray icon. Once everything was set up, I started my application's main message loop (without any Form instances at all) and then I just sat around and waited for the timer to fire an event or for the user to interact with the system tray icon (see Figure 7).
Figure 7 The Windows Forms App
Sub Main() 'load the history and settings 'information from serialized files Reload() 'the Settings files stores the interval in minutes 'Timers work in milliseconds, so a little conversion 'is necessary to make them work together. checkTimer.Interval = appSettings.CheckInterval * 60000 checkTimer.Enabled = True 'add a handler for the Tick event of the timer AddHandler checkTimer.Tick, AddressOf Tick 'Create system tray icon, use a module level variable for the control ni = New NotifyIcon ni.Icon = New Icon(GetType(Main), "camera.ico") ni.Visible = True ni.Text = "Retriever: Waiting..." 'add an event handler for when the user double-clicks the icon AddHandler ni.DoubleClick, AddressOf NotificationIconDoubleClicked 'set up the context menu, including event handlers Dim ctxMenu As New ContextMenu ctxMenu.MenuItems.Add("View Options", _ AddressOf ViewOptionsForm).DefaultItem = True ctxMenu.MenuItems.Add("Exit", _ AddressOf EndApplication) 'and assign it to the NotifyIcon so that it will pop up when 'the icon is right-clicked. ni.ContextMenu = ctxMenu 'run the application, using Application.Run 'to create a new message loop Application.Run() 'when the app is exiting hide the notify icon ni.Visible = False 'save the server info and settings to XML files Persist() End Sub
Application Settings and the Options Form
I used a settings class to store the user's preferences for delivery location, URL of the manifest file, frequency of polling, and whether actions should be taken (Execute or Notify) if they are specified in the manifest file. I also stored in the settings file the last modified date of the manifest file, which is a value the user wouldn't change but that I need in order to manage the download process.
The settings information is stored in isolated storage using XML serialization, with static methods similar to the ones I created on the manifest class earlier. By default, the delivery location is the user's My Pictures folder, the manifest URL is taken from the application's configuration file (installed with the application), and the polling frequency is once per hour. When the user decides to view the options dialog, I populate it with the current settings upon opening and then, if the user clicks OK to close, I store those into the settings file and serialize it back into isolated storage. All the dialog-related code is included in the complete code sample that you can download from the MSDN®Magazine Web site. I used another file, history.dat, also stored in isolated storage, which is serialized to and from a hashtable in my app. I'll cover the history hashtable and file when I go through the image retrieval code.
Checking for New Images
When checking for updates, I disabled the timer to avoid code that reenters whenever the timer fires, and then I created an HttpWebRequest to check for updates to the manifest file. By constructing my request to include the last-modified date, I get a "Not Modified" (304) response from the server if the file hasn't changed, and I receive the file back if it has been modified (see Figure 8).
Figure 8 Checking for Updates
Sub CheckForUpdates() Dim urlToRetrieve As String = appSettings.ManifestURL Dim wr As HttpWebRequest _ = DirectCast(WebRequest.Create(urlToRetrieve), _ HttpWebRequest) wr.IfModifiedSince = appSettings.LastDownload wr.AllowAutoRedirect = True wr.Credentials = CredentialCache.DefaultCredentials Try Dim resp As HttpWebResponse _ = DirectCast(wr.GetResponse(), HttpWebResponse) If resp.StatusCode = HttpStatusCode.OK Then Dim xmlDoc As New Xml.XmlDocument Dim respStream As IO.Stream respStream = resp.GetResponseStream xmlDoc.Load(respStream) Dim rootURL As String _ = xmlDoc("Manifest")("rootURL").InnerText If Not rootURL.EndsWith("/") Then rootURL &= "/" End If Dim navigator As _ Xml.XPath.XPathNavigator = xmlDoc.CreateNavigator Dim manifestItems As Xml.XmlNodeList manifestItems = xmlDoc.GetElementsByTagName("File") For Each n As Xml.XmlNode In manifestItems If Not InHistory(n("GUID").InnerText) Then GetFile(n, rootURL) End If Next appSettings.LastDownload = resp.LastModified Persist() End If Catch ex As WebException Debug.WriteLine(ex.Message) Dim webResp As HttpWebResponse _ = DirectCast(ex.Response, HttpWebResponse) If webResp.StatusCode = HttpStatusCode.NotModified Then appSettings.LastDownload = webResp.LastModified Persist() End If End Try End Sub
Assuming it has been changed, I download that XML file into memory and (using an XPathNavigator) walk through all of the file entries, comparing their GUIDs against entries in my download history. If a file hasn't been downloaded before, I synchronously download it using another instance of HttpWebRequest. I chose not to use the async method (BeginGetResponse) because I wanted this application to have little or no impact on the performance of the client machine (and running several requests at once would certainly increase its impact).
Once downloaded, the file is saved into a buffer. If its hash matches the value from the manifest file, I move it into the final target location and update the history file. Checking the hash is accomplished with almost the exact same code as was used to compute the hash in the manifest generator. If it fails to match, it is deleted but will be downloaded again if it is included in the manifest file the next time it is downloaded (see Figure 9).
Figure 9 Downloading and Validating a File
Private Sub GetFile(ByVal n As Xml.XmlNode, ByVal rootPath As String) Dim basePath As New Uri(rootPath) Dim url As New Uri(basePath, n("fileURL").InnerText) Dim localPath As String = IO.Path.Combine( _ appSettings.DropDirectory, n("fileURL").InnerText) Dim wc As New WebClient wc.Credentials = CredentialCache.DefaultCredentials Dim buffer() As Byte = wc.DownloadData(url.ToString) Dim hash As String = CreateChecksumString( _ New MD5CryptoServiceProvider().ComputeHash(buffer)) If hash = n("checkSum").InnerText Then Dim fs As IO.FileStream Dim bw As IO.BinaryWriter Try fs = New IO.FileStream(localPath, IO.FileMode.Create) bw = New IO.BinaryWriter(fs) bw.Write(buffer) Dim he As New HistoryEntry he.DateDownloaded = Now he.GUID = n("GUID").InnerText he.localPath = localPath history.Add(he.GUID, he) DoAction(n, localPath) Catch ex As Exception MsgBox(ex.Message) Finally bw.Close() fs.Close() End Try Else Debug.WriteLine("Hash Not Matched: " & hash) End If End Sub
If an action (Execute or Notify) has been specified for the file you've just downloaded (and the user has not disabled actions through her settings), then the file should be run or a message should be displayed to the user (Notify). I could see adding an additional action, Print, and choosing to send documents instead of images. It would be an asynchronous, high-quality alternative to using a fax machine (see Figure 10).
Figure 10 Execute or Notify
Private Sub DoAction(ByVal n As Xml.XmlNode, ByVal localPath As String) If appSettings.Actions Then Select Case n("Action").InnerText Case "Execute" System.Diagnostics.Process.Start(localPath) Case "Notify" MsgBox(String.Format("{0} Downloaded", localPath), _ MsgBoxStyle.Information, "New File Downloaded") Case Else 'do nothing End Select End If End Sub
Conclusion
Although it works for its limited purpose, this application is intended to serve as a sample. Starting from this concept, you can create a variety of applications or just extend this one to include more functionality. You can turn it into a BITS-based version, but you can also apply the concept to work-related data, setting up security on the drops and associating certain manifests with specific clients. If your Web host runs ASP.NET, you might want to change the manifest file generation to occur dynamically through an ASPX page or through a custom HTTP Handler, instead of through a static file.
If you are interested in photo blogs, you could modify the app to use RSS instead of a custom format for the manifest, although you'll need to add your own namespace so that you can add a couple of additional elements such as the hash value.
For me, though, I'll probably stick with the solution just as I designed it. Grandma Mackenzie is happy with it, so I can stop coding and get back to my real job—taking pictures of my son.
Send your questions and comments for Duncan to basics@microsoft.com.
Duncan Mackenzie is the MSDN Content Strategist for Visual Basic and C# and the author of the Coding 4 Fun column on MSDN online. He can be reached through his blog at https://www.duncanmackenzie.net.