.NET的資源并不限于.resx文件,你可以采用任意存儲形式 [上篇]
為了構建一個輕量級的資源管理框架以滿足簡單的本地化(Localization)的需求,我試圖直接對現有的Resource編程模型進行擴展。雖然最終沒能滿足我們的需求,但是這兩天也算對.NET如何進行資源的存取進行了深入的學習,所以將我對此的認識通過博文的方式與諸位分享。在本篇文章中,我會通過自定義ResourceManager讓資源的存儲形式不僅僅局限于.ResX文件,你可以根據需要實現任意的存儲方式,比如結構化的XML、數據庫表,甚至是通過遠程訪問獲取資源。(文中的例子從這里下載)
一、從添加資源文件說起
二、ResourceManager、ResourceSet、ResourceReader與ResourceWriter
三、自定義BinaryResourceManager管理單獨二機制資源文件
一、從添加資源文件(.resx文件)說起
說起資源,你首先想到的肯定是通過VS添加的擴展名為.resx的資源文件。在這個資源文件中,你不但可以添加單純的文本資源條目,也可以添加圖片、圖標、文本文件以及其它類型文件。 不但如此,當你在.resx文件中定義任意類型資源條目的時候,默認定義的代碼生成器會為你生成對應的托管代碼,使你可以采用強類型編程的方式獲取某個條目。
比如說,如果你在一個名稱為Resources.resx的資源文件中定義了如上圖所示的兩個字符串資源條目,默認的代碼生成器或為你生成如下的代碼。
[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類(和資源文件同名)的對應的靜態只讀屬性獲取對應的值。
var greeting4NewYear = Resources.Greeting4NewYear;
從通過代碼生成器生成出來的Resources代碼,我們可以看出Greeting4Chris和Greeting4NewYear這兩個屬性的實現是直接通過一個類型為ResourceManager對象的GetString方法獲取的。那么ResourceManager在背后是通過怎樣的機制進行資源文件的讀取的呢?
二、ResourceManager、ResourceSet、ResourceReader與ResourceWriter
ResourceManager應該是.NET資源編程模型的核心,也可以說是整個資源編程模型的外觀類(Facade Class),它提供資源條目提取的API。ResourceManager定義在System.Resources命名空間下,我們不防先來看看ResourceManager的定義。
{
public ResourceManager(Type resourceSource);
public ResourceManager(string baseName, Assembly assembly);
public ResourceManager(string baseName, Assembly assembly, Type usingResourceSet);
public virtual object GetObject(string name);
public virtual object GetObject(string name, CultureInfo culture);
public virtual string GetString(string name);
public virtual string GetString(string name, CultureInfo culture);
public virtual ResourceSet GetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents);
protected virtual ResourceSet InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents);
//Others...
}
雖然我們將相應的條目定義在.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的定義。
{
public ResourceSet(Stream stream);
public ResourceSet(IResourceReader reader);
public ResourceSet(string fileName);
public virtual Type GetDefaultReader();
public virtual Type GetDefaultWriter();
public virtual object GetObject(string name);
public virtual object GetObject(string name, bool ignoreCase);
public virtual string GetString(string name);
public virtual string GetString(string name, bool ignoreCase);
IEnumerator IEnumerable.GetEnumerator();
public virtual IDictionaryEnumerator GetEnumerator();
public void Dispose();
//Others...
}
以持久化文件方式存儲的資源最終需要加載到ResourceSet對象中,肯定需要IO操作,所以ResourceSet構造函數中參數分別是Stream、文件名和一個IResourceReader的對象。GetObject和GetString方法,不用多說你也知道是用于某個命名資源條目。由于資源條目實際上就是簡單Key-Value對,所以ResourceSet僅僅需要為ResourceManager提供針對每個資源條目的迭代功能,所以ResourceSet的核心應該是返回類型為IDictionaryEnumerator虛方法GetEmunerator方法。
而ResourceSet得兩個GetDefaultReader和GetDefaultWriter方法則涉及到另外兩個重要的對象ResourceReader和ResourceWriter,故名思義它們分別負責資源的讀取和寫入。在System.Resources命名空間下,它們各自具有相應的接口:IResourceReader和IResourceWriter,定義如下:
{
void Close();
IDictionaryEnumerator GetEnumerator();
}
public interface IResourceWriter : IDisposable
{
void AddResource(string name, object value);
void AddResource(string name, string value);
void AddResource(string name, byte[] value);
void Close();
void Generate();
}
到這里我們介紹了資源體系下四個重要的對象ResourceManager、ResourceSet、ResourceReader與ResourceWriter,至于他們是如何相互協作以實現對資源的讀取和寫入的,相信下面會給你答案。
三、自定義BinaryResourceManager管理單獨二進制資源文件(.resources文件)
我們說過上述的ResourceManager僅僅提供對內嵌于某個程序集中的.resources文件的操作,如果我們直接將資源定義在一個獨立的.resources文件、.resx文件甚至是自定義結構的XML文件呢?在這種情況下,我們可通過自定義ResourceManager的方式來解決這個問題。為此我定義了如下一個抽象類FileResourceManager作為基于文件的ResourceManager的基類。
{
private string baseName;
public string Directory { get; private set; }
public string Extension { get; private set; }
public override string BaseName
{
get{ return baseName;}
}
public FileResourceManager(string directory, string baseName, string extension)
{
this.Directory = directory;
this.baseName = baseName;
this.Extension = extension;
}
protected override string GetResourceFileName(CultureInfo culture)
{
string fileName = string.Format("{0}.{1}.{2}", this.baseName, culture, this.Extension.TrimStart('.'));
string path = Path.Combine(this.Directory, fileName);
if (File.Exists(path))
{
return path;
}
return Path.Combine(this.Directory, string.Format("{0}.{1}", baseName, this.Extension.TrimStart('.')));
}
}
屬性Directory、BaseName和Extension分別表示資源文件所在的目錄、不包括Culture Code和擴展名的文件名以及擴展名。FileResourceManager集成自ResourceManager類,并重寫了GetResourceFileName方法用于獲取基于某種Culture的資源文件路徑。
現在我們定義如下一個BinaryResourceManager用于操作單獨存在的.resources文件。我自需要重寫InternalGetResourceSet,返回的是基于.resources文件名創建的ResourceSet對象。
{
public BinaryResourceManager(string directory, string baseName)
: base(directory, baseName, ".resources")
{}
protected override ResourceSet InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents)
{
return new ResourceSet(this.GetResourceFileName(culture));
}
}
現在我們來看看如何使用我們創建的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>,第一個代碼語言文化中性,后者則基于美國英語和簡體中文。
{
var fileNames = new string[]{
baseName + "." + extension,
baseName + ".en-US." + extension,
baseName + ".zh-CN." + extension };
Array.ForEach(fileNames, fileName =>{
if (!File.Exists(fileName)) File.Create(fileName).Dispose();});
}
然后是用于資源寫入操作的AddResource方法,該方法兩個參數createWriter和culture表示創建IResourceWriter的委托和對應的語言文化。
{
using (IResourceWriter resourceWriter = createWriter())
{
if (culture.Name.StartsWith("en"))
{
resourceWriter.AddResource("Greeting4Chris", "Merry Christmas!");
resourceWriter.AddResource("Greeting4NewYear", "Happy Chinese New Year!");
}
if (culture.Name.StartsWith("zh"))
{
resourceWriter.AddResource("Greeting4Chris", "圣誕快樂!");
resourceWriter.AddResource("Greeting4NewYear", "新年快樂!");
}
resourceWriter.Generate();
}
}
最后是用于資源讀取和輸出的DisplayResource方法,該方法通過指定的ResourceManager讀取當前需要文化資源并輸出。而我指定了三種不同的語言文化環境:en-US、zh-CN和ja-JP。
{
Thread.CurrentThread.CurrentUICulture = new CultureInfo("en-US");
Console.WriteLine(CultureInfo.CurrentUICulture.EnglishName);
Console.WriteLine("\t"+resourceManager.GetString("Greeting4Chris"));
Console.WriteLine("\t" + resourceManager.GetString("Greeting4NewYear") + "\n");
Thread.CurrentThread.CurrentUICulture = new CultureInfo("zh-CN");
Console.WriteLine(CultureInfo.CurrentUICulture.EnglishName);
Console.WriteLine("\t" + resourceManager.GetString("Greeting4Chris"));
Console.WriteLine("\t" + resourceManager.GetString("Greeting4NewYear") + "\n");
Thread.CurrentThread.CurrentUICulture = new CultureInfo("ja-JP");
Console.WriteLine(CultureInfo.CurrentUICulture.EnglishName);
Console.WriteLine("\t" + resourceManager.GetString("Greeting4Chris"));
Console.WriteLine("\t" + resourceManager.GetString("Greeting4NewYear") + "\n");
}
最后我們的程序是這樣的:
AddResource(() => new ResourceWriter("GreetingMessages.resources"), new CultureInfo("en-US"));
AddResource(() => new ResourceWriter("GreetingMessages.en-US.resources"), new CultureInfo("en-US"));
AddResource(() => new ResourceWriter("GreetingMessages.zh-CN.resources"), new CultureInfo("zh-CN"));
DisplayResource(new BinaryResourceManager("", "GreetingMessages"));
最終的輸出為:
Merry Christmas!
Happy Chinese New Year!
Chinese (Simplified, PRC)
圣誕快樂!
新年快樂!
Japanese (Japan)
Merry Christmas!
Happy Chinese New Year!
在《下篇》中,我將介紹如何通過自定義ResourceManager操作定義在.resx、XML和數據庫表的資源。