Export (0) Print
Expand All

Deployment Patterns for Microsoft .NET Compact Framework

.NET Compact Framework 1.0
 

By Dan Fox and Jon Box
http://atomic.quilogy.com

Date:
November 2004

Applies to:
   Microsoft® .NET Compact Framework
   Microsoft® Visual Studio® .NET 2003

Download Smart Application Updater.

Summary: At the end of the day successful software developers need to ship their products. The most elegant piece of software written using the most powerful new tools is unable to reduce paperwork, improve access to information, or increase data accuracy if the intended users can't get their hands on it. This is true for developers of shrink wrapped software as well as corporate developers working in both the desktop and smart device space. In this whitepaper we'll lay out the patterns you could employ for deploying applications written using the Microsoft .NET Compact Framework. This paper is a superset to our discussion of this topic in chapter 10 of our book Building Solutions with the Microsoft .NET Compact Framework.

Although the title of this whitepaper focuses on the idea of deployment, this space actually encompasses several related topics that can be broken down into three sections; Packaging (the steps required to create a distributable binary), Deployment (how the distributable binary gets to the smart device and installed), and Maintenance (how the application is updated as it changes). In this whitepaper we'll discuss all three aspects and attempt to provide an overview and links to other resources you can use to package, deploy, and maintain your .NET Compact Framework applications. (22 printed pages)

Contents
Packaging
Deployment
Maintenance
Summary

Packaging

The first hurdle to getting a smart device application in the hands of your users is to package it. This includes setting the output folder and build actions on files within your project, changing to release mode, understanding how the Global Assembly Cache (GAC) works in the .NET Compact Framework, and finally packaging your application in a .cab file for eventual deployment.

Setting the Output Folder

The first step in packaging your application is to set the Output File Folder within the Properties window of the project. By default this will be set to the \Program Files\projectname directory on the device and will be the path to which the application is deployed when the .cab file is executed.

Setting Build Actions

Within a smart device project each file can be marked with a Build Action set in the Properties window in the following ways.

  • Marking a file as Compile, which is the default for all code files and forms, will compile the files into the resulting assembly.
  • Marking a file as Content allows the file to be packaged in the .cab file and deployed along with the project and is useful for deploying XML configuration files and SQL Server CE databases.
  • Marking a file as None simply ignores it and is useful for including documentation in the project such as Visio diagrams that should not be deployed
  • Marking a file as an Embedded Resource includes the file in the executable assembly as a resource. This allows code you write to extract the resource programmatically. This is effective for packaging images and script files that can be later used within the application. For example, the following code snippet loads an image into a System.Drawing.Bitmap object and assigns it to the Image property of a PictureBox control.
Dim a As [Assembly] = [Assembly].GetExecutingAssembly()
Dim b As Bitmap = New Bitmap(a.GetManifestResourceStream("logo.gif"))
pbLogo.Image = b

Changing to Release Mode

Before actually compiling your smart device application and creating .cab files for deployment it's important to remember to change the build mode for your project from Debug to Release. Doing so both reduces the size of the executable on the device (which is important on storage constrained smart devices) as well speeds execution. The build mode can be changed either from the Standard toolbar in Visual Studio .NET or the Configuration Manager dialog shown in Figure 1 accessible by choosing Configuration Manager by right clicking on the solution in the Solution Explorer window or from the Build menu.

Figure 1. Setting the Build Mode.

Now you can build your project using the Build Menu.

Using the GAC

Just as in the desktop .NET Framework, the .NET Compact Framework supports sharing assemblies by placing them in the Global Assembly Cache. Although in the desktop framework this is primarily used to support allowing applications to transparently use upgraded versions of assemblies, the .NET Compact Framework does not support configurable version policy and so your executable will always bind to the version of the assembly it was compiled with. Rather, in the .NET Compact Framework the GAC is primarily useful for allowing the deployment of one copy of the assembly to the device in order to save space.

On a smart device the GAC is located in the \Windows directory and simply consists of the assemblies renamed with the prefix "GAC" and appended with the version number and culture such as GAC_System.Data.Common_v1_0_5000_0_cneutral_1.dll. Assemblies are placed into the GAC by the Cgacutil.exe utility invoked by the runtime engine each time a .NET Compact Framework application is executed. This utility looks for ANSI or UTF-8 encoded text files with a .gac extension in the \Windows directory containing lists of shared assemblies to be moved into the GAC.

A typical .gac file might look as follows.

\Program Files\MyApp\AssemblyA.dll
\Program Files\MyApp\AssemblyB.dll

If the .gac file is deleted from the \Windows directory or updated, the appropriate changes (insertions and deletions) will be made to the GAC the next time a .NET Compact Framework application is executed.

Assemblies can also be installed and removed directly to and from the GAC by programmatically launching the Cgacutil.exe from inside an application. For example, your application could use a class that encapsulates the CreateProcess unmanaged function to invoke Cgacutil and use the –i and –u options to install and uninstall assemblies in the GAC as shown here in Listing 1 its simplified form.

Public Structure PROCESS_INFORMATION
      
    Public hProcess As UInt32
    Public hThread As UInt32
    Public dwProcessId As UInt32
    Public dwThreadId As UInt32
End Structure

Public NotInheritable Class WindowsCE
    Private Sub New()
    End Sub

    <DllImport("coredll.dll", EntryPoint:="CreateProcess", _
     SetLastError:=True)> _
    Private Shared Function CreateProcess( _
        ByVal lpszImageName As String, _
        ByVal lpszCmdLine As String, _
        ByVal lpsaProcess As IntPtr, _
        ByVal lpsaThread As IntPtr, _
        ByVal fInheritHandles As Integer, _
        ByVal fdwCreate As UInt32, _
        ByVal lpvEnvironment As IntPtr, _
        ByVal lpszCurDir As IntPtr, _
        ByVal lpsiStartInfo As IntPtr, _
        ByRef lppiProcInfo As PROCESS_INFORMATION) As Integer
    End Function

    Public Shared Function StartProcess(ByVal imageName As String, _
      ByVal cmdLine As String) As Integer
        Dim procInfo As New PROCESS_INFORMATION
        CreateProcess(imageName, cmdLine, _
         IntPtr.Zero, IntPtr.Zero, 0, Convert.ToUInt32(0), _
         IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, procInfo)
        Return Convert.ToInt32(procInfo.dwProcessId)
    End Function

    Public Shared Sub GACInstall(ByVal imageName As String)
        WindowsCE.StartProcess("cgacutil.exe", "-i " & imageName)
    End Sub

    Public Shared Sub GACUninstall(ByVal imageName As String)
        WindowsCE.StartProcess("cgacutil.exe", "-u " & imageName)
    End Sub
End Class

Listing 1. Modifying the GAC Programmatically.

Your client application can then add an assembly to the GAC like so:

WindowsCE.GACUInstall("\Program Files\My App\AssemblyA.dll")

Just as in the desktop Framework assemblies placed into the GAC must be signed with strong names using the AssemblyKeyFile or AssemblyKeyName attributes. The .NET Compact Framework does not support delayed signing using the AssemblyDelaySign attribute.

Creating Cab Files

The simplest way to package your smart device application before deploying it to the device is to create a .cab file. Fortunately, Visual Studio .NET makes this simpler by providing a Build Cab File menu you can invoke by right-clicking on the project. A cab directory will then be created under the current build mode directory that contains a series of .cab files using the naming convention application_platform_cpu.CAB. To view the .cab directory select the Show All Files option as shown in Figure 2.

Figure 2. Cab Files Created for Deployment

Following the naming convention, platform refers to Windows CE (WCE) or Pocket PC (PPC) while the CPU identifies which processor type supported by the .NET Compact Framework the .cab file is targeted for. Supported CPUs include ARM, ARMV4, MIPS, SH3, and X86.

In order to customize the installation process the files needed to customize and rebuild the .cab files are placed in the obj\buildmode directory. In this directory are the BuildCab.bat, Dependencies_platform.txt, and Projectname_platform.inf files along with a configuration file for each processor type. They are used as follows:

  • The batch file can be used to initiate the rebuild of the .cab files. The BuildCab.bat file invokes the CabWiz.exe utility, which can also be invoked in a stand-alone fashion to further customize the build process. To review the syntax for invoking CabWizFor see the topic Cab wizard syntax in the Windows CE .NET 4.2 SDK.
  • The dependencies file contains the list of .cab files that this file is dependant on and always contains a reference to the .NET Compact Framework .cab file for each processor type. At installation time, these dependencies are checked via the vsd_config.txt.platform files to ensure that the correct version of the .NET Compact Framework has been installed on the device. It's important to note that the .NET Compact Framework .cab file is not included in the .cab file created for the project. This is true also of the platform-specific SQL Server CE .cab file needed to install SQL Server CE on the device.
  • The .inf file contains the installation settings to use when the .cab file is executed. To customize the installation on the device the .inf can be modified. A simple example of doing so is to add a shortcut to the Start menu on a Pocket PC. This can be accomplished by modifying the following sections in the .inf file like so:
[DefaultInstall]
CEShortcuts=Shortcuts 

[Shortcuts]
My Application,0,myapp.exe,%CE17%

Here the CEShortcuts section is redirected to the Shortcuts section of the file and there a shortcut (actually a file with a particular format in Windows CE) is created by specifying the text of the shortcut, 0 to identify it as a file, the file to create the shortcut to, and the folder in which to place the shortcut. The identifier %CE17% specifies the \Windows\StartMenu directory on a Windows CE device. For more information about the various sections of the .inf file see the Creating an .INF file topic in the Windows CE .NET 4.2 SDK

Other examples of customizing the .inf file might include adding additional files (for example SQL Server CE databases and .gac files) to be distributed to the device through the .cab file by modifying the SourceDiskFiles section.

Once the .cab files for your project have been created they can be deployed to the smart device, techniques for doing so are discussed in the next section. Simply executing the .cab file on the device will start the installation using the WCELoad executable in Windows CE to perform the unpacking of the .cab file and installation of its contents as well as automatically deleting the .cab file when the installation is complete. Additionally, this process stores information on the device so that the application may be uninstalled by tapping on the Settings, Remove Programs.

Deployment

Actually getting your application onto a smart device can be done in a variety of ways, one of which does not even require you to have created .cab files at all. In this section we'll look at the techniques available to deploy both the .NET Compact Framework and your application and discuss how and when each might be employed.

Deploying the .NET Compact Framework and SQL Server CE

Before your application can be executed the .NET Compact Framework must be installed on the device. And if your application requires SQL Server CE the appropriate .cab file must also be installed. While both are done automatically when deploying the application from Visual Studio .NET using the Deploy menu or when debugging on the device, you'll need a different mechanism to handle this in production.

As mentioned previously, .cab files created for a project do not include the .cab files for the .NET Compact Framework or SQL Server CE. Although Pocket PC 2003 and later devices will typically include the .NET Compact Framework in ROM, for those devices that don't you'll need to deploy the platform specific .cab files. One simple way to do this for the .NET Compact Framework is to download and execute the .NET Compact Framework 1.0 SP2 Redistributable (NETCFSetup.msi). By executing the .msi when the device is cradled the .NET Compact Framework will be installed. If the device is not cradled when the .msi is executed the NCFSetup.msi can be executed from the installation folder once the device is connected. If the device is not able to be cradled, for example for logistical reasons, the platform specific .cab files can be downloaded to the device and executed using one of the techniques discussed in the remainder of this section.

A second more complex technique for deploying and even updating the .NET Compact Framework and SQL Server CE is to work with the sample code provided by Stan Adermann in his article Creating an MSI Package that Detects and Updates the .NET Compact Framework. The sample code shows how you can detect the presence of the .NET Compact Framework on the device by using the CeRapiInitEx and CeFindAllFiles Remote APIs (RAPI) from the desktop and query the registry on the device to determine the version number. You can then detect the device type by calling CeGetVersionEx and then copy the appropriate .cab file to the device and execute it using WCELoad.

XCopy

The simplest way to get a .NET Compact Framework application on a smart device doesn't even require creating .cab files as discussed in the previous section. Just as with the desktop Framework, a .NET Compact Framework application executable and its dependant assemblies (and the .NET Compact Framework .cab itself) can simply be copied to a folder on the device. This is especially the case if the application does not rely on assemblies in the GAC, which would typically entail including a .gac file deployed to the \Windows directory. However, even an XCopy deployed application can register its own assemblies at startup with the GAC using the technique shown above.

There are two primary ways that XCopy deployment can be accomplished.

  • Copy your application to the device using the CECopy utility available through the Windows Mobile Developer Power Toys. This is a command-line utility that relies on the Remote API (RAPI) to connect and copy files.
  • Use the folder synchronization capabilities of ActiveSync configured as shown in Figure 3. Using this feature a .NET Compact Framework application can be copied to a directory on a PC and then automatically copied to the device when an ActiveSync connection is made.

Figure 3. Configuring File Synchronization in ActiveSync 3.7

This technique might be useful during initial development and testing on multiple devices or when the application is very simple and does not require the creation of shortcuts or other installation events.

Web Site

A second way to deploy a .NET Compact Framework application is to place the appropriate cab files on an Intranet site. In this way a user need only navigate to the site using Pocket IE as shown in Figure 4 and click on the hyperlink. In this example the .cab files targeting the devices supported by a particular organization are listed. Of course, this technique works best inside the firewall when the device includes WLAN (wireless LAN) connectivity and outside when it supports WAN connectivity such as TDMA, CDMA, GSDM or GPRS connectivity as with Pocket PC Phone Edition devices.

Figure 4. Deploying from a Web Site

To increase security, the web site can be configured to require Basic authentication and use Secure Sockets Layer (SSL) to protect the .cab files and the communication channel, both of which are supported by Pocket IE on the device.

This technique is relatively simple and would be most useful in Intranet scenarios with power users savvy enough to navigate to the web site. Its' main benefit is that it frees the user from having to cradle the device at a PC. To make this process simpler, if the devices are used for email through Pocket Outlook for example, the hyperlinks could be sent in an email message so that the user need only tap on the hyperlink to install the application.

File Share

A third technique that is almost identical to using a web site is to post the .cab files to a share on a LAN. Obviously, this technique would be used only inside the firewall. The share can also be protected which will prompt the user when opening the share from the File Explorer on the smart device as shown in Figure 5.

Figure 5. Opening a File Share on the Pocket PC.

This technique is effective when the device has WLAN connectivity because once again it frees the user from having to cradle the device but can also be used when the device is cradled using ActiveSync.

Packaged Applications

For larger enterprises that must manage hundreds of devices it is often cost effective to invest in a packaged solution. For example, using XcelleNet's Afaria a central site can be used to monitor and manage a broad range of devices that include Pocket PCs as well as automatically install and maintain applications on those devices.

For scenarios where the device is used within a Wireless LAN (WLAN) in a location-sensitive manner Appear Network's APS (Appear Provisioning Server) can be used. This product enables administrators to configure "Click and Run Zones" so that as devices enter a particular zone software is installed automatically and executed.

In addition Microsoft is adding support for managing mobile devices running Windows CE 4.2 or Windows Mobile 2003 software for the Pocket PC in an upcoming Systems Management Server (SMS) 2003 Device Management Feature Pack (currently in beta).

For more information on managing mobile devices and links to other vendors see the article Pocket PC Systems Management.

Storage Card

One of the common scenarios is that your application may need to be deployed along with a large SQL Server CE database or other large data files. As a result it can be both time consuming and bulky to deploy such an application through a web site or even a cradled connection.

For those reasons the application can be deployed on a memory storage card such as a Compact Flash card. When the card is inserted into the device the user can then browse for the .cab file and execute it to install the application.

However, rather than requiring the user to execute the .cab file once the storage card is inserted into the device, Pocket PC devices include an Autorun feature that can be utilized.

With this feature, when a storage card is inserted into the device, the Pocket PC looks for an executable called Autorun.exe in a folder mapping to the processor type (retrieved from the unmanaged GetSystemInfo function) of the device. For example, if the processor type is ARM, then it will look for the file \Storage Card\2577\Autorun.exe on the storage card. When found, the executable is copied to the \Windows folder and executed with an install parameter. Likewise, when the card is removed from the device, the same executable is launched with an uninstall parameter.

Although beyond the scope of this whitepaper, the Autorun executable needs to perform the following steps.

  • Determine which mode to run in. Since the application is passed either the install or uninstall command-line parameter, the Autorun application must first determine which of the two modes to run in. If the uninstall parameter is detected the Autorun application might simply shut down gracefully or check to see if the application is already running.
  • Verify that the application is not already installed on the device. This can be done by searching for the application's installation directory.
  • Find the storage card name. Since storage card names can differ on localized devices (it won't always be called "Storage Card") and because devices can support more than one storage card, techniques such as using the Windows CE FindFirstFile API function can be used as documented in the article Pocket PC Programming Tips.
  • Find the processor specific .cab file. If multiple processor types must be supported this can be done by first detecting the processor on the device and then mapping that to the appropriate directory on the storage card. The Windows CE GetSystemInfo returns the processor architecture in its SYSTEM_INFO structure. Of course, custom executables for each processor type could be created and placed on the storage card and so this step could be effectively skipped.
  • Execute the .cab file. This can be done using a Windows CE API such as ShellExecuteEx or CreateProcess and can be used to execute both the .NET Compact Framework .cab file and the .cab file for the application since the .NET Compact Framework might not have been previously installed on the device. For this reason the Autorun.exe application is typically written using eMbedded Visual C.

Desktop

Perhaps the most familiar technique for deploying smart device applications is to use the ActiveSync Application Manager so that the application is deployed automatically when the device is cradled and connected to a desktop machine. This process involves passing the Application Manager (CeAppMgr.exe) the path to an .ini file that specifies information about the application to install on the device such as the one shown here.

[CEAppManager]
Version      = 1.0
Component    = MyApp

[MyApp]
Description  = My Application
Uninstall    = MyApp
CabFiles     = MyApp_PPC.ARM.cab, MyApp_PPC.SH3.cab, MyApp_PPC.ARMV4.cab

One of the advantages to this technique is that the Application Manager detects the type of device and therefore copies and installs the correct .cab file on the device. However, this technique obviously requires that the device be brought back into a central location and connected to a PC.

The best way to invoke the Application Manager is to create a custom installer component that you can include in a Setup Project within Visual Studio .NET such as the one Ralph Arvesen discusses in his excellent article Developing and Deploying Pocket PC Setup Applications. The setup project can additionally invoke a prebuild setup in order run the BuildCab.bat file to rebuild the .cab files for your project.

Although well documented in the above referenced article, the core of this technique is to create a class that inherits from System.Configuration.Install.Installer and that can be called by the setup application to invoke the Application Manager like that shown in Listing 2.

<RunInstaller(True)> _
Public Class AppManagerInstaller : Inherits Installer

    Private Sub AppManagerInstaller_AfterInstall(ByVal sender As Object, _
       ByVal e As InstallEventArgs) _
       Handles MyBase.AfterInstall

        Dim ceApp As String = GetCeAppMgr()
        If ceApp = String.Empty Then
            Return
        End If
        Dim args As String = GetIniArgs()

        ' Start the application manager process
        Process.Start(ceApp, args)
    End Sub

    Private Sub AppManagerInstaller_AfterUninstall( _
       ByVal sender As Object, ByVal e As InstallEventArgs) _
       Handles MyBase.AfterUninstall

        Dim ceApp As String = GetCeAppMgr()
        If ceApp = String.Empty Then
            Return
        End If
        Dim args As String = GetIniArgs()

        ' Start the application manager process without parameters
        Process.Start(ceApp, "")
    End Sub

    Private Function GetCeAppMgr() As String

        ' Get the key from the registry
        Dim ceApp As String = KeyExists()
        If ceApp = String.Empty Then
            MessageBox.Show( _
          "The Application Manager is not installed on this machine.", _
             "Setup", MessageBoxButtons.OK, MessageBoxIcon.Error)
            Return String.Empty
        Else
            Return ceApp
        End If

    End Function

    Private Function GetIniArgs() As String
        Return """" & _
         Path.Combine([Assembly].GetExecutingAssembly().Location, _
         "Setup.ini") & """"
    End Function

    Private Function KeyExists() As String
        Dim key As RegistryKey = Registry.LocalMachine.OpenSubKey( _
     "SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\CEAPPMGR.EXE")

        If key Is Nothing Then
            Return String.Empty
        Else
            Return key.GetValue("", String.Empty).ToString
        End If
    End Function
End Class

Listing 2. A Custom Installer for Invoking the ActiveSync Application Manager.

The class in Listing 2 is then compiled as a separate Class Library Assembly and then added as a custom Install and Uninstall action within your setup project. When the AfterInstall event is fired the code looks for the presence of the Application Manager on the machine by querying the registry. If found it constructs the argument to pass to the Application Manager which includes the path to the Setup.ini file. Finally, it starts the Application Manager using Process.Start and passing it in the argument. On uninstall the Application Manager is executed without the argument to allow the user to uninstall the application from the device.

Maintenance

It's one thing to initially deploy a smart device application but quite another to keep it updated as issues are fixed and your business requirements change. This is especially difficult if the devices are not regularly brought back into a central location where they can be cradled and updated using the desktop deployment techniques discussed in the previous section. To address this scenario you can create an auto-updating smart device application using the same concepts frequently used by desktop Framework developers to deploy so-called "Smart Client" applications.

Creating Self-Updating Applications

One approach for creating applications that can update themselves without the use of ActiveSync was documented by Alex Feinman in his fine article Creating Self-Updating Applications With the .NET Compact Framework. In this article a companion Updater application is deployed with your application. This "applet" reads from a configuration file and calls a web service to determine if updates are available for the application. If so, the appropriate .cab file is downloaded and executed on the device.

The advantage to this approach is that it downloads and installs an entirely new version of the application without requiring cradling, it does so in a single .cab file decreasing bandwidth usage, and supports multiple platforms. However, its primary disadvantage is that it doesn't allow your application to interact with the update process.

Creating a Smart Device Application Updater

A more integrated approach would be to create a component that can be referenced by your application in order to integrate the process of updating your application. We've created one called the SmartAppUpdater that we'll discuss in the remainder of this paper and you should feel free to use a starting point for developing your own updater.

Before looking at the functionality, however, you can get an idea for what the component does by looking at the public API shown in Table 1 and the architecture diagram in Figure 6.

Member Description
FileDownloaded Event fired when a file belonging to the new version is updated
Updated Event fired when the new version has been completed downloaded and applied
Updating Event fired when a new version of the application is found
AppName Property that returns the name of the application to update
AppVersion Property that returns the current version of the application
Downloader Property that returns the IDownloadable component used to download files
DownloadLocation Property that returns the server location from where new versions will be downloaded
KeepOld Property that determines whether the previous version of the application should be retained
NewPath Property that returns the path to the new version of the application
RootPath Property that returns the root path of the application
PostProcessors Property that exposes a collection of IPostProcessor objects used to run custom code after the update process
BeginStartUpdate Method that runs the update process on a background thread
LogMessage Overloaded method to log messages
MoveFile Method that moves a file from the current folder to the new folder when an update is made
StartNewVersion Method that starts the new version of the application
StartUpdate Method that runs the update process synchronously

Table 1. The interface for the SmartAppUpdater component.

Figure 6. Architecture of the SmartAppUpdater

To understand how the SmartAppUpdater component can be used we'll break down its functionality in several key areas that include:

  • Bootstrapping the process so that the current version is always loaded
  • Downloading files from a server that include a manifest as well as application files
  • Detecting new versions of the application and downloading updates
  • Raising notifications when a new version is found, files are downloaded, and the application updated
  • Performing post processing so you can inject your own code into the process
  • Starting the new version of the application
  • Cleaning up the old version of the application

Boostrapping the Process

In order to allow a user to execute the most current version of the application a boostrap process is required. This small UI-less .NET Compact Framework application called SmartAppLoader is responsible for reading the SmartAppLoader.xml configuration file shown below and then starting the application.

<?xml version="1.0" encoding="utf-8" ?> 
<!-- Used by the SmartAppLoader to start the current application -->
<SmartAppLoader>
    <AppPath>1.0.0.0</AppPath>
    <AppImage>TestUpdater.exe</AppImage>
    <AppName>Test Updater</AppName>
</SmartAppLoader>

You'll notice that all that is required is the folder relative to the one in which the application is executing along with the name of the executable to start. The SmartAppLoader class shown in Listing 3 is then used to read the configuration file and expose a StartApp method.

Namespace Atomic.CF.Deployment
    Public Class SmartAppLoader
        Private _appPath, _appImage, _appName, _curPath As String
        Public Sub StartApp()
            Atomic.CF.Utils.WindowsCE.StartProcess( _
             _curPath & Path.DirectorySeparatorChar & _appPath & _
             Path.DirectorySeparatorChar & _appImage, Nothing)
        End Sub

        Public Sub New()
            _InitClass()
        End Sub

        Public ReadOnly Property AppName() As String
            Get
                Return _appName
            End Get
        End Property

        Private Sub _InitClass()

            Dim xnl As XmlNodeList
            Dim xd As XmlDocument

            ' Get the current path
            _curPath = Path.GetDirectoryName( _
             [Assembly].GetExecutingAssembly().GetName().CodeBase)

            xd = New XmlDocument
            xd.Load(_curPath & "\SmartAppLoader.xml")

            ' Read the app name
            xnl = xd.GetElementsByTagName("AppName")
            _appName = xnl(0).FirstChild.Value

            ' Read the app path
            xnl = xd.GetElementsByTagName("AppPath")
            _appPath = xnl(0).FirstChild.Value

            ' Read the app image
            xnl = xd.GetElementsByTagName("AppImage")
            _appImage = xnl(0).FirstChild.Value
        End Sub
    End Class
End Namespace

Listing 3. The SmartAppLoader Class.

The StartApp method relies on the WindowsCE class shown in Listing 1 to start the process. As shown below the entry point for the application then simply instantiates the SmartAppLoader object and calls the StartApp method before silently exiting. If an error occurs a message box is displayed. To use the SmartAppLoader any shortcuts specified in the file and created on the device must point to the SmartAppLoader executable.

Public Sub Main()
        Dim l As SmartAppLoader

        Try
            l = New SmartAppLoader
            l.StartApp()
        Catch ex As Exception
            MsgBox("Could not start the application.", _
              MsgBoxStyle.Critical, "Smart App Loader")
        End Try
        ' The loader exits
    End Sub

Downloading Files

Since the SmartAppUpdater component is going to be used to update your application without cradling it is going to need to download files from a server using one of several possible transports, for example from a web site, a web service, or file share. To allow you to plug in your own class to perform the download the SmartAppUpdater assembly includes the IDownloadable interface shown here.

Public Interface IDownloadable
    Sub GetServerFile(ByVal filePath As String, _
      ByVal serverPath As String)
    Property Credentials() As ICredentials
End Interface

This interface simply contains a GetServerFile method that the SmartAppUpdater will invoke to download a file and the Credentials property used to supply credentials for authentication.

Since retrieving new versions from a web site is the most common approach the SmartAppUpdater assembly also includes an HttpDownloader class that implements the interface in Listing 4.

Namespace Atomic.CF.Deployment
    Public Class HttpDownloader
        Implements IDownloadable

        Private _creds As ICredentials

        Public Sub New()
        End Sub

        Public Sub New(ByVal credentials As ICredentials)
            _creds = credentials
        End Sub

        Public Sub GetServerFile(ByVal filePath As String, _
           ByVal serverPath As String) _
           Implements IDownloadable.GetServerFile

            Dim hr As HttpWebRequest
            Dim fs As FileStream
            Dim s As Stream

            Try
                hr = CType(HttpWebRequest.Create(serverPath), _
                   HttpWebRequest)

                If Not _creds Is Nothing Then
                    hr.Credentials = _creds
                End If

                ' Delete file if it exists
                If File.Exists(filePath) Then File.Delete(filePath)

                ' Get new file
                fs = New FileStream(filePath, FileMode.Create)
                s = hr.GetResponse.GetResponseStream

                Dim buffer(4096) As Byte

                Dim bytesRead As Integer = s.Read(buffer, _
                  0, buffer.Length)
                While bytesRead > 0
                    fs.Write(buffer, 0, bytesRead)
                    bytesRead = s.Read(buffer, 0, buffer.Length)
                End While

            Catch e As Exception
                Throw New ApplicationException( _
                  "Could not download file "  & serverPath, e)
            Finally
                If Not s Is Nothing Then s.Close()
                If Not fs Is Nothing Then fs.Close()
            End Try
        End Sub

        Public Property Credentials() As ICredentials _
         Implements IDownloadable.Credentials
            Get
                Return _creds
            End Get
            Set(ByVal Value As ICredentials)
                _creds = Value
            End Set
        End Property
    End Class
End Namespace

Listing 4. The HttpDownloader Class

This simple class implements the GetServerFile method and uses the HttpWebRequest object to download the file using the same technique shown by Jim Wilson in his article Improving .NET Compact Framework HTTP Communications using HttpWebRequest and Custom ASP.NET Providers. Note that the code first applies the ICredentials object if present and deletes the existing file if present before downloading the new file.

The fully qualified name of the IDownloadable component that the SmartAppUpdater will use and optionally the assembly in which it is located is stored in the SmartAppUpdater.xml file. In addition, this configuration file includes the location of the server from which updates will be downloaded, a flag that determines whether old versions of the application are saved, the current version number, the application name, and the path in which the SmartAppLoader lives as shown here.

<?xml version="1.0" encoding="utf-8" ?> 
<!-- Configuration file used by the SmartAppUpdater component -->
<SmartAppUpdater>
    <DownloadLocation>http://192.168.1.101/TestLoader</DownloadLocation>
    <KeepOld>False</KeepOld>
    <Downloader>Atomic.CF.Deployment.HttpDownloader</Downloader>
    <AppVersion>1.0.0.0</AppVersion>
    <AppName>Test Updater</AppName>
    <RootPath>\Program Files\TestUpdater</RootPath>
</SmartAppUpdater>

Note: In order to support multiple platforms the DownloadLocation element in different builds of the application would be configured to point to a platform-specific web sites.

This information is read in a straightforward fashion by a private _InitClass method (which you can inspect in the sample code) called from the component's constructor and placed into private fields (denoted by being prefixed with an underscore). The interesting part of this code, however, is the creation of the IDownloadable component shown in Listing 5.

' Load the configuration file
_curPath = Path.GetDirectoryName( _
     [Assembly].GetExecutingAssembly().GetName().CodeBase)
_xd = New XmlDocument
_xd.Load(_curPath & Path.DirectorySeparatorChar & "SmartAppUpdater.xml")

' Read the loader info and create the type
xnl = _xd.GetElementsByTagName("Downloader")

Dim downloader As String = xnl(0).FirstChild.Value.ToString
Dim split() As Char = {","c}
Dim d() As String = downloader.Split(split)
Dim t As Type

If d.Length = 2 Then ' Load from a different assembly
    Dim a As [Assembly] = [Assembly].LoadFrom(_curPath & 
        Path.DirectorySeparatorChar & d(1))
    t = a.GetType(d(0))
Else
    t = [Assembly].GetExecutingAssembly.GetType(d(0))
End If

_loader = CType(Activator.CreateInstance(t), IDownloadable)

Listing 5. Creating the IDownloadable Component

Here the configuration file is loaded and the Downloader element retrieved from the XmlDocument object. The type name is extracted from the string along with an optional assembly name. If the assembly name is found the assembly is loaded using the LoadFrom method (this assumes the assembly is in the current folder) and the type created. If an assembly name is not found it is assumed the type can be found in the executing assembly. Either way, the object is then instantiated using the CreateInstance method of the System.Activator class and cast back to an IDownloadable object. The _loader reference variable is then used by the component to download the manifest as well as new versions of files.

Detecting and Downloading New Versions

The first responsibility of the SmartAppUpdater component is to detect when new versions of your application are available. As with approaches to do this in desktop Framework apps the SmartAppUpdater looks for a manifest file on a server. For example, the structure of the manifest file looks as follows.

<?xml version="1.0" encoding="utf-8" ?> 
<Application>
<Version>2.0.0.0</Version>
    <Folder>2.0.0.0</Folder>
    <Files>
        <File>Atomic.CF.dll</File>
        <File>NotificationOny.dll</File>
        <File>SmartAppUpdater.dll</File>
        <File>SmartAppUpdater.xml</File>
        <File>TestUpdater.exe</File>
    </Files>
</Application>

This file specifies that the newly available version on the web site is 2.0.0.0 and is located in the folder 2.0.0.0. In addition the Files element contains the names of the files to download.

Note: An enhancement to the SmartAppUpdater component would be to allow a single .cab file to be specified and downloaded that contained all the new files. The IDownloadable component would then execute the .cab file to extract the files in a specific directory. This would save bandwidth when performing the update. A second enhancement would be to add an attribute to the files that need to be installed in the GAC. The SmartAppUpdater could then install the files programmatically as discussed previously in this whitepaper.

When the StartUpdate (or BeginStartUpdate which simply executes the StartUpdate method on a new System.Threading.Thread object) method is called the component first checks for the presence of a network connection using the Network component discussed in our whitepaper Testing for and Responding to Network Connections in the .NET Compact Framework. If a connection is found it calls the private CheckManifest method shown in Listing 6.

Private Function CheckManifest() As XmlNodeList
       Dim newVersion As String
       Dim manifest As XmlDocument
       Dim xnl As XmlNodeList

       Try
           ' Try and download the manifest file if connected
           _loader.GetServerFile(_curPath & Path.DirectorySeparatorChar _
            & "manifest.xml", _downloadLocation & "/manifest.xml")

           ' Open the manifest
           manifest = New XmlDocument
           manifest.Load(_curPath & Path.DirectorySeparatorChar & _
            "manifest.xml")

           ' Read the manifest
           xnl = manifest.GetElementsByTagName("Version")
           _newVersion = xnl(0).FirstChild.Value

           ' Check the versions
           If String.Compare(_newVersion, _appVersion) > 0 Then
               ' Get the new folder
               xnl = manifest.GetElementsByTagName("Folder")
               _serverFolder = xnl(0).FirstChild.Value
               Return manifest.GetElementsByTagName("File")
           Else
               Return Nothing
           End If
       Catch e As Exception
           ' Couldn't get manifest so just go quietly
           Me.LogMessage(e)
           Return Nothing
       End Try
End Function

Listing 6. Checking the Manifest File on the Server

The method shown in Listing 6 uses the IDownloadable class referenced by _loader to download the manifest and load it in an XmlDocument object. It then extracts the version number and compares it against the version number found in the SmartAppUpdater.xml configuration file. Although your application's assembly version information can be obtained by interrogating the Version property of the AssemblyName object retrieved from the System.Reflection.Assembly object, we chose to place the version number in the configuration files for ease of use and flexibility.

If the CheckManifest method finds that the version of the application on the server is greater than the version on the smart device it returns the XmlNodeList that contains the files to be downloaded. At this point the SmartAppUpdater knows that a new version of the application is available so the StartUpdate method continues and must create a new folder on the device and begin downloading the files as shown in Listing 7.

Try
    ' Create a new directory if needed
    _newPath = _rootPath & Path.DirectorySeparatorChar & _newVersion
    If Not Directory.Exists(_newPath) Then
         Directory.CreateDirectory(_newPath)
    End If

    ' Walk through the files and download 
    Dim node As XmlNode
    For Each node In xnl
        Dim newfile As String = _newPath & Path.DirectorySeparatorChar _
          & node.FirstChild.Value
        _loader.GetServerFile(newfile, _downloadLocation & "/" _
         & _serverFolder & "/" & node.FirstChild.Value)
    Next
Catch e As Exception
    Me.LogMessage(e)
    ' Clean up
    If Directory.Exists(_newPath) Then Directory.Delete(_newPath)
    If File.Exists(_curPath & Path.DirectorySeparatorChar & _
     "manifest.xml") Then File.Delete(_curPath & _
     Path.DirectorySeparatorChar & "manifest.xml")
    Throw New ApplicationException("Could not update application", e)
End Try

Listing 7. Creating a new Folder and Downloading Files

In Listing 7 you'll note that the new folder is first created and then each file is downloaded by calling the GetServerFile method of the IDownloadable component. If any errors occur the exception is logged to a text file and the new folder and manifest file deleted.

Once the new version has been downloaded successfully the private UpdateConfigs method is called, which you can peruse in the sample code, to update the SmartAppLoader.xml file. It updates both the AppPath element in order to point to the new application folder and then inserts an OldAppPath element reflecting the current application directory if the KeepOld property is set to False.

Raising Notifications

Throughout the process of detecting an update and downloading files the SmartAppUpdater component can notify your application by raising events. It supports the Updating event which fires when a new version is detected via the manifest file, the FileDownloaded event fired after the GetServerFile method is called to download a file, and the Updated event which is fired when a new update is successfully applied.

Each of these events follows the Event Pattern documented in the .NET Framework SDK by passing the object that threw the event, SmartAppUpdater in this case, as well as an UpdaterEventArgs object that inherits from System.EventArgs as shown here.

Public Class UpdaterEventArgs : Inherits EventArgs
    Public ServerPath As String
    Public AppPath As String
    Public AppName As String
    Public NewVersion As String
    Public CurrentFile As String
End Class

The custom event arguments simply expose public fields that include the path from which the update will be downloaded, the new version's application path, the application name, the new version number, and in the case of the FileDownloaded event the file that was just downloaded. For example, the StartUpdate method instantiates an UpdaterEventArgs object and then raises the Updating event after the manifest is checked and a new version is found.

' Setup the event args
args = New UpdaterEventArgs
args.AppPath = _newPath
args.NewVersion = _newVersion
args.ServerPath = Me.DownloadLocation
args.AppName = Me.AppName

' Raise the updating event
RaiseEvent Updating(Me, args)

From within your application you can add handlers for the events and provide notification to your users or take some specific action. For example, in Listing 8 the SmartAppUpdater is instantiated in the Sync method ostensibly called from a menu item or a button and associates an event handler with the Updating event. The event handler creates a Notification object and adds a balloon notification that provides a visual cue for the user that a new version of the application was found and is now being downloaded as shown in Figure 7.

Note: The Notification class uses the Pocket PC Notification API and was compiled in a separate assembly and referenced by the client application. You'll find this assembly in the sample code for this article.

Private _updater As SmartAppUpdater

Private Sub Sync()
    _updater = New SmartAppUpdater()
    AddHandler _updater.Updating, AddressOf UpdatingApp

    ' Do your synchronization first

    ' Now try to update the application asynchronously
    _updater.BeginStartUpdate()
End Sub

Private Sub UpdatingApp(ByVal sender As Object, _
 ByVal e As UpdaterEventArgs)
        ' Show a notification when the application is being updated
        Dim n As New Notification
        n.Add("Updates found for " & e.AppName & " at " _
          & e.ServerPath, "Smart App Updater", UInt32.Parse(3))
End Sub

Listing 8. Handling Events from the SmartAppUpdater

Figure 7. Notifying the User

Performing Post Processing

One of the most interesting requirements of the SmartAppUpdater component is that it allow you to inject custom code into the process of applying updates. This is particularly important in smart device applications since they'll typically have their own data store such as a SQL Server CE database or set of XML files in the application directory. Often times this data needs to be preserved and so would not be redeployed in a new version. By providing a place where you can run a custom method you can for example, allow your application to compact and copy the SQL Server CE database to the new application directory after an update has been applied.

To assist with this process the SmartAppUpdater assembly includes the simple IPostProcessor interface that exposes a single Process method that takes as its only argument an object of type SmartAppUpdater.

Public Interface IPostProcessor
    Sub Process(ByVal updater As SmartAppUpdater)
End Interface

You can then create your own custom post processors by implementing the IPostProcessor interface as in the case of the class shown in Listing 8.

Public Class MyPostProcessor
    Implements IPostProcessor

    Public Sub Process(ByVal updater As _
      Atomic.CF.Deployment.SmartAppUpdater) _
      Implements Atomic.CF.Deployment.IPostProcessor.Process
        ' Perform other logic here
        updater.MoveFile("MyOtherFile.xml")
        updater.MoveFile("MyOtherFile2.xml")
        updater.LogMessage("Hello from My Post Processor")
    End Sub
End Class

Listing 9. Implementing an IPostProcessor Component.

As you can see custom post processor can use the SmartAppUpdater component to call the MoveFile helper method which moves a file from the old folder to the new folder and even log messages in the SmartAppUpdater's log using the overloaded LogMessage method.

To associate a post processor with the SmartAppUpdater there is an overloaded constructor you can use to pass in the component like so.

Dim updater As New SmartAppUpdater(New MyPostProcessor)

If you need to support multiple post processors the SmartAppUpdater exposes an ArrayList through the PostProcessors property that can be used to add objects.

Inside the SmartAppUpdater, once an update has been applied and the Updated event fired, the StartUpdate method enumerates the ArrayList of PostProcessors casting to the IPostProcessor interface and executing their Process methods passing in a reference to itself.

' Invoke the post processors
Dim o As Object
For Each o In _processors
      If TypeOf (o) Is IPostProcessor Then
         Dim p As IPostProcessor = CType(o, IPostProcessor)
         p.Process(Me)
      Else
         _processors.Remove(o)
      End If
Next

Starting the New Version

By using either a custom post processor or handling the Updated event your application can determine that a new version was downloaded and successfully installed. This allows your code, for example, to prompt the user to restart the application or simply enable a menu item or button indicating the option of restarting.

To assist in restarting the application the SmartAppUpdater exposes a StartNewVersion method that uses the WindowsCE class shown in Listing 1 to execute the SmartAppLoader executable. Since the SmartAppLoader.xml file has been updated the new version of the application will be launched.

Public Sub StartNewVersion()
    WindowsCE.StartProcess(Me.RootPath & _
     Path.DirectorySeparatorChar & "SmartAppLoader.exe", "")
End Sub

To use this in your code you can simply call this method immediately followed by a call to Application.Exit like so.

_updater.StartNewVersion()
Application.Exit()

Cleaning Up Old Versions

The last remaining job is to remove the old version of the application from the device in order to save precious space. This is accomplished by the SmartAppLoader's constructor on the next run of the application if the OldAppPath element is found in the SmartAppLoader.xml file. It is done at this time rather than immediately after the update by the SmartAppUpdater because the old version of the application is then executing and so files would not be able to be deleted.

If the element is found the old folder is deleted using the System.IO.Directory class.

' Read the old path if present
xnl = xd.GetElementsByTagName("OldAppPath")
If xnl.Count > 0 Then
      Dim oldAppPath As String = xnl(0).FirstChild.Value
      Try
           If Directory.Exists(oldAppPath) _
              Then   Directory.Delete(oldAppPath, True)
             ' Clean up the file
             xd.GetElementsByTagName( _
              "SmartAppLoader")(0).RemoveChild(xnl(0))
             xd.Save(_curPath & Path.DirectorySeparatorChar & _
              "SmartAppLoader.xml")
      Catch ex As Exception
          ' Could not clean up
      End Try
End If

Summary

Software created with the .NET Compact Framework provides several challenges for developers in the area of packaging, deployment, and maintenance. Hopefully, using the ideas and resources in this whitepaper you'll be able to more clearly know your options and employ a solution that fits your application's requirements.

Show:
© 2014 Microsoft