使用枚举的 MVVM ComboBox
一个用于将枚举值绑定到 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();
这种方式使事情保持清洁,并避免你必须派生最好单独保留的控件。你可以混合和匹配行为,并在合理时添加它们,而不是拥有试图成为所有人的所有事物的单体类。
你可以从 这里 下载源代码。