有趣的WPF - 枚举和组合框
在 WPF、Silverlight 和 Windows Phone 7 中,将枚举绑定到组合框最快捷、最简单、最有趣的方式。
引言
在本文中,我将向您展示如何创建一个 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
。然而,如果我们运行应用程序,我们会发现组合框不起作用 - 没有可供选择的项。
我们如何解决这个问题?
通用解决方案
将枚举绑定到组合框的最典型方法是使用 ObjectDataProvider
为 ItemsSource
提供数据,如下所示。
<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
需要担心!
Apex
此控件包含在我的 Apex 库中,请访问 http://apex.codeplex.com/ 了解更多信息!