Putting it all Together

In this section we are going to illustrate the process of generating an assembly that has a relatively complex structure by developing a short example called GreetMe. The example is a Windows Forms control that displays a localized welcome message in the user's home language. The screenshot shows the control embedded in a test form, which we'll also code-up just to test the control:

click to expand

As you can see, for the UK version (en-GB) I'm going for the informal Northern English dialect market, particularly prominent around Liverpool. (You probably wouldn't use such dialects in a real business application, but doing it here serves to distinguish the different en cultures better)!

Clicking the button brings up a dialog box that shows the flag of the user's home country, just in case the user wants to know what it looks like. In my case, since I'm from the UK, it's this:

click to expand

I'll freely admit this isn't exactly the most useful application I've ever written in my life in terms of sales potential, but we are going to develop it in a way that illustrates a number of points:

  • We are going to have the control as a public assembly (in case any other applications need to display a welcome message to the user and need to use the services of our control to do so...). This means signing it and installing it to the GAC.

  • The assembly is written in two languages. The initial welcome message was written by the company's C# team (that'd be me), while the job of coding that flag dialog was handed out to the VB team (OK, that's me again, but you get the idea). So we need to compile the two modules (called GreetMe and FlagDlg) that make up the assembly separately.

  • The assembly is localized using the .NET satellite assembly localization model.

We're going to start off by developing the example entirely at the command line with a command-line batch file so we can see what's actually going on. Then we'll redo the sample using VS.NET so we can see how to take advantage of VS.NET's built-in support for localization. By doing this, we'll see that although VS.NET makes it very easy to support some localization, it runs into problems if you want to do anything particularly sophisticated. For this reason, the VS.NET version of the sample won't have quite the same features as the command-line version - in particular it won't have the dynamically constructed Flag: <culture> caption for the dialog box that displays the flag.

Command Line GreetMe Example

In terms of assembly structure, the example should look like this:

click to expand

To develop the example, we first need to create our folder structure for the satellite assemblies. For this example we'll have satellites for en-GB, en-US, and de, so I have subfolders for those cultures. I also have a subfolder called TestForm, which will contain the form we'll be using to test the example - I wanted this form in a separate folder to make sure that when I run it, it will only be able to work correctly if the sample has been correctly installed in the GAC. If the test form were in the same folder as the example, then CLR would be able to load the local, private, copy of the example - which means that executing the example successfully would give you no clue as to whether the example was working correctly and picking up the file from the GAC.

click to expand

The above screenshot shows the two files that will be used to generate the resources for the en-GB culture - a file with the strings and a file with the bitmap. Note the file name Strings.en-GB.txt carefully. Ignore the correct format for this file name (or any of the names of resource files for that matter) and your application won't localize properly. The name of the .jpg file doesn't matter because it won't be directly processed by resgen.exe - it has to be converted to a .resx file by resxgen first; it's the .resx file that will need to have a name in the correct format <Resource-Name>.<Culture-Name> .resx, or in our case Flags.en-GB.resx.

The contents of the resource files are as follows. First the text files:

 Strings.en-US.txt Greeting = Howdy World! 

 Strings.en-GB.txt Greeting = Ariite World! ButtonCaption = See yer flag 

 Strings.de.txt Greeting = Hallo, Welt! ButtonCaption = Flagge zeigen DialogCaption = Flagge: 

There is also an invariant-culture version in the main project folder:

 Strings.txt Greeting = Hello, World! ButtonCaption = See Your Flag DialogCaption = Flag: DialogFormatString = {0}: {1} 

Notice how each culture-specific file contains only those strings that will differ from the parent culture-neutral or culture-invariant version. Also notice the DialogFormatString string. This will be passed to String.Format() and used to format the caption of the dialog box (which will display messages like Flag: en-US). It's important that we allow for the possibility of the format string itself being culture-specific in order to take account of grammatical differences between languages. Having said that, even a localizable format string can be crude and you might find you need to do some language-specific processing to insert names etc. into phrases in different languages in a grammatically correct manner, but we'll keep things simple here.

Now for the flags. The default assembly has a bitmap file called NoFlag, which contains a graphic saying that no flag is available, and is used for the invariant culture. There are GB and US flags, but no German flag - it wouldn't be appropriate here since I have no resources specifically localized to Germany, but only to the German language (which could for example mean Austria). The flags I've supplied look like this:

click to expand

The main project GreetMe folder also contains a key file, AdvDotNet.snk containing the public and private key for the assembly we will generate. I created the key file by typing sn -k AdvDotNet.snk at the command prompt.

The build process for this project is going to be very complex since we have to build a total of seven resource files, as well as the source code files, and then sign most of the assemblies and add them to the assembly cache. The simplest way to do this is through a command-prompt batch file to do the work. The contents of that batch file are what this example is ultimately all about. Before we look at the batch file, we ought to have at least a cursory look at the source code that's going to get compiled.

First here's the 3VB code for the Flag dialog. This code is contained in a file called FlagDlg.vb.

 Option Strict Imports System Imports System.ComponentModel Imports System.Drawing Imports System.Windows.Forms Imports System.Reflection Imports System.Resources Imports System.Globalization Imports System.Threading Namespace Wrox.AdvDotNet.GreetMeSample    Public Class FlagDlg Inherits Form       Private FlagCtrl As PictureBox = NewPictureBox()       Public Sub New()          Dim resManager As ResourceManager = New ResourceManager( _               "Strings", [Assembly].GetExecutingAssembly())           Me.Text = _                 String.Format(resManager.GetString("DialogFormatString"), _                 resManager.GetString("DialogCaption"), _                 Thread.CurrentThread.CurrentUICulture.ToString())           resManager = New ResourceManager("Flags", _                [Assembly].GetExecutingAssembly())           Me.ClientSize = New Size(150,100)           FlagCtrl.Image = DirectCast(resManager.GetObject("Flag"), Image)           FlagCtrl.Location = New Point(20,20)           FlagCtrl.Parent = Me        End Sub     End Class End Namespace 

The dialog box, represented by the FlagDlg class, will contain a PictureBox instance that is used to actually display the flag. We first sort out the caption for the dialog, which we want to be the localized equivalent of Flag: <Your culture name>. So we use the ResourceManager.GetString() method to read the DialogCaption string from the Strings resource (which will have been obtained by compiling the relevant Strings.*.txt file. Notice by the way that the resource name has been picked up from the initial file names, Strings.*.txt.) Then we use the Thread.CurrentUICulture property to find out what our culture actually is so we can display it. Note that the only reason the code is explicitly looking up the culture is to display it in the dialog caption - it's not needed for loading the resources since the ResourceManager handles that automatically. Finally, we load up the bitmap and set it to be the Image property of the PictureBox control.

The above VB code will form part of the GreetMe assembly. The rest of the code in the assembly comes from the C# file, GreetMe.cs, which contains the code to display the greeting and button.

 using System; using System.Drawing; using System.Windows.Forms; using System.Reflection; using System.Resources; using System.Globalization; using System.Threading; [assembly: AssemblyVersion("1.0.1.0")] [assembly: AssemblyCulture("")] [assembly: AssemblyKeyFile("AdvDotNet.snk")] namespace Wrox.AdvDotNet.GreetMeSample {    public class GreetingControl : System.Windows.Forms.Control    {       private Label greeting = new Label();       private Button btnShowFlag = new Button();       public GreetingControl()       {          ResourceManager resManager = new ResourceManager("Strings",                                           Assembly.GetExecutingAssembly());          this.ClientSize = new Size(150,100);          greeting.Text = resManager.GetString("Greeting");          greeting.Location = new Point(20,20);          greeting.Parent = this;          btnShowFlag.Text = resManager.GetString("ButtonCaption");          btnShowFlag.Location = new Point(20,50);          btnShowFlag.Size = new Size(100,30);          btnShowFlag.Parent = this;          btnShowFlag.Click += new EventHandler(btnShowFlag_Click);          btnShowFlag.TabIndex = 0;       }       void btnShowFlag_Click(object sender, System.EventArgs e)       {          Flagplg dlg = new FlagDlg();          dlg.ShowDialog();       }    } } 

This code follows similar principles to the VB code. We have two private fields - in this case to hold the Label control that will display the greeting and for the button. We then initialize the text for these controls to appropriate values read in from the Strings resource. Finally, there is an event handler to be invoked when the button is clicked - this event handler simply displays the flag dialog.

Finally we need the code for the test form that will be used to test the sample:

 using System; using System.Windows.Forms; using System.Globalization; using System.Threading; namespace Wrox.AdvDotNet.GreetMeSample {    public class TestHarness : System.Windows.Forms.Form    {       private GreetingControl greetCtrl = new GreetingControl();       public TestHarness()       {          this.Text = "GreetMe Test Form";          greetCtrl.Parent = this;       }    }    public class EntryPoint    {       static void Main(string [] args)       {          if (args.Length > 0)          {             try             {                Thread.CurrentThread.CurrentUICulture = new                                                   CultureInfo(args[0]);             }             catch (ArgumentException)             {                MessageBox.Show("The first parameter passed in " +                                "must be a valid culture string");             }          }          Application.Run(new TestHarness());       }    } } 

Much of this code is fairly standard code to define and display a form. The only noteworthy part of it is that the Main() method takes an array of strings as an argument, and tries to instantiate a CultureInfo() object from the first string in this array. If successful, it sets the thread's UI culture to this culture. The reason for this is that it allows us to test the ability of the sample to be localized to different cultures without you having to actually change the locale of your machine. You just type in something like TestForm de to run the application under German culture, no matter what locale your machine is set to.

That's the source code files sorted out, so now we can look at the build process. Here's the Compile.bat batch file. (Bear in mind that the build process doesn't include generation of the key, since the key needs to remain the same through all builds):

 rem COMPILE DEFAULT RESOURCES resgen Strings.txt resxgen /i:NoFlag.jpg /o:Flags.resx /n:Flag resgen Flags.resx rem COMPILE SOURCE FILES vbc /t:module /r:System.dll /r:System.drawing.dll     /r:System.windows.Forms.dll FlagDlg.vb csc /addmodule:FlagDlg.netmodule /res:Strings.resources /res:Flags.resources     /t:library GreetMe.cs rem COMPILE en-US RESOURCES cd en-US resgen Strings.en-US.txt resxgen /i:USFlag.jpg /o:Flags.en-US.resx /n:Flag resgen Flags.en-US.resx al /embed:Strings.en-US.resources /embed:Flags.en-US.resources    /c:en-US /v:1.0.1.0 /keyfile:../AdvDotNet.snk /out:GreetMe.resources.dll cd .. rem COMPILE en-GB RESOURCES cd en-GB resgen Strings.en-GB.txt resxgen /i:GBFlag.jpg /o:Flags.en-GB.resx /n:Flag resgen Flags.en-GB.resx al /embed:Strings.en-GB.resources /embed:Flags.en-GB.resources    /c:en-GB /v:1.0.1.0 /keyfile:../AdvDotNet.snk /out:GreetMe.resources.dll cd.. rem COMPILE de RESOURCES rem Note that there is no de flag because de could mean Germany or Austria cd de resgen Strings.de.txt al /embed:Strings.de.resources /c:de /v:1.0.1.0    /keyfile:../AdvDotNet.snk /out:GreetMe.resources.dll cd .. rem INSTALL INTO GLOBAL ASSEMBLY CACHE gacutil /i GreetMe.dll gacutil /i en-US/GreetMe.resources.dll gacutil /i en-GB/GreetMe.resources.dll gacutil /i de/GreetMe.resources.dll rem COMPILE TEST FORM cd Test Form csc /r:../GreetMe.dll TestForm.cs cd.. 

The first thing this file does is to compile the culture-invariant resources that will be embedded in the main assembly:

 rem COMPILE DEFAULT RESOURsgen Strings.txt resxgen /i:NoFlag.jpg /o:Flags.resx /n:Flag resgen Flags.resx 

This process should be fairly clear by now. We use resgen to compile Strings.txt into a Strings.resources file. We then do the same for the NoFlag.jpg bitmap - converting it to a Flags.resources file. For the .jpg file, the process is a two-stage one, since we need to use resxgen to create a resource .resx file containing the image first.

This means our culture-invariant resource files are now ready for inclusion in the main assembly when that gets built. Here's how that happens.

 vbc /t:module /r:System.dll /r:System.drawing.dll     /r:System.Windows.Forms.dll FlagDlg.vb csc /addmodule:FlagDlg.netmodule /res:Strings.resources /res:Flags.resources     /t:library GreetMe.cs 

We first compile the VB code for the flag dialog into a module. Then we compile the C# code for the rest of the assembly into a DLL, adding the module and embedding the resources as we go. The order of building is important here: the C# code references the FlagDlg defined in the VB code, which means that the VB code has to be compiled first or we'd have unresolved references. Notice also that the C# compiler is able to automatically load up the MS base class assemblies System.dll, System.Drawing.dll, and System.Windows.Forms.dll, but the VB compiler needs to be informed explicitly of these references - hence the relative lack of references in the csc command.

Next the batch file changes directory and builds the satellite assemblies - starting with the en-US one. (Note the order of building the satellite assemblies, as well as whether the main assembly is built before or after the satellite ones, is immaterial - this is just the order I happen to have picked here.) The process is no different from that for compiling the resources for the main assembly, except that now there is the additional step of using al.exe to actually create an assembly from the resource files. In this code, note the file names and change of folder:

 cd en-US resgen Strings.en-US.txt resxgen /i:USFlag.jpg /o:Flags.en-US.resx /n:Flag resgen Flags.en-US.resx al /embed:Strings.en-US.resources /embed:Flags.en-US.resources /c:en-US    /v:1.0.1.0 /keyfile:../AdvDotNet.snk /out:GreetMe.resources.dll cd .. 

Notice the explicit specification of the key file to be used to sign the assembly in the al command. That was not necessary when compiling the main assembly, since that assembly was created using csc from C# source code - and the C# code contained an assembly attribute to indicate the key file.

The en-GB and de satellites follow suit, with the difference that there is no .jpg file to be included for the German satellite.

Finally, we install all the files into the Global Assembly Cache. We individually install each satellite:

 gacutil /i GreetMe.dll gacutil /i en-US/GreetMe.resources.dll gacutil /i en-GB/GreetMe.resources.dll gacutil /i de/GreetMe.resources.dll 

Finally the test form is compiled in its separate folder:

 cd Test Form csc /r:../GreetMe.dll TestForm.cs cd .. 

That completes the example. If you download the code from www.wrox.com, you'll find all you need to do is run the batch file, then run the TestForm.exe assembly.

Finally I'll mention that there is a Cleanup.bat batch file included with the example, which you can use to remove the sample from the Global Assembly Cache and delete all the files created when the example is compiled. We won't worry about the code in Cleanup.bat here - it's mostly DOS Del commands. However, I will point out the syntax for removing files from the GAC:

 rem REMOVE FILES FROM GLOBAL ASSEMBLY CA----------- gacutil /u GreetMe gacutil /u GreetMe.resources 

Whereas when installing a file, we simply specify the file name (such as GreetMe.dll), when removing an assembly we need to specify the assembly identity. However, if we only specify the name part of the identity, as we have done here, then all assemblies with that name will be removed. Hence, since all the satellite assemblies have the name GreetMe.resources (the different identities of these assemblies being distinguished by their different cultures), the single command gacutil /u GreetMe.resources is sufficient to remove all the satellites from the cache.

VS.NET GreetMe Example

The previous example has shown us in principle how creating a complex assembly works, and in particular how localization in .NET is implemented. However, you'll also have gathered that trying to run everything from the command prompt can be tedious and prone to errors, even though it does have the advantage of giving you a fine degree of control, as well as the ability to automate the process through batch files. In this section, we'll develop an example analogous to the previous one, but we'll do so using Visual Studio .NET, so we can see how VS.NET helps us out. For this example, what I'm really interested in doing is showing you the mechanisms that VS.NET uses to assist with localization. Because of this, I'll only work through the things that are different with VS.NET. In particular, VS.NET can't really help us with the flag dialog caption, which had to be generated from a localizable format string, nor can it install assemblies into the GAC (short of adding custom build steps). Therefore the VS.NET example will not cover those areas.

There is one other change for this example. VS.NET at the time of writing does not support using more than one language in the code for the same assembly, so we'll instead create two separate assemblies - one for the flag dialog (in VB again), and one for the greeting control (in C#, as before).

Thus the project involves asking VS.NET to create a multi-project solution, containing:

  • A C# Windows Forms project called TestForm for the main test form.

  • A C# Windows control project called GreetMe for the greeting control.

  • A VB Windows Forms project called FlagDlg for the flag dialog.

In creating this project, I also changed the VB project settings from the VB default of an executable to a Class Library project, as well as changing its default namespace to Wrox.AdvDotNet.GreetMeSample. And I added all the namespaces we need to reference in the project properties (for the C# projects, these last two changes are made directly in the source code rather than project properties). I also made the obvious changes to the Project Dependencies and Project References to ensure that each project has the appropriate references to the assemblies it references.

I won't present the code for the TestForm project - all we do to this sample is modify the wizard-generated code so that it roughly matches that for the TestForm project in the previous sample.

The GreetMe project is more interesting - other than modifying the assembly attributes for the key file, and version, etc., and adding the event handler for the button, there is no code to add manually. We simply use the design view to drop a label and button onto the control and to add a Click event handler for the button. The code for the button click event handler is the same as in the previous sample, other than the wizard-generated event handler name:

 private void btnShowFlag_Click(object sender, System.EventArgs e) {    FlagDlg dlg = new FlagDlg();    dlg.ShowDialog(); } 

Now we need to localize the project. Now for this project, localizing it simply means providing culture-specific strings for the text of the button and label controls - and the amazing thing is that VS.NET allows us to do this entirely using the Design View and Properties window. First, we locate the Localizable property in for the top level (GreetMe) control in the Properties window, and set it to True. This has quite a significant effect on the code in the wizard-generated InitializeComponent() method. Remember that when you first create a project, InitializeComponent() looks something like this:

 private void InitializeComponent() {    this.btnShowFlag = new System.Windows.Forms.Button();    this.SuspendLayout();    //    // btnShowFlag    //    this.btnShowFlag.Location = new System.Drawing.Point(40, 64);    this.btnShowFlag.Name = "btnShowFlag";    this.btnShowFlag.TabIndex = 0;    this.btnShowFlag.Text = "Show Your Flag";    this.btnShowFlag.Click += new System.EventHandler(this.button1_Click); 

I haven't presented the code for the whole method here - just enough of the part that sets up the Button control, btnShowFlag, to remind us what's going on. Any properties on any controls that are not set in the Properties window to their default values (as indicated by the DefaultValueAttribute attribute in the code for the controls that Microsoft has written) are explicitly initialized by VS.NET-generated code.

Now look at the equivalent code when we set Localizable to True in the Properties window:

 private void InitializeComponent() {    System.Resources.ResourceManager resources = new         System.Resources.ResourceManager(typeof(GreetingControl));    this.btnShowFlag = new System.Windows.Forms.Button();    this.SuspendLayout();    //    // btnShowFlag    //    this.btnShowFlag.AccessibleDescription = ((string)(resources.GetObject(         "btnShowFlag.AccessibleDescription")));    this.btnShowFlag.AceessibleName = ((string)(resources.GetObject(         "btnShowFlag.AccessibleName")));    this.btnShowFlag.Anchor = ((System.Windows.Forms.AnchorStyles)(         resources.GetObject("btnShowFlag.Anchor")));    this.btnShowFlag.BackgroundImage = ((System.Drawing.Image)(         resources.GetObject("btnShowFlag.BackgroundImage")));    this.btnShowFlag.Dock = ((System.Windows.Forms.DockStyle)(         resources.GetObject("btnShowFlag.Dock")));    // etc. 

There is now an awful lot of code - I've only shown a small fraction of the code that sets up btnShowFlag. Two changes are apparent: firstly, the values of all the properties are being read in from a resource using the ResourceManager class, instead of being hard-coded into the program. Secondly, whereas before only those properties whose required values differed from the default values were set, now all properties are being set. (It has to be done like that because VS.NET has no way of knowing whether at run time the resource will contain the default value or not.) Notice, however, that this means we do pay a considerable run-time performance penalty (as well as an assembly-size penalty) for using VS.NET: when we developed our application at the command line, we only had the program load those strings from a resource that we knew were going to be localized. With VS.NET, everything gets loaded that way.

What's happened is that VS.NET has created a default .resx resources file behind the scenes for us, and written values into this resource for every property that is visible in the Properties window for every control that is visible in the Design View. This is the file we saw a small section of earlier in the chapter.

There's no magic going on here - although VS.NET has done so much work that it almost looks like magic. This is a perfectly normal .resx file that we could have written ourselves if we'd had the time (quite a lot of time!) to do so. It's a .resx file rather than a .txt file, so it can be used to define properties that are not strings (such as the sizes of controls). The file has been initially set up to contain the normal default values for each property, and we can now just edit these values using the Properties window. In the Properties window for the parent control (or form), you find the Language property for the form in its Properties window - initially it'll be set to Default, indicating the invariant culture. Just click to select a culture from the drop-down box:

click to expand

Now any changes you make in the Properties window either for that control (or form) or for all other controls displayed in the Design View will only affect the localized resources for that language. So with the above screenshot we can now use the Properties window to set up our localized text for en-US, then return to change the Language from this drop-down list to set up the text for some other language. For a project which is not set to Localizable, changing properties in the Properties window causes VS.NET to directly modify the code in the InitializeComponent() method. For a Localizable project, the changes you make in the Properties window do not affect the source code, but instead cause VS.NET to modify the relevant .resx file. Note that selecting a new culture in the Localization drop-down list will cause a new .resx file to be created if one for that culture does not already exist.

VS.NET will automatically handle the compiling of resources into correctly named satellite assemblies for you, and will create the appropriate folder structure to hold these assemblies in the bin/debug (or bin/release) subfolder of the GreetMe project. However, we actually need this folder structure in the bin/debug (or bin/release) subfolder of the Test Form project, where the executable will execute from. Unfortunately there appears to be a bug in VS.NET whereby on some installations it fails to copy these folders across, preventing the application from localizing. If you find that problem affects you, you'll need to copy the folders and satellite assemblies over manually, or add a custom build step in VS.NET to do so.

Finally, let's examine the code for the new FlagDlg project. Once again, we can let VS.NET handle most of the work. We use the Design View to create a picture box on the form, and can then set Localizable property of the form to True, and work through the cultures we are going to support, for each one setting the Image property of the picture box to the appropriate .jpg file for the flag. Note that the JPG files are supplied in the main folder for this project in the code download. That's the point at which we will leave that example. There is one resource that we haven't localized: the caption for the FlagDlg dialog box. The reason is that, although VS.NET will allow us to localize it to a constant string using the same technique as for all the other localized resources, that's not good enough for us: we need to generate the caption dynamically with a localized formatting string. Unfortunately, VS.NET won't really help us there, so we will need to fall back on defining our own resource files - which is likely to involve adding custom build steps to the VS.NET project.

If you start working with localization and VS.NET, there is another tool that's worth checking out, but we don't have space to go into its details here. The tool is winres.exe tool - its a dedicated resource editor, supplied with the VS.NET, and can be started by typing winres at the VS.NET command prompt.



Advanced  .NET Programming
Advanced .NET Programming
ISBN: 1861006292
EAN: 2147483647
Year: 2002
Pages: 124

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net