WPF ComboBox 使用 ResourceBinding 动态绑定资源键并支持语言切换
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 动态绑定资源键并支持语言切换的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 如何洗牌 ListT 中的元素?
- 下一篇: 如何更改 C# Record 构造函数的