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
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
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
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
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
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
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
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)
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
ApplyResources and repeats them again but now with replaced resource manager. With this we are finished with our localizer component.
The localizer component itself does not contain or load/save translations. It uses another class called
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:
- 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
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.
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.