使用 DataTemplateSelector 绑定到 DataTemplate 中的枚举描述的多语言 ComboBox
Multilingual ComboBox Bound to Enum Descriptions In DataTemplate Using DataTemplateSelector
我正在做一些事情,其中各个部分都得到了很好的讨论,但我无法将它们放在一起。我们有一个应用程序,其中有很多插件需要不同的输入参数,我正在尝试制作多语言。我一直在研究一个动态 GUI,它检查插件以创建输入参数数组,并使用 DataTemplateSelector 根据参数类型选择正确的控件。对于枚举器,我们试图将本地化显示名称绑定到组合框。 Whosebug 上有很多关于如何进行 enum/combobox 绑定的线程,但我发现 none 是多语言和动态的(数据模板或其他)。
Brian Lagunas 有一个很棒的博客 post,几乎让我们到达那里:http://brianlagunas.com/localize-enum-descriptions-in-wpf。但是,他静态绑定了 XAML 中的枚举。我们有数百个枚举,并且一直在创建新的枚举。所以我正在努力思考如何最好地实现更有活力的东西。沿线的某个地方,我需要使用反射来确定枚举器的类型并将其绑定到组合框,但我不太清楚在哪里、何时或如何。
我在此处上传了一个扩展示例:https://github.com/bryandam/Combo_Enum_MultiLingual。我会尝试在此处包含相关内容,但很难将其压缩。
public partial class MainWindow : Window
{
public ObservableCollection<Object> InputParameterList { get; set; } = new ObservableCollection<Object>();
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
//Create an example input object.
InputParameter bitlocker_drive = new InputParameter();
bitlocker_drive.Name = "BitLocker Enabled";
bitlocker_drive.Type = typeof(String);
InputParameterList.Add(bitlocker_drive);
InputParameter bitlocker_status = new InputParameter();
bitlocker_status.Name = "Status";
bitlocker_status.Type = typeof(Status);
InputParameterList.Add(bitlocker_status);
InputParameter bitlocker_foo = new InputParameter();
bitlocker_foo.Name = "Foo";
bitlocker_foo.Type = typeof(Foo);
InputParameterList.Add(bitlocker_foo);
}
}
这是我的 XAML:
<Window x:Class="BindingEnums.MainWindow"
....
<Window.Resources>
...
<DataTemplate x:Key="ComboBox">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name, Mode=TwoWay}" />
<ComboBox ItemsSource="{Binding Source={local:EnumBindingSource {x:Type local:Status}}}" Grid.Column="1"/>
</Grid>
</DataTemplate>
...
<local:InputParameterTemplateSelector x:Key="InputDataTemplateSelector" Checkbox="{StaticResource Checkbox}" ComboBox="{StaticResource ComboBox}" DatePicker="{StaticResource DatePicker}" TextBox="{StaticResource TextBox}"/>
</Window.Resources>
<Grid>
<ListBox Name="InputParameters" KeyboardNavigation.TabNavigation="Continue" HorizontalContentAlignment="Stretch" ItemsSource="{Binding InputParameterList}" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Auto" Background="Transparent" BorderBrush="Transparent" ItemTemplateSelector="{StaticResource InputDataTemplateSelector}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsTabStop" Value="False" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Grid>
这是我正在测试的两个示例枚举:
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
public enum Status
{
[Display(Name = nameof(Resources.EnumResources.Good), ResourceType = typeof(Resources.EnumResources))]
Good,
[Display(Name = nameof(Resources.EnumResources.Better), ResourceType = typeof(Resources.EnumResources))]
Better,
Best
}
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
public enum Foo
{
[Display(Name = nameof(Resources.EnumResources.Foo), ResourceType = typeof(Resources.EnumResources))]
Foo,
[Display(Name = nameof(Resources.EnumResources.Bar), ResourceType = typeof(Resources.EnumResources))]
Bar
}
这是枚举类型转换器:
public class EnumDescriptionTypeConverter : EnumConverter
{
public EnumDescriptionTypeConverter(Type type)
: base(type)
{}
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
{
if (destinationType == typeof(string))
{
if (value != null)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
if (fi != null)
{
//Reflect into the value's type to get the display attributes.
FieldInfo fieldInfo = value.GetType().GetField(value.ToString());
DisplayAttribute displayAttribute = fieldInfo?
.GetCustomAttributes(false)
.OfType<DisplayAttribute>()
.SingleOrDefault();
if (displayAttribute == null)
{
return value.ToString();
}
else
{
//Look up the localized string.
ResourceManager resourceManager = new ResourceManager(displayAttribute.ResourceType);
string name = resourceManager.GetString(displayAttribute.Name);
return string.IsNullOrWhiteSpace(name) ? displayAttribute.Name : name;
}
}
}
return string.Empty;
}
return base.ConvertTo(context, culture, value, destinationType);
}
这是枚举绑定源标记扩展:
public class EnumBindingSourceExtension : MarkupExtension
{
...
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (null == this._enumType)
throw new InvalidOperationException("The EnumType must be specified.");
Type actualEnumType = Nullable.GetUnderlyingType(this._enumType) ?? this._enumType;
Array enumValues = Enum.GetValues(actualEnumType);
if (actualEnumType == this._enumType)
return enumValues;
Array tempArray = Array.CreateInstance(actualEnumType, enumValues.Length + 1);
enumValues.CopyTo(tempArray, 1);
return tempArray;
}
}
同样,我的目标是弄清楚如何避免静态绑定到单个枚举类型(如下面的 XAML),而是根据输入参数可能是什么类型来绑定它:
<ComboBox ItemsSource="{Binding Source={local:EnumBindingSource {x:Type local:Status}}}" Grid.Column="1"/
我已经在 Window 代码隐藏、数据模板选择器甚至自定义控件中尝试过这样做,但没有取得太大成功。我的第一个 'real' WPF 应用程序,所以我承认我有点不擅长将所有这些放在一起而不是它们各自的部分。
Here's the example running
一个可能的 "solution" 是完全放弃使用枚举进行本地化。这就是我所做的,它允许我自己和项目中的其他开发人员将简单的英语插入我们的 XAML 中,如下所示:
<TextBlock Text="{Translate 'Scanning Passport'}" />
我写了一个小实用程序来扫描我们的 XAML 文件并将这些文件的所有实例提取到一个 Excel 电子表格中,该电子表格被发送给翻译人员,第二个实用程序获取我们返回的翻译并将它们打包成 XML 个文件(每种语言一个)。这些文件基本上是字典,其中 XAML 中的英文文本作为查找当前所选语言的翻译的关键:
<Translation key="Scan Passport" text="扫描护照" />
这有很多好处:
- 开发人员仍然 XAML 使用通用语言(我的情况是英语)。
- 您不必在每次向前端添加新文本时都重建解决方案中的每个项目。
- 如果您添加尚未翻译的新文本,那么
“翻译”扩展只是退回到英语
翻译.
- XML文件存储在本地,所以客户可以随意更改本地化文本(包括英文)。
- 您可以完全控制 GUI 中的哪些字段进行翻译,哪些不进行翻译。
当然,如果您在 运行 时更改了翻译,那么正在翻译的控件都会自行更新并立即切换到新语言。
显然,这个系统的关键是编写 'Translate' 自定义标记扩展,幸运的是有人已经为您完成了:
好吧,花了几天时间,但我终于弄明白了。在 MarkupExtensions 的 ProvideValue 调用中,您可以获取 IProvideValueTarget 服务来获取目标。这允许你做两件事。首先,您可以检查目标是否为空,从而绕过初始启动调用并延迟绑定,直到应用数据模板。其次,应用模板后,您可以获得对象的数据上下文,允许您反映到其中,从而消除在设计时声明它的需要(我的最终目标)。
这是我的 MarkupExtension class' ProvideValue 函数:
public override object ProvideValue(IServiceProvider serviceProvider)
{
//Get the target control
var pvt = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
if (pvt == null) { return null; }
var target = pvt.TargetObject as FrameworkElement;
//If null then return the class to bind at runtime.
if (target == null) { return this; }
if (target.DataContext.GetType().IsEnum)
{
Array enumValues = Enum.GetValues(target.DataContext.GetType());
return enumValues;
}
return null;
}
最终结果是我可以在不指定单个静态类型的情况下指定组合框项目源:
<ComboBox ItemsSource="{local:EnumBindingSource}"
我正在做一些事情,其中各个部分都得到了很好的讨论,但我无法将它们放在一起。我们有一个应用程序,其中有很多插件需要不同的输入参数,我正在尝试制作多语言。我一直在研究一个动态 GUI,它检查插件以创建输入参数数组,并使用 DataTemplateSelector 根据参数类型选择正确的控件。对于枚举器,我们试图将本地化显示名称绑定到组合框。 Whosebug 上有很多关于如何进行 enum/combobox 绑定的线程,但我发现 none 是多语言和动态的(数据模板或其他)。
Brian Lagunas 有一个很棒的博客 post,几乎让我们到达那里:http://brianlagunas.com/localize-enum-descriptions-in-wpf。但是,他静态绑定了 XAML 中的枚举。我们有数百个枚举,并且一直在创建新的枚举。所以我正在努力思考如何最好地实现更有活力的东西。沿线的某个地方,我需要使用反射来确定枚举器的类型并将其绑定到组合框,但我不太清楚在哪里、何时或如何。
我在此处上传了一个扩展示例:https://github.com/bryandam/Combo_Enum_MultiLingual。我会尝试在此处包含相关内容,但很难将其压缩。
public partial class MainWindow : Window
{
public ObservableCollection<Object> InputParameterList { get; set; } = new ObservableCollection<Object>();
public MainWindow()
{
InitializeComponent();
this.DataContext = this;
//Create an example input object.
InputParameter bitlocker_drive = new InputParameter();
bitlocker_drive.Name = "BitLocker Enabled";
bitlocker_drive.Type = typeof(String);
InputParameterList.Add(bitlocker_drive);
InputParameter bitlocker_status = new InputParameter();
bitlocker_status.Name = "Status";
bitlocker_status.Type = typeof(Status);
InputParameterList.Add(bitlocker_status);
InputParameter bitlocker_foo = new InputParameter();
bitlocker_foo.Name = "Foo";
bitlocker_foo.Type = typeof(Foo);
InputParameterList.Add(bitlocker_foo);
}
}
这是我的 XAML:
<Window x:Class="BindingEnums.MainWindow"
....
<Window.Resources>
...
<DataTemplate x:Key="ComboBox">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name, Mode=TwoWay}" />
<ComboBox ItemsSource="{Binding Source={local:EnumBindingSource {x:Type local:Status}}}" Grid.Column="1"/>
</Grid>
</DataTemplate>
...
<local:InputParameterTemplateSelector x:Key="InputDataTemplateSelector" Checkbox="{StaticResource Checkbox}" ComboBox="{StaticResource ComboBox}" DatePicker="{StaticResource DatePicker}" TextBox="{StaticResource TextBox}"/>
</Window.Resources>
<Grid>
<ListBox Name="InputParameters" KeyboardNavigation.TabNavigation="Continue" HorizontalContentAlignment="Stretch" ItemsSource="{Binding InputParameterList}" ScrollViewer.HorizontalScrollBarVisibility="Disabled" ScrollViewer.VerticalScrollBarVisibility="Auto" Background="Transparent" BorderBrush="Transparent" ItemTemplateSelector="{StaticResource InputDataTemplateSelector}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="IsTabStop" Value="False" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
</Style>
</ListBox.ItemContainerStyle>
</ListBox>
</Grid>
这是我正在测试的两个示例枚举:
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
public enum Status
{
[Display(Name = nameof(Resources.EnumResources.Good), ResourceType = typeof(Resources.EnumResources))]
Good,
[Display(Name = nameof(Resources.EnumResources.Better), ResourceType = typeof(Resources.EnumResources))]
Better,
Best
}
[TypeConverter(typeof(EnumDescriptionTypeConverter))]
public enum Foo
{
[Display(Name = nameof(Resources.EnumResources.Foo), ResourceType = typeof(Resources.EnumResources))]
Foo,
[Display(Name = nameof(Resources.EnumResources.Bar), ResourceType = typeof(Resources.EnumResources))]
Bar
}
这是枚举类型转换器:
public class EnumDescriptionTypeConverter : EnumConverter
{
public EnumDescriptionTypeConverter(Type type)
: base(type)
{}
public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
{
if (destinationType == typeof(string))
{
if (value != null)
{
FieldInfo fi = value.GetType().GetField(value.ToString());
if (fi != null)
{
//Reflect into the value's type to get the display attributes.
FieldInfo fieldInfo = value.GetType().GetField(value.ToString());
DisplayAttribute displayAttribute = fieldInfo?
.GetCustomAttributes(false)
.OfType<DisplayAttribute>()
.SingleOrDefault();
if (displayAttribute == null)
{
return value.ToString();
}
else
{
//Look up the localized string.
ResourceManager resourceManager = new ResourceManager(displayAttribute.ResourceType);
string name = resourceManager.GetString(displayAttribute.Name);
return string.IsNullOrWhiteSpace(name) ? displayAttribute.Name : name;
}
}
}
return string.Empty;
}
return base.ConvertTo(context, culture, value, destinationType);
}
这是枚举绑定源标记扩展:
public class EnumBindingSourceExtension : MarkupExtension
{
...
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (null == this._enumType)
throw new InvalidOperationException("The EnumType must be specified.");
Type actualEnumType = Nullable.GetUnderlyingType(this._enumType) ?? this._enumType;
Array enumValues = Enum.GetValues(actualEnumType);
if (actualEnumType == this._enumType)
return enumValues;
Array tempArray = Array.CreateInstance(actualEnumType, enumValues.Length + 1);
enumValues.CopyTo(tempArray, 1);
return tempArray;
}
}
同样,我的目标是弄清楚如何避免静态绑定到单个枚举类型(如下面的 XAML),而是根据输入参数可能是什么类型来绑定它:
<ComboBox ItemsSource="{Binding Source={local:EnumBindingSource {x:Type local:Status}}}" Grid.Column="1"/
我已经在 Window 代码隐藏、数据模板选择器甚至自定义控件中尝试过这样做,但没有取得太大成功。我的第一个 'real' WPF 应用程序,所以我承认我有点不擅长将所有这些放在一起而不是它们各自的部分。
Here's the example running
一个可能的 "solution" 是完全放弃使用枚举进行本地化。这就是我所做的,它允许我自己和项目中的其他开发人员将简单的英语插入我们的 XAML 中,如下所示:
<TextBlock Text="{Translate 'Scanning Passport'}" />
我写了一个小实用程序来扫描我们的 XAML 文件并将这些文件的所有实例提取到一个 Excel 电子表格中,该电子表格被发送给翻译人员,第二个实用程序获取我们返回的翻译并将它们打包成 XML 个文件(每种语言一个)。这些文件基本上是字典,其中 XAML 中的英文文本作为查找当前所选语言的翻译的关键:
<Translation key="Scan Passport" text="扫描护照" />
这有很多好处:
- 开发人员仍然 XAML 使用通用语言(我的情况是英语)。
- 您不必在每次向前端添加新文本时都重建解决方案中的每个项目。
- 如果您添加尚未翻译的新文本,那么 “翻译”扩展只是退回到英语 翻译.
- XML文件存储在本地,所以客户可以随意更改本地化文本(包括英文)。
- 您可以完全控制 GUI 中的哪些字段进行翻译,哪些不进行翻译。
当然,如果您在 运行 时更改了翻译,那么正在翻译的控件都会自行更新并立即切换到新语言。
显然,这个系统的关键是编写 'Translate' 自定义标记扩展,幸运的是有人已经为您完成了:
好吧,花了几天时间,但我终于弄明白了。在 MarkupExtensions 的 ProvideValue 调用中,您可以获取 IProvideValueTarget 服务来获取目标。这允许你做两件事。首先,您可以检查目标是否为空,从而绕过初始启动调用并延迟绑定,直到应用数据模板。其次,应用模板后,您可以获得对象的数据上下文,允许您反映到其中,从而消除在设计时声明它的需要(我的最终目标)。
这是我的 MarkupExtension class' ProvideValue 函数:
public override object ProvideValue(IServiceProvider serviceProvider)
{
//Get the target control
var pvt = (IProvideValueTarget)serviceProvider.GetService(typeof(IProvideValueTarget));
if (pvt == null) { return null; }
var target = pvt.TargetObject as FrameworkElement;
//If null then return the class to bind at runtime.
if (target == null) { return this; }
if (target.DataContext.GetType().IsEnum)
{
Array enumValues = Enum.GetValues(target.DataContext.GetType());
return enumValues;
}
return null;
}
最终结果是我可以在不指定单个静态类型的情况下指定组合框项目源:
<ComboBox ItemsSource="{local:EnumBindingSource}"