Как связать RadioButtons с перечислением?

У меня есть такое перечисление:

public enum MyLovelyEnum
{
    FirstSelection,
    TheOtherSelection,
    YetAnotherOne
};

У меня есть свойство в моем DataContext:

public MyLovelyEnum VeryLovelyEnum { get; set; }

И я получил три кнопки RadioButton в моем клиенте WPF.

<RadioButton Margin="3">First Selection</RadioButton>
<RadioButton Margin="3">The Other Selection</RadioButton>
<RadioButton Margin="3">Yet Another one</RadioButton>

Теперь, как мне привязать RadioButtons к свойству для правильной двусторонней привязки?

Ответ 1

Вы можете использовать более общий конвертер

public class EnumBooleanConverter : IValueConverter
{
  #region IValueConverter Members
  public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
  {
    string parameterString = parameter as string;
    if (parameterString == null)
      return DependencyProperty.UnsetValue;

    if (Enum.IsDefined(value.GetType(), value) == false)
      return DependencyProperty.UnsetValue;

    object parameterValue = Enum.Parse(value.GetType(), parameterString);

    return parameterValue.Equals(value);
  }

  public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
  {
    string parameterString = parameter as string;
    if (parameterString == null)
        return DependencyProperty.UnsetValue;

    return Enum.Parse(targetType, parameterString);
  }
  #endregion
}

И в XAML-части вы используете:

<Grid>
    <Grid.Resources>
      <l:EnumBooleanConverter x:Key="enumBooleanConverter" />
    </Grid.Resources>
    <StackPanel >
      <RadioButton IsChecked="{Binding Path=VeryLovelyEnum, Converter={StaticResource enumBooleanConverter}, ConverterParameter=FirstSelection}">first selection</RadioButton>
      <RadioButton IsChecked="{Binding Path=VeryLovelyEnum, Converter={StaticResource enumBooleanConverter}, ConverterParameter=TheOtherSelection}">the other selection</RadioButton>
      <RadioButton IsChecked="{Binding Path=VeryLovelyEnum, Converter={StaticResource enumBooleanConverter}, ConverterParameter=YetAnotherOne}">yet another one</RadioButton>
    </StackPanel>
</Grid>

Ответ 2

Вы можете еще больше упростить принятый ответ. Вместо того, чтобы печатать перечисления в виде строк в xaml и выполнять в конвертере больше работы, чем необходимо, вы можете явно передать значение перечисления вместо строкового представления, и, как прокомментировал CrimsonX, ошибки генерируются во время компиляции, а не во время выполнения:

ConverterParameter = {x: Статический локальный: YourEnumType.Enum1}

<StackPanel>
    <StackPanel.Resources>          
        <local:ComparisonConverter x:Key="ComparisonConverter" />          
    </StackPanel.Resources>
    <RadioButton IsChecked="{Binding Path=YourEnumProperty, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static local:YourEnumType.Enum1}}" />
    <RadioButton IsChecked="{Binding Path=YourEnumProperty, Converter={StaticResource ComparisonConverter}, ConverterParameter={x:Static local:YourEnumType.Enum2}}" />
</StackPanel>

Затем упростите конвертер:

public class ComparisonConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return value?.Equals(parameter);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return value?.Equals(true) == true ? parameter : Binding.DoNothing;
    }
}

Примечание. NullReferenceException (10 октября '18 г.):

Обновлен пример, чтобы исключить возможность создания исключения NullReferenceException. IsChecked является обнуляемым типом, поэтому возврат Nullable<Boolean> кажется разумным решением.

Примечание. Несколько групп радиокнопок в одном контейнере (17 февраля '11 г.):

В xaml, если переключатели совместно используют один и тот же родительский контейнер, выбор одного из них отменяет выбор всех остальных в этом контейнере (даже если они связаны с другим свойством). Поэтому постарайтесь, чтобы ваш RadioButton, связанный с общим свойством, был сгруппирован в свой собственный контейнер, например панель стека. В тех случаях, когда связанные с вами RadioButtons не могут совместно использовать один родительский контейнер, установите для свойства GroupName каждого RadioButton общее значение, чтобы логически сгруппировать их.

Примечание. Тип перечисления, вложенный в класс (28 апреля '11 г.):

Если ваш тип перечисления вложен в класс (а не непосредственно в пространство имен), вы можете использовать синтаксис "+" для доступа к перечислению в XAML, как указано в (не отмеченном) ответе на вопрос Не удалось найти тип перечисления для статической ссылки в WPF:

ConverterParameter = {x: статический локальный: YourClass+ YourNestedEnumType.Enum1}

Однако из-за этой проблемы с Microsoft Connect конструктор в VS2010 больше не будет загружать информацию о "Type 'local:YourClass+YourNestedEnumType' was not found.", но проект компилируется и успешно выполняется. Конечно, вы можете избежать этой проблемы, если сможете напрямую переместить тип enum в пространство имен.

Изменить (16 декабря '10):

Спасибо, что предложили вернуть Binding.DoNothing вместо DependencyProperty.UnsetValue.

Изменить (5 апреля '11):

Упрощенный ConvertBack if-else для использования троичного оператора.

Изменить (27 января '12):

При использовании флагов Enum конвертер будет выглядеть следующим образом:

public class EnumToBooleanConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return ((Enum)value).HasFlag((Enum)parameter);
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return value.Equals(true) ? parameter : Binding.DoNothing;
    }
}

Изменить (7 мая '15):

В случае Nullable Enum (который не задается в вопросе, но может быть необходим в некоторых случаях, например, ORM возвращает ноль из БД или всякий раз, когда может иметь смысл, что в логике программы значение не является не забудьте добавить начальную нулевую проверку в методе Convert и вернуть соответствующее значение bool, которое обычно равно false (если вы не хотите, чтобы была выбрана какая-либо радиокнопка), как показано ниже:

    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (value == null) {
            return false; // or return parameter.Equals(YourEnumType.SomeDefaultValue);
        }
        return value.Equals(parameter);
    }

Ответ 3

Для ответа EnumToBooleanConverter: Вместо возврата DependencyProperty.UnsetValue рассмотрите вопрос о возврате Binding.DoNothing для случая, когда значение переключателя IsChecked становится ложным. Первое указывает на проблему (и может показать пользователю красный прямоугольник или аналогичные показатели проверки), в то время как последний просто указывает, что ничего не должно быть сделано, что и нужно в этом случае.

http://msdn.microsoft.com/en-us/library/system.windows.data.ivalueconverter.convertback.aspx http://msdn.microsoft.com/en-us/library/system.windows.data.binding.donothing.aspx

Ответ 5

Для UWP это не так просто: вы должны перепрыгнуть через дополнительный обруч, чтобы передать значение поля в качестве параметра.

Пример 1

Действителен как для WPF, так и для UWP.

<MyControl>
    <MyControl.MyProperty>
        <Binding Converter="{StaticResource EnumToBooleanConverter}" Path="AnotherProperty">
            <Binding.ConverterParameter>
                <MyLibrary:MyEnum>Field</MyLibrary:MyEnum>
            </Binding.ConverterParameter>
        </MyControl>
    </MyControl.MyProperty>
</MyControl>

Пример 2

Действителен как для WPF, так и для UWP.

...
<MyLibrary:MyEnum x:Key="MyEnumField">Field</MyLibrary:MyEnum>
...

<MyControl MyProperty="{Binding AnotherProperty, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter={StaticResource MyEnumField}}"/>

Пример 3

Действует только для WPF!

<MyControl MyProperty="{Binding AnotherProperty, Converter={StaticResource EnumToBooleanConverter}, ConverterParameter={x:Static MyLibrary:MyEnum.Field}}"/>

UWP не поддерживает x:Static, поэтому пример 3 не может быть и речи; предполагая, что вы идете с примером 1, результатом является более подробный код. Пример 2 немного лучше, но все же не идеален.

Решение

public abstract class EnumToBooleanConverter<TEnum> : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, string language)
    {
        var Parameter = parameter as string;

        if (Parameter == null)
            return DependencyProperty.UnsetValue;

        if (Enum.IsDefined(typeof(TEnum), value) == false)
            return DependencyProperty.UnsetValue;

        return Enum.Parse(typeof(TEnum), Parameter).Equals(value);
    }

    public object ConvertBack(object value, Type targetType, object parameter, string language)
    {
        var Parameter = parameter as string;
        return Parameter == null ? DependencyProperty.UnsetValue : Enum.Parse(typeof(TEnum), Parameter);
    }
}

Затем для каждого типа, который вы хотите поддержать, определите конвертер, в котором указан тип перечисления.

public class MyEnumToBooleanConverter : EnumToBooleanConverter<MyEnum>
{
    //Nothing to do!
}

Причина, по которой он должен быть в коробке, заключается в том, что, похоже, нет способа ссылаться на тип в методе ConvertBack; бокс позаботится об этом. Если вы перейдете к одному из первых двух примеров, вы можете просто ссылаться на тип параметра, исключая необходимость наследования из класса в штучной упаковке; если вы хотите сделать все это в одной строке и с наименьшей многословностью, последнее решение идеально.

Использование похоже на пример 2, но, на самом деле, менее подробное.

<MyControl MyProperty="{Binding AnotherProperty, Converter={StaticResource MyEnumToBooleanConverter}, ConverterParameter=Field}"/>

Недостатком является то, что вы должны определить конвертер для каждого типа, который вы хотите поддерживать.

Ответ 6

Расширенные идеи, описанные выше, позволяют привязывать радиокнопки к любому типу (перечисление, логическое, строковое, целочисленное и т.д.) и предоставляют здесь рабочий пример кода:

http://www.codeproject.com/Tips/720497/Binding-Radio-Buttons-to-a-Single-Property

Ответ 7

Это работает и для флажка.

public class EnumToBoolConverter:IValueConverter
{
    private int val;
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        int intParam = (int)parameter;
        val = (int)value;

        return ((intParam & val) != 0);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        val ^= (int)parameter;
        return Enum.Parse(targetType, val.ToString());
    }
}

Привязка одного перечисления к нескольким флажкам.

Ответ 8

Я создал новый класс для обработки привязки RadioButtons и CheckBoxes к перечислениям. Он работает для помеченных перечислений (с несколькими вариантами флажков) и непомеченных перечислений для флажков с одним выбором или переключателей. Это также не требует никаких ValueConverters вообще.

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

public class EnumSelection<T> : INotifyPropertyChanged where T : struct, IComparable, IFormattable, IConvertible
{
  private T value; // stored value of the Enum
  private bool isFlagged; // Enum uses flags?
  private bool canDeselect; // Can be deselected? (Radio buttons cannot deselect, checkboxes can)
  private T blankValue; // what is considered the "blank" value if it can be deselected?

  public EnumSelection(T value) : this(value, false, default(T)) { }
  public EnumSelection(T value, bool canDeselect) : this(value, canDeselect, default(T)) { }
  public EnumSelection(T value, T blankValue) : this(value, true, blankValue) { }
  public EnumSelection(T value, bool canDeselect, T blankValue)
  {
    if (!typeof(T).IsEnum) throw new ArgumentException($"{nameof(T)} must be an enum type"); // I really wish there was a way to constrain generic types to enums...
    isFlagged = typeof(T).IsDefined(typeof(FlagsAttribute), false);

    this.value = value;
    this.canDeselect = canDeselect;
    this.blankValue = blankValue;
  }

  public T Value
  {
    get { return value; }
    set 
    {
      if (this.value.Equals(value)) return;
      this.value = value;
      OnPropertyChanged();
      OnPropertyChanged("Item[]"); // Notify that the indexer property has changed
    }
  }

  [IndexerName("Item")]
  public bool this[T key]
  {
    get
    {
      int iKey = (int)(object)key;
      return isFlagged ? ((int)(object)value & iKey) == iKey : value.Equals(key);
    }
    set
    {
      if (isFlagged)
      {
        int iValue = (int)(object)this.value;
        int iKey = (int)(object)key;

        if (((iValue & iKey) == iKey) == value) return;

        if (value)
          Value = (T)(object)(iValue | iKey);
        else
          Value = (T)(object)(iValue & ~iKey);
      }
      else
      {
        if (this.value.Equals(key) == value) return;
        if (!value && !canDeselect) return;

        Value = value ? key : blankValue;
      }
    }
  }

  public event PropertyChangedEventHandler PropertyChanged;

  private void OnPropertyChanged([CallerMemberName] string propertyName = "")
  {
    PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }
}

А о том, как его использовать, допустим, у вас есть перечисление для запуска задачи вручную или автоматически, и его можно запланировать на любые дни недели, а также некоторые дополнительные параметры...

public enum StartTask
{
  Manual,
  Automatic
}

[Flags()]
public enum DayOfWeek
{
  Sunday = 1 << 0,
  Monday = 1 << 1,
  Tuesday = 1 << 2,
  Wednesday = 1 << 3,
  Thursday = 1 << 4,
  Friday = 1 << 5,
  Saturday = 1 << 6
}

public enum AdditionalOptions
{
  None = 0,
  OptionA,
  OptionB
}

Теперь, как легко использовать этот класс:

public class MyViewModel : ViewModelBase
{
  public MyViewModel()
  {
    StartUp = new EnumSelection<StartTask>(StartTask.Manual);
    Days = new EnumSelection<DayOfWeek>(default(DayOfWeek));
    Options = new EnumSelection<AdditionalOptions>(AdditionalOptions.None, true, AdditionalOptions.None);
  }

  public EnumSelection<StartTask> StartUp { get; private set; }
  public EnumSelection<DayOfWeek> Days { get; private set; }
  public EnumSelection<AdditionalOptions> Options { get; private set; }
}

А вот как легко связать флажки и переключатели с этим классом:

<StackPanel Orientation="Vertical">
  <StackPanel Orientation="Horizontal">
    <!-- Using RadioButtons for exactly 1 selection behavior -->
    <RadioButton IsChecked="{Binding StartUp[Manual]}">Manual</RadioButton>
    <RadioButton IsChecked="{Binding StartUp[Automatic]}">Automatic</RadioButton>
  </StackPanel>
  <StackPanel Orientation="Horizontal">
    <!-- Using CheckBoxes for 0 or Many selection behavior -->
    <CheckBox IsChecked="{Binding Days[Sunday]}">Sunday</CheckBox>
    <CheckBox IsChecked="{Binding Days[Monday]}">Monday</CheckBox>
    <CheckBox IsChecked="{Binding Days[Tuesday]}">Tuesday</CheckBox>
    <CheckBox IsChecked="{Binding Days[Wednesday]}">Wednesday</CheckBox>
    <CheckBox IsChecked="{Binding Days[Thursday]}">Thursday</CheckBox>
    <CheckBox IsChecked="{Binding Days[Friday]}">Friday</CheckBox>
    <CheckBox IsChecked="{Binding Days[Saturday]}">Saturday</CheckBox>
  </StackPanel>
  <StackPanel Orientation="Horizontal">
    <!-- Using CheckBoxes for 0 or 1 selection behavior -->
    <CheckBox IsChecked="{Binding Options[OptionA]}">Option A</CheckBox>
    <CheckBox IsChecked="{Binding Options[OptionB]}">Option B</CheckBox>
  </StackPanel>
</StackPanel>
  1. Когда пользовательский интерфейс загружается, будет выбран переключатель "Ручной", и вы можете изменить свой выбор между "Ручной" или "Автоматический", но всегда должен быть выбран любой из них.
  2. Каждый день недели будет не отмечен, но любое их число может быть отмечено или отключено.
  3. "Вариант A" и "Вариант B" будут изначально не отмечены. Вы можете проверить один или другой, отметив, что один снимает флажок с другого (аналогично RadioButton), но теперь вы также можете снять оба флажка (чего нельзя сделать с WPF RadioButton, именно поэтому CheckBox используется здесь)

Ответ 9

На основе EnumToBooleanConverter от Скотта. Я заметил, что метод ConvertBack не работает в Enum с кодом флага.

Я пробовал следующий код:

public class EnumHasFlagToBooleanConverter : IValueConverter
    {
        private object _obj;
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            _obj = value;
            return ((Enum)value).HasFlag((Enum)parameter);
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            if (value.Equals(true))
            {
                if (((Enum)_obj).HasFlag((Enum)parameter))
                {
                    // Do nothing
                    return Binding.DoNothing;
                }
                else
                {
                    int i = (int)_obj;
                    int ii = (int)parameter;
                    int newInt = i+ii;
                    return (NavigationProjectDates)newInt;
                }
            }
            else
            {
                if (((Enum)_obj).HasFlag((Enum)parameter))
                {
                    int i = (int)_obj;
                    int ii = (int)parameter;
                    int newInt = i-ii;
                    return (NavigationProjectDates)newInt;

                }
                else
                {
                    // do nothing
                    return Binding.DoNothing;
                }
            }
        }
    }

Единственное, что я не могу заставить работать, - это сделать от int до targetType, поэтому я сделал его жестко привязанным к NavigationProjectDates, перечислению, которое я использую. И targetType == NavigationProjectDates...


Изменить для более общего конвертера конвертов флагов:

    public class FlagsEnumToBooleanConverter : IValueConverter {
        private int _flags=0;
        public object Convert(object value, Type targetType, object parameter, string language) {
            if (value == null) return false;
            _flags = (int) value;
            Type t = value.GetType();
            object o = Enum.ToObject(t, parameter);
            return ((Enum)value).HasFlag((Enum)o);
        }

        public object ConvertBack(object value, Type targetType, object parameter, string language)
        {
            if (value?.Equals(true) ?? false) {
                _flags = _flags | (int) parameter;
            }
            else {
                _flags = _flags & ~(int) parameter;
            }
            return _flags;
        }
    }