• .NET的资源并不限于.resx文件


    为了构建一个轻量级的资源管理框架以满足简单的本地化(Localization)的需求,我试图直接对现有的Resource编程模型进行扩展。虽然最终没能满足我们的需求,但是这两天也算对.NET如何进行资源的存取进行了深入的学习。在本篇文章中,我会通过自定义ResourceManager让资源的存储形式不仅仅局限于.ResX文件,你可以根据需要实现任意的存储方式,比如结构化的XML、数据库表,甚至是通过远程访问获取资源

    一、从添加资源文件(.resx文件)说起

    说起资源,你首先想到的肯定是通过VS添加的扩展名为.resx的资源文件。在这个资源文件中,你不但可以添加单纯的文本资源条目,也可以添加图片、图标、文本文件以及其它类型文件。 不但如此,当你在.resx文件中定义任意类型资源条目的时候,默认定义的代码生成器会为你生成对应的托管代码,让你可以采用强类型编程的方式获取某个条目。

    比如说,如果你在一个名称为Resources.resx的资源文件中定义了两个字符串资源条目,默认的代码生成器或为你生成如下的代码。

    [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
    [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
    [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
    internal class Resources {
        
        private static global::System.Resources.ResourceManager resourceMan;
        
        private static global::System.Globalization.CultureInfo resourceCulture;
        
        [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
        internal Resources() {
        }    
     
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Resources.ResourceManager ResourceManager {
            get {
                if (object.ReferenceEquals(resourceMan, null)) {
                    global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Demo.Properties.Resources", typeof(Resources).Assembly);
                    resourceMan = temp;
                }
                return resourceMan;
            }
        }   
     
        [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
        internal static global::System.Globalization.CultureInfo Culture {
            get {
                return resourceCulture;
            }
            set {
                resourceCulture = value;
            }
        }    
     
        internal static string Greeting4Chris {
            get {
                return ResourceManager.GetString("Greeting4Chris", resourceCulture);
            }
        }
        internal static string Greeting4NewYear {
            get {
                return ResourceManager.GetString("Greeting4NewYear", resourceCulture);
            }
        }
    }
    

    那么你就可以通过生成的这个Resources类(和资源文件同名)的对应的静态只读属性获取对应的值。

       1: var greeting4Chris = Resources.Greeting4Chris;
       2: var greeting4NewYear = Resources.Greeting4NewYear;
    

    从通过代码生成器生成出来的Resources代码,我们可以看出Greeting4ChrisGreeting4NewYear这两个属性的实现是直接通过一个类型为ResourceManager对象的GetString方法获取的。
    那么ResourceManager在背后是通过怎样的机制进行资源文件的读取的呢?

    二、ResourceManager、ResourceSet、ResourceReader与ResourceWriter

    ResourceManager应该是.NET资源编程模型的核心,也可以说是整个资源编程模型的外观类(Facade Class),它提供资源条目提取的API。ResourceManager定义在System.Resources命名空间下,我们不防先来看看ResourceManager的定义。

       1: public class ResourceManager
       2: {
       3:     public ResourceManager(Type resourceSource);
       4:     public ResourceManager(string baseName, Assembly assembly);
       5:     public ResourceManager(string baseName, Assembly assembly, Type usingResourceSet);
       6:  
       7:     public virtual object GetObject(string name);
       8:     public virtual object GetObject(string name, CultureInfo culture);
       9:     public virtual string GetString(string name);
      10:     public virtual string GetString(string name, CultureInfo culture);
      11:  
      12:     public virtual ResourceSet GetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents);
      13:     protected virtual ResourceSet InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents);
      14:     //Others...
      15: }
    

    虽然我们将相应的条目定义在.resx资源文件中(该文件实际上就是一个XML),但是该文件在编译的时候会变成.resources文件(二进制文件)被内嵌到程序集中,所以ResourceManager操作的实际上是内嵌在某个程序集中的.resources文件,这也是为什么在构造函数中需要指定Assembly的原因。构造函数的另一个参数BaseName表示不包括扩展名和Culture Code的.resources文件名,比如说资源文件名为Foo.en-US.resoures对应的BaseName就是Foo

    对于字符串类型的资源条目,通过GetString方法获取,其他类型的文件则通过GetObject获取。而ResourceManager的核心实际上是一个叫做GetResourceSet的方法,方法将所有的资源条目读取出来保存到一个类型为ResourceSet的对象中(该方法最终会调用受保护的方法InternalGetResourceSet)。而ResourceSet在整个资源体系中是一个重要的对象,它充当ResourceManager和物理存储的中介,下面是ResourceSet的定义。

       1: public class ResourceSet : IDisposable, IEnumerable
       2: {
       3:     public ResourceSet(Stream stream);
       4:     public ResourceSet(IResourceReader reader);
       5:     public ResourceSet(string fileName);
       6:    
       7:     public virtual Type GetDefaultReader();
       8:     public virtual Type GetDefaultWriter();
       9:  
      10:     public virtual object GetObject(string name);
      11:     public virtual object GetObject(string name, bool ignoreCase);
      12:     public virtual string GetString(string name);
      13:     public virtual string GetString(string name, bool ignoreCase);
      14:  
      15:     IEnumerator IEnumerable.GetEnumerator();
      16:     public virtual IDictionaryEnumerator GetEnumerator();
      17:     public void Dispose();
      18:     //Others...
      19: }
    

    以持久化文件方式存储的资源最终需要加载到ResourceSet对象中,肯定需要IO操作,所以ResourceSet构造函数中参数分别是Stream、文件名和一个IResourceReader的对象。GetObject和GetString方法,不用多说你也知道是用于某个命名资源条目。由于资源条目实际上就是简单Key-Value对,所以ResourceSet仅仅需要为ResourceManager提供针对每个资源条目的迭代功能,所以ResourceSet的核心应该是返回类型为IDictionaryEnumerator虚方法GetEmunerator方法。

    而ResourceSet得两个GetDefaultReaderGetDefaultWriter方法则涉及到另外两个重要的对象ResourceReaderResourceWriter,故名思义它们分别负责资源的读取和写入。在System.Resources命名空间下,它们各自具有相应的接口:IResourceReaderIResourceWriter,定义如下:

       1: public interface IResourceReader : IEnumerable, IDisposable
       2: {
       3:     void Close();
       4:     IDictionaryEnumerator GetEnumerator();
       5: } 
    
       1: public interface IResourceWriter : IDisposable
       2: {
       3:     void AddResource(string name, object value);
       4:     void AddResource(string name, string value);
       5:     void AddResource(string name, byte[] value);
       6:     void Close();
       7:     void Generate();
       8: }
    

    到这里我们介绍了资源体系下四个重要的对象ResourceManager、ResourceSet、ResourceReader与ResourceWriter,至于他们是如何相互协作以实现对资源的读取和写入的,相信下面会给你答案。

    三、自定义BinaryResourceManager管理单独二进制资源文件(.resources文件)

    我们说过上述的ResourceManager仅仅提供对内嵌于某个程序集中的.resources文件的操作,如果我们直接将资源定义在一个独立的.resources文件、.resx文件甚至是自定义结构的XML文件呢?
    在这种情况下,我们可通过自定义ResourceManager的方式来解决这个问题。为此我定义了如下一个抽象类FileResourceManager作为基于文件的ResourceManager的基类。

     1: public abstract class FileResourceManager: ResourceManager
       2: {
       3:     private string baseName;
       4:     public string Directory { get; private set; }
       5:     public string Extension { get; private set; }
       6:  
       7:     public override string BaseName
       8:     {
       9:         get{ return baseName;}
      10:     }
      11:  
      12:     public FileResourceManager(string directory, string baseName, string extension)
      13:     {
      14:         this.Directory = directory;
      15:         this.baseName = baseName;
      16:         this.Extension = extension;
      17:     }
      18:  
      19:     protected override string GetResourceFileName(CultureInfo culture)
      20:     {
      21:         string fileName = string.Format("{0}.{1}.{2}", this.baseName, culture, this.Extension.TrimStart('.'));
      22:         string path = Path.Combine(this.Directory, fileName);
      23:         if (File.Exists(path))
      24:         {
      25:             return path;
      26:         }
      27:         return Path.Combine(this.Directory, string.Format("{0}.{1}", baseName, this.Extension.TrimStart('.')));
      28:     }
      29: }
    

    属性DirectoryBaseNameExtension分别表示资源文件所在的目录、不包括Culture Code和扩展名的文件名以及扩展名。FileResourceManager集成自ResourceManager类,并重写了GetResourceFileName方法用于获取基于某种Culture的资源文件路径。

    现在我们定义如下一个BinaryResourceManager用于操作单独存在的.resources文件。我自需要重写InternalGetResourceSet,返回的是基于.resources文件名创建的ResourceSet对象。

       1: public class BinaryResourceManager : FileResourceManager
       2: {
       3:     public BinaryResourceManager(string directory, string baseName)
       4:         : base(directory, baseName, ".resources")
       5:     {}
       6:  
       7:     protected override ResourceSet InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents)
       8:     {
       9:         return new ResourceSet(this.GetResourceFileName(culture));
      10:     }
      11: }
    

    现在我们来看看如何使用我们创建的BinaryResourceManager。由于它直接操作ResourceSet来维护资源条目列表,当我们通过指定资源文件名创建ResourceSet的时候,系统会创建一个类型为System.Resources.ResourceReader的对象来读取二进制的.resources文件并将内容写入ResourceSet对象。而.resources文件具有默认的ResourceWrtier,即System.Resources.ResourceWriter

    为了让我们的Demo能够适用于后续的自定义ResourceManager,我写了一些辅助方法,首先是预先创建资源文件的方法PrepareFiles方法。通过传入的BaseName和扩展名,我会创建三个资源文件:<BaseName>.<Extension>、<BaseName>.en-US.<Extension><BaseName>.zh-CN.<Extension>,第一个代码语言文化中性,后者则基于美国英语和简体中文。

      1: static void PrepareFiles(string baseName, string extension)
       2: {
       3:     var fileNames = new string[]{ 
       4:         baseName + "." + extension, 
       5:         baseName + ".en-US." + extension, 
       6:         baseName + ".zh-CN." + extension };
       7:  
       8:     Array.ForEach(fileNames, fileName =>{
       9:             if (!File.Exists(fileName)) File.Create(fileName).Dispose();});
      10: }
    

    然后是用于资源写入操作的AddResource方法,该方法两个参数createWriterculture表示创建IResourceWriter的委托和对应的语言文化。

       1: static void AddResource(Func<IResourceWriter> createWriter, CultureInfo culture)
       2: {
       3:     using (IResourceWriter resourceWriter = createWriter())
       4:     {
       5:         if (culture.Name.StartsWith("en"))
       6:         {
       7:             resourceWriter.AddResource("Greeting4Chris", "Merry Christmas!");
       8:             resourceWriter.AddResource("Greeting4NewYear", "Happy Chinese New Year!");
       9:         }
      10:         if (culture.Name.StartsWith("zh"))
      11:         {
      12:             resourceWriter.AddResource("Greeting4Chris",  "圣诞快乐!");
      13:             resourceWriter.AddResource("Greeting4NewYear", "新年快乐!");
      14:         }
      15:         resourceWriter.Generate();
      16:     }            
      17: }
    

    最后是用于资源读取和输出的DisplayResource方法,该方法通过指定的ResourceManager读取当前需要文化资源并输出。而我指定了三种不同的语言文化环境:en-US、zh-CN和ja-JP

     1: static void DisplayResource(ResourceManager resourceManager)
       2: {
       3:     Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
       4:     Console.WriteLine(CultureInfo.CurrentUICulture.EnglishName);
       5:     Console.WriteLine("	"+resourceManager.GetString("Greeting4Chris"));
       6:     Console.WriteLine("	" + resourceManager.GetString("Greeting4NewYear") + "
    ");
       7:  
       8:     Thread.CurrentThread.CurrentUICulture = new CultureInfo("zh-CN");
       9:     Console.WriteLine(CultureInfo.CurrentUICulture.EnglishName);
      10:     Console.WriteLine("	" + resourceManager.GetString("Greeting4Chris"));
      11:     Console.WriteLine("	" + resourceManager.GetString("Greeting4NewYear") + "
    ");
      12:  
      13:     Thread.CurrentThread.CurrentUICulture = new CultureInfo("ja-JP");
      14:     Console.WriteLine(CultureInfo.CurrentUICulture.EnglishName);
      15:     Console.WriteLine("	" + resourceManager.GetString("Greeting4Chris"));
      16:     Console.WriteLine("	" + resourceManager.GetString("Greeting4NewYear") + "
    ");
      17: }        
    

    最后我们的程序是这样的:

      1: PrepareFiles("GreetingMessages", "resources");
       2:  
       3: AddResource(() => new ResourceWriter("GreetingMessages.resources"), new CultureInfo("en-US"));
       4: AddResource(() => new ResourceWriter("GreetingMessages.en-US.resources"), new CultureInfo("en-US"));
       5: AddResource(() => new ResourceWriter("GreetingMessages.zh-CN.resources"), new CultureInfo("zh-CN"));
       6:  
       7: DisplayResource(new BinaryResourceManager("", "GreetingMessages"));
    

    最终的输出为:

      1: English (United States)
       2:         Merry Christmas!
       3:         Happy Chinese New Year!
       4:  
       5: Chinese (Simplified, PRC)
       6:         圣诞快乐!
       7:         新年快乐!
       8:  
       9: Japanese (Japan)
      10:         Merry Christmas!
      11:         Happy Chinese New Year!
    

    在《下篇》中,我将介绍如何通过自定义ResourceManager操作定义在.resx、XML和数据库表的资源。

  • 相关阅读:
    2019学期第十周编程总结
    2019学期第九周编程总结
    第七次作业
    第六次作业
    第五次作业
    jsp第四次作业
    3.10
    3.4
    3.3jsp作业
    最后一次安卓作业
  • 原文地址:https://www.cnblogs.com/wxlevel/p/7833220.html
Copyright © 2020-2023  润新知