动态字符串资源\本地化和MVVM。





5.00/5 (5投票s)
动态字符串资源\本地化和 XAML 绑定。
引言
本文包含两个要素;首先,目的是提供一种字符串资源和本地化的机制,该机制不需要在每次字面量更改或需要定义新语言时重新编译应用程序。其次,该方法需要提供一种 MVVM 绑定技术来利用该技术。
背景
Code Project 上有很多不错的文章解释了字符串资源和本地化。
本文背后的特别想法是开发一个字符串资源/本地化解决方案,在该方案中,可以在应用程序发布后更改字符串并添加对新语言的支持。但是,我仍然希望尽可能多地使用内置的资源处理组件,尤其是 ResourceManager 类。从 msdn 和 Visual Studio 的 Intellisense 搜索中,我发现了 CreateFileBasedResourceManager 方法。此方法允许在运行时导入外部资源文件。从 msdn 文章中也提到,可以使用自定义资源读取器来解析资源。这似乎很完美,但不幸的是,文章没有详细介绍如何实现这一点。
然后,我很幸运地发现了 CodeProject 上的 这篇优秀的文章,它演示了这个想法。
我自己的文章是将这两个来源的信息结合在一起的结果。我还包含了一个巧妙的绑定技巧/窍门,以维护 MVVM 模式。
使用代码
首先是自定义的 ResourceSet 和 ResourceReader 对象,它们负责理解和解析外部资源文件。
ResourceSet
这是我的自定义 ResourceSet 类的定义。它非常基本,它只是返回我的自定义资源读取器类的实例。
/// <summary> /// Custom resource set implementation. /// </summary> class StringsResourceSet : ResourceSet { /// <summary> /// Default constructor /// </summary> /// <param name="fileName">The fil</param> public StringsResourceSet(string fileName) : base(new StringResourceReader(fileName)) { } /// <summary> /// //Return custom reader as default one for reading language resources /// </summary> /// <returns></returns> public override Type GetDefaultReader() { return typeof(StringResourceReader); } }
ResourceReader
这是我的自定义 ResourceReader 类的定义,它实际上负责读取字符串资源。
/// <summary> /// Custom resource reader for strings. /// </summary> class StringResourceReader : IResourceReader { #region Fields /// <summary> /// The resource file name to load. /// </summary> string _fileName; #endregion #region Constructors /// <summary> /// Default constructor /// </summary> /// <param name="baseName">The resource filename to load resources from.</param> public StringResourceReader(string resourceFileName) { //Set fields. _fileName = resourceFileName; } #endregion #region IResourceReader Members /// <summary> /// //Implement IEnumerable interface /// </summary> /// <returns></returns> IEnumerator IEnumerable.GetEnumerator() { return this.GetEnumerator(); } /// <summary> /// Required for interface implementation. /// </summary> public void Close() { //throw new NotImplementedException(); } }
请注意,在我的方法中,资源文件只是文本文件,资源键和值由等号分隔,例如:
RESOURCEKEY=resourcevalue. TESTSTRING=this is a test
但是,通过更改 GetEnumerator 实现,您的资源文件实际上可以采用您想要的任何格式。在前面提到的文章中,作者使用了 sql 数据库。
/// <summary> /// To get enumerator to iterate through the resources hashtable /// </summary> /// <returns></returns> public IDictionaryEnumerator GetEnumerator() { //Hashtable to store Key-values pairs for resources Hashtable htLanguage = new Hashtable(); //Read the file. using (StreamReader sr = new StreamReader(_fileName)) { //While not end of file. while (!sr.EndOfStream) { //Split the resource key and value. string[] lineParams = sr.ReadLine().Split('='); //Add resource to hash table. htLanguage.Add(lineParams[0], lineParams[1]); } } //Return enumerator. return htLanguage.GetEnumerator(); }
这是 StringResourceReader 类的最后一部分
#region IDisposable Members /// <summary> /// Required for interface implementation. /// </summary> public void Dispose() { //throw new NotImplementedException(); } #endregion
ResourceViewModel
这里是视图模型类,它也负责基于上述资产初始化资源管理器类。
我向资源管理器提供了三条信息。
第一个是我的资源文件的基本名称。在我的例子中,我所有的资源文件都应该具有基本名称 Strings。例如,我的资源文件应该命名为 Strings.Culture.resources,其中 Culture 代表区域和语言代码。例如,Strings.fr-FR.resources 将是包含法国/法语资源的的文件。您不需要担心如何选择正确的区域文件,该责任将由 .NET 处理。您只需要确保正确的命名方案。
第二个参数指示相对于应用程序工作目录的包含文件夹。在我的例子中,所有的资源文件都将在一个名为 strings 的子文件夹中。
第三个参数告诉 ResourceManager 使用前面讨论的自定义 ResourceSet 和 ResourceReader。
/// <summary> /// Resources view model. /// </summary public class ResourceViewModel { /// <summary> /// Resource manager for string literals. /// </summary> ResourceManager rm; /// <summary> /// Default constructor. /// </summary> public ResourceViewModel() { //Initialize resource manager using custom resource set and resource reader. rm = ResourceManager.CreateFileBasedResourceManager("Strings", "Strings", typeof(StringsResourceSet)); } /// <summary> /// Gets the resource manager. /// </summary> public ResourceManager Manager { get { return rm; } } }
XAML 绑定访问器
这里是支持从 XAML 绑定的技巧/窍门。我将 ResourceManager GetString 方法包装在一个 XAML 支持绑定的索引器属性中。
/// <summary> /// Gets the resource matching the specified name. /// </summary> /// <param name="name">The name of the resource of </param> /// <returns></returns> public string this[string name] { get { return rm.GetString(name); } }
在 xaml 中,您可以按如下方式绑定到索引器属性。
<TextBlock FontSize="50" Text="{Binding Path=[TESTSTRING]}"/>
ResourceViewModel 类的最后一部分包含一个用于覆盖应用程序所有线程的文化的 方法。这对于在不更改操作系统上的区域信息的情况下测试本地化很有用。这段代码取自以下优秀的文章,该文章也解释了为什么需要这段特定的代码。 http://blog.rastating.com/setting-default-currentculture-in-all-versions-of-net/。
/// <summary> /// Special implementation to set application wide culture. /// </summary> /// <param name="culture">The culture to set for the application.</param> public void SetDefaultCulture(CultureInfo culture) { Type type = typeof(CultureInfo); try { type.InvokeMember("s_userDefaultCulture", BindingFlags.SetField | BindingFlags.NonPublic | BindingFlags.Static, null, culture, new object[] { culture }); type.InvokeMember("s_userDefaultUICulture", BindingFlags.SetField | BindingFlags.NonPublic | BindingFlags.Static, null, culture, new object[] { culture }); } catch { } try { type.InvokeMember("m_userDefaultCulture", BindingFlags.SetField | BindingFlags.NonPublic | BindingFlags.Static, null, culture, new object[] { culture }); type.InvokeMember("m_userDefaultUICulture", BindingFlags.SetField | BindingFlags.NonPublic | BindingFlags.Static, null, culture, new object[] { culture }); } catch { } } } }
在 XAML 窗口类中,我初始化了视图模型类的实例,并将其设置为窗口的 datacontext。如果需要,我还提供了一个钩子用于覆盖基于应用程序设置的文化。
/// <summary> /// Initialize view model class. /// </summary> ResourceViewModel viewModel = new ResourceViewModel(); /// <summary> /// Default constructor. /// </summary> public Window1() { //Initialize. InitializeComponent(); //Set the view model. DataContext = viewModel; //If there is another culture defined. if (!String.IsNullOrEmpty(Properties.Settings.Default.CultureInfo)) { //Override the default culture of all threads in the application. viewModel.SetDefaultCulture(CultureInfo.CreateSpecificCulture(Properties.Settings.Default.CultureInfo)); } }
关注点
应该指出的是,这种方法不幸的是,当运行时更改文化时,不会为视图提供新的绑定通知。即使我在我的视图模型类上实现了 INotifyPropertyChanged 接口,我也不会实现通知,因为索引器属性实际上不会在任何时候更改。