Recently while working on a customer project I had a request to provide ability to enable the end user to customize the labels and captions on Form(s) and UserControl(s). In general this is not a hard thing to achieve but it makes the developers life harder since you need some way to track/mark labels and text control and later on in code provide some sort of a mechanism to replace the text that developer entered in Visual Studio Forms designer with text provided by the end user.
I've decided to create a small library for that which you can find at Codeplex: Windows Forms custom localizer
Basic concepts
While thinking about such a solution I realized that Visual Studio forms designer already provides such a functionality. Inside Visual Studio Forms designer each root component (Form or UserControl) gets a custom boolean property called Localizable
. When set to True
this property changes the way that Visual Studio serializes the controls into the appropriate .designer.cs/vb
file.
It creates a ComponentResourceManager
inside the InitializeComponent
method and then uses this resource manager is to provide values for all properties of components which are marked with <Localizable>
attribute
Example:
Private Sub InitializeComponent()
Dim resources As System.ComponentModel.ComponentResourceManager = New System.ComponentModel.ComponentResourceManager(GetType(MainForm))
'
'btnCancel
'
resources.ApplyResources(Me.btnCancel, "btnCancel")
Me.btnCancel.Name = "btnCancel"
Me.btnCancel.UseVisualStyleBackColor = True
'
'Label1
'
resources.ApplyResources(Me.Label1, "Label1")
Me.Label1.Name = "Label1"
'
'TextBox1
'
resources.ApplyResources(Me.TextBox1, "TextBox1")
Me.TextBox1.Name = "TextBox1"
...
This got me thinking. The resource manager does exactly what I would need to do and if I could somehow replace it with my own custom resource manager then I could achieve everything I wanted. I could even handle 3rd party controls because if 3rd party control supports localization then my custom resource manager could provide custom text for it as well. Also if I can get the previous resource manager which was generated by VS Forms designer I could retain the existing functionality of the resource manager which is to provide localization based on satellite resource assemblies and localization of non-text properties. (If you take a closer look at the Visual Studio ComponentResourceManager
you will see that it not only provides text for String
properties of the component but it can also localize additional properties such as Visible
and Size
. This however I chose to ignore that functionality since the original request that I had was just to enable the end user to change captions and label and not to change size or visibility of the control.)
Visual Studio Forms designer challenge
This all looked good while thinking about it but now I had to put my thinking into code. In essence I had to somehow get into the VS Form designer code generation and instead of instantiating the .NET standard ComponentResourceManager
I had to get it to instantiate my own LocalizerComponentResourceManager
.
First thing I decided to do is to encapsulate this functionality into a Component
derived type which I called LocalizerComponent
. My reasoning was that this would enable me to just drop the component to UserControl or Form and have it work its magic automatically. No need to have my (your) code derive from a specific base class if that is in anyway possible.
Creating the component itself was easy. But how to make it replace code generated by VS? Specifically the code generated inside the InitializeComponent()
method? As it turns out there is way to have the component affect the generated code. You need to provide your own serializer for your component. This can be done by attaching the DesignerSerializerAttribute
to your class. For LocalizerComponent
I've created LocalizerComponentSerializer
.
Once you have created your own serializer you have two methods that you need to override: - Serialize
which enables you to provide CodeDom statements which represent serialized data from your component - Deserialize
which enables you to consume the CodeDom statements intended for your component
Once you start messing with serialization you will notice that you do receive the CodeDom structure for the entire InitializeComponent
method, but unfortunately you may not modify anything except provide your own batch of CodeDom statements. This causes issues since you may not affect the initial statement which creates the actual ComponentResourceManager
. What you can do however is generate CodeDom statements which replace the resources
variable with your own resources and this is what the LocalizerComponentSerializer
does inside its Serialize
method. Note: The actual CodeDom statement which *overrides the resource
variable has to be ignored within the Deserialize
method.*
Now that we have successfully replaced the resource manager in the InitializeComponent
designer method we are left with the problem of statements which use the resource manager which have already been executed prior to our component code. Remember that I've said that you may not change the code of other serialized components in the designer method? Visual Studio designer serializes components/controls as it finds them inside its component collection which is ordered in the way that user was placing components/controls on the designer surface. This means that if our LocalizerComponent
was placed last it will also get serialized last and this means that we will override the default resource manager after it has already been used to provide text to other controls.
In order to make our component come first in serialization process we have to ensure that it is the first component in the component collection of the designer. Unfortunately our serializer does not help us there because it is already late when it comes into play. What we can do is to provide our own component/control designer via the DesignerAttribute
attribute attached to our class. For LocalizerComponent
I've created designer just for that purpose called LocalizerComponentDesigner
. This designer does two things: - It ensures that when our LocalizerComponent
is dragged onto the designer surface it gets inserted as first component in the component collection (actually as second since the actual Form/UserControl being designed is the first component) - It also sets the Localizable
property of the root control (Form/UserControl)
With both LocalizerComponentSerializer
and LocalizerComponentDesigner
our LocalizerComponent
is pretty much done. In order to use it the developer just needs to drag it onto the designer surface and thats it .... well there is still one place that it fails. Although the component is now placed as first component in the InitializeComponent
method there is still code which executes before it and that is the actual root component of the designer. For a standard .NET Form control this is not a problem since the control itself has just a few properties affected by resource manager and they do get set at the end of the InitializeComponent
method. But if you have a custom Forms (or custom UserControl) derived class which contains additional child items then these items get serialized right at the beginning of the designer generated method. One example of such a class is the DevExpress RibbonForm where the ribbon and all of its items get serialized into designer method before any of child components do.
It seems like there is not much you can do here because you can not affect that code ... but you can read it ... and then repeat it :). This is a bit of hack and it might not work for all such root classes but it does provide a solid workaround. In essence the LocalizerComponentSerializer
not only serves to override the default resource manager but also to scan any previous statements for any calls of resource manager methods (such as GetResource
and ApplyResources
and repeats them again but now with replaced resource manager. With this we are finished with our localizer component.
Loading/Saving translations
The localizer component itself does not contain or load/save translations. It uses another class called Localizer
. Localizer
class is a translation manager. It holds the actual list of translations and performs saving and loading of them. This however again it does not do by itself but rather through a class deriving from ITranslationProvider. There are two translation providers provided in the library SqlTranslationProvider
(which uses MS SQL database) and SqlCeTranslationProvider
which uses CE database.
So in order to use the localizer library in a project we should create/use a database of appropriate type and create a table which holds our translations. Although the table name and the name of the table fields can be customized for each provider the defaults are the following:
Translations
- Key nvarchar(1000)
- LanguageID int
- Text nvarchar(1000)
- UserDefined bit
More information on how to create table and how it is filled in in the Codeplex project site. Check out the sample demo application to see how this is used.
Creating translate base class
In order to make it easy for the developer to work with our localizer component it would be nice if we could provide base class deriving from Form class so that the developer just needs to inherit from it and everything just works. Now even though it might look that it should be sufficient to just create Form derived class, drag the localizer component to it and call it base class it would not work. The problem here is that each type on its own is localizable. Meaning that if you have hierarchy such as: - Form -> MyBaseForm -> MyWorkerForm each one of those classes has its own resource manager and resource file (assuming Localizable == true
for each one of them). If you want our localizer component to affect all of them you have to drag a new instance of it to each one of them.
Does this mean that we can not make base class? No, it just means we have to be a bit more clever.
Inside the library there is a class called FormTranslatable
which inherits from default .NET Form
which can be used as base class for each form that you want to localized/translated. This class uses the fact that the component being designed (root component in the designer) is sited in the designer and that it can access the designer and its services during design phase. At that point when it detects that it is sited in the designer and there is no LocalizerComponent
already in the designer component collection it just adds a new instance of LocalizerComponent
.
End user translation management
Besides having the ability to perform our own resource translations via database or similar storage system in some scenarios it would also be nice if we could provide the end user of our application to provide translations by him/herself if needed. With that in mind the FormTranslatable
base class also provides a way for the end user to perform the actual translation. It installs a new menu item called 'NLS' in the 'Systems' menu of the form. The systems menu is the menu which appears when you left click on the form icon. This is the only menu which almost always appears on all forms so it is a pretty nice place to invoke localization from. When the user clicks on the new 'NLS' menu item a new form pops out (dlgTranslationManagement
) where the user can see all the text used inside the form and perform translations.
The interesting thing about end user translation is how the FormTranslatable
knows which text it needs to provide for translation. The real form actual type might be several types derived from the FormTranslatable, each one of those types having their own localizer component. The real form might also contain several user controls which contain their own localizer, which in turn might contain other user controls and so on.
The FormTranslatable
solves this by reflection. It goes over itself and all of its children (and their children) and gets all content of all class fields (public or private) which contain our localizer component. It aggregates all of those components and passes it to translation form.
Thanks, I have recently been looking for info about this topic for ages and yours is the best I’ve discovered so far. Keep writing, I can’t wait to read you other article. Best regards