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

文章目录

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

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

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

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

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

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

xml 复制代码
<Button Content="{Binding Source={x:Static local:Resources.save}}" />

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

csharp 复制代码
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改为:

xml 复制代码
<Button Content="{Binding Save}" />

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

csharp 复制代码
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中使用:

csharp 复制代码
private readonly LanguageManager _languageManager = LanguageManager.Instance;
public string Save => _languageManager.GetString("save");

加上切换命令:

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

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

csharp 复制代码
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中,依赖属性更适合绑定场景。于是我改用依赖属性:

csharp 复制代码
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更新更可靠。下一步,我把语言切换按钮改成了下拉框:

xml 复制代码
<ComboBox ItemsSource="{Binding Languages}"
          DisplayMemberPath="DisplayName"
          SelectedValuePath="CultureName"
          SelectedValue="{Binding SelectedLanguage, Mode=TwoWay}"/>
csharp 复制代码
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和语言切换逻辑,太繁琐了。我决定把国际化集中化,先创建了一个基类:

csharp 复制代码
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接管了语言管理:

csharp 复制代码
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:

csharp 复制代码
public class PageTemplateViewModel : BaseViewModel { }
public partial class PageTemplate : UserControl
{
    public PageTemplate()
    {
        InitializeComponent();
        DataContext = new PageTemplateViewModel();
    }
}

XAML绑定到全局服务:

xml 复制代码
<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中为每个字符串写属性,于是尝试了动态方案:

csharp 复制代码
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改为:

xml 复制代码
<Button Content="{Binding Strings.save}" />
<TextBlock Text="{Binding Strings.title}" />

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

总结与反思

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

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

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

相关推荐
码观天工7 分钟前
【.NET必读】RabbitMQ 4.0+重大变更!C#开发者必须掌握的6大升级要点
c#·rabbitmq·.net·mq
shengjk18 分钟前
序列化和反序列化:从理论到实践的全方位指南
java·大数据·开发语言·人工智能·后端·ai编程
passionSnail24 分钟前
《用MATLAB玩转游戏开发》推箱子游戏的MATLAB趣味实现
开发语言·游戏·matlab
Once_day1 小时前
C++之fmt库介绍和使用(1)
开发语言·c++·fmt
摆烂且佛系1 小时前
FastByteArrayOutputStream和ByteArrayInputStream有什么区别
java·开发语言
Chandler241 小时前
Go语言:json 作用和语法
开发语言·golang·json
凤年徐1 小时前
【C/C++】自定义类型:结构体
c语言·开发语言·c++·经验分享·笔记·算法
绿龙术士1 小时前
构建现代化WPF应用:数据驱动开发与高级特性解析
c#·wpf
能来帮帮蒟蒻吗1 小时前
Python -将MP4文件转为GIF图片
开发语言·python·学习·视频
忆源1 小时前
【Qt】之音视频编程2:QtAV的使用篇
开发语言·qt·音视频