代码改变世界

自定义多语言的实现

2013-03-07 21:24  Mike.Jiang  阅读(1891)  评论(2编辑  收藏  举报

1背景

界面支持多种语言,在使用ASP.NET自带的多语言方案时遇到下列问题:

  • 在做管理类的功能时,有添加、修改和查看页面,需要支持多语言的控件基本相同,但要维护多处,产生冗余(ASP.NET有共享的资源,但它是全局的,不能分 模 块,我们不能所模块的信息入在全局资源中);
  • 在页面中必须要指定资源文件中的KEY;
  • 当页面慢来慢多时,页面与资源的匹配实在难以维护;

所以我认为一个理想的支持多语言框架,需要有以下特性:

  • 分模块解决数据冗余问题;
  • 自动匹配页面与资源文件之间的联系;
  • 易于维护,能通过页面快速定位到资源文件中;

 

2 资源存储于单个文件

先不将最后解决方案贴出来,我们先看看解决方案的第一个版本(在此版本中未解决分模块解决数据冗余的问题, 但更容易理解一些),此方案与ASP.NET原有方案类似,Resource中的KEY是控件的ID,这样我们在BasePage中直接循环页面中的控件,然后根据控件ID找资源。

类图:

代码如下(其中ResourceAccess用ResourceAccess1表示,BasePage用BasePage1表示,CommonResource_EN 和CommonResource_CN是资源文件):

View Code
    public class ResourceAccess1
    {
        private ResourceManager resourceManager = null;

        public ResourceAccess1(ResourceManager resourceManager)
        {
            this.resourceManager = resourceManager;
        }


        public string GetString(string name)
        {
            string str = this.resourceManager.GetString(name);

            if (string.IsNullOrEmpty(str))
            {
                str = string.Format("【{0}】not exist", name);
            }
            return str;
        }
    }


    public class CommonResourceFactory
    {

        public static ResourceAccess GetResource(string lang)
        {
            ResourceAccess resourceAccess = null;
            switch (lang)
            {
                case "CN":
                    {
                        resourceAccess = new ResourceAccess(CommonResource_CN.ResourceManager);
                        break;
                    }
                default:
                    {
                        resourceAccess = new ResourceAccess(CommonResource_EN.ResourceManager);
                        break;
                    }
            }
            return resourceAccess;
        }
    }


    public class BasePage1 : Page
    {
        #region Mult Language

        private ResourceAccess resourceAccess;
        /// <summary>
        /// 是否重新加载语音
        /// </summary>
        public bool ReRenderLanguage;

        private Literal lit;
        private Button btn;
        private LinkButton libtn;

        protected override void OnPreRenderComplete(EventArgs e)
        {
            if (!IsPostBack || ReRenderLanguage)
            {
                resourceAccess = ProductResourceFactory.GetResource("EN");//此处的语言EN,实际运行时,从SESSION中取出
                
                foreach (Control c in Page.Controls)//页面大对象
                {
                    foreach (Control childC in c.Controls)//页面中的普通控件
                    {
                        BindControlLanguage(childC);

                        foreach (Control childCC in childC.Controls) //处理一些容器控件
                        {
                            BindControlLanguage(childCC);

                            foreach (Control childCCC in childCC.Controls)
                            {
                                BindControlLanguage(childCCC);
                                foreach (Control childCCCC in childCCC.Controls)
                                {
                                    BindControlLanguage(childCCCC);
                                }
                            }
                        }
                    }
                }
            }
            base.OnPreRenderComplete(e);
        }
        private void BindControlLanguage(Control c)
        {
            if (c is Literal)
            {
                lit = (Literal)c;
                lit.Text = resourceAccess.GetString(lit.ID);
            }
            else if (c is Button)
            {
                btn = (Button)c;
                btn.Text = resourceAccess.GetString(btn.ID);
            }
            else if (c is LinkButton)
            {
                libtn = (LinkButton)c;
                libtn.Text = resourceAccess.GetString(libtn.ID);
            }
        }

        #endregion
    }

说明:

  • 因为有一个系列对象创建的工作(CommonResource_EN,CommonResource_CN),所以用了一个简单工厂方法;
  • 因为原有简单工厂方法产生对象是ResourceManager, 在System.Resources 命名空间下,这样在引用这个工厂的地方就要引用这个空间,以及为了能在获取信息时做一些处理,所以我们加了一个代理,即ResourceAccess;
  • BasePage, 在OnPreRenderComplete事件与BindControlLanguage方法,是处理控件与资源文件映射的功能。其中如果需要加一些多语言的控件,可以在BindControlLanguage中添加;如果要处理一些功能,如为Repeater类的控件,动态添加一行时需要重新加载多语音,可以设置IsRenderLanguage=true,否则默认仅在页面第一次加载时做多语言。
  • 为什么在OnPreRenderComplete事件中做多语言,因为此事件在Page load 和控件事件之后,在呈现之前,这样我们就可以在控件事件中做一些设置处理。

3 资源存储于多个文件

此解决方案中解决了数据冗余的问题,并可以分模块来存储资源。

类图:

部分代码:

View Code
    public interface IResourceFactory
    {
        ResourceAccess GetResource(string lang);
    }



    public class ProductResourceFactory : IResourceFactory
    {
        public ResourceAccess GetResource(string lang)
        {
            ResourceAccess resourceAccess = null;
            switch (lang)
            {
                case "CN":
                    {
                        resourceAccess = new ResourceAccess(ProductResource_CN.ResourceManager, CommonResource_CN.ResourceManager);
                        break;
                    }
                default:
                    {
                        resourceAccess = new ResourceAccess(ProductResource_EN.ResourceManager, CommonResource_EN.ResourceManager);
                        break;
                    }
            }
            return resourceAccess;
        }
    }


    public class ResourceAccess
    {
        private ResourceManager resourceManager = null;

        private ResourceManager commonResourceManager = null;

        public ResourceAccess(ResourceManager commonResourceManager)
        {
            this.commonResourceManager = commonResourceManager;
        }

        public ResourceAccess(ResourceManager resourceManager,ResourceManager commonResourceManager) 
        {
            this.resourceManager = resourceManager;
            this.commonResourceManager = commonResourceManager;
        }

        public string GetString(string name) 
        {
            string str =  this.resourceManager.GetString(name);

            if (string.IsNullOrEmpty(str))
            {
                str = this.commonResourceManager.GetString(name);
                if (string.IsNullOrEmpty(str))
                {
                    str = string.Format("【{0}】not exist", name);
                }
            }
            return str;
        }

        public string GetComString(string name) 
        {
            string str = this.commonResourceManager.GetString(name);
            if (string.IsNullOrEmpty(str))
            {
                str = string.Format("【{0}】not exist", name);
            }
            return str;
        }
    }

public class BasePage : Page
    {

        public IResourceFactory BaseResourceFactory { set; get; }

        /// <summary>
        /// 是否重新加载语音
        /// </summary>
        public bool ReRenderLanguage;

        private ResourceAccess resourceAccess;
        private Literal lit;
        private Button btn;
        private LinkButton libtn;

        protected override void OnPreRenderComplete(EventArgs e)
        {
            if (!IsPostBack || ReRenderLanguage)
            {
                if (BaseResourceFactory == null)// if BaseResourceFactory is null, then we see it as default "Name+ResourceFacotry"
                {
                    string[] names = this.GetType().BaseType.Namespace.Split('.');
                    string name = names[names.Length - 1];
                    string resourceLayer = "BTS.Resource";
                    BaseResourceFactory = (IResourceFactory)Assembly.Load(resourceLayer).CreateInstance(string.Format("{0}.{1}ResourceFactory",resourceLayer, name));
                }
                if (BaseResourceFactory != null) 
                {
                    resourceAccess = BaseResourceFactory.GetResource(loginInfo.Language);
                    //we do not use recursion, because it create too much stack.
                    foreach (Control c in Page.Controls)//Large control of page,like HTML,Form
                    {
                        foreach (Control childC in c.Controls)//Normal control of page, like button, literal
                        {
                            BindControlLanguage(childC);

                            foreach (Control childCC in childC.Controls) //Container control of page(if one contrl has child controls)
                            {
                                BindControlLanguage(childCC);

                                foreach (Control childCCC in childCC.Controls)
                                {
                                    BindControlLanguage(childCCC);
                                    foreach (Control childCCCC in childCCC.Controls)
                                    {
                                        BindControlLanguage(childCCCC);
                                    }
                                }
                            }
                        }
                    }
                }
            }
            base.OnPreRenderComplete(e);
        }
        private void BindControlLanguage(Control c)
        {
            if (c is Literal)
            {
                lit = (Literal)c;
                lit.Text = resourceAccess.GetString(lit.ID);
            }
            else if (c is Button)
            {
                btn = (Button)c;
                btn.Text = resourceAccess.GetString(btn.ID);
            }
            else if (c is LinkButton)
            {
                libtn = (LinkButton)c;
                libtn.Text = resourceAccess.GetString(libtn.ID);
            }
        }
    }

说明:

  • 因为要分不同的文件去存储资源信息,这样就产生了多系统的资源文件。并且在BasePage中处理这些代码相同,所以用了一个工厂方法,来解决多系列对象创建的工作。
  • 在BasePage中的OnPreRenderComplete事件中,判断了如果BaseResourceFactory不存在,则根据默认命名空间创建ResourceFactory,即页面的命名空间名称默认为工厂名称(去掉ResourceFactory),这样页面就不用做任何的处理了。如果页面的命名空间不是默认的,则需要设置BaseResourceFactory.
  • ResourceAccess中的GetString方法中的规则是:如果模块资源文件中未找到对应的资源,则去公共资源文件中去找。

 

希望对您有所帮助,如果您有更好的解决方法,请告诉我。。。