Practical ClickOnce - Simple Example
Like many organizations, the company I work for has a pretty diverse history (up to present times) when it comes to programming languages used, application type created and…to the point of this post…deployment methods. It is difficult to picture the future however, without picturing Clickonce deployed applications. Right now a number of our internal developer apps use Clickonce. This was an easy decision - we wanted easy deployment and total control over application versioning (and I don't mean source control) - Clickonce delivers in a big way.
So I've worked with Clickonce a little while now - setting up applications and deploying updates - the basics. But, in an effort to keep up to date (on a technology already at least 3 years old!!!) I decided to invest some time and learn a little more….leading to this first simple example.
In the following example I develop a simple launchpad-type application - a winform with 3 buttons - Accounting, LegalForms and RiskManagement in order to demonstrate one simple task - delayed assembly deployment.
Nobody can accuse this of being a realistic demonstration - but I wanted to demonstrate the ability to delay the deployment of assemblies until specifically required at runtime. The solution itself contains 5 projects - the startup form, and 4 assemblies called at runtime. The accounting/legal forms/risk management projects are basically shells returning string values - for demonstrative purposes.
The deployment strategy is as follows: the ClickOnceDemo (project) is the only required download group. This is the startup project and obviously must be downloaded (or else the application couldn't run to begin with…). I created 3 other download groups - one for accounting assemblies and one each for risk management and legal assemblies.
When the New method is invoked in my main form, an event handler is added for the AppDomain.CurrentDomain.AssemblyResolve event - and the LoadNeededAssemblies function is called. In short, when the application attempts to resolve an assembly that is not in memory, it calls this function. For the purposes of this demo, the relationship between deployment groups and physical DLLs is stored in a two dimensional array named 'assemblyToGroupMapping'. When LoadNeededAssemblies is invoked, we loop through this array searching for an assembly that matches the name passed in parameter 'args' of type ResolveEventArgs in the LoadNeededAssemblies function. Once we find an assembly that matches the one being resolved, we download the entire group using the DownloadFileGroup procedure in the System.Deployment procedure.
Once the filegroup has been downloaded (for brevity we used a synchronous download in this example but asynchronous download of groups is just as straight forward) we load the assembly into memory using the LoadFile procedure in the System.Reflection.Assembly namespace.
Once the assembly is resolved (i.e. the assembly is returned and the function exited) the application seamlessly continues its processing - using the objects in that assembly, in this case without the user ever knowing what was happening behind the scenes.
<br /> Imports System.Reflection<br /> Imports System.Deployment.Application<br /> Imports System.Collections.Generic<br /> Imports System.Security.Permissions<br /> <br /> Public Class MainForm<br /> <br /> Private assemblyToGroupMapping(3, 3) As String<br /> Private WithEvents currentDeployment As ApplicationDeployment<br /> <br /> <securitypermission(securityaction.demand,> _<br /> Public Sub New()<br /> InitializeComponent()<br /> assemblyToGroupMapping(0, 0) = ("AccountingAssemblies")<br /> assemblyToGroupMapping(0, 1) = ("AccountingFunctions")<br /> assemblyToGroupMapping(1, 0) = ("AccountingAssemblies")<br /> assemblyToGroupMapping(1, 1) = ("AccountingGeneralLedger")<br /> assemblyToGroupMapping(2, 0) = ("RMAssemblies")<br /> assemblyToGroupMapping(2, 1) = ("RiskManagementFunctions")<br /> assemblyToGroupMapping(3, 0) = ("LegalAssemblies") 'NOTE: INCORRECT NAME FOR DEMO PURPOSES<br /> assemblyToGroupMapping(3, 1) = ("LegalForms")<br /> AddHandler AppDomain.CurrentDomain.AssemblyResolve, AddressOf LoadNeededAssemblies<br /> End Sub<br /> <br /> Private Function LoadNeededAssemblies(ByVal sender As Object, ByVal args As ResolveEventArgs) As System.Reflection.Assembly<br /> Dim loadedAssembly As Assembly = Nothing<br /> If (ApplicationDeployment.IsNetworkDeployed) Then<br /> currentDeployment = ApplicationDeployment.CurrentDeployment<br /> Dim groupName As String = String.Empty<br /> Dim neededAssembly As String = args.Name.Split(",")(0)<br /> For i = 0 To assemblyToGroupMapping.GetUpperBound(0)<br /> If (assemblyToGroupMapping(i, 1) = neededAssembly) Then<br /> currentDeployment.DownloadFileGroup(assemblyToGroupMapping(i, 0))<br /> loadedAssembly = Assembly.LoadFile(Application.StartupPath & "\" & assemblyToGroupMapping(i, 1) & ".dll")<br /> End If<br /> i = i + 1<br /> Next<br /> End If<br /> Return loadedAssembly<br /> End Function<br /> <br /> Private Sub btnLegalForms_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnLegalForms.Click<br /> Dim lforms As New LegalForms.LegalFormsBase<br /> lforms.CreateLegalForm()<br /> End Sub<br /> <br /> Private Sub btnRiskManagement_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnRiskManagement.Click<br /> Dim rm As New RiskManagementFunctions.RiskManagementBaseClass<br /> MessageBox.Show(rm.CalculateRisk())<br /> End Sub<br /> <br /> Private Sub btnAccounting_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btnAccounting.Click<br /> Dim af As New AccountingFunctions.AccountingFunctionsCommon<br /> MessageBox.Show(af.ReturnFinancialStatus())<br /> End Sub<br /> End Class<br />
In the case of this demo, the event handler for each of the three buttons attempts to instantiate objects contained in assemblies which have not yet been downloaded - i.e. in the deployment groups that we have explicitly named. Clicking on a button (the first time only) results in the system attempting to resolve the required assembly and invoking the functionality just described. Once done, functions are called returning some feedback for demonstrative purposes.
There are two additional things to note in the above source code:
- In the mapping array I have purposefully used the wrong group name in one instance - using LegalAssemblies in place of LegalFormsAssemblies. When the legal forms button is clicked an exception will occur as the necessary assembly will not be available.
- The AccountingFunctions assembly references the AccountingGeneralLedger assembly. Since they are part of the same group, when we attempt to resolve the first assembly (AccountingFunctions) both assemblies are actually downloaded. Since they are both downloaded, we do not need to return to the function to resolve the second assembly