这里是绑定到UI的CommonViewModel属性,UILanguageDefn类给出了数据的定义:
/// <summary>
/// Gets or sets the language definition used by the entire interface.
/// </summary>
/// <value>The language definition.</value>
public UILanguageDefn LanguageDefn
{
get { return _languageDefn; }
set
{
if (_languageDefn != value)
{
_languageDefn = value;
OnPropertyChanged("LanguageDefn");
OnPropertyChanged("HeadingFontSize");
OnPropertyChanged("MinFontSize");
OnPropertyChanged("IsRightToLeft");
}
}
}
public double HeadingFontSize
{
get
{
if (_languageDefn != null)
return (double)_languageDefn.HeadingFontSize;
return (double)UILanguageDefn.DefaultHeadingFontSize;
}
}
public double MinFontSize
{
get
{
if (_languageDefn != null)
return (double)_languageDefn.MinFontSize;
return (double)UILanguageDefn.DefaultMinFontSize;
}
}
public bool IsRightToLeft
{
get
{
if (_languageDefn != null)
return _languageDefn.IsRightToLeft;
return false;
}
}
MainWindowViewModel处在ViewModel架构最前端, 负责在MainWindowModel值发生变化时,更新CommonViewModel中的当前语言:
/// <summary>
/// Refreshes the UI text to display in the current language.
/// </summary>
public void RefreshUILanguage()
{
_model.UpdateLanguageData();
CommonViewModel.Current.LanguageDefn = _model.CurrentLanguage;
//Notify any other internal logic to prompt a refresh (as necessary)
if (LanguageChanged != null)
LanguageChanged(this, new EventArgs());
}
视图
正如我所提到的,本地化文本通过数据绑定显示到视图中。然而WPF自身并不知道如何处理UILanguageDefn类,更不用说提取合适的本地化文本值。这也是最后一个难题。
值转换器
请记住,CommonViewModel.Current.LanguageDefn是一个UILaunguageDefn,不是TextBlock的Text属性期待的一个字符串。因此,此时需要一个值转换器来完成这项转换工作。这个值转换器使用ConverterParameter来指定创建查找关键字,用来恢复来自UILanguage实例中局部符合条件的文本。记住,当接口改变了,UILanuageDefn也改变。
这项工作的优点在于对每一段局限在接口当中的文本,符合条件的元素需要被添加到language XML文件,确保ConverterParameter和元素名称匹配。此外不需要定义任何额外的属性——不管是在视图层,UILanguageDefn,还是在模型层的其他部分。
这个converter相对简单. 只需在类级别上指定 IValueConverter (在System.Windows.Data中)的 ValueConversion 属性:
[ValueConversion(typeof(UILanguageDefn), typeof(string))]
并且实现类似如下的函数 Convert :
public object Convert(object value, Type targetType,
object parameter, CultureInfo culture)
{
string key = parameter as string;
UILanguageDefn defn = value as UILanguageDefn;
if (defn == null || key == null) return "";
return defn.GetTextValue(key);
}
绑定
现在我们获得了了一个 value converter, 我们可以将它放置在一个 Binding 表达式中:
<TextBlock Text="{Binding Path=LanguageDefn,
Converter={StaticResource UIText}, ConverterParameter=ApplyLabel,
Source={x:Static vm:CommonViewModel.Current}}" />
如果想要它工作, 这个 XML 的 命名空间必须设置为 vm(指向 ViewModel的命名空间),并且 UIText 的资源需要被定义 (假设conv 是这个 value converter 的 XML 的命名空间):
zlt;conv:UITextLookupConverter x:Key="UIText" />
简单明了——自定义标记扩展
如果你当前的状态(像我一样)又想要愉快的方式,在大多数的XAML文件中的长绑定表达式里,你发现它变得乏味,是同一样东西的重复。甚至不考虑重命名类或者把属性作为重构的一部分!
当然,有一种方式能使其更简洁,考虑到这些绑定之间的唯一变化就是ConverterParameter。解决方案是使用使用自定义标记扩展。
为了做到这一点,自定义标记扩展是一个简单的类,它派生自MarkupExtension(在System.Windows.Markup),按照惯例被命名为[name]Extension。在其核心处,关键点是需要重载ProvideValue方法。但是这该怎么做呢?
自定义标记拓展的重点就是在XAML中写下类似这样的代码:
因此,自定义拓展被称作LocalisedTextExtension,并添加一个Key,它的类型是public string.因为在后台中,绑定一直处于使用状态,所以我创建了一个private 绑定域,并从构造器中实例化它 :
public LocalisedTextExtension()
{
_lookupBinding = UITextLookupConverter.CreateBinding("");
}
而静态的CreateBinding方法定义在值转换器(value converter)中:
public static Binding CreateBinding(string key)
{
Binding languageBinding = new Binding("LanguageDefn")
{
Source = CommonViewModel.Current,
Converter = _sharedConverter,
ConverterParameter = key,
};
return languageBinding;
}
所以定义好了Binding后,可以通过ConverterParameter参数来获取和设置Key属性的值。这也使得ProvideValue方法可以大展身手:
public override object ProvideValue(IServiceProvider serviceProvider)
{
return _lookupBinding.ProvideValue(serviceProvider);
}
而一个Binding是一个MarkupExtension,所以它有自己的可以调用的ProvideValue方法。
Rinse and Repeat - 字体大小与流方向
某些语言的字符集包含十分复杂的图形元素,以致在拉丁文可以辨认的字符大小,用来显示这些语言的时候,变得模糊不清了。你注意到CommonViewModel提供了HeadingFontSize和MinFontSize属性。这就为本地化标题和剩余的本地化文本相应地提供了字体大小。例如日文的字体大小就大于英文。
幸运的是,使用类似下面的这个模式就可以把上述的文字尺寸绑定到共享的样式中,而不需要值转换器:
<Style TargetType="{x:Type TextBlock}">
<Setter Property="FontSize" Value="{Binding Path=MinFontSize,
Source={x:Static vm:CommonViewModel.Current}}" />
<!-- Remaining setters ... -->
</Style>
下图显示的是两个同样界面不同语言下的差异:
也有一些语言是从右向左读的,例如阿拉伯语和希伯来语。为了让UI正确的定位到这些语言,反转接口是有意义的,否则会带来一些混淆,如果在使用程序的时候读取的顺序和逻辑的顺序不一致。
幸运的是,WPF有一个方便的属性可以完成反转整个UI的艰苦工作:
FrameworkElement.FlowDirection
是什么让这个功能相当强大,我只需要绑定一个包含在主窗口内的根级别控件,因为这个值是由它下面的每个FrameworkElement的在视觉层次继承。绑定仅仅需要查看CommonViewModel的IsRightToLeft属性,转换到(通过其他的值转换器)FlowDirection的枚举值。自定义的标记扩展被创建,遵循以前类似的模板,简化为XAML:
<Window x:Class="RePaver.UI.MainWindow" ... >
<DockPanel FlowDirection="{ext:LocalisedFlowDirection}">
<!-- Contents -->
</DockPanel>
</Window>
鉴于到上述功能的强大,这里仍然要考虑一些陷阱和要点:
- 自定义面板自动反转布局,所以你不需要创建一个IsRevered属性(或者类似的)或者按照你的估算调整ArrangeOverride。
- 位图和形状(如线路)是反转的。如果您想要保留这些,呈现独立的流向(如公司的logo或者商标),那么你需要重写FlowDirection,设置它为LeftToRight。
- 如果接口有RightToLeft的FlowDirection,而元素(如Image)具有LeftToRight的FlowDirection,那么元素的Margin会以RightToLeft的方式展示。由于Padding展示在元素内部可视层次,所以一个padding将会以LeftToRight的方式展示。
- TextBoxes包含语言恒定的数据,应当将FlowDirection设置为LeftToRight。理想情况下,此属性应设置为尽量减少重复并保证一致性的风格。
所以,下面就是赶时髦的“处理后”的截图:
注意路径,旋转选择控件,输入输出文本框是以从左至右的方式展示,这与语言无关。这是因为这些元素是特定的问题区域,如果它们以从右至左的方式展示,就没有道理了(可能会引起误解)。
总结
现在明白了——一个局部的WPF应用程序可以在运行时动态地改变UI。第一次运行它是在法语的本地计算机环境中,瞧, il est affiché en Français. 它们都来自同一种语言版本。
最后一个要点需要注意,这里不做详细介绍,整个UI布局以流体方式布局,这样的布局会自动调整以适应内容。 而不是显式地设置宽度和高度, 网格的行/列定义,等等。这些都是“自动”为左的,同时还可以定义最小和最大值。这是很普通的实例中极佳的一个(而不是特定的本地化), 但当切换语言的时候,不允许这样的实例真的显示出来。
后记
软件开发中本地化是一个热门的话题,理所当然,我也不是唯一一个写这方面的人。事实上,我也发现了一些人在做同样的事:
- Sebastian Przybylski (article) 也把UI文本存储在XML文件作为嵌入资源,而把XAML直接绑定到XML资源上而不是通过ViewModel.
- David Sleeckx (article) 使用自定义标记拓展来检索本地缓存的翻译文本,或者调用Google语言API来实现实时翻译。
- 'SeriousM' 在CodePlex上更新了 WPF本地化拓展 . 它是通过提取资源文件/资源程序集中的本地化文本(或其他值)来实现的。
显然,实现WPF程序的本地化有很多种选择,它们并不互斥。根据你的权衡,我所提到的实现方法仅适用于你程序的部分,另一部分则会出现在其他的地方。所以你要根据你的需求,随意调整实现方法。