Многоязычный ComboBox, привязанный к описанию перечисления в DataTemplate с помощью DataTemplateSelector

Я работаю над чем-то, где отдельные части были хорошо обсуждены, но мне трудно собрать их все вместе. У нас есть приложение, в котором есть множество плагинов, требующих разных входных параметров, которые я пытаюсь сделать многоязычными. Я работал над динамическим графическим интерфейсом, который проверяет плагин для создания массива входных параметров и использует DataTemplateSelector для выбора правильного элемента управления на основе типа параметра. Для счетчиков мы пытаемся привязать локализованное отображаемое имя к полю со списком. В StackOverflow есть много потоков о том, как выполнять привязку enum / combobox, но я не мог найти ни одного многоязычного и динамического (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>

Here's two example enums I'm testing with:

[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);
    }

Вот расширение разметки источника привязки Enum:

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"/

Я пробовал делать это в коде программной части окна, в селекторе шаблонов данных и даже в настраиваемом элементе управления без особого успеха. Мое первое «настоящее» приложение WPF, так что я, по общему признанию, немного не из моей лиги, собирая все это вместе, а не их отдельные части.

Вот работающий пример


person Bryan Dam    schedule 10.12.2018    source источник
comment
Добро пожаловать в SO. Не совсем понятно, о чем вы здесь спрашиваете, вы говорите, что пытаетесь избежать привязки к перечислению .... почему? В конечном итоге вы так или иначе будете привязаны к этому перечислению, даже если вы не сделаете это через механизм привязки WPF. Есть много-много способов добиться того, что вы пытаетесь сделать, но вам нужно будет немного конкретизировать причины, стоящие за вашими требованиями (что, насколько мы знаем, вполне может не быть проблемой вообще) .   -  person Mark Feldman    schedule 10.12.2018
comment
Справедливо, отредактировал этот последний бит для ясности и добавил в пример второе перечисление. Я хочу привязать, мне просто нужно сделать это динамически. Решение пока работает только в том случае, если я статически привязываю поле со списком к одному типу перечислителя непосредственно в XAML. Я пытаюсь расширить его, чтобы он поддерживал любой перечислитель, который мы ему добавляем. Я подозреваю, что это означает привязку его в коде где-то позади, но это становится сложным (для меня), потому что он находится в шаблоне данных, поэтому сначала вы должны выяснить, к чему контролирует ваша привязка. Тогда теоретически может быть несколько перечислений как часть объекта массива входных параметров.   -  person Bryan Dam    schedule 10.12.2018


Ответы (2)


Одно из возможных "решений" - просто полностью отказаться от использования перечислений для локализации. Это то, что я делаю, это позволяет мне и другим разработчикам проекта вставлять простой английский в наш XAML следующим образом:

<TextBlock Text="{Translate 'Scanning Passport'}" />

Я написал небольшую утилиту для сканирования наших файлов XAML и извлечения всех их экземпляров в электронную таблицу Excel, которая отправляется переводчикам, вторая утилита принимает переводы, которые мы получаем, и упаковывает их в файлы XML (по одному для каждого языка). Эти файлы в основном являются словарями, где английский текст в XAML используется в качестве ключа для поиска перевода на текущем выбранном языке:

<Translation key="Scan Passport" text="扫描护照" />

Это дает ряд преимуществ:

  • Разработчики по-прежнему пишут свой XAML на общем языке (в моем случае на английском).
  • Вам не нужно перестраивать каждый проект в своем решении каждый раз, когда вы добавляете новый текст во внешний интерфейс.
  • Если вы добавляете новый текст, который еще не был переведен, расширение Translate просто возвращается к английскому переводу.
  • Файлы XML хранятся локально, поэтому клиент может свободно изменять текст локализации (включая английский).
  • У вас есть полный контроль над тем, какие поля вашего графического интерфейса проходят перевод, а какие - нет.

И, конечно, если вы измените перевод во время выполнения, все элементы управления, которые переводятся, обновятся и немедленно переключатся на новый язык.

Ключ к этой системе, очевидно, заключается в написании специального расширения разметки Translate, к счастью, кто-то уже сделал это за вас:

https://www.wpftutorial.net/LocalizeMarkupExtension.html

person Mark Feldman    schedule 10.12.2018
comment
Я подумаю. Перечисления - это более сложная часть общей проблемы, над которой мы работаем. Здесь есть компонент сервер / клиент, который раньше обменивался чисто строковыми типами, что затрудняло создание хорошего графического интерфейса на основе отражения. Мы планируем использовать несколько графических интерфейсов и постоянные новые потоки плагинов, поэтому мы пытаемся перейти к реальным типам объектов, чтобы мы могли отражать их и делать более разумные варианты графического интерфейса (например, combo для enum, флажок для bool и т. Д.) Спасибо за обдумывая это вместе со мной, Марк. - person Bryan Dam; 10.12.2018

Хорошо, заняло несколько дней взлома, но я наконец понял это. В вызове ProvideValue MarkupExtensions вы можете получить службу IProvideValueTarget для получения цели. Это позволяет делать две вещи. Во-первых, вы можете проверить, имеет ли цель значение null, и таким образом обойти вызов первоначального запуска и отложить привязку до тех пор, пока не будет применена табличка данных. Во-вторых, после применения шаблона вы можете получить контекст данных объекта, который позволит вам отразиться в нем и, таким образом, избавиться от необходимости объявлять его во время разработки (моя конечная цель).

Вот моя функция ProvideValue класса 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 без указания единственного статического типа:

<ComboBox ItemsSource="{local:EnumBindingSource}"
person Bryan Dam    schedule 12.12.2018