Связывание ComboBoxes с перечислением... в Silverlight!

Итак, в Интернете и StackOverflow есть много приятных ответов о том, как привязать combobox к свойству enum в WPF. Но в Silverlight отсутствуют все возможности, которые делают это возможным:( Например:

  • Вы не можете использовать общий EnumDisplayer-style IValueConverter, который принимает параметр типа, поскольку Silverlight не поддерживает x:Type.
  • Вы не можете использовать ObjectDataProvider, как в этот подход, поскольку он не существует в Silverlight.
  • Вы не можете использовать собственное расширение разметки, как в комментариях к ссылке из # 2, так как расширения разметки не существуют в Silverlight.
  • Вы не можете выполнить версию # 1 с использованием генериков вместо свойств Type объекта, поскольку generics не поддерживаются в XAML (а хаки, чтобы заставить их работать, все зависит от расширений разметки, не поддерживаемых в Silverlight).

Массивный сбой!

Как я вижу, единственный способ сделать эту работу - либо

  • Обманите и привяжите свойство string в моей модели ViewModel, чей сеттер/получатель выполняет преобразование, загружая значения в ComboBox с использованием кода в представлении.
  • Создайте пользовательский IValueConverter для каждого перечисления, к которому я хочу привязать.

Существуют ли какие-либо альтернативы, которые являются более универсальными, т.е. не требуют много раз писать один и тот же код для каждого перечисления, которое я хочу? Я полагаю, я мог бы сделать решение № 2, используя общий класс, принимающий enum как параметр типа, а затем создайте новые классы для каждого перечисления, которое я хочу, просто

class MyEnumConverter : GenericEnumConverter<MyEnum> {}

Каковы ваши мысли, ребята?

Ответ 1

Аг, я говорил слишком рано! Существует отлично подходящее решение, по крайней мере, в Silverlight 3. (Это может быть только в 3, так как этот поток указывает, что ошибка, связанная с этим материалом, была исправлена ​​в Silverlight 3.)

В принципе, для свойства ItemsSource необходим один конвертер, но он может быть полностью общим без использования каких-либо запрещенных методов, если вы передадите ему имя свойства, тип которого MyEnum. И привязка данных к SelectedItem абсолютно безболезненна; конвертер не требуется! Ну, по крайней мере, до тех пор, пока вам не нужны пользовательские строки для каждого значения перечисления через, например, DescriptionAttribute, hmm..., вероятно, понадобится другой конвертер для этого; надеюсь, я могу сделать это родовым.

Обновление: Я сделал конвертер, и он работает! Я должен привязываться к SelectedIndex сейчас, к сожалению, но это нормально. Используйте этих парней:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows.Data;

namespace DomenicDenicola.Wpf
{
    public class EnumToIntConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            // Note: as pointed out by Martin in the comments on this answer, this line
            // depends on the enum values being sequentially ordered from 0 onward,
            // since combobox indices are done that way. A more general solution would
            // probably look up where in the GetValues array our value variable
            // appears, then return that index.
            return (int)value;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return Enum.Parse(targetType, value.ToString(), true);
        }
    }
    public class EnumToIEnumerableConverter : IValueConverter
    {
        private Dictionary<Type, List<object>> cache = new Dictionary<Type, List<object>>();

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            var type = value.GetType();
            if (!this.cache.ContainsKey(type))
            {
                var fields = type.GetFields().Where(field => field.IsLiteral);
                var values = new List<object>();
                foreach (var field in fields)
                {
                    DescriptionAttribute[] a = (DescriptionAttribute[])field.GetCustomAttributes(typeof(DescriptionAttribute), false);
                    if (a != null && a.Length > 0)
                    {
                        values.Add(a[0].Description);
                    }
                    else
                    {
                        values.Add(field.GetValue(value));
                    }
                }
                this.cache[type] = values;
            }

            return this.cache[type];
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

С таким типом привязки XAML:

<ComboBox x:Name="MonsterGroupRole"
          ItemsSource="{Binding MonsterGroupRole,
                                Mode=OneTime,
                                Converter={StaticResource EnumToIEnumerableConverter}}"
          SelectedIndex="{Binding MonsterGroupRole,
                                  Mode=TwoWay,
                                  Converter={StaticResource EnumToIntConverter}}" />

И этот вид объявления ресурсов XAML:

<Application xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:ddwpf="clr-namespace:DomenicDenicola.Wpf">
    <Application.Resources>
        <ddwpf:EnumToIEnumerableConverter x:Key="EnumToIEnumerableConverter" />
        <ddwpf:EnumToIntConverter x:Key="EnumToIntConverter" />
    </Application.Resources>
</Application>

Любые комментарии будут оценены, так как я немного XAML/Silverlight/WPF/etc. новичок. Например, будет EnumToIntConverter.ConvertBack медленным, так что я должен рассмотреть использование кеша?

Ответ 2

Существует еще один способ привязать ComboBox к перечислениям без необходимости использования специального конвертера для выбранного элемента. Вы можете проверить его на

http://charlass.wordpress.com/2009/07/29/binding-enums-to-a-combobbox-in-silverlight/

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

Ответ 3

Я нахожу, что простая инкапсуляция данных перечисления проще в использовании.

public ReadOnly property MonsterGroupRole as list(of string)
  get
    return [Enum].GetNames(GetType(GroupRoleEnum)).Tolist
  End get
End Property

private _monsterEnum as GroupRoleEnum
Public Property MonsterGroupRoleValue as Integer
  get
    return _monsterEnum
  End get
  set(value as integer)
    _monsterEnum=value
  End set
End Property

...

<ComboBox x:Name="MonsterGroupRole"
      ItemsSource="{Binding MonsterGroupRole,
                            Mode=OneTime}"
      SelectedIndex="{Binding MonsterGroupRoleValue ,
                              Mode=TwoWay}" />

И это полностью устранит необходимость конвертера...:)

Ответ 4

Вот такая же настройка для универсального приложения Windows 8.1/Windows Phone, основные изменения: -

  • Отсутствует описаниеАтрибут в структуре (или, по крайней мере, я не могу найти его)
  • Различия в том, как работает отражение (используя поля TypeInfo.Declared)

Кажется, что порядок XAML тоже важен, мне пришлось поставить ItemsSource перед SelectedIndex, иначе он не вызвал бы привязку ItemsSource например.

<ComboBox
ItemsSource="{Binding Path=MyProperty,Mode=OneWay, Converter={StaticResource EnumToIEnumerableConverter}}"
SelectedIndex="{Binding Path=MyProperty, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}" 
/>

Код ниже

namespace MyApp.Converters
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using Windows.UI.Xaml.Data;
    public class EnumToIntConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, string language)
        {
            // Note: as pointed out by Martin in the comments on this answer, this line
            // depends on the enum values being sequentially ordered from 0 onward,
            // since combobox indices are done that way. A more general solution would
            // probably look up where in the GetValues array our value variable
            // appears, then return that index.
            return (int) value;
        }

        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            return value;
        }
    }

    public class EnumToIEnumerableConverter : IValueConverter
    {
        private readonly Dictionary<TypeInfo, List<object>> _cache = new Dictionary<TypeInfo, List<object>>();

        public object Convert(object value, Type targetType, object parameter, string language)
        {
            var type = value.GetType().GetTypeInfo();
            if (!_cache.ContainsKey(type))
            {
                var fields = type.DeclaredFields.Where(field => field.IsLiteral);
                var values = new List<object>();
                foreach (var field in fields)
                {
                    var a = (DescriptionAttribute[]) field.GetCustomAttributes(typeof(DescriptionAttribute), false);
                    if (a != null && a.Length > 0)
                    {
                        values.Add(a[0].Description);
                    }
                    else
                    {
                        values.Add(field.GetValue(value));
                    }
                }
                _cache[type] = values;
            }
            return _cache[type];
        }

        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            throw new NotImplementedException();
        }
    }
    [AttributeUsage(AttributeTargets.Field)]
    public class DescriptionAttribute : Attribute
    {
        public string Description { get; private set; }

        public DescriptionAttribute(string description)
        {
            Description = description;
        }
    }
}