WPF ComboBox 使用 ResourceBinding 动态绑定资源键并支持语言切换

独立观察员  2021 年 8 月 23 日

我们平常在 WPF 中进行资源绑定操作,一般就是用 StaticResource 或者 DynamicResource 后面跟上资源的 key 这种形式,能满足大部分需求。但是有的时候,我们需要绑定的是代表了资源的 key 的变量,也就是动态绑定资源的 key(注意和 DynamicResource 区分开),比如本文将要演示的支持国际化的场景。这种动态绑定资源 key 的功能,在 WPF 中没有被原生支持,所以还是得在网上找找解决方法。

最终在 stackoverflow 网站上看到一篇靠谱的讨论帖(Binding to resource key, WPF),里面几个人分别用 标记扩展、附加属性、转换器 的方式给出了解决方法,本文使用的是 Gor Rustamyan 给出的 标记扩展 的方案,核心就是一个 ResourceBinding 类(代码整理了下,下文给出)。

先来看看本次的使用场景吧,简单来说就是一个下拉框控件绑定了键值对列表,显示的是其中的键,但是要求是支持国际化(多语言),如下图:

由于要支持多语言,所以键值对的键不是直接显示的值,而是显示值的资源键:

/// <summary>
/// 时间列表
/// </summary>
public ObservableCollection<KeyValuePair<string, int>> TimeList { get; set; } = new ObservableCollection<KeyValuePair<string, int>>()
{new KeyValuePair<string, int>("LockTime-OneMinute", 1),new KeyValuePair<string, int>("LockTime-FiveMinute", 5),new KeyValuePair<string, int>("LockTime-TenMinute", 10),new KeyValuePair<string, int>("LockTime-FifteenMinute", 15),new KeyValuePair<string, int>("LockTime-ThirtyMinute", 30),new KeyValuePair<string, int>("LockTime-OneHour", 60),new KeyValuePair<string, int>("LockTime-TwoHour", 120),new KeyValuePair<string, int>("LockTime-ThreeHour", 180),new KeyValuePair<string, int>("LockTime-Never", 0),
};

字符串资源放在资源字典中:

界面 Xaml 代码为:

xmlns:markupExtensions="clr-namespace:Mersoft.Mvvm.MarkupExtensions"<GroupBox Header="演示 ComboBox 绑定资源键(国际化支持)" Height="100"><StackPanel Orientation="Horizontal"><ComboBox MinWidth="200" MaxWidth="400" Height="35" Margin="10" FontSize="18" VerticalContentAlignment="Center"ItemsSource="{Binding TimeList}" SelectedItem="{Binding SelectedTime}"><ComboBox.ItemTemplate><DataTemplate><TextBlock Text="{markupExtensions:ResourceBinding Key}"></TextBlock></DataTemplate></ComboBox.ItemTemplate></ComboBox><Button Width="100" Command="{Binding SwitchCnCmd}"> 切换中文 </Button><Button Width="100" Command="{Binding SwitchEnCmd}"> 切换英文 </Button><TextBlock Text="{markupExtensions:ResourceBinding SelectedTime.Key}" VerticalAlignment="Center"></TextBlock></StackPanel>
</GroupBox>

可以看到,给 ComboBox 的 ItemTemplate 设置了一个 DataTemplate,里面通过 TextBlock 来绑定键值对中的 Key。关键在于,此处不是使用普通的 Binding,而是使用了自定义的标记扩展 ResourceBinding,其代码如下:

using System;
using System.ComponentModel;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;namespace Mersoft.Mvvm.MarkupExtensions
{/// <summary>/// 用于处理 绑定代表资源键 (key) 的变量 业务的标记扩展类/// markup extension to allow binding to resourceKey in general case./// https://stackoverflow.com/questions/20564862/binding-to-resource-key-wpf/// </summary>/// <example>/// <code>/// (Image Source="{local:ResourceBinding ImageResourceKey}"/>/// </code>/// </example>public class ResourceBinding : MarkupExtension{#region Helper propertiespublic static object GetResourceBindingKeyHelper(DependencyObject obj){return (object)obj.GetValue(ResourceBindingKeyHelperProperty);}public static void SetResourceBindingKeyHelper(DependencyObject obj, object value){obj.SetValue(ResourceBindingKeyHelperProperty, value);}// Using a DependencyProperty as the backing store for ResourceBindingKeyHelper.  This enables animation, styling, binding, etc...public static readonly DependencyProperty ResourceBindingKeyHelperProperty =DependencyProperty.RegisterAttached("ResourceBindingKeyHelper", typeof(object), typeof(ResourceBinding), new PropertyMetadata(null, ResourceKeyChanged));static void ResourceKeyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e){var target = d as FrameworkElement;var newVal = e.NewValue as Tuple<object, DependencyProperty>if (target == null || newVal == null)return;var dp = newVal.Item2;if (newVal.Item1 == null){target.SetValue(dp, dp.GetMetadata(target).DefaultValue);return;}target.SetResourceReference(dp, newVal.Item1);}#endregionpublic ResourceBinding(){}public ResourceBinding(string path){Path = new PropertyPath(path);}public override object ProvideValue(IServiceProvider serviceProvider){var provideValueTargetService = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));if (provideValueTargetService == null)return null;if (provideValueTargetService.TargetObject != null &&provideValueTargetService.TargetObject.GetType().FullName == "System.Windows.SharedDp")return this;var targetObject = provideValueTargetService.TargetObject as FrameworkElement;var targetProperty = provideValueTargetService.TargetProperty as DependencyProperty;if (targetObject == null || targetProperty == null)return null;#region bindingBinding binding = new Binding{Path = Path,XPath = XPath,Mode = Mode,UpdateSourceTrigger = UpdateSourceTrigger,Converter = Converter,ConverterParameter = ConverterParameter,ConverterCulture = ConverterCulture,FallbackValue = FallbackValue};if (RelativeSource != null)binding.RelativeSource = RelativeSource;if (ElementName != null)binding.ElementName = ElementName;if (Source != null)binding.Source = Source;#endregionvar multiBinding = new MultiBinding{Converter = HelperConverter.Current,ConverterParameter = targetProperty};multiBinding.Bindings.Add(binding);multiBinding.NotifyOnSourceUpdated = true;targetObject.SetBinding(ResourceBindingKeyHelperProperty, multiBinding);return null;}#region Binding Members/// <summary>/// The source path (for CLR bindings)./// </summary>public object Source { get; set; }/// <summary>/// The source path (for CLR bindings)./// </summary>public PropertyPath Path { get; set; }/// <summary>/// The XPath path (for XML bindings)./// </summary>[DefaultValue(null)]public string XPath { get; set; }/// <summary>/// Binding mode/// </summary>[DefaultValue(BindingMode.Default)]public BindingMode Mode { get; set; }/// <summary>/// Update type/// </summary>[DefaultValue(UpdateSourceTrigger.Default)]public UpdateSourceTrigger UpdateSourceTrigger { get; set; }/// <summary>/// The Converter to apply/// </summary>[DefaultValue(null)]public IValueConverter Converter { get; set; }/// <summary>/// The parameter to pass to converter./// </summary>/// <value></value>[DefaultValue(null)]public object ConverterParameter { get; set; }/// <summary>/// Culture in which to evaluate the converter/// </summary>[DefaultValue(null)][TypeConverter(typeof(System.Windows.CultureInfoIetfLanguageTagConverter))]public CultureInfo ConverterCulture { get; set; }/// <summary>/// Description of the object to use as the source, relative to the target element./// </summary>[DefaultValue(null)]public RelativeSource RelativeSource { get; set; }/// <summary>/// Name of the element to use as the source/// </summary>[DefaultValue(null)]public string ElementName { get; set; }#endregion#region BindingBase Members/// <summary>/// Value to use when source cannot provide a value/// </summary>/// <remarks>/// Initialized to DependencyProperty.UnsetValue; if FallbackValue is not set, BindingExpression/// will return target property's default when Binding cannot get a real value./// </remarks>public object FallbackValue { get; set; }#endregion#region Nested typesprivate class HelperConverter : IMultiValueConverter{public static readonly HelperConverter Current = new HelperConverter();public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture){return Tuple.Create(values[0], (DependencyProperty)parameter);}public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture){throw new NotImplementedException();}}#endregion}
}

主要就是继承 MarkupExtension 并重写 ProvideValue 方法,具体的本人也没怎么研究,就先不说了,大家感兴趣可以自己查一查。这里直接拿来使用,可以达到动态绑定资源 key 的目的。

如果使用的是普通的 Binding,则只能显示原始值:

最后来看看中英文切换,当然,如果有其它语言,也是一样可以切换的。

首先是移除现有语言资源的方法:

/// <summary>
/// 语言名称列表
/// </summary>
private readonly List<string> _LangKeys = new List<string>() { "en-us", "zh-cn" };/// <summary>
/// 移除语言资源
/// </summary>
/// <param name="removeKeyList"> 需要移除的资源中包含的 key 的列表,默认为空,为空移除所有的 </param>
private void RemoveLangThemes(List<string> removeKeyList = null)
{if (removeKeyList == null){removeKeyList = _LangKeys;}var rd = Application.Current.Resources;List<ResourceDictionary> removeList = new List<ResourceDictionary>();foreach (var dictionary in rd.MergedDictionaries){// 判断是否是对应的语言资源文件;bool isExists = removeKeyList.Exists(x => dictionary.Contains("LangName") && dictionary["LangName"]+"" == x);if (isExists){removeList.Add(dictionary);}}foreach (var removeResource in removeList){rd.MergedDictionaries.Remove(removeResource);}
}

主要是对 Application.Current.Resources.MergedDictionaries 进行操作,移除有 LangName 键,且值为对应语言代号的资源字典。

然后是应用对应语言资源的方法及调用:

/// <summary>
/// 应用语言
/// </summary>
/// <param name="packUriTemplate"> 资源路径模板,形如:"/WPFPractice;component/Resources/Language/{0}.xaml"</param>
/// <param name="langName"> 语言名称,形如:"zh-cn"</param>
private void ApplyLanguage(string packUriTemplate, string langName = "zh-cn")
{var rd = Application.Current.Resources;//RemoveLangThemes();var packUri = string.Format(packUriTemplate, langName);RemoveLangThemes(new List<string>() { langName });// 将资源加载在最后,优先使用;rd.MergedDictionaries.Add((ResourceDictionary)Application.LoadComponent(new Uri(packUri, UriKind.Relative)));
}/// <summary>
/// 语言资源路径模板字符串
/// </summary>
private string _LangResourceUriTemplate = "/WPFPractice;component/Resources/Language/{0}.xaml";/// <summary>
/// 命令方法赋值(在构造方法中调用)
/// </summary>
private void SetCommandMethod()
{SwitchCnCmd ??= new RelayCommand(o => true, async o =>{ApplyLanguage(_LangResourceUriTemplate, "zh-cn");});SwitchEnCmd ??= new RelayCommand(o => true, async o =>{ApplyLanguage(_LangResourceUriTemplate, "en-us");});
}

逻辑就是,先移除要切换到的语言资源的已存在的实例,然后将新的实例放在最后,以达到比其它语言资源(如果有的话)更高优先级的目的。

源码地址:https://gitee.com/dlgcy/Practice/tree/Blog20210823

发行版地址:https://gitee.com/dlgcy/Practice/releases/Blog20210823

WPF

【翻译】WPF 中附加行为的介绍 Introduction to Attached Behaviors in WPF

WPF 使用 Expression Design 画图导出及使用 Path 画图

WPF MVVM 弹框之等待框

解决 WPF 绑定集合后数据变动界面却不更新的问题(使用 ObservableCollection)

WPF 消息框 TextBox 绑定新数据时让光标和滚动条跳到最下面

真・WPF 按钮拖动和调整大小

WPF MVVM 模式下的弹窗

WPF 让一组 Button 实现 RadioButton 的当前样式效果

WPF 原生绑定和命令功能使用指南

WPF 用户控件的自定义依赖属性在 MVVM 模式下的使用备忘

在WPF的MVVM模式中使用OCX组件

WPF ComboBox 使用 ResourceBinding 动态绑定资源键并支持语言切换相关推荐

  1. android 指定语言的资源,Android国际化多语言切换

    最近工作中突然要求要项目进行国际化,之前没遇到过.但是也很简单呀,只需要把添加一个相应语言的的strings.xml的资源文件就好了,不是吗?这样只要切换系统语言就能切换app的文字语言了. 但是由此 ...

  2. .NET资源文件实现多语言切换

    1.创建对应的资源文件 lang.en.resx  英文 lang.resx   中文,默认 lang.zh-tw.resx  繁体 首先说明,这三个文件前面部分名称需要一样,只是 点 后面的语言代号 ...

  3. WPF快速指导1:资源

    WPF快速指导1:资源     本文摘要:     1:资源应用场景:     2:静态资源和动态资源:     3:Application.Current.Resources和Application ...

  4. wpf Combobox 样式的问题

    wpf Combobox 样式的问题 <!-- Combobox--><Style x:Key="ComboBoxReadonlyToggleButton" Ta ...

  5. WPF中的嵌入的资源与Resource

    WPF中的嵌入的资源与Resource 我们将资源文件添加至.net C#工程时,文件的生成操作有多种可选方式.通常用的多的是两种:[嵌入的资源]和[Resource],如果从需要从代码中使用这些资源 ...

  6. 如何让ASP.NET默认的资源编程方式支持非.ResX资源存储

    之前写了两篇文章<.NET资源并不限于.ResX文件>(上篇.下篇),介绍了如何通过自定义ResourceManager的方式来扩展资源的存储形式.在那篇文章中,我定义了三种基于独立文件的 ...

  7. android:SeekBar控制系统音量(媒体音量键和通话音量键的diallog自动切换调节控制)

    前言:之前看了很多关于android的关于音量调节的文章,始终没有自己想要的那种或者没查询到,于是下载了一位网友上传的资源demo上进行了改编和调整,修改出来一个可以在媒体和通话过程中两种场景下自动切 ...

  8. VC2008以资源形式实现多语言版本

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 越来越多 ...

  9. 联想笔记本大小写、数字键、触摸板切换图标不显示的解决方案

    Ctrl+Alt+Del打开任务管理器 右击Lenovo Hotkeys->启动,即设置为开机自启动,大小写.数字键.触摸板切换的图标将正常展示在桌面中央.

最新文章

  1. SpringBoot 操作elasticsearch
  2. mysql修改用户密码
  3. 百度地图 使用两条平行线表示路线
  4. python分词和词频统计
  5. c语言锐龙,AMD官方:锐龙5000抗热可以达到95°C 没问题!
  6. 利用异步I/O复制文件及详解
  7. mysql binlog DDL_MySQL binlog原理及应用
  8. JS实现表单多文件上传样式美化支持选中文件后删除相关项
  9. 用python画图代码-常用激活函数的python画图代码
  10. 推荐系统中传统模型——LightGBM + LR融合
  11. FDDB人脸检测测评数据集介绍
  12. 基于FBX SDK的FBX模型解析与加载 -(二)
  13. “5杯水,怎么分给6个领导?” 答案暴露认知层次...
  14. Transact-SQL编程
  15. 聊聊大学室友在 TikTok 的工作和生活体验
  16. 使用梯度下降求函数的极小值
  17. python音频降噪处理_Python | 简单的扩音,音频去噪,静音剪切
  18. vue uniapp 动态设置页面背景图
  19. mysql 验证用户名重复,Ajax案例——检验用户名是否重复
  20. python基础_面向对象进阶

热门文章

  1. Razor视图引擎浅析
  2. 敏捷开发组织【北京及其他地区QQ群】【长三角QQ群】【珠三角QQ群】
  3. found.000文件夹的问题
  4. 单点登录(sso)入门
  5. shell编程的一些例子4
  6. delphi 函数内创建对象 释放_JavaScript 的函数底层运行机制
  7. assistant字体_如何使用Google Assistant设置和致电家庭联系人
  8. t-mobile频段_T-Mobile再次被黑客入侵:超过200万个帐号和地址可能泄漏
  9. android启用hdcp_如何在Android上启用优先收件箱(和设置仅重要通知)
  10. Kubernetes共享使用Ceph存储