Многоязычный ComboBox, связанный с перечислением описаний в DataTemplate с использованием DataTemplateSelector
Я работаю над тем, что отдельные части были хорошо обсуждены, но у меня проблемы с их объединением. У нас есть приложение, которое имеет множество плагинов, требующих различных входных параметров, которые я пытаюсь сделать многоязычными. Я работал над динамическим графическим интерфейсом, который проверяет плагин для создания массива входных параметров и использует DataTemplateSelector, чтобы выбрать правильный элемент управления в зависимости от типа параметра. Для перечислителей мы пытаемся связать локализованное отображаемое имя со списком. В Stackru много потоков о том, как выполнять привязку перечислений / списков, но я не нашел многоязычных и динамических (datatemplate или других).
У Брайана Лагунаса есть отличная запись в блоге, которая почти привела нас туда: 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
}
Вот преобразователь типа enum:
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);
}
Вот расширение Enum Binding Source Markup:
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, селекторе шаблонов данных и даже в пользовательском элементе управления без особого успеха. Мое первое "настоящее" приложение WPF, так что я по общему признанию немного вне моей лиги, соединяющего все это вместе с их отдельными частями.
2 ответа
Хорошо, заняло несколько дней, но я наконец-то понял это. В вызове ProvideValue MarkupExtensions вы можете получить сервис IProvideValueTarget для получения цели. Это позволяет вам сделать две вещи. Во-первых, вы можете проверить, является ли цель нулевой, и, таким образом, обойти начальный вызов запуска и отложить привязку до применения таблицы данных. Во-вторых, после применения шаблона вы можете получить datacontext объекта, который позволит вам отразить его и тем самым избавить от необходимости объявлять его во время разработки (моя конечная цель).
Вот мой класс ProvisionValue класса MarkupExtension:
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}"
Одним из возможных "решений" этого является полное исключение использования перечислений для локализации. Это то, что я делаю, это позволяет мне и другим разработчикам проекта вставлять простой английский в наш XAML следующим образом:
<TextBlock Text="{Translate 'Scanning Passport'}" />
Я написал небольшую утилиту для сканирования наших файлов XAML и извлечения всех их экземпляров в электронную таблицу Excel, которая отправляется переводчикам, а вторая утилита берет переводы, которые мы получаем, и упаковывает их в файлы XML (по одному на язык). Эти файлы в основном являются словарями, где английский текст в XAML используется в качестве ключа для поиска перевода на текущем выбранном языке:
<Translation key="Scan Passport" text="扫描护照" />
Это имеет ряд преимуществ:
- Разработчики по-прежнему пишут свои XAML на общем языке (в моем случае это английский).
- Вам не нужно перестраивать каждый проект в своем решении каждый раз, когда вы добавляете новый текст в ваш интерфейс.
- Если вы добавляете новый текст, который еще не был переведен, то расширение `Translate'просто возвращается к английскому переводу.
- XML-файлы хранятся локально, поэтому клиент может изменять текст локализации по своему усмотрению (включая английский).
- Вы полностью контролируете, какие поля в вашем графическом интерфейсе проходят перевод, а какие нет.
И, конечно, если вы измените перевод во время выполнения, то все элементы управления, которые переводятся, обновляются и немедленно переключаются на новый язык.
Очевидно, что ключом к этой системе является написание расширения разметки "Translate", к счастью, кто-то уже сделал это для вас: