.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>去显示字段的资源。

 

 

  

你可能感兴趣的:(.net)