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

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

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

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

  1. [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
  2. [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
  3. [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
  4. internal class Resources {
  5. private static global::System.Resources.ResourceManager resourceMan;
  6. private static global::System.Globalization.CultureInfo resourceCulture;
  7. [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
  8. internal Resources() {
  9. }
  10. [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
  11. internal static global::System.Resources.ResourceManager ResourceManager {
  12. get {
  13. if (object.ReferenceEquals(resourceMan, null)) {
  14. global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Demo.Properties.Resources", typeof(Resources).Assembly);
  15. resourceMan = temp;
  16. }
  17. return resourceMan;
  18. }
  19. }
  20. [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
  21. internal static global::System.Globalization.CultureInfo Culture {
  22. get {
  23. return resourceCulture;
  24. }
  25. set {
  26. resourceCulture = value;
  27. }
  28. }
  29. internal static string Greeting4Chris {
  30. get {
  31. return ResourceManager.GetString("Greeting4Chris", resourceCulture);
  32. }
  33. }
  34. internal static string Greeting4NewYear {
  35. get {
  36. return ResourceManager.GetString("Greeting4NewYear", resourceCulture);
  37. }
  38. }
  39. }

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

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

从通过代码生成器生成出来的Resources代码,我们可以看出Greeting4ChrisGreeting4NewYear这两个属性的实现是直接通过一个类型为ResourceManager对象的GetString方法获取的。

那么ResourceManager在背后是通过怎样的机制进行资源文件的读取的呢?

二、ResourceManager、ResourceSet、ResourceReader与ResourceWriter

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

  1. 1: public class ResourceManager
  2. 2: {
  3. 3: public ResourceManager(Type resourceSource);
  4. 4: public ResourceManager(string baseName, Assembly assembly);
  5. 5: public ResourceManager(string baseName, Assembly assembly, Type usingResourceSet);
  6. 6:
  7. 7: public virtual object GetObject(string name);
  8. 8: public virtual object GetObject(string name, CultureInfo culture);
  9. 9: public virtual string GetString(string name);
  10. 10: public virtual string GetString(string name, CultureInfo culture);
  11. 11:
  12. 12: public virtual ResourceSet GetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents);
  13. 13: protected virtual ResourceSet InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents);
  14. 14: //Others...
  15. 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. 1: public class ResourceSet : IDisposable, IEnumerable
  2. 2: {
  3. 3: public ResourceSet(Stream stream);
  4. 4: public ResourceSet(IResourceReader reader);
  5. 5: public ResourceSet(string fileName);
  6. 6:
  7. 7: public virtual Type GetDefaultReader();
  8. 8: public virtual Type GetDefaultWriter();
  9. 9:
  10. 10: public virtual object GetObject(string name);
  11. 11: public virtual object GetObject(string name, bool ignoreCase);
  12. 12: public virtual string GetString(string name);
  13. 13: public virtual string GetString(string name, bool ignoreCase);
  14. 14:
  15. 15: IEnumerator IEnumerable.GetEnumerator();
  16. 16: public virtual IDictionaryEnumerator GetEnumerator();
  17. 17: public void Dispose();
  18. 18: //Others...
  19. 19: }

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

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

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

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

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

我们说过上述的ResourceManager仅仅提供对内嵌于某个程序集中的.resources文件的操作,如果我们直接将资源定义在一个独立的.resources文件、.resx文件甚至是自定义结构的XML文件呢?

在这种情况下,我们可通过自定义ResourceManager的方式来解决这个问题。为此我定义了如下一个抽象类FileResourceManager作为基于文件的ResourceManager的基类。

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

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

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

  1. 1: public class BinaryResourceManager : FileResourceManager
  2. 2: {
  3. 3: public BinaryResourceManager(string directory, string baseName)
  4. 4: : base(directory, baseName, ".resources")
  5. 5: {}
  6. 6:
  7. 7: protected override ResourceSet InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents)
  8. 8: {
  9. 9: return new ResourceSet(this.GetResourceFileName(culture));
  10. 10: }
  11. 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. 1: static void PrepareFiles(string baseName, string extension)
  2. 2: {
  3. 3: var fileNames = new string[]{
  4. 4: baseName + "." + extension,
  5. 5: baseName + ".en-US." + extension,
  6. 6: baseName + ".zh-CN." + extension };
  7. 7:
  8. 8: Array.ForEach(fileNames, fileName =>{
  9. 9: if (!File.Exists(fileName)) File.Create(fileName).Dispose();});
  10. 10: }

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

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

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

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

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

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

最终的输出为:

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

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

.NET的资源并不限于.resx文件的更多相关文章

  1. .NET的资源并不限于.resx文件(二)

    ResourceManager在默认的情况下只能提供对内嵌于程序集的.resources资源文件的存取. 为了实现对独立二进制.resources资源文件的支持,我们自定义了BinaryResoruc ...

  2. resx文件在X64位编译,提示“未能加载文件或程序集”的问题?

    原文:resx文件在X64位编译,提示"未能加载文件或程序集"的问题? resx文件在X64位编译,提示"未能加载文件或程序集"的问题? 解答: 错误现象如下 ...

  3. .resources文件转.resx 文件

    最近在进行.net winform应用程序的反向工程,资源文件反向出来后都是.resources文件,工程编译和运行都没有问题,但.resources文件为二级制文件,无法在Visual Studio ...

  4. C#窗体的resx文件

    这些图片在项目文件中没找到,原来都存在了resx文件中. 属性界面的Image.BackgroundImage属性手动选择的图片会自动存储到resx文件中,之后这些图片源文件就可以删除了.resx中的 ...

  5. C#的Winform多语言实现(resx文件)

    1. 简体中文 2. 繁体中文 3. 英文 下面子丰介绍一下实现的过程: 1. 为每个窗口创建相应语言的resx文件.子丰以英文为例,右键->添加->新建项->资源文件,文件名为窗口 ...

  6. WPF 的另类资源方式 Resources.resx

      类似Winform的搞法,可以把资源放到Resources.resx中. 1.字符串 打开这个编辑器后,输入Name和Value就可以了. CS代码里面,很简单的调用: var title = W ...

  7. C#Winform中resx文件无效 找不到路径

    问题由来 笔者因为更改了添加的图片的路径,再把路径改成图片所在的路径还是报resx文件无效,未能找到路径 问题原因 其实这个问题是因为对对象的引用修改了,但是resx文件中的应用还是没有修改.因为re ...

  8. Java中用ClassLoader载入各种资源(类、文件、web资源)的方法

    lassLoader主要对类的请求提供服务,当JVM需要某类时,它根据名称向ClassLoader要求这个类,然后由ClassLoader返回这个类的class对象. ClassLoader负责载入系 ...

  9. 【转载】IIS7.5(经典模式)访问静态资源(.css和.js文件)提示:未能执行 URL

    IIS7.5(经典模式)静态资源(.css和.js文件)提示:未能执行 URL “/”应用程序中的服务器错误. 未能执行 URL. 说明: 执行当前 Web 请求期间,出现未处理的异常.请检查堆栈跟踪 ...

随机推荐

  1. MyBatis连接MySQL8配置

    <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</a ...

  2. android-interview

    如何减小安装包的大小 主要是减小资源的大小 不常使用的资源,使用时再从网络下载. 绘制代替图片资源 OOM (Out Of Memory) https://www.zhihu.com/question ...

  3. 用RecyclerView做一个小清新的Gallery效果 - Ryan Lee的博客

    一.简介 RecyclerView现在已经是越来越强大,且不说已经被大家用到滚瓜烂熟的代替ListView的基础功能,现在RecyclerView还可以取代ViewPager实现Banner效果,当然 ...

  4. HTML笔记03------cookie

    新浪布局 初始布局代码: div.header+(div.container>(div.left+div.right))+div.footer ---------- .header{height ...

  5. IP 多播

    IP 多播 一.IP 多播的基本概念 1.1.简介 不使用多播时需要发送 90 次单播: 使用多播时只需要发送 1 次多播: 1.2.IP 多播的一些特点 多播使用组地址:D 类IP地址支持多播.多播 ...

  6. 93-time模块

    目录 time模块 一.time模块 1.1 时间戳 1.2 格式化时间 1.3 结构化时间 1.4 不同格式时间之间的转换 1.5 其它用法 time模块 一.time模块 import time ...

  7. sql -- 获取商品分类的最新销售情况

    表设计: 需求: 1.先找出各个分类中销售的最新日期 select prod_class,max(sales_date) as sn from prod_sales group by prod_cla ...

  8. Java Web环境配置

    准备工作 jdk-8u241 apache-tomcat-9.0.31-windows-x64.zip Eclipse IDE for Enterprise Java Developers 关于版本选 ...

  9. FCC 成都社区·前端周刊 第 7 期

    01. ES2016, 2017, 2018 中的新特性 文章介绍了 18 个 ECMAScript 2016,2017 和 2018 中新增加的特性,这些特性已被加入到 TC39 提案中.包括Arr ...

  10. 广告行业中那些趣事系列6:BERT线上化ALBERT优化原理及项目实践(附github)

    摘要:BERT因为效果好和适用范围广两大优点,所以在NLP领域具有里程碑意义.实际项目中主要使用BERT来做文本分类任务,其实就是给文本打标签.因为原生态BERT预训练模型动辄几百兆甚至上千兆的大小, ...