Thursday, March 16, 2006 10:03 PM
jsmith
ClickOnce partial download thoughts
One interesting feature of ClickOnce deployments is the capability to dynamically download files. These files can be anything: assemblies, images, help files, etc. This feature makes sense when you are trying to speed up installation time, or your application requires files that are used occasionally.
When compared to the default ClickOnce scenario, taking advantage of this functionality requires two additional steps:
- changing the application manifest to include one or more optional file groups
- using the System.Deployment types to programmatically download the files in these optional file groups
When I searched on how to do this, all of the code samples I found used this feature for downloading everything except for assemblies. In my application I wanted to use this feature to download an assembly. My assumption was that I could just add the assemblies to a file group and all would be right. Well, that isn't the way it works out. Let me illustrate with an example.
Let's say I am writing a Notepad variant called SecureMyNotepad, and I want to offer email functionality in my version of SecureMyNotepad. Let's also say that I want to split the email functionality into a separate assembly. We now have two assemblies - SecureMyNotepad.exe and WintellectMail.dll. Let's also assume that I want WintellectMail.dll to download only when the menu item "Email" is selected (see below)
The first thing I need to do is setup my applicatoin manifest. To do this, I go to the Publish tab in the Properties of my VS project and select "Application Files"
Notice that the Download Group is changed to "Mail". Internally Visual Studio uses this information to create the following manifest snippet:
<dependency optional="true">
<dependentAssembly dependencyType="install" allowDelayedBinding="true" codebase="WintellectMail.dll" size="20480" group="Mail">
<assemblyIdentity name="WintellectMail" version="1.0.0.0" publicKeyToken="EF7FE5810D23B378" language="neutral" processorArchitecture="msil" />
<hash>
<dsig:Transforms>
<dsig:Transform Algorithm="urn:schemas-microsoft-com:HashTransforms.Identity" />
</dsig:Transforms>
<dsig:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<dsig:DigestValue>g3tJEPdbE6y27FVgz/JrlF9Y1Ks=</dsig:DigestValue>
</hash>
</dependentAssembly>
</dependency>
Now when we publish the application and install it via the normal ClickOnce process the WintellectMail.dll assembly is not downloaded with SecureMyNotepad.exe. We have to write some code in the menu item event handler to download the files in the Mail file group. That code looks like the following:
private void emailToolStripMenuItem_Click(object sender, EventArgs e)
{
// ClickOnce: Check to see if this app was deployed via ClickOnce
// throw exception if it wasn't
if (ApplicationDeployment.IsNetworkDeployed)
{
// ClickOnce: Get the current deployment
deployment = ApplicationDeployment.CurrentDeployment;
// ClickOnce: Check if the Mail FileGroup has been downloaded previously
if (!deployment.IsFileGroupDownloaded("Mail"))
{
// if not, get it
deployment.DownloadFileGroup("Mail");
}
}
else
{
throw new InvalidProgramException("this application was not deployed via ClickOnce.");
}
// ClickOnce: Call method defined in WintellectMail.dll
// Bad Code!
WintellectMail.SendMail();
}
The important point here is the SendMail call (last line of code). The type WintellectMail is defined in the newly downloaded WintellectMail.dll.
At runtime, this code throws an AssemblyLoadException. My first reaction was that I must not be downloading the assembly. After some thinking and some checking, I realized that indeed I wasn't, but the reason has nothing to do with the System.Deployment types.
Remember the JIT Compiler! When this method is invoked for the first time, the JIT Compiler goes to work. Among other things, the JIT Compiler triggers the assembly loader. The assembly loader is trying to load the WintellectMail assembly before the first instruction has been executed, hence the exception.
To fix our bad code, all we have to do is refactor our call to Wintellect.SendMail() into a local method. The JIT Compiler will not complain. The new code is below:
private void emailToolStripMenuItem_Click(object sender, EventArgs e)
{
// ClickOnce: Check to see if this app was deployed via ClickOnce
// throw exception if it wasn't
if (ApplicationDeployment.IsNetworkDeployed)
{
// ClickOnce: Get the current deployment
deployment = ApplicationDeployment.CurrentDeployment;
// ClickOnce: Check if the Mail FileGroup has been downloaded previously
if (!deployment.IsFileGroupDownloaded("Mail"))
{
// if not, get it
deployment.DownloadFileGroup("Mail");
}
}
else
{
throw new InvalidProgramException("this application was not deployed via ClickOnce.");
}
// ClickOnce: Call SendMail method
SendMail();
}
// prevent inlining for all builds (good catch Daniel!)
[MethodImpl(MethodImplOptions.NoInlining)]
private void SendMail()
{
// ClickOnce: Call method defined in WintellectMail.dll
WintellectMail.SendMail();
}
As I said, this isn't exactly groundbreaking, but hopefully it saves someone some time...