65.9K
CodeProject 正在变化。 阅读更多。
Home

枚举单选按钮列表框控件

starIconstarIconstarIconstarIconstarIcon

5.00/5 (11投票s)

2012年1月3日

CPOL

8分钟阅读

viewsIcon

24003

downloadIcon

431

一个枚举单选按钮列表框控件。

引言

我拥有计算机系统中的人类因素方面的背景,并且是 RadioButton 控件的忠实拥护者。这是因为用户倾向于偏爱 RadioButton 控件而不是其他输入选项来选择多个选项,至少在选项数量较少的情况下是这样。这有两个很好的理由:输入速度更快(单击即可,而不是组合框的双击),并且选项一目了然(这也使其更快)。当然,ListBox 也可以用于相同目的,但视觉效果不同。

WPF 中一个不受欢迎的地方是需要在 View 中指定 RadioButton 控件。这通常是因为 ViewModel 中没有相应的控件。因此,RadioButton 控件几乎总是定义在 View 中,随之而来的是在 View 中定义它们以及确保 ViewViewModel 在每个 RadioButton 的用途上正确对齐的所有维护上的麻烦。当然,与 RadioButton 关联的文本可以使用绑定到一个静态值或 ViewModel 中的属性(我认为这会给 ViewModel 增加太多内容)。

也许定义一组关联的 RadioButton 控件并将它们组合成一个控件的最佳方法是以枚举的形式表示。这种方法的缺点是仅支持枚举值的子集或需要动态行为。这立即意味着无法使用单个绑定,如果选项是完全动态的,则使用枚举驱动控件的优势将丧失。对于最简单、最常见的用例,其中控件将提供枚举的所有选择,可以创建一个看起来像 RadioButton 控件,并且与包含 RadioButton 控件的标准 ListBox 一样通用,并且只需要一个绑定到一个枚举值。

实现

我之前曾致力于使用值转换器来实现相同的功能,但在值转换器在容器中的重用方面存在一些严重的限制。它还需要 IValueConverter 和一些 XAML ,这些 XAML 要么需要为每次使用进行复制,要么使用样式。创建一个继承自 ListBox 的控件要好得多,并且所需的代码量大致相同,同时它还提供了使用 XAML 进行自定义的巨大灵活性。

创建易于使用且无需 XAML 支持的东西的关键在于在构造函数中创建 DataTemplate。此 DataTemplate 需要为每个枚举值定义 RadioButton。Microsoft 最初提供了一个 FrameworkElementFactory 类来构建 DataTemplates(出于某种原因,Microsoft 并不认为有必要允许使用标准控件创建 DataTemplates)。使用 FrameworkElementFactory 很麻烦,并且要求 DataTemplate 中的所有控件都添加为 FrameworkElementFactory。显然,这种方法无法完成 XAML 中的所有操作,因此 Microsoft 的新建议是在 XAML 中创建 DataTemplate 并使用 XamlReader 来解析字符串。

public DataTemplate RadioButtonDataTemplate()
{
  var xaml = @"<DataTemplate 
    xmlns=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"">
          <Grid>
            <RadioButton IsChecked=""{Binding IsChecked}"" 
                 Content=""{Binding Text}"" />
          </Grid>
        </DataTemplate>";
  object load = XamlReader.Parse(xaml);
  return (DataTemplate)load;
}

因此,在控件的构造函数中,我将 ItemTemplate 设置为这个动态创建的 DataTemplate,并将 ListBoxBorderThickness 设置为“0”,这样 ListBox 的边框就不会显示。

public EnumRadioButtonListBox()
{
  ItemTemplate = RadioButtonDataTemplate();
  BorderThickness = new Thickness(0);
}

我为这个 ListBox 所需的唯一其他东西是枚举值的 DependencyProperty。我不想使用 ItemsSourceSelectedItem 作为 EnumerationValue,部分原因是 DependencyProperty 将两者都用于,所以两者都不太合适,使用单独的 DependencyProperty 意味着我不会干扰现有属性的操作。

public object EnumerationValue
{
  get { return (object)GetValue(EnumerationValueProperty); }
  set { SetValue(EnumerationValueProperty, value); }
}

public static readonly DependencyProperty EnumerationValueProperty =
    DependencyProperty.Register("EnumerationValue", 
    typeof(object), typeof(EnumRadioButtonListBox), 
    new UIPropertyMetadata(new PropertyChangedCallback(EnumerationChanged)));

请注意,为 DependencyProperty 定义了属性更改回调。这显然是必需的,因为当枚举类型更改时,必须响应枚举值的更改来更新 RadioButton 控件列表,并且当枚举值更改时,必须更新 RadioButton 控件。

private static void EnumerationChanged(DependencyObject d, 
        DependencyPropertyChangedEventArgs e)
{
  if (e.NewValue == null)
    return;
  ((EnumRadioButtonListBox)d).UpdateEnumerationValue(e.NewValue);
}

    private void UpdateEnumerationValue(object value)
{
  if (!value.GetType().IsEnum)
    throw new Exception(string.Format(
      "The type '{0}' is not an Enum type, and is not supported for “ +
      "EnumerationValue", value.GetType()));
  if (value.GetType() != _listType)
  {
    _listType = value.GetType();
    _list = new List<EnumDrivenRadioButtonBinding>();
    foreach (var item in Enum.GetValues(_listType))
      _list.Add(new EnumDrivenRadioButtonBinding(item, EnumerationChanged));
    ItemsSource = _list;
  }

      if (_value == null || !value.Equals(_value))
  {
    foreach (var item in _list)
      item.UpdateIsChecked(value);
    _value = value;
  }
}

首先要做的是确保值的类型是枚举,因为如果不是,继续下去就没有意义了。如果值不是枚举,则抛出异常。

接下来检查枚举类型是否已更改,以便 ItemsSourceIEnumerable 对应于枚举类型的值。为了使列表框中的选项对应于当前枚举,每次更改枚举类型时都必须更新列表。为了让 RadioButton 控件正常工作,需要一个新类(RadioButton ViewModel)。它有一个属性来表示单选按钮的状态,以及一个属性来表示与每个 RadioButton 关联的文本。在这个 class 的构造函数中,会传递特定的枚举值和事件处理程序的指针。从值中,该类可以获取与单选按钮关联的文本,并且我们现在还将拥有与之关联的值,以便在选择时在 delegate 中提供此值。处理事件的委托地址是构造函数中的第二个参数。为此类创建的每个枚举值都创建了一个实例,并将其添加到作为控件的 ItemsSource 的枚举中。

代码的最后一部分负责确保列表中选中的 RadioButton 对应于 DependencyProperty EnumerationValue 中的值。它也是响应用户输入的代码,因为当单击 RadioButton 时,RadioButton ViewModel 的类会以包含被单击的枚举值的 Action 进行响应,从而导致以下代码执行。

private void EnumerationChanged(object newValue)
{
  EnumerationValue = newValue;
  if (PropertyChanged != null)
    PropertyChanged(this, new PropertyChangedEventArgs("EnumerationValue"));
}

此代码使用 RadioButton ViewModel 提供的 EnumerationValue 值进行更新,这会导致 UpdateEnumerationValue 方法被执行,从而更新所有 RadioButton 控件以匹配新的 EnumerationValue

作为 RadioButton ViewModel 的类如下:

public class EnumDrivenRadioButtonBinding : INotifyPropertyChanged
{
  public string Text { get; private set; }

      public bool IsChecked
  {
    get { return _isChecked; }
    set
    {
      //Only need to change to true
      _isChecked = true;
      _isCheckedChangedCallback(_enumeration);
    }
  }

      private readonly object _enumeration;
  private bool _isChecked;
  private readonly Action<object> _isCheckedChangedCallback;

      internal EnumDrivenRadioButtonBinding(object value, 
        Action<object> isCheckedChangedCallback)
  {
    FieldInfo info = value.GetType().GetField(value.ToString());
    var valueDescription = (DescriptionAttribute[])info.GetCustomAttributes
              (typeof(DescriptionAttribute), false);

        Text = valueDescription.Length == 1 ?
            valueDescription[0].Description : value.ToString();
    _enumeration = value;
    _isCheckedChangedCallback += isCheckedChangedCallback;
  }
 
      internal void UpdateIsChecked(object value)
  {
    if (_enumeration.Equals(value) != IsChecked)
    {
      _isChecked = _enumeration.Equals(value);
      if (PropertyChanged != null)
        PropertyChanged(this, new PropertyChangedEventArgs("IsChecked"));
    }
  }

      public event PropertyChangedEventHandler PropertyChanged;

      public override string ToString()
  {
    if (_isChecked)
      return "true - " + Text;
    return "false - " + Text;
  }
}

这个类包含了大部分的智能。构造函数确定枚举值是否具有 DescriptionAttribute,如果有,则使用它作为标题,否则使用枚举值的名称。下面是一个包含描述属性的枚举示例:

public enum SampleEnum
{
    [DescriptionAttribute("I like the color blue")]
    Blue,
    [DescriptionAttribute("I like the color green")]
    Green,
    [DescriptionAttribute("I like the color yellow")]
    Yellow,
    Orange,
    [DescriptionAttribute("I like the color red")]
    Red
}

上面所有的枚举类型都有 DescriptionAttribute,除了 Orange 枚举类型。在这种情况下,每个枚举类型的类实例的文本值都将等于关联的 DescriptionAttribute,除了 Orange 枚举值的实例,它将是“Orange”值。构造函数还保存枚举值,该值作为参数在保存的处理程序委托中返回,该委托在 IsChecked 值变为 true 时执行。

RadioButton ViewModel 还包含一个 UpdateIsChecked 方法,该方法检查传递的参数是否等于实例的枚举值,并确保仅当相等时,IsChecked 值才为 true

您会注意到我在比较值时使用了 Equals 方法。我发现“==”通常不起作用。这可能是因为我处理的是编译器看到的,而不是对象本身包含的内容。只有 Equals 方法是可靠的。

一个有趣的注意点是,IsChecked 属性在设置新值时从不使用值,因为 UI 触发 IsChecked 的唯一时间是从 false 变为 true。这也是为什么回调委托只需要一个枚举值参数而不是一个 checked 参数的原因,它将始终为 true。

使用控件

这个控件的优点是只需要将 EnumerationValueViewModel 中的枚举进行一次绑定。最简单的,使用此 ControlXAML 如下:

<local:EnumRadioButtonListBox EnumerationValue="{Binding SampleEnum,Mode=TwoWay}"/>

注意:此 Control 需要 TwoWay 绑定模式才能正常工作。

该示例使用了略有不同的 XAML 来展示标准的 XAML ListBoxRadioButton 控件的更改如何用于以多种方式自定义此控件。

<local:EnumRadioButtonListBox 
        EnumerationValue="{Binding SampleEnum,Mode=TwoWay}">
  <ListBox.Resources>
    <Style TargetType="RadioButton">
      <Setter Property="Margin" Value="2"/>
    </Style>
  </ListBox.Resources>
  <ListBox.ItemsPanel>
    <ItemsPanelTemplate>
      <WrapPanel Width="200" IsItemsHost="True" />
    </ItemsPanelTemplate>
  </ListBox.ItemsPanel>
</local:EnumRadioButtonListBox>

在这里,我在控件的 Resources 中为 RadioButton 放置了一个样式,然后将 RadioButton 的边距设置为“2”。ListBox 的自定义方式与任何 ListBox 的自定义方式相同,在本例中,将 ItemsPanel 更改为 WrapPanel

结论

这应该在许多应用程序中是一个有用的控件,因为它支持使用枚举来定义 RadioButton 组,并且唯一需要的绑定是对多个 RadioButton 控件将控制的值进行一次绑定。ViewModel 中不需要每个单选按钮的属性,也不需要 ItemsSource 绑定。

此外,如果定义了 DescriptionAttribute,则会使用与每个枚举值关联的 DescriptionAttribute 作为每个 RadioButton 的关联文本。能够将文本定义为枚举名称以外的其他内容非常有用,因为名称中不能包含任何特殊字符,包括空格。此外,枚举值可能希望遵循某些命名约定。

© . All rights reserved.