基于 WPF 模块化架构下的本地化设计实践
背景描述
最近接到一个需求,就是要求我们的 WPF 客户端具备本地化功能,实现中英文多语言界面。刚开始接到这个需求,其实我内心是拒绝的的,但是没办法,需求是永无止境的。所以只能想办法解决这个问题。
首先有必要说一下我们的系统架构。我们的系统是基于 Prism 来进行设计的,所以每个业务模块之间都是相互独立,互不影响的 DLL
,然后通过主 Shell 来进行目录的动态扫描来实现动态加载。
为了保证在不影响系统现有功能稳定性的前提下,如何让所有模块支持多语言成为了一个亟待解决的问题。
刚开始,我 Google 了一下,查阅了一些资料,很多都是介绍如何在单体程序中实现多语言,但是在模块化架构中,我个人觉得这样做并不合适。做过本地化的朋友应该都知道,在进行本地化翻译的时候,都需要创建对应语言的资源文件,无论是使用 .xaml
.resx
或 .xml
,这里面会存放我们的本地化资源。对于单体系统而言,这些资源直接放到主程序下即可,方便快捷。但是对于模块化架构的程序,这样做就不太好,而是应该将这些资源都分别放到自己模块内部由自己来维护,主程序只需规定整个系统的区域语言即可。
设计思路
面对上面的背景描述,我们可以大致描述一下我们期望的解决方式,主程序只负责对整个系统进行区域语言设置,每个模块的本地化由本模块内部完成,所有模块的本地化切换方式保持一致,依赖于共有的一种实现。如下图所示:
实现方案
由于如何使用 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.xaml 和 zh-CN.xaml 文件。这里建议最好以语言名称作为文件名称,这样方便到时候查找。文件内容如下所示:
- en-US.xaml
<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
<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 项目中定义一个依赖属性了,然后通过依赖属性的方式来进行设置。由于语言种类有很多,所以我们定义一个文件夹目录的依赖属性,来指定当前页面需要用到的资源的文件夹路径,然后由辅助类到时候依据具体的语言类型来到指定目录查找指当的资源文件。
示例代码如下所示:
[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 项目中定义该类,示例代码如下所示:
[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 中也有数据需要进行本地化操作的化,我们可以定义一个扩展方法,示例代码如下所示:
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 层使用示例
<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 层使用示例
"message".GetTranslationString("BlackApp.ModuleA.Strings")
最后,我们就可以在我们的 BlackApp 项目中的 App.cs 构造函数中来设置我们程序的语言类型,示例代码如下所示:
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 格式的资源文件会更简单一下,可以直接通过
BlackApp.ModuleA.Strings.zh_cn.ResourceManager("string1")
BlackApp.ModuleA.Strings.en_us.ResourceManager("string1")
的方式来访问。但前提是需要将对应资源文件的访问修饰符设置为 public。
参考
- Localization of a WPF app - the simple approach
- wpf-localization-multiple-resource-resx-one-language
- LocalizeMarkupExtension
- Markup Extensions and WPF XAML
基于 WPF 模块化架构下的本地化设计实践的更多相关文章
- 分布式架构下的会话追踪实践【基于Cookie和Redis实现】
分布式架构下的会话追踪实践[基于Cookie和Redis实现] 博客分类: NoSQL/Redis/MongoDB session共享rediscookie分布式架构session 在单台Tomcat ...
- 《MVC架构下网站的设计与实现》论文笔记(十八)
标题:MVC架构下网站的设计与实现 一.基本信息 时间:2017 来源:广东海洋大学数学与计算机学院 关键词:网站设计:MVC 框架:数据库:网络安全 二.研究内容 1.系统的整体架构设计(以广东海洋 ...
- China .NET Conf 2019-.NET技术架构下的混沌工程实践
这个月的8号.9号,个人很荣幸参加了China.NET Conf 2019 , 中国.NET开发者峰会,同时分享了技术专题<.NET技术架构下的混沌工程实践>,给广大的.NET开发小伙伴介 ...
- App后台开发运维和架构实践学习总结(3)——RestFul架构下API接口设计注意点
1. 争取相容性和统一性 这里就要求让API设计得是可预测的.按照这种方式写出所有接口和接口所需要的参数.现在就要确保命名是一致的,接口所需的参数顺序也是一致的.你现在应该有products,orde ...
- 基于B/S架构的在线考试系统的设计与实现
前言 这个是我的Web课程设计,用到的主要是JSP技术并使用了大量JSTL标签,所有代码已经上传到了我的Github仓库里,地址:https://github.com/quanbisen/online ...
- CI Weekly #5 | 微服务架构下的持续部署与交付
CI Weekly 围绕『 软件工程效率提升』 进行一系列技术内容分享,包括国内外持续集成.持续交付,持续部署.自动化测试. DevOps 等实践教程.工具与资源,以及一些工程师文化相关的程序员 Ti ...
- 大数据分析的下一代架构--IOTA架构设计实践[下]
大数据分析的下一代架构--IOTA架构设计实践[下] 原创置顶 代立冬 发布于2018-12-31 20:59:53 阅读数 2151 收藏 展开 IOTA架构提出背景 大数据3.0时代以前,Lam ...
- 基于WPF系统框架设计(5)-Ribbon整合Avalondock 2.0实现多文档界面设计(二)
AvalonDock 是一个.NET库,用于在停靠模式布局(docking)中排列一系列WPF/WinForm控件.最新发布的版本原生支持MVVM框架.Aero Snap特效并具有更好的性能. Ava ...
- PLUTO平台是由美林数据技术股份有限公司下属西安交大美林数据挖掘研究中心自主研发的一款基于云计算技术架构的数据挖掘产品,产品设计严格遵循国际数据挖掘标准CRISP-DM(跨行业数据挖掘过程标准),具备完备的数据准备、模型构建、模型评估、模型管理、海量数据处理和高纬数据可视化分析能力。
http://www.meritdata.com.cn/article/90 PLUTO平台是由美林数据技术股份有限公司下属西安交大美林数据挖掘研究中心自主研发的一款基于云计算技术架构的数据挖掘产品, ...
随机推荐
- JDBC连接mysql数据库操作
一.创建所需对象,并进行初始化 Connection connection=null; Statement statement=null; PreparedStatement pst; ResultS ...
- 新手上路—Java的"瑞士军刀"
“ Jodd 是一个开源的 Java 工具集, 包含一些实用的工具类和小型框架.简单,却很强大!这在我们的日常开发工作中,无疑是如虎添翼,事半功倍. Jodd = Tools + IoC + MVC ...
- 雅阁微信群、雅阁车友群、十代雅阁交流微信QQ群
最近一直在关注第十代雅阁,不论是普通汽油版本还是油电混动版本都很不错,在网上看到很多评测文章和视频 后续都会整理发布到微信群中. 由于论坛发帖,博客发文都不是很方便,为了及时沟通,先创建了微信群,方便 ...
- Mybatis__模糊查询
在一个Web工程中,查询功能几乎都要用到姓名模糊查询,,虽然学号,工号等可以最准确最快的定位,但如果清楚信息到连学号,工号都一个数不差,应该也没必要去查询了. 故需要用到一下语句实现模糊查询: sel ...
- bzoj1584 9.20考试 cleaning up 打扫卫生
1584: [Usaco2009 Mar]Cleaning Up 打扫卫生 Time Limit: 10 Sec Memory Limit: 64 MBSubmit: 549 Solved: 38 ...
- 数据结构-树以及深度、广度优先遍历(递归和非递归,python实现)
前面我们介绍了队列.堆栈.链表,你亲自动手实践了吗?今天我们来到了树的部分,树在数据结构中是非常重要的一部分,树的应用有很多很多,树的种类也有很多很多,今天我们就先来创建一个普通的树.其他各种各样的树 ...
- 调用另一个进程,createprocess返回值正确,但被调进程连入口函数都没进入。
1.单独运行被调进程(提示atl不匹配). 2.编译选项设置为不依赖atl即可. 3.启发:能单独测试的,先单独测试.
- canvas粒子线条效果
在正式开始之前,先上个效果图看看: 很酷炫有木有??? 那么如何实现这个效果呢? 首先,我做这个特效的基本步骤是这样的: 1.将若干个粒子随机分布在画布(canvas)上,并且给他们一个初始速度 2. ...
- python面向过程编程 - ATM
前面程序整合加自定义日志 1.文件摆放 ├── xxxx │ ├── src.py │ └── fil_mode.py │ └── data_time.py │ └── loading.py │ └─ ...
- 百度OCR 文字识别 Android安全校验
百度OCR接口使用总结: 之前总结一下关于百度OCR文字识别接口的使用步骤(Android版本 不带包名配置 安全性弱).这边博客主要介绍,百度OCR文字识别接口,官方推荐使用方式,授权文件(安全模式 ...