.Net平台下工作好幾年了,資源文件么,大多數使用的是.resx文件。它是個好東西,很容易上手,工作效率高,性能穩定。使用.resx文件,會在編譯期動態生成已文件名命名的靜態類,因此它的訪問速度當然是最快的。但是它也有個最大的缺點,就是修改資源文件后,項目必須重新編譯,否則修改的資源不能被識別。這對於維護期的工作來講,非常麻煩。尤其是已經上線的項目,即使是修改一個title的顯示,也需要停掉項目。由於本人做了好幾年的維護,應該是從工作到現在,一直沒有間斷過的做維護項目,因此深受其害!必須找到一個方案,規避掉這個令人頭疼的問題。
好了,鋪墊的夠多了,進入正題:使用自定義XML文件作為資源,完成本地化、國際化(該篇參考Artech的如何讓ASP.NET默認的資源編程方式支持非.ResX資源存儲)。
首先,我們需要一個資源訪問接口,IResourceManager。它提供一組返回資源內容的方法簽名:

1 /// <summary> 2 /// 資源訪問接口 3 /// </summary> 4 public interface IResourceManager 5 { 6 /// <summary> 7 /// 從資源文件中獲取資源 8 /// </summary> 9 /// <param name="name">資源名稱</param> 10 /// <returns></returns> 11 string GetString(string name); 12 13 /// <summary> 14 /// 從資源文件中獲取資源 15 /// </summary> 16 /// <param name="name">資源名稱</param> 17 /// <param name="culture">區域語言設置</param> 18 /// <returns></returns> 19 string GetString(string name, CultureInfo culture);
接下來實現這個接口(注意,我們還需要實現System.Resources.ResourceManager,因為這個類提供了“回溯”訪問資源的功能,這對我們是非常有用的)

1 public class XmlResourceManager : System.Resources.ResourceManager, IResourceManager 2 { 3 #region Constants 4 5 private const string _CACHE_KEY = "_RESOURCES_CACHE_KEY_{0}_{1}"; 6 private const string extension = ".xml"; 7 8 #endregion 9 10 11 #region Variables 12 13 private string baseName; 14 15 #endregion 16 17 18 #region Properties 19 20 /// <summary> 21 /// 資源文件路徑 22 /// </summary> 23 public string Directory { get; private set; } 24 25 /// <summary> 26 /// 資源文件基類名(不包含國家區域碼)。 27 /// 覆蓋基類的實現 28 /// </summary> 29 public override string BaseName 30 { 31 get { return baseName; } 32 } 33 34 /// <summary> 35 /// 資源節點名稱 36 /// </summary> 37 public string NodeName { get; private set; } 38 39 #endregion 40 41 42 #region Constructor 43 44 [Microsoft.Practices.Unity.InjectionConstructor] 45 public XmlResourceManager(string directory, string baseName, string nodeName) 46 { 47 this.Directory = System.Web.HttpRuntime.AppDomainAppPath + directory; 48 this.baseName = baseName; 49 this.NodeName = nodeName; 50 51 base.IgnoreCase = true; //資源獲取時忽略大小寫 52 } 53 54 #endregion 55 56 57 #region Functions 58 59 /// <summary> 60 /// 獲取資源文件名 61 /// </summary> 62 /// <param name="culture">國家區域碼</param> 63 /// <returns></returns> 64 protected override string GetResourceFileName(CultureInfo culture) 65 { 66 string fileName = string.Format("{0}.{1}.{2}", this.baseName, culture, extension.TrimStart('.')); 67 string path = Path.Combine(this.Directory, fileName); 68 if (File.Exists(path)) 69 { 70 return path; 71 } 72 return Path.Combine(this.Directory, string.Format("{0}.{1}", baseName, extension.TrimStart('.'))); 73 } 74 75 76 /// <summary> 77 /// 獲取特定語言環境下的資源集合 78 /// 該方法使用了服務端高速緩存,以避免頻繁的IO訪問。 79 /// </summary> 80 /// <param name="culture">國家區域碼</param> 81 /// <param name="createIfNotExists">是否主動創建</param> 82 /// <param name="tryParents">是否返回父級資源</param> 83 /// <returns></returns> 84 protected override ResourceSet InternalGetResourceSet(CultureInfo culture, bool createIfNotExists, bool tryParents) 85 { 86 string cacheKey = string.Format(_CACHE_KEY, BaseName, culture.LCID); //緩存鍵值 87 XmlResourceSet resourceSet = CacheHelper.GetCache(cacheKey) as XmlResourceSet; //從緩存中獲取當前資源 88 if (resourceSet == null) 89 { 90 string fileName = this.GetResourceFileName(culture); 91 resourceSet = new XmlResourceSet(fileName, NodeName, "key", "value"); 92 CacheHelper.SetCache(cacheKey, resourceSet, new System.Web.Caching.CacheDependency(fileName)); //將資源加入緩存中 93 } 94 return resourceSet; 95 } 96 97 #endregion 98 }
在這個資源訪問的實現中,我使用了服務端高速緩存。原因是我們想要使得修改的資源能夠直接被識別,就必須在訪問資源的時候,去文件中查找。這樣的話每個資源的訪問都需要一次IO消耗。這樣性能損失太大,真的就得不償失了。因此使用依賴於資源文件的高速緩存,已確保只有在資源文件發生變化時,才進行IO讀取。(其實,也可以考慮在運行期間,創建單例的資源訪問器,讓資源訪問權的枚舉器枚舉時,檢測一個緩存變量(該變量依賴於資源文件),當資源文件發生變化時,枚舉器重新讀取資源,已實現如上同樣的效果。這個我們以后有空再一起探討。)該實現中,我也加入了Unity的構造注入方式,讓我們的資源文件可配置。
另外,InternalGetResourceSet方法還存在一些問題,就是可能會重復緩存同一個資源文件。例如,我們的資源文件有Resource.en-US.xml, 和Resource.xml,這時會生成三個緩存,將Resource.xml文件緩存了兩次。這都是“回溯”惹的禍。在我的第一個版本源碼公布時,我會修復這個問題。
接下來,我們要實現資源的Set、Reader和Writer,用於資源文件的讀寫。
XmlResourceReader:

1 public class XmlResourceReader : IResourceReader 2 { 3 #region Properties 4 5 public string FileName { get; private set; } 6 7 public string NodeName { get; private set; } 8 9 public string KeyAttr { get; private set; } 10 11 public string ValueAttr { get; private set; } 12 13 #endregion 14 15 #region Constructors 16 17 public XmlResourceReader(string fileName, string nodeName, string keyAttr, string valueAttr) 18 { 19 NodeName = nodeName; 20 FileName = fileName; 21 KeyAttr = keyAttr; 22 ValueAttr = valueAttr; 23 } 24 25 #endregion 26 27 #region Enumerator 28 29 public IDictionaryEnumerator GetEnumerator() 30 { 31 Dictionary<string, string> set = new Dictionary<string, string>(); 32 33 XmlDocument doc = new XmlDocument(); 34 doc.Load(FileName); 35 36 //將資源以鍵值對的方式存儲到字典中。 37 foreach (XmlNode item in doc.GetElementsByTagName(NodeName)) 38 { 39 set.Add(item.Attributes[KeyAttr].Value, item.Attributes[ValueAttr].Value); 40 } 41 42 return set.GetEnumerator(); 43 } 44 45 /// <summary> 46 /// 枚舉器 47 /// </summary> 48 /// <returns></returns> 49 IEnumerator IEnumerable.GetEnumerator() 50 { 51 return GetEnumerator(); 52 } 53 54 #endregion 55 56 public void Dispose() { } 57 58 public void Close() { } 59 }
XmlResourceWriter:

1 public interface IXmlResourceWriter : IResourceWriter 2 { 3 void AddResource(string nodeName, IDictionary<string, string> attributes); 4 void AddResource(XmlResourceItem resource); 5 } 6 7 public class XmlResourceWriter : IXmlResourceWriter 8 { 9 public XmlDocument Document { get; private set; } 10 private string fileName; 11 private XmlElement root; 12 13 public XmlResourceWriter(string fileName) 14 { 15 this.fileName = fileName; 16 this.Document = new XmlDocument(); 17 this.Document.AppendChild(this.Document.CreateXmlDeclaration("1.0", "utf-8", null)); 18 this.root = this.Document.CreateElement("resources"); 19 this.Document.AppendChild(this.root); 20 } 21 22 public XmlResourceWriter(string fileName, string root) 23 { 24 this.fileName = fileName; 25 this.Document = new XmlDocument(); 26 this.Document.AppendChild(this.Document.CreateXmlDeclaration("1.0", "utf-8", null)); 27 this.root = this.Document.CreateElement(root); 28 this.Document.AppendChild(this.root); 29 } 30 31 public void AddResource(string nodeName, IDictionary<string, string> attributes) 32 { 33 var node = this.Document.CreateElement(nodeName); 34 attributes.AsParallel().ForAll(p => node.SetAttribute(p.Key, p.Value)); 35 this.root.AppendChild(node); 36 } 37 38 public void AddResource(XmlResourceItem resource) 39 { 40 AddResource(resource.NodeName, resource.Attributes); 41 } 42 43 public void AddResource(string name, byte[] value) 44 { 45 throw new NotImplementedException(); 46 } 47 48 public void AddResource(string name, object value) 49 { 50 throw new NotImplementedException(); 51 } 52 53 public void AddResource(string name, string value) 54 { 55 throw new NotImplementedException(); 56 } 57 58 public void Generate() 59 { 60 using (XmlWriter writer = new XmlTextWriter(this.fileName, Encoding.UTF8)) 61 { 62 this.Document.Save(writer); 63 } 64 } 65 public void Dispose() { } 66 public void Close() { } 67 }
XmlResourceSet:

1 public class XmlResourceSet : ResourceSet 2 { 3 public XmlResourceSet(string fileName, string nodeName, string keyAttr, string valueAttr) 4 { 5 this.Reader = new XmlResourceReader(fileName, nodeName, keyAttr, valueAttr); 6 this.Table = new Hashtable(); 7 this.ReadResources(); 8 } 9 10 public override Type GetDefaultReader() 11 { 12 return typeof(XmlResourceReader); 13 } 14 public override Type GetDefaultWriter() 15 { 16 return typeof(XmlResourceWriter); 17 } 18 }
在此,我有個疑問,希望有人能回答:為什么我每次通過Writer修改資源文件后,自問文件中沒有換行,都是一行到底呢?
OK,我們的XML訪問模塊的所有成員都到齊了。另外,我在自己的WebApp中增加了全局資源訪問類型Resource:

1 public class Resource 2 { 3 public static string GetDisplay(string key, string culture = "") 4 { 5 var display = ((AppUnityDependencyResolver)System.Web.Mvc.DependencyResolver.Current).GetService(typeof(IResourceManager), "Display") as IResourceManager; 6 if (string.IsNullOrWhiteSpace(culture)) 7 return display.GetString(key); 8 else 9 return display.GetString(key, new System.Globalization.CultureInfo(culture)); 10 } 11 12 public static string GetMessage(string key, string culture = "") 13 { 14 var display = ((AppUnityDependencyResolver)System.Web.Mvc.DependencyResolver.Current).GetService(typeof(IResourceManager), "Message") as IResourceManager; 15 return display.GetString(key); 16 } 17 18 public static string GetTitle() 19 { 20 var routes = HttpContext.Current.Request.RequestContext.RouteData.Values; 21 var key = string.Format("{0}.{1}.title", routes["controller"], routes["action"]); 22 return GetDisplay(key); 23 } 24 }
注意到我的Resource類有三個靜態方法,GetDisplay、GetMessage和GetTitle。由於我的源碼中,字段名稱和提示信息是在不同的資源文件中的,因此我寫了兩個靜態方法,第一個是獲取字段名稱的,第二個是獲取用戶自定義提示信息的。而第三個是獲取頁面標題的,它依賴於當前執行的Action。
現在,看看資源文件:
ResourceDisplay.en-US.xml

1 <?xml version="1.0" encoding="utf-8"?> 2 <resources> 3 <resource key="home.index.title" value="Home Page" /> 4 <resource key="usermanage.index.title" value="User Management"/> 5 <resource key="setting.index.title" value="Settings"/> 6 7 <resource key="UserProfile.UserName" value="English User Name"/> 8 <resource key="UserProfile.UserCode" value="English User Code"/> 9 </resources>
ResourceDisplay.xml

1 <?xml version="1.0" encoding="utf-8"?> 2 <resources> 3 <resource key="home.index.title" value="Home Page" /> 4 <resource key="usermanage.index.title" value="User Management"/> 5 <resource key="setting.index.title" value="Settings"/> 6 7 <resource key="UserProfile.UserName" value="Basic User Name"/> 8 <resource key="UserProfile.UserCode" value="Basic User Code"/> 9 <resource key="UserProfile.Email" value="Basic User Email Address"/> 10 </resources>
接下來是在WebApp中使用資源文件了。
在AccountController中添加EditUser方法:

[HttpGet] public ActionResult EditUser(int? id) { if (id.HasValue == false) { return View(new UserProfile()); } else { var model = UserService.GetSingle<UserProfile>(id); return View(model); } }
對應的視圖文件內容:

1 @model Framework.DomainModels.UserProfile 2 3 @{ 4 ViewBag.Title = "EditUser"; 5 } 6 7 <h2>EditUser</h2> 8 @using (Html.BeginForm()) 9 { 10 <p>@Resource.GetDisplay("UserProfile.UserName")</p> 11 <p>@Html.TextBoxFor(p=> p.UserName)</p> 12 <p>@Resource.GetDisplay("UserProfile.UserCode")</p> 13 <p>@Html.TextBoxFor(p=> p.UserCode)</p> 14 <p>@Resource.GetDisplay("UserProfile.Email")</p> 15 <p>@Html.TextBoxFor(p=> p.Email)</p> 16 }
運行項目,輸入http://localhost:****/Account/EditUser
看看前兩個字段顯示的內容是在ResourceDisplay.en-US.xml中定義的,而第三個字段是在ResourceDisplay.xml中定義的(這就是所謂的“回溯”)。
這樣就完了嗎?當然沒有,高潮來了......
不要停止debug,打開資源文件ResourceDisplay.en-US.xml,刪除我們對UserProfile.UserCode的定義,刷新頁面看看(在此我就不截圖了)。接下來,隨便改改ResourceDisplay.en-US.xml中UserName的定義,刷新頁面再瞧瞧,是否立刻應用?
這樣,我們前面預想的不進行編譯,直接應用資源文件的修改就實現了。
對了,還有一個GetTitle(),我們打開_Layout.cshtml,做如下修改:
1 <title>@Resource.GetTitle() - My ASP.NET MVC Application</title>
再次在資源文件中添加一行
1 <resource key="Account.EditUser.title" value="User Edit"/>
仍然刷新頁面,注意上圖中紅線位置,是不是對應的Title信息已經顯示?
劇透:下一篇,我們會借助Metadata實現字段資源的自動顯示,也就是說,我們不需要類似<p>@Resource.GetDisplay("UserProfile.UserName")</p>這樣的顯示調用,而改用<p>@Html.LabelFor(p=>p.UserName)</p>去顯示字段的資源。