Based on the localization design practice in WPF modular architecture

Original: Localized Design Under the WPF-based modular architecture

Background Description #

Recently received a demand that requires our WPF client with localization features to achieve multi-language interface in English. At first we received the demand, in fact, my heart was rejected, but no way, demand is endless. I can only think of ways to solve this problem.

First of all it is necessary to talk about our system architecture. Our system is based on the Prism to design, so between each business modules are independent of each other, affect each other DLL, and then through the main Shell for dynamic scanning directories to achieve dynamic loading.

In order to ensure existing functionality without affecting the stability of the system, how to make all modules support multiple languages ​​has become a problem to be solved.

At first, I Google for a moment, looked up some information on how many of them are multi-language program in the monomer, but in a modular architecture, I personally feel this is not appropriate. Done localization friends should all know, during the time localization translation, we need to create the corresponding language resource file, using either .xaml .resxor .xml, it will be stored inside our localized resources. For single system, these resources can be placed directly under the main program, and convenient. But for the modular architecture of the program, this is not good, but should these resources are put inside the module by themselves to maintain their own main program only need to specify the regional language to the entire system.

Design ideas #

The face of the background described above, we can generally describe what we expect of solutions, the main program is only responsible for regional language settings of the entire system, the localization of each module is done internally by this module, localized switching module consistent all the way , depending on the total of an implementation. As shown below:

Implementation #

由于如何使用 Prism 不是本文的重点,所以这里就略过主程序和模块程序中相关的模板代码,感兴趣的小伙伴可以自行在园子里搜索相关技术文章。

参照上述的思路,我们可以做一个小示例来展示一下如何进行多模块多语言的本地化实践。

在这个示例中,我以 DotNetCore 3.0 版本的 WPF 和 Prism 进行示例说明。在我们的示例工程中创建三个项目

  • BlackApp
    • 引用 Prism.Unity 包
    • WPF App(.NET Core 版本),作为启动程序
  • BlackApp.ModuleA
    • 引用 Prism.Wpf 包
    • WPF UseControl(.NET Core 版本),作为示例模块
  • BlackApp.Common
    • ClassLibrary(.NET Core 版本),作为基础的公共服务层

BlackApp.ModuleA 添加对 BlackApp.Common 的引用,并将 BlackApp 和 BlackApp.ModuleA 的项目输出修改为相同的输出目录。然后修改对应的基础代码,以确保主程序能正常加载并显示 ModuleA 模块及其内容。

上述操作完成后,我们就可以编写我们的测试代码了。按照我们的设计思路,我需要先在 BlackApp.ModuleA 定义我们的本地化资源文件,对于这个资源文件的类型选择,理论上我们是可以选择任何一种基于 XML 的文件,但是不同类型的文件对于后面是否是埋坑行为这个需要认真考虑一下。这里我建议使用 XAML 格式的文件。我们在 BlackApp.ModuleA 项目的根目录下创建一个 Strings 的文件夹,然后里面分别创建 en-US.xamlzh-CN.xaml 文件。这里建议最好以语言名称作为文件名称,这样方便到时候查找。文件内容如下所示:

  • en-US.xaml
 
 
Copy
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:BlackApp.ModuleA.Strings" xmlns:system="clr-namespace:System;assembly=System.Runtime"> <system:String x:Key="string1">Hello world</system:String> </ResourceDictionary>
  • zh-CN.xaml
 
 
Copy
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:BlackApp.ModuleA.Strings" xmlns:system="clr-namespace:System;assembly=System.Runtime"> <system:String x:Key="string1">世界你好</system:String> </ResourceDictionary>

资源文件定义好了,接下来就是如何使用了。

对于我们需要进行本地化的 XAML 页面,首先我们需要指当前使用到的资源文件,这个时候就需要在我们的 BlackApp.Common 项目中定义一个依赖属性了,然后通过依赖属性的方式来进行设置。由于语言种类有很多,所以我们定义一个文件夹目录的依赖属性,来指定当前页面需要用到的资源的文件夹路径,然后由辅助类到时候依据具体的语言类型来到指定目录查找指当的资源文件。
示例代码如下所示:

 
 
Copy
[RuntimeNameProperty(nameof(ExTranslationManager))] public class ExTranslationManager : DependencyObject { public static string GetResourceDictionary(DependencyObject obj) { return (string)obj.GetValue(ResourceDictionaryProperty); } public static void SetResourceDictionary(DependencyObject obj, string value) { obj.SetValue(ResourceDictionaryProperty, value); } // Using a DependencyProperty as the backing store for ResourceDictionary. This enables animation, styling, binding, etc... public static readonly DependencyProperty ResourceDictionaryProperty = DependencyProperty.RegisterAttached("ResourceDictionary", typeof(string), typeof(ExTranslationManager), new PropertyMetadata(null)); }

本地化资源指定完毕后,我们就可以使用里面资源文件进行本地化操作。如果想在 XAML 对相应属性进行 标签式 访问,需要定义一个继承自 MarkupExtension 类的自定义类,并在该类中实现 ProvideValue 方法。接下来在我们的 BlackApp.Common 项目中定义该类,示例代码如下所示:

 
 
Copy
[RuntimeNameProperty(nameof(ExTranslation))] public class ExTranslation : MarkupExtension { public string StringName { get; private set; } public ExTranslation(string stringName) { this.StringName = stringName; } public override object ProvideValue(IServiceProvider serviceProvider) { object targetObject = (serviceProvider as IProvideValueTarget)?.TargetObject; ResourceDictionary dictionary = GetResourceDictionary(targetObject); if (dictionary == null) { object rootObject = (serviceProvider as IRootObjectProvider)?.RootObject; dictionary = GetResourceDictionary(rootObject); } if (dictionary == null) { if (targetObject is FrameworkElement frameworkElement) { dictionary = GetResourceDictionary(frameworkElement.TemplatedParent); } } return dictionary != null && StringName != null && dictionary.Contains(StringName) ? dictionary[StringName] : StringName; } private ResourceDictionary GetResourceDictionary(object target) { if (target is DependencyObject dependencyObject) { object localValue = dependencyObject.ReadLocalValue(ExTranslationManager.ResourceDictionaryProperty); if (localValue != DependencyProperty.UnsetValue) { var local = localValue.ToString(); var (baseName,stringName) = SplitName(local); var str = $"pack://application:,,,/{baseName};component/{stringName}/{Thread.CurrentThread.CurrentCulture}.xaml"; var dict = new ResourceDictionary { Source = new Uri(str) }; return dict; } } return null; } public static (string baseName, string stringName) SplitName(string name) { int idx = name.LastIndexOf('.'); return (name.Substring(0, idx), name.Substring(idx + 1)); } }

此外,如果我们的 ViewModel 中也有数据需要进行本地化操作的化,我们可以定义一个扩展方法,示例代码如下所示:

 
 
Copy
public static class ExTranslationString { public static string GetTranslationString(this string key, string resourceDictionary) { var (baseName, stringName) = ExTranslation.SplitName(resourceDictionary); var str = $"pack://application:,,,/{baseName};component/{stringName}/{Thread.CurrentThread.CurrentCulture}.xaml"; var dictionary = new ResourceDictionary { Source = new Uri(str) }; return dictionary != null && !string.IsNullOrWhiteSpace(key) && dictionary.Contains(key) ? (string)dictionary[key] : key; } }

通过在 BlackApp.Common 中定义上述 3 个辅助类,基本可以满足我们的需求,我们可以却换到 BlackApp.ModuleA 项目中,并进行如下示例修改

  • View 层使用示例
 
 
Copy
<UserControl x:Class="BlackApp.ModuleA.Views.MainView" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:ex="clr-namespace:BlackApp.Common;assembly=BlackApp.Common" xmlns:local="clr-namespace:BlackApp.ModuleA.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:prism="http://prismlibrary.com/" d:DesignHeight="300" d:DesignWidth="300" ex:ExTranslationManager.ResourceDictionary="BlackApp.ModuleA.Strings" prism:ViewModelLocator.AutoWireViewModel="True" mc:Ignorable="d"> <Grid> <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center"> <TextBlock Text="{Binding Message}" /> <TextBlock Text="{ex:ExTranslation string1}" /> </StackPanel> </Grid> </UserControl>
  • ViewModel 层使用示例
 
 
Copy
"message".GetTranslationString("BlackApp.ModuleA.Strings")

最后,我们就可以在我们的 BlackApp 项目中的 App.cs 构造函数中来设置我们程序的语言类型,示例代码如下所示:

 
 
Copy
public partial class App { public App() { //CultureInfo ci = new CultureInfo("zh-cn"); CultureInfo ci = new CultureInfo("en-US"); Thread.CurrentThread.CurrentCulture = ci; } protected override Window CreateShell() { return Container.Resolve<MainWindow>(); } protected override void RegisterTypes(IContainerRegistry containerRegistry) { } protected override IModuleCatalog CreateModuleCatalog() { return new DirectoryModuleCatalog() { ModulePath = AppDomain.CurrentDomain.BaseDirectory }; } }

写到这里,我们应该就可以进行本地化的测试工作了,尝试编译运行我们的示例程序,如果不出意外的话,应该是可以通过在 主程序中设置区域类型来更改模块程序中的对应本地化资源内容。

最后,整个示例项目的组织结构如下图所示:

总结#

对于模块化架构的本地化实现,有很多的实现方式,我这里介绍的只是一种符合我们的业务场景的一种实现,期待大佬们在评论区留言提供更好的解决方案。

补充#

经同事验证,使用 .resx 格式的资源文件会更简单一下,可以直接通过

 
 
Copy
BlackApp.ModuleA.Strings.zh_cn.ResourceManager("string1") BlackApp.ModuleA.Strings.en_us.ResourceManager("string1")

的方式来访问。但前提是需要将对应资源文件的访问修饰符设置为 public

参考#

Guess you like

Origin www.cnblogs.com/lonelyxmas/p/12348361.html