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

有趣的WPF - 枚举和组合框

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (21投票s)

2012年1月17日

CPOL

5分钟阅读

viewsIcon

66993

downloadIcon

1352

在 WPF、Silverlight 和 Windows Phone 7 中,将枚举绑定到组合框最快捷、最简单、最有趣的方式。

EnumerationComboBoxSample

引言

在本文中,我将向您展示如何创建一个 EnumerationComboBox,这将是一种方便快捷地将枚举绑定到 ComboBoxes 的方法。

问题

数据绑定非常棒 - WPF 中的 MVVM 允许我们创建包含数据和逻辑的 ViewModel,以及处理数据呈现的 View。然而,如果您和我一样,在必须将组合框绑定到枚举时会犹豫一下。再次,那怎么运作呢?

通常,当您使用组合框时,会指定一个 ItemsSource - 这是可供选择的项集合,以及一个 SelectedItem - 已选中的实际项。通常,在 ViewModel 中,您可能有一个枚举类型的属性,但您不能直接绑定到它 - 您还需要 ItemsSource - 可用枚举值的集合。

为了提供此数据,我们可以使用 ObjectDataProvider - 这是一个相当直接的机制,但有点繁琐。在本文中,我们将创建一个带有新属性 SelectedEnumeration 的组合框,它将为您处理所有繁琐的工作。

枚举

我喜欢《飞出个未来》。所以,让我们创建一个表示一些主要角色的枚举。

public enum Character
{
    Fry,
    Leela,
    Zoidberg,
    Professor
}

这是一个相当简单的枚举。现在我们将创建一个暴露 'Character' 类型属性的 View Model。

介绍 ViewModel

现在让我们创建一个 ViewModel。

/// <summary>
/// The MainViewModel. This is the main view model for the application.
/// </summary>
public class MainViewModel : ViewModel
{
    /// <summary>
    /// The Character notifying property.
    /// </summary>
    private NotifyingProperty CharacterProperty =
      new NotifyingProperty("Character", typeof(Character), Character.Fry);

    /// <summary>
    /// Gets or sets the character.
    /// </summary>
    /// <value>
    /// The character.
    /// </value>
    public Character Character
    {
        get { return (Character)GetValue(CharacterProperty); }
        set { SetValue(CharacterProperty, value); }
    }
}

如果您不熟悉基类 ViewModel,请不用担心。当您使用 Apex 库时,它是所有 ViewModel 的基类。NotifyingProperty 对象为我们处理 INotifyPropertyChanged 的进出。如果您正在使用自己的 INotifyPropertyChanged 实现,如 Prism 或 Cinch 等框架,那么只需创建等效的 ViewModel。重要的是它暴露一个 Character 类型的属性。

View

最后,我们定义 View。这是最简单的定义 - 示例应用程序中的那个有一个网格和一些文本以改善布局。

<Window x:Class="EnumerationComboBoxSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:EnumerationComboBoxSample"
        Title="EnumerationComboBox Sample" Height="191" Width="442">

    <!-- Set the data context to an instance of the view model. -->
    <Window.DataContext>
        <local:MainViewModel x:Name="mainViewModel" />
    </Window.DataContext>

    <StackPanel Orientation="Vertical">

        <!-- The label for the combo box. -->
        <Label Content="Selected Character" />

        <!-- The combo box, bound to an enumeration. -->
        <ComboBox SelectedItem="{Binding Character}" />

    </StackPanel>
</Window>

这就是我们想要做的 - 只绑定到 SelectedItem。然而,如果我们运行应用程序,我们会发现组合框不起作用 - 没有可供选择的项。

我们如何解决这个问题?

通用解决方案

将枚举绑定到组合框的最典型方法是使用 ObjectDataProviderItemsSource 提供数据,如下所示。

<Window.Resources>
    <ObjectDataProvider MethodName="GetValues"
        ObjectType="{x:Type sys:Enum}"
        x:Key="CharacterEnumValues">
        <ObjectDataProvider.MethodParameters>
            <x:Type TypeName="Character" />
        </ObjectDataProvider.MethodParameters>
    </ObjectDataProvider>
</Window.Resources>

然后

<ComboBox SelectedItem="{Binding Character}" ItemsSource="{Binding Source={StaticResource CharacterValues}} "/>

但这很笨拙 - 我们必须为每种枚举类型创建一个 ObjectDataProvider,记住语法,等等。

更好的解决方案

如果我们能像这样绑定怎么办?

<!-- The combo box, bound to an enumeration. -->
<ComboBox SelectedEnumeration="{Binding Character}" />

并让我们完成所有繁重的工作?好吧,我们正在使用 C# 和 WPF,所以通常,如果你能想象到,你就可以做到。所以让我们创建一个像这样工作的组合框。

首先,我们将创建一个派生自 ComboBox 的新类,它将专门用于我们设定的任务。

/// <summary>
/// A EnumerationComboBox shows a selected enumeration value
/// from a set of all available enumeration values.
/// If the enumeration value has the 'Description' attribute, this is used.
/// </summary>
public class EnumerationComboBox : ComboBox
{

到目前为止一切顺利。现在我们知道我们需要一个新的依赖项属性 - 一个代表 SelectedEnumeration 的属性。计划是当设置此属性时,我们将即时创建我们自己的 ItemsSource。如下创建依赖项属性。

/// <summary>
/// The SelectedEnumerationProperty dependency property.
/// </summary>
public static readonly DependencyProperty SelectedEnumerationProperty =
  DependencyProperty.Register("SelectedEnumeration", typeof(object), typeof(EnumerationComboBox),
#if !SILVERLIGHT
 new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, 
 new PropertyChangedCallback(OnSelectedEnumerationChanged)));
#else
 new PropertyMetadata(null, new PropertyChangedCallback(OnSelectedEnumerationChanged)));
#endif

/// <summary>
/// Gets or sets the selected enumeration.
/// </summary>
/// <value>
/// The selected enumeration.
/// </value>
public object SelectedEnumeration
{
    get { return (object)GetValue(SelectedEnumerationProperty); }
    set { SetValue(SelectedEnumerationProperty, value); }
}

此属性几乎肯定会通过 XAML 中的绑定设置,即控件的用户设置初始属性。然而,我们希望在用户从组合框中选择新值时更改选定的枚举,因此我们将始终希望它以双向绑定。这没关系 - 除非在 Silverlight 中,它没有 BindsTwoWaysByDefault 选项!在 Silverlight 中,我们能做的最好的就是希望用户记住双向绑定!

与 Apex 中的大多数控件一样,EnumerationCombobBox 适用于 WPF、Silverlight 和 WP7,因此我们非常注意理解 WPF 和 Silverlight 之间的差异,例如这个!

我们已指定当属性更改时将调用 OnSelectedEnumerationChanged 函数 - 在这里我们可以挂钩我们的逻辑来创建 ItemsSource

/// <summary>
/// Called when the selected enumeration is changed.
/// </summary>
/// <param name="o">The o.</param>
/// <param name="args">The <see
///    cref="System.Windows.DependencyPropertyChangedEventArgs"/>
///    instance containing the event data.</param>
private static void OnSelectedEnumerationChanged(DependencyObject o, 
                    DependencyPropertyChangedEventArgs args)
{
    //  Get the combo box.
    EnumerationComboBox me = o as EnumerationComboBox;

    //  Populate the items source.
    me.PopulateItemsSource();
}

现在我们可以构建一个 'PopulateItemsSource' 函数来设置 ItemsSource 属性。

既然我们正在费力地做这一切 - 让我们允许用户使用 System.ComponentModel.Description 属性为枚举指定描述。我们的枚举将看起来像这样。

public enum Character
{
    [Description("Philip J. Fry")]
    Fry,

    [Description("Turunga Leela")]
    Leela,

    [Description("Doctor John Zoidberg")]
    Zoidberg,

    [Description("Professor Hubert J. Farnsworth")]
    Professor
}

我们的 ItemsSource 将必须是具有名称和值的对象集合,所以让我们为此创建一个内部类。

/// <summary>
/// A name-value pair.
/// </summary>
internal class NameValue
{
    /// <summary>
    /// Initializes a new instance of the <see cref="NameValue"/> class.
    /// </summary>
    public NameValue()
    {
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="NameValue"/> class.
    /// </summary>
    /// <param name="name">The name.</param>
    /// <param name="value">The value.</param>
    public NameValue(string name, object value)
    {
        Name = name;
        Value = value;
    }

    /// <summary>
    /// Gets or sets the name.
    /// </summary>
    /// <value>
    /// The name.
    /// </value>
    public string Name
    {
        get;
        set;
    }

    /// <summary>
    /// Gets or sets the value.
    /// </summary>
    /// <value>
    /// The value.
    /// </value>
    public object Value
    {
        get;
        set;
    }
}

这是一个非常简单的类。EnumerationComboBox 现在需要一组我们可以构建的 NameValue 对象。现在让我们添加属性。

/// <summary>
/// Gets or sets the enumerations.
/// </summary>
/// <value>
/// The enumerations.
/// </value>
private List<NameValue> enumerations;

我们现在准备开始主要函数。

/// <summary>
/// Populates the items source.
/// </summary>
private void PopulateItemsSource()
{
    //  We must have an items source and an item which is an enum.
    if (ItemsSource != null || SelectedEnumeration is Enum == false)
        return;

现在,如果 ItemsSource 已经被设置,则无需重新创建它,因此我们做的第一件事就是在此工作完成时退出函数。

    //  Get the enum type.
    var enumType = SelectedEnumeration.GetType();

    //  Get the enum values. Use the helper rather than Enum.GetValues
    //  as it works in Silverlight too.
    var enumValues = Apex.Helpers.EnumHelper.GetValues(enumType);

    //  Create some enum value/descriptions.
    enumerations = new List<NameValue>();

    //  Go through each one.
    foreach (var enumValue in enumValues)
    {
        //  Add the enumeration item.
        enumerations.Add(new NameValue(((Enum)enumValue).GetDescription(), enumValue));
    }

    //  Set the items source.
    ItemsSource = enumerations;

    //  Initialise the control.
    Initialise();
}

我们在这里所做的只是构建枚举列表。我们使用 Apex.Helpers.EnumHelper 类,因为 Enum.GetValues 在 Silverlight 中不存在。EnumHelper 适用于 Silverlight、WPF 和 Windows Phone 7。然后我们设置 ItemsSource 并调用 Initialise(为任何必须完成的最终初始化)。Initialise 只是下面的代码。

/// <summary>
/// Initialises this instance.
/// </summary>
private void Initialise()
{
    //  Set the display member path and selected value path.
    DisplayMemberPath = "Name";
    SelectedValuePath = "Value";

    //  If we have enumerations and a selected enumeration, set the selected item.
    if (enumerations != null && SelectedEnumeration != null)
    {
        var selectedEnum = from enumeration in enumerations
                           where enumeration.Value.ToString() == 
                           SelectedEnumeration.ToString() select enumeration;
        SelectedItem = selectedEnum.FirstOrDefault();
    }

    //  Wait for selection changed events.
    SelectionChanged += 
      new SelectionChangedEventHandler(EnumerationComboBox_SelectionChanged);
}

Initialise 只是设置初始值(如果有的话!)并为 SelectionChanged 事件创建一个事件处理程序。事件处理程序只是设置 SelectedEnumeration 值(以便当用户更改选定项时,绑定的 SelectedEnumeration 也会被设置)。

/// <summary>
/// Handles the SelectionChanged event of the EnumerationComboBoxTemp control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see
///    cref="System.Windows.Controls.SelectionChangedEventArgs"/>
///    instance containing the event data.</param>
void EnumerationComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    //  Get the new item.
    if (e.AddedItems.Count == 0 || e.AddedItems[0] is NameValue == false)
        return;

    //  Keep the selected enumeration up to date.
    NameValue nameValue = e.AddedItems[0] as NameValue;
    SelectedEnumeration = nameValue.Value;
}

现在唯一剩下的函数是获取枚举的 Description,或者如果未设置,则将其作为字符串返回;我们可以通过构建一个扩展方法来做到这一点。

/// <summary>
/// Extensions for the enum class.
/// </summary>
public static class EnumExtensions
{
    /// <summary>
    /// Gets the description of an enumeration.
    /// </summary>
    /// <param name="me">The enumeration.</param>
    /// <returns>The value of the [Description] attribute for the enum, or the name of
    /// the enum value if there isn't one.</returns>
    public static string GetDescription(this Enum me)
    {
      //  Get the enum type.
      var enumType = me.GetType();

      //  Get the description attribute.
      var descriptionAttribute = enumType.GetField(me.ToString())
        .GetCustomAttributes(typeof(DescriptionAttribute), false)
        .FirstOrDefault() as DescriptionAttribute;

      //  Get the description (if there is one) or the name of the enum otherwise.
      return descriptionAttribute != null
        ? descriptionAttribute.Description
        : me.ToString();
    }
}

我们做到了!我们现在可以像这样使用我们的 EnumerationComboBox

<!-- The combo box, bound to an enumeration. -->
<apexControls:EnumerationComboBox SelectedEnumeration="{Binding Character}" />

而且一切正常!将来非常易于使用,而且没有 ObjectDataSource 需要担心!

example.jpg

Apex

此控件包含在我的 Apex 库中,请访问 http://apex.codeplex.com/ 了解更多信息!

© . All rights reserved.