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

使用枚举的 MVVM ComboBox

starIconstarIcon
emptyStarIcon
starIcon
emptyStarIconemptyStarIcon

2.88/5 (3投票s)

2012年1月19日

CPOL

4分钟阅读

viewsIcon

31406

downloadIcon

292

一个用于将枚举值绑定到 WPF 中 ComboBox 的 MVVM 版本。

我决定编写一个用于将枚举值绑定到 WPF 中 ComboBox 的 MVVM 版本。我知道有很多关于如何将枚举值放入组合框的示例,但它们大多数要么使用 ObjectProvider,要么从 ComboBox 派生它们自己的类。我不喜欢继承,我更喜欢扩展类,因为它使事情更简洁,并允许你选择是否使用该功能。

为此,我提出了一个行为来使用枚举值填充 ComboBox

我们将从我要使用的名为 JobTitles 的枚举开始。

public enum JobTitles
{
    [Description("Grunt")]
    Grunt,
    [Description("Programmer")]
    Programmer,
    [Description("Analyst Programmer")]
    AnalystProgrammer,
    [Description("Project Manager")]
    ProjectManager,
    [Description("Chief Information Officer")]
    ChiefInformationOfficer,
}

在这里,我定义了枚举,并在 Description 属性中给它一些用户友好的字符串。这些是将显示给最终用户的字符串。

一些 View Model 来建模我们预期的数据

public class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

这只是一个辅助 view model,允许我们从派生类中很好地调用 OnPropertyChanged。啊!我说我不喜欢继承。我可能不喜欢它,但这并不是说你不应该使用它,只是尽可能找到解决方法。

下一个是平淡无奇的 NormalViewModel

public class NormalViewModel : ViewModelBase
{
    private JobTitles jobTitle;
    public JobTitles JobTitle
    {
        get { return jobTitle; }
        set
        {
            if (jobTitle != value)
            {
                jobTitle = value;
                OnPropertyChanged(“JobTitle”);
            }
        }
    }
}

它只有一个属性,即我们的枚举类型。

下一个是 MainViewModel,我将把 MainWindow DataContext 绑定到它

public class MainViewModel
{
    public MainViewModel()
    {
        NormalViewModel = new NormalViewModel {JobTitle = JobTitles.ProjectManager};
        DynamicViewModel = new ExpandoObject();
        DynamicViewModel.JobTitle = JobTitles.AnalystProgrammer;
    }
    public NormalViewModel NormalViewModel
    {
        get;
        private set;
    }
    public dynamic DynamicViewModel
    {
        get;
        private set;
    }
}

在这里,我设置了两个内部 view model。一个是刚刚提到的 NormalViewModel,另一个是动态对象。为了完整起见,我包含了这一个,因为我将要展示的行为绕过了绑定机制,并使用反射来处理属性。当然,这不适用于动态对象,因此我在行为中有一个解决方法。

好的,view model 已经处理完毕,现在开始处理行为本身

public class ComboEnumBehaviour : Behavior<ComboBox>
{
    public static readonly DependencyProperty SelectedItemPathProperty =
                DependencyProperty.Register(
                “SelectedItemPath”,
                typeof(string),
                typeof(ComboEnumBehaviour));
    public string SelectedItemPath
    {
        get { return (string)GetValue(SelectedItemPathProperty); }
        set { SetValue(SelectedItemPathProperty, value); }
    }
    private readonly List<ComboViewModel> values = new List<ComboViewModel>();
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.DataContextChanged += AssociatedObjectDataContextChanged;
    }
    protected override void OnDetaching()
    {
        AssociatedObject.SelectionChanged -= AssociatedObjectSelectionChanged;
        AssociatedObject.DataContextChanged -= AssociatedObjectDataContextChanged;
        base.OnDetaching();
    }
    private void AssociatedObjectDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        Type enumType;
        object currentValue = GetCurrentValue(out enumType);
        var fieldInfos = enumType.GetFields();
        foreach (var fieldInfo in fieldInfos)
        {
            var attr = (DescriptionAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(DescriptionAttribute));
            if (attr != null)
            {
                values.Add(new ComboViewModel(fieldInfo.Name, attr.Description));
            }
        }
        var notifyPropertyChanged = AssociatedObject.DataContext as INotifyPropertyChanged;
        if (notifyPropertyChanged != null)
        {
            notifyPropertyChanged.PropertyChanged += PropertyChanged;
        }
        AssociatedObject.SelectedItem = values.Where(x => x.Value.ToString() == currentValue.ToString()).Single();
        AssociatedObject.DisplayMemberPath = “Description”;
        AssociatedObject.ItemsSource = values;
        AssociatedObject.SelectionChanged += AssociatedObjectSelectionChanged;
    }
    private object GetCurrentValue(out Type enumType)
    {
        object currentValue;
        var dynamicLookup = AssociatedObject.DataContext as IDictionary<string, object>;
        if (dynamicLookup != null)
        {
            enumType = dynamicLookup[SelectedItemPath].GetType();
            currentValue = dynamicLookup[SelectedItemPath];
        }
        else
        {
            var propertyInfo = AssociatedObject.DataContext.GetType().GetProperty(SelectedItemPath);
            enumType = propertyInfo.PropertyType;
            currentValue = propertyInfo.GetValue(AssociatedObject.DataContext, null);
        }
        return currentValue;
    }
    private void PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == SelectedItemPath)
        {
            Type enumType;
            var currentValue = GetCurrentValue(out enumType);
            AssociatedObject.SelectedItem = 
              values.Where(x => x.Value.ToString() == currentValue.ToString()).Single();
        }
    }
    private void AssociatedObjectSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        var item = (ComboViewModel)e.AddedItems[0];
        var dynamicLookup = AssociatedObject.DataContext as IDictionary<string, object>;
        if (dynamicLookup != null)
        {
            var enumType = dynamicLookup[SelectedItemPath].GetType();
            var enumValue = Enum.Parse(enumType, item.Value);
            dynamicLookup[SelectedItemPath] = enumValue;
        }
        else
        {
            var propertyInfo = AssociatedObject.DataContext.GetType().GetProperty(SelectedItemPath);
            var enumType = propertyInfo.PropertyType;
            var enumValue = Enum.Parse(enumType, item.Value);
            propertyInfo.SetValue(AssociatedObject.DataContext, enumValue, null);
        }
    }
}

我公开了一个用于属性路径的依赖属性。这是属性的名称,而不是绑定。在我们的例子中,从 XAML 中,我们将把它设置为 'JobTitle',因为那是我们 view model 上具有枚举的属性。

因为我们没有使用绑定,所以 DataContext 在行为发生时不会附加。这意味着我们必须监听 DataContextChanged 事件并在那里连接我们的东西。我们做的第一件事是从已使用的枚举类型中提取所有值及其描述。然后,将这些信息包装在 ComboViewModel

public class ComboViewModel
{
    public ComboViewModel(string value, string description)
    {
        Value = value;
        Description = description;
    }
    public string Value
    {
        get;
        private set;
    }
    public string Description
    {
        get;
        private set;
    }
}

这仅仅允许我们将组合框的内容控制为值和描述对。然后,我们将组合框设置为使用此 view model,而不是 xaml 中提供的内容。

现在,当我们的 DataContext 上的属性更改时,我们会检查它是否是我们绑定的内容。如果是,那么我们知道枚举值已更改,并且我们在组合框中进行相关的选择更改。这是为了允许某人在 view model 上设置属性,然后在组合框中反映该更改。

反之亦然,当组合框选择更改(在这种情况下为 AssociatedObject)时,我们将这些更改反映回 view model。

我添加了一个检查 view model 是否是 IDictonary 的检查,而动态 ExpandoObject 就是。如果是这种情况,那么更新机制略有不同。

现在来看 XAML

<Window
x:Class=”WpfApplication3.MainWindow”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:i=”clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity”
xmlns:local=”clr-namespace:WpfApplication3″
Title=”MainWindow” Height=”350″ Width=”525″>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height=”*”/>
<RowDefinition Height=”*”/>
</Grid.RowDefinitions>
<Grid DataContext=”{Binding NormalViewModel}”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
</Grid.RowDefinitions>
<TextBlock Text=”This one is a fixed ViewModel” FontSize=”16″/>
<ComboBox Grid.Row=”1″>
<i:Interaction.Behaviors>
<local:ComboEnumBehaviour SelectedItemPath=”JobTitle”/>
</i:Interaction.Behaviors>
</ComboBox>
<StackPanel Orientation=”Horizontal” Grid.Row=”2″>
<TextBlock Text=”Job Title:” Margin=”0,0,15,0″/>
<TextBlock Text=”{Binding JobTitle}”/>
</StackPanel>
</Grid>
<Grid DataContext=”{Binding DynamicViewModel}” Grid.Row=”1″>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
<RowDefinition Height=”Auto”/>
</Grid.RowDefinitions>
<TextBlock Text=”This one is a fixed ViewModel” FontSize=”16″/>
<ComboBox Grid.Row=”1″>
<i:Interaction.Behaviors>
<local:ComboEnumBehaviour SelectedItemPath=”JobTitle”/>
</i:Interaction.Behaviors>
</ComboBox>
<StackPanel Orientation=”Horizontal” Grid.Row=”2″>
<TextBlock Text=”Job Title:” Margin=”0,0,15,0″/>
<TextBlock Text=”{Binding JobTitle}”/>
</StackPanel>
</Grid>
</Grid>
</Window>

我放入了两个组合框来演示绑定到普通 view model 和一个绑定到动态 view model。只是让你知道我没有作弊 ;-)

主要部分是

<ComboBox Grid.Row=”1″>
<i:Interaction.Behaviors>
<local:ComboEnumBehaviour SelectedItemPath=”JobTitle”/>
</i:Interaction.Behaviors>
</ComboBox>

正如你所看到的,我们保留一个普通的 ComboBox,只是附加一个具有所选路径的行为。现在完成了所有繁重的工作,以便在组合框中显示正确的字符串并在更改时更新 view model。

MainWindow.cs 中,我们只需要将我们的 DataContext 设置为 MainViewModel

DataContext = new MainViewModel();

这种方式使事情保持清洁,并避免你必须派生最好单独保留的控件。你可以混合和匹配行为,并在合理时添加它们,而不是拥有试图成为所有人的所有事物的单体类。

你可以从 这里 下载源代码。

© . All rights reserved.