Search code examples
c#wpfenumsmultilingualdatatemplateselector

Multilingual ComboBox Bound to Enum Descriptions In DataTemplate Using DataTemplateSelector


I'm working on something where the individual parts have been well discussed but I'm having trouble putting them all together. We have an app that has lots of plugins that require different input parameters which I'm trying to make multi-lingual. I've been working on a dynamic GUI that inspects the plugin to create an array of input parameters and uses a DataTemplateSelector to pick the correct control based on the parameter's type. For enumerators we're trying to bind a localized display name to a combobox. There's lots of threads on StackOverflow on how to do enum/combobox binding but none that I could find that are multi-lingual and dynamic (datatemplate or other).

Brian Lagunas has a great blog post that almost gets us there: http://brianlagunas.com/localize-enum-descriptions-in-wpf. However, he statically binds the enum in the XAML. We have hundreds of enums and are creating new ones all the time. So I'm trying to get my head around how to best achieve something more dynamic. Somewhere along the line I need to use reflection to figure out the enumerator's type and bind it to the combobox but I can't quite figure out where, when, or how.

I've uploaded an extended example here: https://github.com/bryandam/Combo_Enum_MultiLingual. I'll try and include the relevant pieces here but it's hard to condense it down.

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

Here's my 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
}

Here's the enum type convertor:

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

Here's the Enum Binding Source Markup Extension:

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

Again, my goal is to figure out how to avoid statically binding to a single enum type (like in the XAML below) and instead bind it based on whatever type the input parameter might be:

<ComboBox ItemsSource="{Binding Source={local:EnumBindingSource {x:Type local:Status}}}" Grid.Column="1"/

I've played around with doing it in the Window code-behind, the data template selector, and even a custom control without too much success. My first 'real' WPF app so I'm admittedly a tad out of my league putting all of this together versus their individual parts.

Here's the example running


Solution

  • Ok, took a few days of hacking around but I finally figured this out. In the MarkupExtensions's ProvideValue call you can get the IProvideValueTarget service to get the target. This allows you to do two things. First, you can check to see if the target is null and thus bypass the initial startup call and delay the binding until the datatemplate is applied. Second, once the template is applied you can get the datacontext of the object allowing you to reflect into it and thus eliminate the need to declare it at design time (my ultimate goal).

    Here's my MarkupExtension class' ProvideValue function:

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

    The end result being that I can specify the combobox itemsource without specifying a single, static type:

    <ComboBox ItemsSource="{local:EnumBindingSource}"