当前位置: 首页 > article >正文

从零到多页复用:我的WPF MVVM国际化实践

文章目录

    • 第一步:基础实现,资源文件入门
    • 第二步:依赖属性,提升WPF体验
    • 第三步:多页面复用,减少重复代码
    • 第四步:动态化,应对更多字符串
    • 总结与反思

作为一名WPF开发者,我最近在一个配置工具项目中遇到了国际化需求。起初,我只是想让按钮文本支持英文和中文切换,但随着页面增多,需求复杂化,我逐渐摸索出一套从简单到专业化的实现方案。这篇文章记录了我的探索过程,从最基础的资源文件开始,到多页面复用的优化,希望能给有类似需求的开发者一些启发。

第一步:基础实现,资源文件入门

我的项目是一个基于WPF和MVVM的配置工具,界面上有“Save”和“Refresh”两个按钮,需要支持英文和中文切换。WPF的国际化通常从资源文件(.resx)入手,于是我先尝试了最简单的方法。

在项目中,我创建了一个Resources文件夹,添加了两个资源文件:

  • Resources.resx(默认英文):
    • save: Save
    • refresh: Refresh
  • Resources.zh-CN.resx(中文):
    • save: 保存
    • refresh: 刷新

在XAML中,我尝试直接绑定到资源:

<Button Content="{Binding Source={x:Static local:Resources.save}}" />

但很快发现,这种方式在运行时切换语言时不会更新UI,因为静态绑定无法响应动态变化。于是,我转向代码隐藏文件,在UserControl中定义属性:

public partial class PageTemplate : UserControl
{public string Save => Resources.ResourceManager.GetString("save");public string Refresh => Resources.ResourceManager.GetString("refresh");public PageTemplate(){InitializeComponent();DataContext = this;}
}

XAML改为:

<Button Content="{Binding Save}" />

这时候,按钮显示了英文,但点击“中文”按钮后,文本没变。我意识到,语言切换需要更新CultureInfo,于是引入了一个单例类LanguageManager:

public class LanguageManager
{private static readonly Lazy<LanguageManager> _instance = new Lazy<LanguageManager>(() => new LanguageManager());public static LanguageManager Instance => _instance.Value;private CultureInfo _currentCulture = new CultureInfo("en-US");public CultureInfo CurrentCulture{get => _currentCulture;set{_currentCulture = value;Thread.CurrentThread.CurrentUICulture = value;}}public string GetString(string key) => Resources.ResourceManager.GetString(key, _currentCulture) ?? $"[{key}]";
}

在PageTemplate中使用:

private readonly LanguageManager _languageManager = LanguageManager.Instance;
public string Save => _languageManager.GetString("save");

加上切换命令:

<Button CommandParameter="zh-CN" Command="{Binding SwitchLanguageCommand}" Content="中文" />
public ICommand SwitchLanguageCommand => new RelayCommand<string>(lang =>
{_languageManager.CurrentCulture = new CultureInfo(lang);
});

然而,切换后UI还是没更新。我调试发现,虽然CultureInfo变了,但绑定没有刷新。加上INotifyPropertyChanged后问题解决:

public partial class PageTemplate : UserControl, INotifyPropertyChanged
{private readonly LanguageManager _languageManager = LanguageManager.Instance;public string Save => _languageManager.GetString("save");public PageTemplate(){InitializeComponent();DataContext = this;_languageManager.PropertyChanged += (s, e) => OnPropertyChanged(null);}public event PropertyChangedEventHandler PropertyChanged;protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

终于,切换语言时按钮文本正常更新了!

第二步:依赖属性,提升WPF体验

虽然基础功能实现了,但这种只读属性方式让我觉得不够“WPF”。在WPF中,依赖属性更适合绑定场景。于是我改用依赖属性:

public static readonly DependencyProperty SaveProperty = DependencyProperty.Register(nameof(Save), typeof(string), typeof(PageTemplate), new PropertyMetadata(string.Empty));public string Save
{get => (string)GetValue(SaveProperty);set => SetValue(SaveProperty, value);
}public PageTemplate()
{InitializeComponent();DataContext = this;UpdateLocalizedStrings();_languageManager.PropertyChanged += (s, e) => UpdateLocalizedStrings();
}private void UpdateLocalizedStrings()
{Save = _languageManager.GetString("save");Refresh = _languageManager.GetString("refresh");
}

这样,绑定更符合WPF的习惯,而且UI更新更可靠。下一步,我把语言切换按钮改成了下拉框:

<ComboBox ItemsSource="{Binding Languages}"DisplayMemberPath="DisplayName"SelectedValuePath="CultureName"SelectedValue="{Binding SelectedLanguage, Mode=TwoWay}"/>
public List<LanguageOption> Languages { get; } = new List<LanguageOption>
{new LanguageOption("English", "en-US"),new LanguageOption("中文", "zh-CN")
};public static readonly DependencyProperty SelectedLanguageProperty = DependencyProperty.Register(nameof(SelectedLanguage), typeof(string), typeof(PageTemplate),new PropertyMetadata("en-US", OnSelectedLanguageChanged));public string SelectedLanguage
{get => (string)GetValue(SelectedLanguageProperty);set => SetValue(SelectedLanguageProperty, value);
}private static void OnSelectedLanguageChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{var page = (PageTemplate)d;page._languageManager.CurrentCulture = new CultureInfo((string)e.NewValue);
}

这让界面更友好,用户体验也提升了。

第三步:多页面复用,减少重复代码

项目发展到有多个页面时,我发现每个页面都重复定义Save、Refresh和语言切换逻辑,太繁琐了。我决定把国际化集中化,先创建了一个基类:

public class BaseViewModel : INotifyPropertyChanged
{protected readonly LanguageService _languageService = LanguageService.Instance;public string Save => _languageService.GetString("save");public string Refresh => _languageService.GetString("refresh");public event PropertyChangedEventHandler PropertyChanged;protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

LanguageService接管了语言管理:

public class LanguageService : INotifyPropertyChanged
{private static readonly Lazy<LanguageService> _instance = new Lazy<LanguageService>(() => new LanguageService());public static LanguageService Instance => _instance.Value;private CultureInfo _currentCulture = new CultureInfo("en-US");public CultureInfo CurrentCulture{get => _currentCulture;set{_currentCulture = value;Thread.CurrentThread.CurrentUICulture = value;OnPropertyChanged(null);}}public string GetString(string key) => Resources.ResourceManager.GetString(key, _currentCulture) ?? $"[{key}]";public List<LanguageOption> Languages { get; } = new List<LanguageOption>{new LanguageOption("English", "en-US"),new LanguageOption("中文", "zh-CN")};public string SelectedLanguage{get => _currentCulture.Name;set => CurrentCulture = new CultureInfo(value);}public event PropertyChangedEventHandler PropertyChanged;protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

页面只需继承BaseViewModel:

public class PageTemplateViewModel : BaseViewModel { }
public partial class PageTemplate : UserControl
{public PageTemplate(){InitializeComponent();DataContext = new PageTemplateViewModel();}
}

XAML绑定到全局服务:

<ComboBox ItemsSource="{Binding Languages, Source={x:Static services:LanguageService.Instance}}"SelectedValue="{Binding SelectedLanguage, Source={x:Static services:LanguageService.Instance}, Mode=TwoWay}"/>

第四步:动态化,应对更多字符串

页面越来越多,字符串也从save、refresh扩展到title、user等十几个。我不想在BaseViewModel中为每个字符串写属性,于是尝试了动态方案:

public class BaseViewModel : INotifyPropertyChanged
{protected readonly LanguageService _languageService = LanguageService.Instance;public dynamic Strings => new LocalizedStrings(_languageService);public BaseViewModel(){_languageService.PropertyChanged += (s, e) => OnPropertyChanged(nameof(Strings));}public event PropertyChangedEventHandler PropertyChanged;protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}public class LocalizedStrings : DynamicObject
{private readonly LanguageService _languageService;public LocalizedStrings(LanguageService languageService){_languageService = languageService;}public override bool TryGetMember(GetMemberBinder binder, out object result){result = _languageService.GetString(binder.Name.ToLower());return true;}
}

XAML改为:

<Button Content="{Binding Strings.save}" />
<TextBlock Text="{Binding Strings.title}" />

现在,无论有多少字符串,我只需在资源文件里添加键值对,代码完全不用改动。这种方式让我从繁琐的属性定义中解放出来。

总结与反思

从最初的简单资源文件,到依赖属性,再到多页面复用,最后用动态对象优化,我的国际化之旅走了不少弯路,但每一步都让我更理解WPF和MVVM的精髓:

  • 起步简单:资源文件和基本绑定能快速实现单页面国际化。
  • 提升体验:依赖属性和下拉框让切换更自然。
  • 复用为王:集中化管理避免重复劳动。
  • 动态扩展:用动态对象应对未来需求。

如果你的项目也有国际化需求,不妨从基础开始,根据规模逐步优化。你遇到过哪些国际化难题?欢迎留言分享!

相关文章:

从零到多页复用:我的WPF MVVM国际化实践

文章目录 第一步&#xff1a;基础实现&#xff0c;资源文件入门第二步&#xff1a;依赖属性&#xff0c;提升WPF体验第三步&#xff1a;多页面复用&#xff0c;减少重复代码第四步&#xff1a;动态化&#xff0c;应对更多字符串总结与反思 作为一名WPF开发者&#xff0c;我最近…...

C++11新特性 3.constexpr

目录 一.简介 1.基本概念 2.语法 &#xff08;1&#xff09;constexpr 变量 &#xff08;2&#xff09;constexpr 函数 二.使用示例 示例1&#xff1a;constexpr 修饰变量 示例2&#xff1a;constexpr 修饰函数 示例3&#xff1a;constexpr 修饰构造函数 三.注意事项 …...

什么是AI Agent

AI Agent&#xff08;人工智能代理&#xff09;是一种能够感知环境、自主决策并采取行动以实现特定目标的智能实体。它结合了人工智能技术&#xff08;如机器学习、自然语言处理、计算机视觉等&#xff09;&#xff0c;能够通过与环境交互不断学习和优化行为。 核心特征 自主…...

LeetCode 解题思路 12(Hot 100)

解题思路&#xff1a; 定义三个指针&#xff1a; prev&#xff08;前驱节点&#xff09;、current&#xff08;当前节点&#xff09;、nextNode&#xff08;临时保存下一个节点&#xff09;遍历链表&#xff1a; 每次将 current.next 指向 prev&#xff0c;移动指针直到 curre…...

HTML-05NPM使用踩坑

2025-03-04-NPM使用踩坑 本文讲述了一个苦逼程序员在使用NPM的时候突然来了一记nmp login天雷&#xff0c;然后一番折腾之后&#xff0c;终究还是没有解决npm的问题&#x1f61e;&#x1f61e;&#x1f61e;,最终使用cnpm完美解决的故事。 文章目录 2025-03-04-NPM使用踩坑[toc…...

学校地摊尝试实验

学校地摊尝试实验 诸位&#xff0c;我要告诉诸位一件大消息&#xff0c;那就是&#xff0c;我将会利用学校时光的最后一段时间进行疯狂摆摊练习&#xff0c;如何进行摆摊&#xff0c;大家 听我娓娓道来。我要确定摆摊的目的&#xff0c; 第一&#xff0c;赚钱&#xff0c;第二…...

MHA集群

一.MHA集群 MHA master high avavibility 主服务器高可用 如上图所示&#xff0c;我们之前说过&#xff0c;如果在主从复制架构中主服务器出现故障&#xff0c;就需要我们将从服务器作为主服务器&#xff0c;等故障的主服务器修复好之后&#xff0c;再将修好的主服务器作为从服…...

Bazel搭建CUDA工程入门

环境版本&#xff1a; 工程目录&#xff1a; 测试输出&#xff1a; WORKSPACE 参考仓库&#xff1a;CUDA rules for Bazel 及 examples load("bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")http_archive(name "rules_cuda…...

linux awk命令和awk语言

linux awk和awk语言 通常大家说的awk几乎都是在linux/unix中使用的awk命令&#xff0c;见下&#xff0c; https://www.geeksforgeeks.org/awk-command-unixlinux-examples/ 作为命令使用的话&#xff0c;存在下内容 Awk 是一个工具&#xff0c;使程序员能够编写小巧但有效的…...

基于字符的卷积网络在文本分类中的应用与探索

该论文探讨了使用基于字符的卷积网络(ConvNets)进行文本分类的方法,并通过构建大规模数据集展示了其在文本分类任务中的优越性能。与传统的词袋模型、N-gram模型及其TF-IDF变体,以及基于词的卷积网络和循环神经网络等深度学习模型进行了对比。研究发现,基于字符的卷积网络…...

uniapp使用蓝牙,usb,局域网,打印机打印

使用流程&#xff08;支持安卓和iOS&#xff09; 引入SDK 引入原生插件包地址如下 https://github.com/oldfive20250214/UniPrinterDemo 连接设备 安卓支持经典蓝牙、ble蓝牙、usb、局域网&#xff08;参考API&#xff09; iOS支持ble蓝牙、局域网&#xff08;参考API&…...

MyBatis 与 JDBC 的关系?

MyBatis 与 JDBC 存在密切的关系&#xff0c;可以理解为&#xff1a;MyBatis 是对 JDBC 的封装和增强&#xff0c;但并没有完全取代 JDBC。 1. JDBC (Java Database Connectivity): 底层 API: JDBC 是 Java 访问数据库的底层 API&#xff0c;它提供了一套标准的接口和类&…...

QILSTE灯珠:尺寸光电全解析

QILSTE灯珠&#xff1a;尺寸光电全解析 &#x1f31f; 型号H4-115BGRYA/5M&#xff0c;由QILSTE&#xff08;HongKong&#xff09;Technology Co., Ltd精心打造&#xff0c;以其1.6x1.5x0.4mm的紧凑外观尺寸&#xff0c;展现高亮红光、翠绿、蓝的缤纷色彩。 &#x1f4cf; 尺寸…...

golang从入门到做牛马:第一篇-我与golang的缘分,go语言简介

还记得2018年的夏天,刚毕业的我不知道该做些什么,于是自学了一周的go语言,想要找一份go语言工作的代码,当时的go还没有go mod来管理依赖包,在北京找了一个月的工作,找到了一个小公司做了后端开发,当然使用go语言开发,带着兴奋劲,年轻身体也好,边努力学习,边工作。 时…...

用IdleHandler来性能优化及原理源码分析

背景&#xff1a; 经常在做一些app冷启动速度优化等性能优化工作时候&#xff0c;经常可能会发现有时候需要引入一些第三方sdk&#xff0c;或者库&#xff0c;这些库一般会要求我们在onCreate中进行初始化等&#xff0c;但是onCreate属于生命周期的回调方法&#xff0c;如果on…...

如何在WPS中接入DeepSeek并使用OfficeAI助手(超细!成功版本)

目录 第一步&#xff1a;下载并安装OfficeAI助手 第二步&#xff1a;申请API Key 第三步:两种方式导入WPS 第一种:本地大模型Ollama 第二种APIKey接入 第四步&#xff1a;探索OfficeAI的创作功能 工作进展汇报 PPT大纲设计 第五步&#xff1a;我的使用体验(体验建议) …...

长短期记忆网络(LSTM)学习指南

长短期记忆网络&#xff08;LSTM&#xff09;学习指南 1. 定义和背景 长短期记忆网络&#xff08;Long Short-Term Memory, LSTM&#xff09;是一种递归神经网络&#xff08;RNN&#xff09;的变体&#xff0c;旨在解决传统RNN在处理长期依赖关系时遇到的梯度消失或爆炸问题。…...

Swagger-01.介绍和使用方式

一.Swagger介绍 有了接口文档&#xff0c;我们就可以根据接口文档来开发后端的代码了。如果我们开发完了某个功能&#xff0c;后端如何验证我们开发的是否正确呢&#xff1f;我们就需要测试&#xff0c;使用Swagger就可以帮助后端生成接口文档&#xff0c;并且可以进行后端的接…...

Unity 使用NGUI制作无限滑动列表

原理&#xff1a; 复用几个子物体&#xff0c;通过子物体的循环移动实现&#xff0c;如下图 在第一个子物体滑动到超出一定数值时&#xff0c;使其放到最下方 --------------------------------------------------------------》 然后不停的循环往复&#xff0c;向下滑动也是这…...

【并发编程】聊聊定时任务ScheduledThreadPool的实现原理和源码解析

ScheduledThreadPoolExecutor 是在线程池的基础上 拓展的定时功能的线程池&#xff0c;主要有四种方式&#xff0c;具体可以看代码&#xff0c; 这里主要描述下 scheduleAtFixedRate &#xff1a; 除了第一次执行的时间&#xff0c;后面任务执行的时间 为 time MAX(任务执行时…...

HarmonyOS Next元服务网络请求封装实践

【HarmonyOS Next实战】元服务网络通信涅槃&#xff1a;深度封装如何实现80%性能跃升与零异常突破 ————从架构设计到工程落地的全链路优化指南 一、架构设计全景 1.1 分层架构模型 #mermaid-svg-VOia4RMx7iqmLnu7 {font-family:"trebuchet ms",verdana,arial,…...

如何在语言模型的参数中封装知识?——以T5模型为例

【摘要】 这篇论文探讨了大型语言模型在无需外部知识的情况下&#xff0c;能否通过预训练来存储和检索知识以回答开放领域的问题。作者通过微调预训练模型来回答问题&#xff0c;而这些模型在训练时并未提供任何额外的知识或上下文。这种方法随着模型规模的增加而表现出良好的…...

微服务的认识与拆分

微服务架构通过将应用分解为一组小的、独立的服务来实现&#xff0c;每个服务围绕特定业务功能构建&#xff0c;并能独立部署与扩展。这种架构增强了开发灵活性、提高了系统的可维护性和扩展性&#xff0c;使得团队可以更快地响应变化和市场需求。 目录 认识微服务 单体架构 …...

Java-servlet(三)Java-servlet-Web环境搭建(下)详细讲解利用maven和tomcat搭建Java-servlet环境

Java-servlet&#xff08;三&#xff09;Java-servlet-Web环境搭建&#xff08;下&#xff09;利用maven和tomcat搭建Java-servlet环境 前言一、配置maven阿里镜像二、利用IDEA创建maven文件创建maven文件删除src文件创建新的src模版删除example以及org文件 三、在第二个xml文件…...

Spring 构造器注入和setter注入的比较

一、比较说明 在 Spring 框架中&#xff0c;构造器注入&#xff08;Constructor Injection&#xff09;和 Setter 注入&#xff08;Setter Injection&#xff09;是实现依赖注入&#xff08;DI&#xff09;的两种主要方式。它们的核心区别在于依赖注入的时机、代码设计理念以及…...

如何选择DevOps平台?GitHub、GitLab、BitBucket、Jenkins对比与常见问题解答

本文内容来源github.com&#xff0c;由GitHub中国授权合作伙伴-创实信息进行翻译整理。 欢迎通过021-61210910、customershcsinfo.com联系我们&#xff0c;免费试用GitHub企业版。 软件是当今领先企业的核心&#xff0c;而开发者则是软件的核心。GitHub作为一个完整的开发者平台…...

react中的fiber和初次渲染

源码中定义了不同类型节点的枚举值 组件类型 文本节点HTML标签节点函数组件类组件等等 src/react/packages/react-reconciler/src/ReactWorkTags.js export const FunctionComponent 0; export const ClassComponent 1; export const IndeterminateComponent 2; // Befo…...

闭包+求解候选码+最小函数依赖集

一、闭包 直接上例题 简单明了 A的闭包ABC ABC的闭包ABCD ABCD的闭包ABCDE ABCDE的闭包ABCDEG 等于集合R的全集 所以A的闭包为ABCDEG AB的闭包为ABC 二、候选码 答案&#xff1a; 三、最小函数依赖集 求F的最小函数依赖集 去掉多余的 然后&#xff01; 化为最简...

DeepSeek 助力 Vue3 开发:打造丝滑的表格(Table)之功能优化,添加表格空状态提示,带插图的空状态,Table7空状态2

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;今天给大家分享一篇文章&#xff01;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01;创作不易&#xff0c;如果能帮助到大家或者给大家一些灵感和启发&#xff0c;欢迎收藏关注哦 &#x1f495; 目录 Deep…...

Unity Shader 学习15:可交互式雪地流程

本质是 利用顶点变换实现的&#xff1a; 通过一个俯视整个场地的正交摄像机&#xff0c;根据绑定在移动物体身上的粒子系统&#xff0c;来获取物体移动过的位置&#xff0c;记录到一张RenderTexture上作为轨迹图&#xff0c;再通过这张图来对雪地做顶点变换。 1. 由于顶点变换需…...