Need to deploy satellite assemblies for your ClickOnce app? One way to do it would be to generate a new deployment for each localized version of your app, and include only the satellite assembly required for each localized version. Needless to say, if you localize to a number of cultures, this will result in the generation of a number of deployments.

Fortunately, new WinForms 2.0 features - including the AutoSize property, the new FlowLayoutPanel and TableLayoutPanel controls, and globally-aware controls such as DataGridView and ToolStrip - make it easier to develop a single application that runs in multiple languages, with the controls growing and shrinking as the UI strings change from language to language. This means that you can deploy a single ClickOnce application with multiple satellite assemblies; WinForms will use the correct one at run time, depending on the setting of Thread.CurrentThread.CurrentUICulture.

But there's an inefficiency here. Obviously, it's silly to force the client to download multiple satellite assemblies when it'll end up using, at most, one. To streamline this scenario, you can mark the satellite assemblies as optional, and download them on demand using the DownloadFileGroup() method.

First, you'll need to mark the assemblies as optional, and assign a group name. Here are the instructions for pulling this off in Visual Studio. (You can do the same thing with MageUI.exe; the next doc update will have topics that cover using both tools to accomplish this.)

  1. Right-click your project in Solution Explorer and select Properties.
  2. Select the Publish tab, and click the Application Files... button.
  3. By default, satellite assemblies will not be included in your deployment, and will not be visible in this dialog. To display them, select the Show all files checkbox.
  4. Satellite assemblies will have names of the form isoCode\ApplicationName.resources.dll, where isoCode is a language identifier in RFC 1766 format. For each satellite assembly, set Publish Status to Include
  5. For each language identifier, select New... from the Download Group drop-down. When prompted for a download group name, enter the language identifier. For example, for a Japanese satellite assembly, you would specify a download group name of ja-JP.
  6. Close the Application Files dialog.

Now you need to include the code to do the on demand download. This needs to happen before the first form in your application is launched. In a C# application in Visual Studio, you can add this code to Main() in Program.cs. In Visual Basic, you will need to add it to the Startup event, which you can access by going to the Project Properties, selecting the Application tab, clicking the View Application Events button, and adding a Startup() event handler using the event drop-downs. (If you are re-using this sample, however, you can just cut and paste this code into ApplicationEvents.vb. Ah, the power of the Handles keyword...)

C#:

using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Threading;
using System.Globalization;
using System.Deployment.Application;
using System.Reflection;

namespace ClickOnce.SatelliteAssemblies
{
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Thread.CurrentThread.CurrentUICulture = new CultureInfo("ja-JP");

            // Call this before initializing the main form, which will cause
            // the resource manager to look for the appropriate satellite assembly.
            GetSatelliteAssemblies(Thread.CurrentThread.CurrentCulture.ToString());

            Application.Run(new Form1());
        }

        static void GetSatelliteAssemblies(string groupName)
        {
            if (ApplicationDeployment.IsNetworkDeployed)
            {
                ApplicationDeployment deploy = ApplicationDeployment.CurrentDeployment;

                if (deploy.IsFirstRun)
                {
                    try
                    {
                        deploy.DownloadFileGroup(groupName);
                    }
                    catch (DeploymentException de)
                    {
                        // Log error. Do not report error to the user, as there may not be a satellite
                        // assembly if the user's culture and the application's default culture match.
                    }
                }
            }
        }

    }
}

VB:

Imports System.Deployment.Application
Imports System.Globalization
Imports System.Threading
Namespace My

    Partial Friend Class MyApplication

        Private Sub MyApplication_Startup(ByVal sender As Object, ByVal e As _
Microsoft.VisualBasic.ApplicationServices.StartupEventArgs)_
Handles Me.Startup
            Thread.CurrentThread.CurrentUICulture = New CultureInfo("ja-JP")
            GetSatelliteAssemblies(Thread.CurrentThread.CurrentUICulture.ToString())
        End Sub

        Private Shared Sub GetSatelliteAssemblies(ByVal groupName As String)
            If (ApplicationDeployment.IsNetworkDeployed) Then

                Dim deploy As ApplicationDeployment = ApplicationDeployment.CurrentDeployment

                If (deploy.IsFirstRun) Then
                    Try
                        deploy.DownloadFileGroup(groupName)
                    Catch de As DeploymentException
                        ' Log error. Do not report error to the user, as there may not be a satellite
                        ' assembly if the user's culture and the application's default culture match.
                    End Try
                End If
            End If
        End Sub
    End Class

End Namespace

Note: In a real-world app, you would remove the line that force-sets ja-JP. This is included merely for testing purposes on English Windows systems. Instead, you can test whether or not the current UI culture is the default for your application. If not, you call GetSatelliteAssemblies(); if so, you leave well enough alone.

Another Note: This technique differs from the way you traditionally do on demand downloading of assemblies, where you listen for the AssemblyResolve event. The AssemblyResolve event appears never to fire for satellite assemblies. I speculate this is because the Resource Manager does its own search for the resource assembly, and resorts to the default without firing AssemblyResolve. I haven't dug deep into the code to confirm this, but my colleagues on the ClickOnce dev team think it's the most plausible explanation.