基于 WPF 模块化架构下的本地化设计实践

640?wx_fmt=png

背景描述

最近接到一个需求,就是要求我们的 WPF 客户端具备本地化功能,实现中英文多语言界面。刚开始接到这个需求,其实我内心是拒绝的的,但是没办法,需求是永无止境的。所以只能想办法解决这个问题。

首先有必要说一下我们的系统架构。我们的系统是基于 Prism 来进行设计的,所以每个业务模块之间都是相互独立,互不影响的 DLL,然后通过主 Shell 来进行目录的动态扫描来实现动态加载。

为了保证在不影响系统现有功能稳定性的前提下,如何让所有模块支持多语言成为了一个亟待解决的问题。

刚开始,我 Google 了一下,查阅了一些资料,很多都是介绍如何在单体程序中实现多语言,但是在模块化架构中,我个人觉得这样做并不合适。做过本地化的朋友应该都知道,在进行本地化翻译的时候,都需要创建对应语言的资源文件,无论是使用 .xaml .resx 或 .xml,这里面会存放我们的本地化资源。对于单体系统而言,这些资源直接放到主程序下即可,方便快捷。但是对于模块化架构的程序,这样做就不太好,而是应该将这些资源都分别放到自己模块内部由自己来维护,主程序只需规定整个系统的区域语言即可。

设计思路

面对上面的背景描述,我们可以大致描述一下我们期望的解决方式,主程序只负责对整个系统进行区域语言设置,每个模块的本地化由本模块内部完成,所有模块的本地化切换方式保持一致,依赖于共有的一种实现。如下图所示:

640?wx_fmt=png

实现方案

由于如何使用 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

<ResourceDictionaryxmlns="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

<ResourceDictionaryxmlns="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);}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 层使用示例

<UserControlx: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("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 };}
}

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

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

640?wx_fmt=png

总结

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

补充

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

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

参考

  • Localization of a WPF app - the simple approach

  • wpf-localization-multiple-resource-resx-one-language

  • LocalizeMarkupExtension

  • Markup Extensions and WPF XAML

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/314384.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HDU 6836 Expectation(矩阵生成树 + 期望)

Expectation 思路 题目要求每个生成树边权&\&&的期望值,假设当前这颗生成树对二进制数的第iii位有贡献,则这个位上的构成生成树的边权值一定是111,所以我们可以跑313131位二进制数的,矩阵树,每个位上的贡…

你会轻易打破规则吗?

这里是Z哥的个人公众号每周五11:45 按时送达当然了,也会时不时加个餐~我的第「86」篇原创敬上俗话说的好,不以规矩,不成方圆。但是有些时候,可能破坏规则反而是一个更有效的方式,这个时候到底该…

string(STL)

string 优点 常数相对较小,动态分配空间,自定义小于运算符和加法运算符。 转char 可以写成c_str()返回为char指针 获取长度 s.size()和s.lenth() 寻找某字符串第一次出现位置 s.find(t) 截取子串 s.substr(pos,len)长度不足则截取后缀。 访问…

P4449 于神之怒加强版

P4449 于神之怒加强版 推式子 ∑i1n∑j1ngcd(i,j)h\sum_{i 1} ^{n} \sum_{j 1} ^{n} gcd(i, j) ^ h i1∑n​j1∑n​gcd(i,j)h ∑d1ndh∑i1nd∑j1mdgcd(i,j)1\sum_{d 1} ^{n} d ^ h \sum_{i 1} ^{\frac{n}{d}} \sum_{j 1} ^{\frac{m}{d}}gcd(i, j) 1 d1∑n​dhi1∑dn​​…

架构杂谈《十》

常用开发模式一、瀑布式开发瀑布式开发是在1970年提出的软件开发模型,是一种较老的计算机软件开发模式,也是典型的预见性的开发模式,在瀑布式开发中,开发严格遵循预先计划的需求分析、设计、编码、集成、测试、维护的步骤进行&…

B-Donut Drone(循环/分块/DP)

B - Donut Drone 这是一道神题,其中蕴含的思维很巧妙。 在一个循环的二维矩阵中,每个点有一个权值,然后每次一个点只能向右上、正右和右下三个地方中权值最大的地方移动,要求支持两类操作,一种是移动k次,一…

[2020多校A层11.18] 三角田地(因式分解)

[2020多校A层11.18] 三角田地 对于平面上一些点&#xff0c;求解所有与含有边与x轴平行和与y轴平行的三角形面积和 n<100000 显然我们可以讨论每个直角顶点的贡献&#xff0c;然后本质上就是一个经典的数学问题&#xff0c;我们只要求出两个方向上的分量和&#xff0c;然后…

斗地主(矩阵快速幂)

地斗主 思路 看到这nnn非常大&#xff0c;感觉一定是个结论公式题&#xff0c;但是感觉又不像是排列组合&#xff0c;于是可以考虑矩阵快速幂了&#xff0c;所以关键就是得得到递推公式了。 我们将棋盘分成两部分n−num,numn - num, numn−num,num我们假定显然对num1,2,3,4,…

如何删除GIT仓库中的敏感信息

1. 前言正常Git仓库中应该尽量不包含数据库连接/AWS帐号/巨大二进制文件&#xff0c;否则一旦泄漏到Github&#xff0c;这些非常敏感信息会影响客户的信息安全已经公司的信誉。公司可能其它还有相关规定&#xff0c;如禁止私人邮件加入GIT仓库。如果违反这些规定&#xff0c;可…

CF896E Welcome home, Chtholly(分块/并查集/第二分块)

CF896E Welcome home, Chtholly 对于给定一个长度为n(n<1e5)的序列&#xff0c;值域范围为1e5,要求支持两类操作。 将区间[l,r]内所有大于x的数减x查询区间[l,r]内值为x的数的个数 首先由于n和值域同阶&#xff0c;所以我们应该在值域上进行操作&#xff0c;但是这个东西…

ASP.NET Core on K8S深入学习(4)你必须知道的Service

本篇已加入《.NET Core on K8S学习实践系列文章索引》&#xff0c;可以点击查看更多容器化技术相关系列文章。前面几篇文章我们都是使用的ClusterIP供集群内部访问&#xff0c;每个Pod都有一个自己的IP地址&#xff0c;那么问题来了&#xff1a;当控制器使用新的Pod替代发生故障…

输出程序运行时间

输出程序运行时间 用函数clock()返回一个时钟类型&#xff0c;本质上是一个long类型&#xff0c;然后表示从程序开始到当前经过的时钟单位&#xff0c;所以我们可以通过两个位置的差来得到一段的运行时间但是要除以一个常量CLOCKS_PER_SEC表示每秒的时钟单位&#xff0c;然后用…

P3768 简单的数学题(杜教筛)

P3768 简单的数学题 推式子 ∑i1n∑j1mijgcd(i,j)∑d1nd∑i1n∑j1mij(gcd(i,j)d)∑d1nd3∑i1nd∑j1ndij∑k∣gcd(i,j)μ(k)∑d1nd3∑k1ndk2μ(k)∑i1nkdi∑j1nkdj∑d1nd3∑k1ndk2μ(k)(⌊nkd⌋(1⌊nkd⌋)2)2我们假设tkd∑t1nt2(⌊nt⌋(1⌊nt⌋)2)2∑k∣ttkμ(k)∑t1nt2ϕ(t)(⌊…

博客园翻车启示录

开发者的日常作为一名996的开发者&#xff0c;我几乎每天只有两件事&#xff0c;制造bug和解决bug&#xff0c;这两件事&#xff0c;既替我解决了温饱问题、也替产品经理、测试工程师等一票人解决了吃穿问题。嗯&#xff0c;有人为我这种程序员评了一个等级&#xff0c;我大概是…

[2020多校A层11.25]最大K段和(反悔贪心)

[2020多校A层11.25]最大K段和 对于一个长度为n的序列&#xff0c;求解不相交的k段使得他们的总和最大&#xff0c;输出最大值。 n<1e5 对于这种问题&#xff0c;我们没有思路求解&#xff0c;可以考虑枚举&#xff0c;发现无法枚举&#xff0c;然后考虑dp&#xff0c;发现…

asp.net core 从单机到集群

asp.net core 从单机到集群Intro这篇文章主要以我的活动室预约的项目作为示例&#xff0c;看一下一个 asp.net core 应用从单机应用到集群部署需要做什么。示例项目活动室预约提供了两个版本&#xff0c;集群版和 单机版单机版方便部署&#xff0c;不依赖其他环境&#xff0c;数…

杜教筛模板(P4213 【模板】杜教筛(Sum))

P4213 【模板】杜教筛&#xff08;Sum&#xff09; 套路推式子 求s(n)∑i1nf(i)∑i1n(f∗g)(i)∑i1n∑d∣if(d)g(id)∑d1n∑i1⌊nd⌋f(i)g(d)∑d1ng(d)S(⌊nd⌋)g(1)S(n)∑d2ng(d)S(⌊nd⌋)则有g(1)S(n)∑i1n(f∗g)(i)−∑d2ng(d)S(⌊nd⌋)求s(n) \sum_{i 1} ^{n}f(i)\\ \su…

[2020多校A层12.1]树(倍增/单调栈/dfs栈)

[2020多校A层12.1]树 求解树上从u到v的最长贪心上升序列&#xff0c;也就是只要有比它大的就选择它&#xff0c;可以发现这个问题性质&#xff0c;就是每个点对应了唯一的一个第一个比它大的点&#xff0c;那么我们可以向它们之间连边&#xff0c;然后问题就转化为求解从当前点…

通过Blazor使用C#开发SPA单页面应用程序(3)

通过Blazor使用C#开发SPA单页面应用程序(1)通过Blazor使用C#开发SPA单页面应用程序(2)今天我们来看看Blazor开发的一些基本知识。Blazor中组件的基本结构可以分为3个部分&#xff0c;如下所示&#xff1a;//Counter.razor//Directives section 指令部分page "/counter&qu…

NC14250 MMSet2

MMSet2 思路 这道题目显然能够通过31051063 \times 10 ^ 5 \times 10 ^ 63105106的复杂度来暴力&#xff0c;这显然不能达到题目要求的复杂度&#xff0c;因此我们可以对题目要求我们计算的东西进行转换。 某个点到所有点集的最大距离最小&#xff0c;这就有点像是重心的求法…