基于 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,一经查实,立即删除!

相关文章

你会轻易打破规则吗?

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

架构杂谈《十》

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

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

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

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

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

博客园翻车启示录

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

asp.net core 从单机到集群

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

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

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

ASP.NET CORE 2.* 利用集成测试框架覆盖HttpClient相关代码

ASP.NET CORE 集成测试官方介绍我的asp.net core 项目里面大部分功能都是去调用别人的API ,大量使用HttpClient,公司单元测试覆盖率要求95%以上,很难做到不mock HttpClient 达到这个指数。以下方法是我自己总结的在单元测试里 mock httpClien…

Let's Encrypt网站推出中文版

如今很多网站都强制使用 HTTPS 加密协议访问,安全性有了很大的提高,最起码在数据传输的初始阶段数据包不会被劫持,保证了客户端与服务器端的通讯安全性。说到 HTTPS 加密协议,就不得不提 Let’s Encrypt。Let’s Encrypt 是一家不…

使用WebDeploy部署远程IIS网站

目录 使用WebDeploy部署远程IIS网站后台服务部署服务器配置本地WebDeploy发布文件配置前端页面部署WebDeploy服务端配置WebDeploy发布文件配置使用WebDeploy部署远程网站后台服务部署服务器配置打开IIS管理器(开始->控制面板->管理工具->IIS管理器)添加网站(右键网站…

CF436F Banners(分块/凸包/单调队列)

CF436F Banners 首先有n个物品分别有ai和bi,然后定义价值为 c∗wp∗(ai大于p且bi小于c的用户个数)c*wp*(ai大于p且bi小于c的用户个数)c∗wp∗(ai大于p且bi小于c的用户个数) 然后我们需要求解对于每一个c的最大价值和对应的p 首先我们先枚举c,然后每次加…

译 | 改进 Visual Studio 及 Windows 上 .NET Core 的安装体验

点击上方蓝字关注“汪宇杰博客”原文:Lee Coward翻译:Edi Wang导语Visual Studio 2019 16.3 和 .NET Core 3.0 Preview 7 改进了 Windows 上 .NET Core 的安装体验。目标是减少计算机上可能存在的 .NET Core 版本的数量。这些改进基于客户反馈和我们自己…

SonarQube系列三、Jenkins集成SonarQube(dotnetcore篇)

来源:https://www.cnblogs.com/7tiny/p/11348785.html【前言】本系列主要讲述sonarqube的安装部署以及如何集成jenkins自动化分析.netcore项目。目录如下:SonarQube系列一、Linux安装与部署SonarQube系列二、分析dotnet core/C#代码SonarQube系列三、Jen…

CF198D Cube Snake(三维空间/增量构造)

CF198D Cube Snake n<50 显然是一个构造题&#xff0c;然后很容易想到增量构造&#xff0c;可以考虑每次保证一层值域连续&#xff0c;然后再增加一个值域连续的层&#xff0c;就会产生两个值域连续的正方体&#xff0c;但是只移动头是不够的&#xff0c;所以我们还需要移…

发布ABP v0.19包含Angular UI选项

ABP v0.19已发布,包含解决的~90个问题和600次提交.新功能Angular UI终于,ABP有了一个SPA UI选项,使用最新的Angular框架.Angular的集成不是简单地创建了一个启动模板.创建了一个基础架构来处理ABP的模块化,主题和其他一些功能.此基础结构已部署为NPM包.为帐户,身份和租户管理等…

P4151 [WC2011]最大XOR和路径(线性基应用)

P4151 [WC2011]最大XOR和路径 思路 如果单纯的只是树形图&#xff0c;那么答案显然易见只有一种&#xff0c;也就是从头到尾的路径从头到尾的异或值&#xff0c;但是这里不同的就是有可能在道路上有许多的环。 题目有一个重点提示的一句话 理解这句话之后那么我们可以显然…

Docker(一)-CentOS7中安装Docker视频教程

一、前言Docker的使用越来越多&#xff0c;安装也相对简单。本文使用视频的方式展示在CentOS7系统中安装Docker&#xff0c;本文更适合于准备入门学习Docker的童靴&#xff0c;同时也欢迎各路大神给我们指点提建议。二、环境CentOS7三、安装Docker确认Linux内核版本# uname -a卸…

CF773E Blog Post Rating(推导min的通项/线段树)

CF773E Blog Post Rating 现在有一个值F&#xff0c;每次如果序列加入一个数后重新排列&#xff0c;每次如果F小于期望值&#xff0c;就会1&#xff0c;如果等于就不变&#xff0c;如果大于就-1&#xff0c;求解每一次加入后F的最大值。 这道题的确是非常的巧妙&#xff0c;尤…

用Keras.NET 做一个图像识别的训练

.NET Core 的应用场景越来越广&#xff0c;开源社区也不断壮大&#xff0c; .NET Core在机器学习领域不断发展ML.NET外&#xff0c;也通过结合Tensorflow.NET去完善ML.NET在深度学习领域的功能&#xff0c;在ML.NET 1.3开始迈出了非常重要的一步。这不仅是微软拥抱开源的策略&a…

「数据ETL」从数据民工到数据白领蜕变之旅(六)-将Python的能力嫁接到SSIS中...

前一篇推文中&#xff0c;给大家演示了在SSIS上使用dotNET脚本&#xff0c;实现一些原生SSIS难以实现的功能&#xff0c;并冠以无限可能的说法。充分复用python的现有优势python的确是一门非常优秀的编程语言&#xff0c;特别是在数据领域&#xff0c;网络爬虫、数据处理、分析…