1

I want to have an Enum on my ViewModel, which in this case represent a set of operations the user can select between. Currently I hard-code each Radio Buttons in the XAML with binding to the respective Enum member, using IValueConverters to get display value and set the selected Enum.

This works fin, however, I would like it to generate and (if possible) group the RadioButtons dynamically for each member in the Enum. Ideally, I would like to have the radio button for an Enum member added and placed in the group (Expander) it belongs, based on the Enum's GroupName.

I have searched and searched for a way to accomplish this, but have as of yet not found anything that fits and is not to complicated for me to understand. But maybe someone out there has some input to help me on the way or an ingenious solution.

Enum:

using System.ComponentModel.DataAnnotations;

namespace CommonLibrary.SystemSetup.Enums
{
    public enum UtilityOperation
    {
        [Display(Name="Reboot", GroupName ="Client Control")]
        ClientReboot,
        [Display(Name = "Shutdown", GroupName = "Client Control")]
        ClientShutdown,
        [Display(Name = "Remote Desktop", GroupName = "Client Control")]
        ClientRDPControl,
    }
}

View:

<UserControl.Resources>
    <views:EnumToBoolConverter x:Key="EnumToBoolConverter" />
    <views:EnumToDisplayNameConverter x:Key="EnumToNameConverter" />
</UserControl.Resources>
    
<Expander Foreground="White" VerticalAlignment="Center" Background="Transparent" Margin="10 10 5 5">
                       
    <!-- Sub-menu header. -->
    <Expander.Header>
        <TextBlock Text="Client Control"/>
    </Expander.Header>
                        
    <StackPanel>
                        
        <RadioButton Margin="10 0 0 10"
        Content="{Binding Source={x:Static enum:UtilityOperation.ClientReboot}, Converter={StaticResource EnumToNameConverter}}"
        IsChecked="{Binding Path=E, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static enum:UtilityOperation.ClientReboot}}" />

        <RadioButton Margin="10 0 0 10"
        Content="{Binding Source={x:Static enum:UtilityOperation.ClientShutdown}, Converter={StaticResource EnumToNameConverter}}"
        IsChecked="{Binding Path=E, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static enum:UtilityOperation.ClientShutdown}}" />

        <RadioButton Margin="10 0 0 10"
        Content="{Binding Source={x:Static enum:UtilityOperation.ClientRDP}, Converter={StaticResource EnumToNameConverter}}"
        IsChecked="{Binding Path=E, Converter={StaticResource EnumToBoolConverter}, ConverterParameter={x:Static enum:UtilityOperation.ClientRDP}}" />
                       
    </StackPanel>

</Expander>

IValueConverters

public class EnumToBoolConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return parameter != null && parameter.Equals(value);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return value != null && value.Equals(true) ? parameter : DependencyProperty.UnsetValue;
    }
}

public class EnumToDisplayNameConverter : MarkupExtension, IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        return ((Enum)value).GetAttributeOfType<DisplayAttribute>().Name;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new NotSupportedException();
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }
}

}

norsemanGrey
  • 345
  • 4
  • 10

1 Answers1

0

Here's an example of how I've done something similar. The basic approach was to re-template a ListBox and use it to dynamically create the radio buttons. The viewmodel contains a list of values to be displayed which are created (one time, on initialization) based on enum values. Those are bound to the ListBox.

This code is cut out of a real app, but you may have to tweak it to compile/run without errors.

Some of this was of course based on things I learned on SO, as noted.

First - the class to represent each radio button entry:

    public class ListItem
    {
        public string Desc { get; set; }
        public EnumType ID { get; set; }
        public int SortOrder { get; set; }
    }

Custom attribute we use to attach data to enums:

[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)]
public class EnumMetadataAttribute : Attribute
{
    public string Description { get; set; }

    public int SortOrder { get; set; }

    public EnumMetadataAttribute(string desc)
    {
        Description = desc;
        SortOrder = 0;
    }

    public EnumMetadataAttribute(string desc, int sortOrder)
    {
        Description = desc;
        SortOrder = sortOrder;
    }
}

Code used to read the attribute values:

// based on https://stackoverflow.com/a/19621488/3195477
public static class EnumerationExtensions
{
    // This extension method is broken out so you can use a similar pattern with
    // other MetaData elements in the future. This is your base method for each.
    static T GetAttribute<T>(this Enum value) where T : Attribute
    {
        var type = value.GetType();
        var memberInfo = type.GetMember(value.ToString());
        var attributes = memberInfo[0].GetCustomAttributes(typeof(T), false);
        return attributes.Length > 0
          ? (T)attributes[0]
          : null;
    }

    static EnumMetadataAttribute GetEnumMetadataAttribute(this Enum value)
    {
        var attr = value.GetAttribute<EnumMetadataAttribute>();
        return attr ?? new EnumMetadataAttribute(value.ToString(), 0);
    }

    public static string GetDescription(this Enum value) => value.GetEnumMetadataAttribute().Description;

    public static int GetSortOrder(this Enum value) => value.GetEnumMetadataAttribute().SortOrder;
}

Its definitely arguable that it would be better to use a static list of objects to store this kind of data instead of attributed enums; but this does work well and had the advantage of being something we could bolt onto existing enums.

Sample enum:

public enum EnumName
{
    [EnumMetadata("None", 1)]
    None,

    [EnumMetadata("Option2", 2)]
    Option2,

    [EnumMetadata("Option3", 3)]
    Option3,
}

Code to automatically loop through all enum values:

    void InitRadioButtonList()
    {
        foreach (EnumType enumval in Enum.GetValues(typeof(EnumName)))
        {
            RadioButtonList.Add(new ListItem
            {
                ID = enumval,
                Item = enumval.GetDescription(),
                SortOrder = enumval.GetSortOrder(),
            });
        }
    }

Inside the viewmodel class:

   public List<ListItem> RadioButtonList { get; } = new List<ListItem>();
   public List<ListItem> SortedRadioButtonList { get { return RadioButtonList.OrderBy(x => x.SortOrder); } }

Here's the XAML which is bound to the list created above:

                    <WrapPanel>
                        <Label>Field Name:</Label>

                        <ListBox
                            ItemsSource="{Binding SortedRadioButtonList}"
                            SelectedValuePath="ID"
                            SelectedValue="{Binding SortedRadioButtonList}"
                            Style="{StaticResource RadioButtonListBoxStyle}"
                            >
                            <ListBox.ItemTemplate>
                                <DataTemplate>
                                    <TextBlock Text="{Binding Desc}"/>
                                </DataTemplate>
                            </ListBox.ItemTemplate>

                        </ListBox>
                    </WrapPanel>

Finally the ListBox style:

<!--  https://stackoverflow.com/a/28571411/3195477  -->
<!--  https://stackoverflow.com/a/7487670/3195477  -->
<Style
    x:Key="RadioButtonListBoxStyle"
    TargetType="{x:Type ListBox}"
    >
    <Setter
        Property="BorderBrush"
        Value="Transparent"
        />
    <Setter
        Property="Background"
        Value="Transparent"
        />
    <Setter
        Property="KeyboardNavigation.DirectionalNavigation"
        Value="Cycle"
        />
    <Setter Property="ItemContainerStyle">
        <Setter.Value>
            <Style TargetType="{x:Type ListBoxItem}">
                <Setter
                    Property="Margin"
                    Value="2,2,2,0"
                    />
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate>
                            <Border Background="Transparent">
                                <RadioButton
                                    Content="{TemplateBinding ContentPresenter.Content}"
                                    ContentTemplate="{TemplateBinding ContentPresenter.ContentTemplate}"
                                    ContentTemplateSelector="{TemplateBinding ContentPresenter.ContentTemplateSelector}"
                                    IsChecked="{Binding Path=IsSelected, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
                                    />
                            </Border>
                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </Setter.Value>
    </Setter>
</Style>

Update - I recently read https://brianlagunas.com/a-better-way-to-data-bind-enums-in-wpf/ which has some similarities & differences, worth reading also.

StayOnTarget
  • 11,743
  • 10
  • 52
  • 81