首页 > 代码库 > .NET MVC4 实训记录之五(访问自定义资源文件)
.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);
接下来实现这个接口(注意,我们还需要实现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 Variables12 13 private string baseName;14 15 #endregion16 17 18 #region Properties19 20 /// <summary>21 /// 资源文件路径22 /// </summary>23 public string Directory { get; private set; }24 25 /// <summary>26 /// 资源文件基类名(不包含国家区域码)。27 /// 覆盖基类的实现28 /// </summary>29 public override string BaseName30 {31 get { return baseName; }32 }33 34 /// <summary>35 /// 资源节点名称36 /// </summary>37 public string NodeName { get; private set; }38 39 #endregion40 41 42 #region Constructor43 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 #endregion55 56 57 #region Functions58 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 #endregion98 }
在这个资源访问的实现中,我使用了服务端高速缓存。原因是我们想要使得修改的资源能够直接被识别,就必须在访问资源的时候,去文件中查找。这样的话每个资源的访问都需要一次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 #endregion14 15 #region Constructors16 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 #endregion26 27 #region Enumerator28 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 #endregion55 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=http://www.mamicode.com/"Home Page" />4 <resource key="usermanage.index.title" value=http://www.mamicode.com/"User Management"/>5 <resource key="setting.index.title" value=http://www.mamicode.com/"Settings"/>6 7 <resource key="UserProfile.UserName" value=http://www.mamicode.com/"English User Name"/>8 <resource key="UserProfile.UserCode" value=http://www.mamicode.com/"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 =http://www.mamicode.com/= 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>去显示字段的资源。