.NET MVC4 實訓記錄之五(訪問自定義資源文件)


  .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);
View Code

  接下來實現這個接口(注意,我們還需要實現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     }
View Code

  在這個資源訪問的實現中,我使用了服務端高速緩存。原因是我們想要使得修改的資源能夠直接被識別,就必須在訪問資源的時候,去文件中查找。這樣的話每個資源的訪問都需要一次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     }
View Code

  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     }
View Code

  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     }
View Code

  在此,我有個疑問,希望有人能回答:為什么我每次通過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     }
View Code

  注意到我的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>
View Code

  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>
View Code

  接下來是在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);
            }
        }
View Code

  對應的視圖文件內容:

 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 }
View Code

  運行項目,輸入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>去顯示字段的資源。

 

 

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM