.NET WinForms localization - replacing ComponentResourceManager
Asked Answered
S

2

8

In my current project (.NET Windows Forms application) I have a requirement that the .NET windows forms should be localized but the localization elements (just the translations, not the images or control positions) should come from the database in order to enable the end users to modify the control localizable properties (just the caption/text) as they wish. To keep the developers unburdened with localization issues the best solution for me would be to simply mark the form as Localizable in the VS designer. This will place all localizable property values in the forms .resx file.

Now my problem is how to provide translations from the database. The moment that the form is marked as Localizable the VS Forms designer will place everything that can be localized is forms .resx file. The designer will also modify the the standard designer.cs InitializeComponent method so that it instantiates ComponentResourceManager and then use that resource manager the load the localizable properties of objects (controls and components).

I have seen solutions where people have build up their own method of applying the localized properties to Form and its controls. All the solutions that I've seen usually boil down to recursively iterating through the Controls collection of Form and its controls and applying translations. Unfortunately the .NET Forms localization is not so simple and this does not cover all scenarios especially if you have some 3rd party controls. For example:

this.navBarControl.OptionsNavPane.ExpandedWidth = ((int)(resources.GetObject("resource.ExpandedWidth")));
// 
// radioGroup1
// 
resources.ApplyResources(this.radioGroup1, "radioGroup1");
...
this.radioGroup1.Properties.Items.AddRange(new DevExpress.XtraEditors.Controls.RadioGroupItem[] {
new DevExpress.XtraEditors.Controls.RadioGroupItem(resources.GetString("radioGroup1.Properties.Items"), resources.GetString("radioGroup1.Properties.Items1")),
new DevExpress.XtraEditors.Controls.RadioGroupItem(resources.GetString("radioGroup1.Properties.Items2"), resources.GetString("radioGroup1.Properties.Items3"))});

All the solutions I've seen could not do translations such as required by the above components.

Since the VS has already generated the code which provides translation where needed my ideal solution would be to somehow replace the ComponentResourceManager with my own derived class. If I could just replace the following line in InitializeComponent:

System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(Form1));

with

System.ComponentModel.ComponentResourceManager resources = new MyComponentResourceManager(typeof(Form1));

then I could solve everything else without any problems.

Unfortunately I did not find how I could do something like that so here I am on SO asking a question on how it could be done.

P.S. I would also accept any other localization solution which matches the requirements: 1. Changing translations should be possible without redeploying the application 2. The developer should not take care about translation when creating forms/user controls

Thank you.

EDIT: Larry provided a great reference to a book which has code which partly solves my problem. With that help I was able to create my own component which replaces the default ComponentResourceManager in the InitializeComponent method. Here is the code for a component called Localizer which replaces the ComponentResourceManager with custom MyResourceManager so that someone else can also benefit from it:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.ComponentModel.Design.Serialization;
using System.CodeDom;
using System.ComponentModel.Design;

namespace LocalizationTest
{
    [Designer(typeof(LocalizerDesigner))]
    [DesignerSerializer(typeof(LocalizerSerializer), typeof(CodeDomSerializer))]
    public class Localizer : Component
    {

        public static void GetResourceManager(Type type, out ComponentResourceManager resourceManager)
        {
            resourceManager = new MyResourceManager(type);
        }

    }

    public class MyResourceManager : ComponentResourceManager
    {
        public MyResourceManager(Type type) : base(type)
        {
        }

    }


    public class LocalizerSerializer : CodeDomSerializer
    {
        public override object Deserialize(IDesignerSerializationManager manager, object codeDomObject)
        {
            CodeDomSerializer baseSerializer = (CodeDomSerializer)
                manager.GetSerializer(typeof(Component), typeof(CodeDomSerializer));
            return baseSerializer.Deserialize(manager, codeDomObject);
        }

        public override object Serialize(IDesignerSerializationManager manager, object value)
        {
            CodeDomSerializer baseSerializer =
                (CodeDomSerializer)manager.GetSerializer(typeof(Component), typeof(CodeDomSerializer));

            object codeObject = baseSerializer.Serialize(manager, value);

            if (codeObject is CodeStatementCollection)
            {
                CodeStatementCollection statements = (CodeStatementCollection)codeObject;
                CodeTypeDeclaration classTypeDeclaration =
                    (CodeTypeDeclaration)manager.GetService(typeof(CodeTypeDeclaration));
                CodeExpression typeofExpression = new CodeTypeOfExpression(classTypeDeclaration.Name);
                CodeDirectionExpression outResourceExpression = new CodeDirectionExpression(
                    FieldDirection.Out, new CodeVariableReferenceExpression("resources"));
                CodeExpression rightCodeExpression =
                    new CodeMethodInvokeExpression(new CodeTypeReferenceExpression("LocalizationTest.Localizer"), "GetResourceManager",
                    new CodeExpression[] { typeofExpression, outResourceExpression });

                statements.Insert(0, new CodeExpressionStatement(rightCodeExpression));
            }
            return codeObject;
        }
    }

    public class LocalizerDesigner : ComponentDesigner
    {
        public override void Initialize(IComponent c)
        {
            base.Initialize(c);
            var dh = (IDesignerHost)GetService(typeof(IDesignerHost));
            if (dh == null)
                return;

            var innerListProperty = dh.Container.Components.GetType().GetProperty("InnerList", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.FlattenHierarchy);
            var innerList = innerListProperty.GetValue(dh.Container.Components, null) as System.Collections.ArrayList;
            if (innerList == null)
                return;
            if (innerList.IndexOf(c) <= 1)
                return;
            innerList.Remove(c);
            innerList.Insert(0, c);

        }
    }


}
Sacchariferous answered 5/3, 2013 at 11:22 Comment(1)
I found more explanation of how Localizer works here: flylib.com/books/en/3.147.1.147/1Whirlwind
R
7

I'm the author of a localization tool for Visual Studio developers (in the interest of full disclosure). I strongly suggest getting hold of a copy of Guy Smith-Ferrier's book, ".NET Internationalization, The Developer's Guide to Building Global Windows and Web Applications". I believe that's the correct book anyway (correct author for sure), but you'll need to check, since it's been a long time since I looked at it (and maybe he's even written something newer since then). Guy is a MSFT MVP and localization guru. He shows you how to do exactly what you're attempting, in his case, by creating a component that you can drag onto the tray area of each of your forms. The component will then allow you to replace the "ComponentResourceManager" with your own (there are several classes involved in his design). You can then read your strings from any source including a DB. IIRC, his own example even uses a DB. You can probably find the code online without having to purchase his book, since I think he may even provide it on his own site. You can also find free passages from his book on reputable book purchasing sites, since the info is invaluable even if you don't use his techniques (very hard to find elsewhere). Note that I tested his code once upon a time (a long time ago), and it works as advertised.

Rittenhouse answered 5/3, 2013 at 16:54 Comment(5)
Thank you very much. I am going to check it out and flag your answer as correct.Guan
@larry how can i replace that of winforms?Bergwall
Great. Thanks. Let me just go read an entire book to get a quick answer.Ifill
@Ifill Who said you have to read the entire book. Just locate the relevant info. It still won't be simple but if you're expecting a "quick" and easy answer to an inherently obscure and non-trivial problem (that few people are familiar with in the real world, circumventing the normal way localization works in Visual Studio), then good luck finding a more timely answer. The op apparently found the book a "great reference" (see his "EDIT") and I concur.Rittenhouse
@Rittenhouse Fair enough.Ifill
A
2

I did replacement this way:

public class CustomCodeDomSerializer : CodeDomSerializer
{
    public override object Serialize(IDesignerSerializationManager manager, object value)
    {
        for (var i = 0; manager.Context[i] != null; i++)
        {
            var collection = manager.Context[i] as CodeStatementCollection;
            if (collection != null)
            {
                foreach (var statement in collection)
                {
                    var st = statement as CodeVariableDeclarationStatement;
                    if (st?.Type.BaseType == typeof(ComponentResourceManager).FullName)
                    {
                        var ctr = new CodeTypeReference(typeof(CustomComponentResourceManager));
                        st.Type = ctr;
                        st.InitExpression = new CodeObjectCreateExpression(ctr, new CodeTypeOfExpression(manager.GetName(value)));
                    }
                }
            }
        }
        var baseClassSerializer = (CodeDomSerializer)manager.GetSerializer(value.GetType().BaseType, typeof(CodeDomSerializer));
        var codeObject = baseClassSerializer.Serialize(manager, value);
        return codeObject;
    }
}

[DesignerSerializer(typeof(CustomCodeDomSerializer), typeof(CodeDomSerializer))]
public class CustomUserControl : UserControl { }

Then view should inherit CustomUserControl instead of UserControl (or CustomForm : Form):

public partial class SomeView : CustomUserControl { ... }

And then result in generated designer file:

private void InitializeComponent()
{
    CustomComponentResourceManager resources = new CustomComponentResourceManager(typeof(SomeView));
    ...
}
Anders answered 23/3, 2018 at 11:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.