C# 动态绑定到枚举
我想在我的ViewModel上有一个枚举,比如说代表一个人的性别。表示ViewModel的视图应该能够提供提供该值的方法;无论是一组单选按钮还是一个组合框(如果有很多)。还有很多例子,你可以在XAML中硬编码单选按钮,每一个按钮都表示它代表的值。更好的方法还将使用Display属性的名称为单选按钮提供文本 我希望能更进一步。我希望它能够根据枚举的值以及DisplayAttribute的名称和描述等动态生成单选按钮。理想情况下,我希望它选择创建一个组合框(而不是单选按钮),如果它有6个以上的项(可能实现为某种控件);但在尝试跑步之前,让我们先看看我们是否能走路 我的谷歌搜索让我非常接近。。。以下是我得到的:C# 动态绑定到枚举,c#,wpf,mvvm,data-binding,C#,Wpf,Mvvm,Data Binding,我想在我的ViewModel上有一个枚举,比如说代表一个人的性别。表示ViewModel的视图应该能够提供提供该值的方法;无论是一组单选按钮还是一个组合框(如果有很多)。还有很多例子,你可以在XAML中硬编码单选按钮,每一个按钮都表示它代表的值。更好的方法还将使用Display属性的名称为单选按钮提供文本 我希望能更进一步。我希望它能够根据枚举的值以及DisplayAttribute的名称和描述等动态生成单选按钮。理想情况下,我希望它选择创建一个组合框(而不是单选按钮),如果它有6个以上的项(可
public enum Gender
{
[Display(Name="Gentleman", Description = "Slugs and snails and puppy-dogs' tails")]
Male,
[Display(Name = "Lady", Description = "Sugar and spice and all things nice")]
Female
}
窗口:
<Window x:Class="WpfApplication2.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApplication2"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:EnumMultiConverter x:Key="EnumMultiConverter"/>
<ObjectDataProvider
MethodName="GetValues"
ObjectType="{x:Type local:EnumDescriptionProvider}"
x:Key="AdvancedGenderTypeEnum">
<ObjectDataProvider.MethodParameters>
<x:Type TypeName="local:Gender"/>
</ObjectDataProvider.MethodParameters>
</ObjectDataProvider>
</Window.Resources>
<StackPanel>
<ItemsControl ItemsSource="{Binding Source={StaticResource AdvancedGenderTypeEnum}}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<RadioButton GroupName="{Binding GroupName}" Content="{Binding Name}" ToolTip="{Binding Description}">
<RadioButton.IsChecked>
<MultiBinding Converter="{StaticResource EnumMultiConverter}" Mode="TwoWay">
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="DataContext.Gender" Mode="TwoWay" />
<Binding Path="Value" Mode="OneWay"/>
</MultiBinding>
</RadioButton.IsChecked>
</RadioButton>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Window>
以及多转换器(因为IValueConverter不能为ConverterParameter进行绑定):
所以我唯一的问题是,我不能回去。但也许有人有一个绝妙的解决办法。正如我所说,理想情况下,我只需要一些神奇的控件,可以绑定到ViewModel上的枚举,并为该枚举的每个值动态创建单选按钮。但是我会采纳我能得到的任何建议。我建议您使用自定义行为,这将使您能够将所有Enum-to-ViewModel逻辑放入一段可重用的代码中。这样,您就不必与复杂的值转换器争吵 有一篇很棒的文章和GitHub示例演示了这个问题的解决方案,请参见下面的链接
我希望这能让你找到你想要的东西,你就快到了,关键是要意识到,当用户点击RadioButton的
命令时,它的事件总是会被触发,即使属性已被选中。您只需将设置为选中
多值绑定单向
,并添加一个在用户选中单选按钮时调用的命令处理程序,例如:
<DataTemplate>
<RadioButton Content="{Binding Name}" ToolTip="{Binding Description}"
Command="{Binding RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}, Path=DataContext.CheckedCommand}"
CommandParameter="{Binding}">
<RadioButton.IsChecked>
<MultiBinding Converter="{StaticResource EnumMultiConverter}" Mode="OneWay">
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="DataContext.Gender" />
<Binding Path="Value" />
</MultiBinding>
</RadioButton.IsChecked>
</RadioButton>
</DataTemplate>
然后回到视图模型中,您为命令提供了一个处理程序,该处理程序手动设置性别值,而不是依靠单选按钮将值传播回自己:
public ICommand CheckedCommand { get { return new RelayCommand<Gender>(value => this.Gender = value); } }
public ICommand checked命令{get{返回新的RelayCommand(value=>this.Gender=value);}
请注意,您甚至不需要组名,它都是根据您在视图模型中绑定到的属性和命令自动处理的(这对于测试目的来说更好)。我最终找到了这篇文章:
如果你仔细看答案,他提出了一个解决方案,并给出了一个链接(现在已断开),然后因为给出了一个可能断开的链接而受到惩罚:)我联系了他,他立即给我发送了信息。例如,它允许我在XAML中使用以下内容:
<local:EnumRadioButton
SelectedItem="{Binding Path=Gender, Mode=TwoWay}"
EnumType="{x:Type local:Gender}"
RadioButtonStyle="{StaticResource MyStyle}"/>
这里是神奇的一点:
public class EnumRadioButton : ItemsControl
{
public static readonly DependencyProperty EnumTypeProperty =
DependencyProperty.Register(nameof(EnumType), typeof(Type), typeof(EnumRadioButton), new PropertyMetadata(null, EnumTypeChanged));
public static readonly DependencyProperty SelectedItemProperty =
DependencyProperty.Register(nameof(SelectedItem), typeof(object), typeof(EnumRadioButton));
public static readonly DependencyProperty RadioButtonStyleProperty =
DependencyProperty.Register(nameof(RadioButtonStyle), typeof(Style), typeof(EnumRadioButton));
public Type EnumType
{
get { return (Type)GetValue(EnumTypeProperty); }
set { SetValue(EnumTypeProperty, value); }
}
public object SelectedItem
{
get { return GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
public Style RadioButtonStyle
{
get { return (Style)GetValue(RadioButtonStyleProperty); }
set { SetValue(RadioButtonStyleProperty, value); }
}
private static void EnumTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
EnumRadioButton enumRadioButton = (EnumRadioButton)d;
enumRadioButton.UpdateItems(e.NewValue as Type);
}
private void UpdateItems(Type newValue)
{
Items.Clear();
if (!newValue.IsEnum)
{
throw new ArgumentOutOfRangeException(nameof(newValue), $"Only enum types are supported in {GetType().Name} control");
}
var enumerationItems = EnumerationItemProvider.GetValues(newValue);
foreach (var enumerationItem in enumerationItems)
{
var radioButton = new RadioButton { Content = enumerationItem.Name, ToolTip = enumerationItem.Description };
SetCheckedBinding(enumerationItem, radioButton);
SetStyleBinding(radioButton);
Items.Add(radioButton);
}
}
private void SetStyleBinding(RadioButton radioButton)
{
var binding = new Binding
{
Source = this,
Mode = BindingMode.OneWay,
Path = new PropertyPath(nameof(RadioButtonStyle))
};
radioButton.SetBinding(StyleProperty, binding);
}
private void SetCheckedBinding(EnumerationItem enumerationItem, RadioButton radioButton)
{
var binding = new Binding
{
Source = this,
Mode = BindingMode.TwoWay,
Path = new PropertyPath(nameof(SelectedItem)),
Converter = new EnumToBooleanConverter(), // would be more efficient as a singleton
ConverterParameter = enumerationItem.Value
};
radioButton.SetBinding(ToggleButton.IsCheckedProperty, binding);
}
}
我发布另一个答案已经有几年了,所以我想我应该发布我采用这种方法的经验的好处,以及一个更新、更好的解决方案
我想用一个控件来表示RadioButton
s的集合,这个想法绝对正确(例如,您可以轻松地在拥有一组单选按钮或一个组合框之间来回切换。但是,在我的另一个回答中,将项的生成塞进该控件是一个错误。允许控件用户将他们喜欢的任何内容绑定到您的控件中要比WPF-y多得多。(当我想要修补在特定时间显示的值时,这也导致了线程问题。)
这个新的解决方案看起来更干净,尽管它(必要时)由相当多的部分组成;但它确实实现了使用单个控件来表示单选按钮集合的目标。例如,您将能够:
<local:EnumRadioButtons SelectedValue="{Binding Gender, Mode=TwoWay}" ItemsSource="{Binding Genders}"/>
我们需要设置默认的样式,但是稍后我会回到这里。让我们看看个人<代码> EnumRadioButton <代码>控件。这里最大的问题是在我原来的问题中提出的一个问题……转换器不能通过<代码>绑定<代码> > <代码> >转换参数< /代码>。这意味着我不能LEA。我需要知道项集合的类型,所以我定义了这个接口来表示每个项
public interface IEnumerationItem
{
string Name { get; set; }
object Value { get; set; }
string Description { get; set; }
bool IsEnabled { get; set; }
}
下面是一个示例实现
using System.Diagnostics;
// I'm making the assumption that although the values can be set at any time, they will not be changed after these items are bound,
// so there is no need for this class to implement INotifyPropertyChanged.
[DebuggerDisplay("Name={Name}")]
public class EnumerationItem : IEnumerationItem
{
public object Value { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public bool IsEnabled { get; set; }
}
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
internal class EnumerationItemProvider : IEnumerationItemProvider
{
public IList<IEnumerationItem> GetValues(Type enumType)
{
var result = new List<IEnumerationItem>();
foreach (var value in Enum.GetValues(enumType))
{
var item = new EnumerationItem { Value = value };
FieldInfo fieldInfo = enumType.GetField(value.ToString());
var obsoleteAttribute = (ObsoleteAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(ObsoleteAttribute));
item.IsEnabled = obsoleteAttribute == null;
var displayAttribute = (DisplayAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(DisplayAttribute));
item.Name = displayAttribute?.Name ?? value.ToString();
item.Description = displayAttribute?.Description ?? value.ToString();
result.Add(item);
}
return result;
}
}
显然,有一些东西可以帮助您创建这些东西是很有用的,所以这里是界面
using System;
using System.Collections.Generic;
public interface IEnumerationItemProvider
{
IList<IEnumerationItem> GetValues(Type enumType);
}
正如您在上面看到的,我们仍然需要一个转换器,您可能已经有了这样一个转换器;但是为了完整性,这里是
using System;
using System.Globalization;
using System.Windows.Data;
public class EnumToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value?.Equals(parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value.Equals(true) ? parameter : Binding.DoNothing;
}
}
剩下的唯一一件事就是为这些控件设置默认样式。(请注意,如果您已经为RadioButton
和ItemsControl
定义了默认样式,那么您需要添加BasedOn
子句。)
希望这能有所帮助。当字典可以同样很好地完成任务时,为什么要使用枚举?当我们有编译时常量时,就使用枚举。在您的情况下,字典
也可以。删除上面的问题我误读了问题,为什么要转换回?您已经有了索引-只需将所选索引绑定到查看模型,您可以将索引(int)强制转换为枚举。@code4life这是一个双向绑定,因此我需要转换器双向工作。是的,我可以向我的Vie添加其他内容
using System.Windows;
using System.Windows.Controls;
public class EnumRadioButtons : ItemsControl
{
public static readonly DependencyProperty SelectedValueProperty =
DependencyProperty.Register(nameof(SelectedValue), typeof(object), typeof(EnumRadioButtons));
public object SelectedValue
{
get { return GetValue(SelectedValueProperty); }
set { SetValue(SelectedValueProperty, value); }
}
}
public interface IEnumerationItem
{
string Name { get; set; }
object Value { get; set; }
string Description { get; set; }
bool IsEnabled { get; set; }
}
using System.Diagnostics;
// I'm making the assumption that although the values can be set at any time, they will not be changed after these items are bound,
// so there is no need for this class to implement INotifyPropertyChanged.
[DebuggerDisplay("Name={Name}")]
public class EnumerationItem : IEnumerationItem
{
public object Value { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public bool IsEnabled { get; set; }
}
using System;
using System.Collections.Generic;
public interface IEnumerationItemProvider
{
IList<IEnumerationItem> GetValues(Type enumType);
}
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
internal class EnumerationItemProvider : IEnumerationItemProvider
{
public IList<IEnumerationItem> GetValues(Type enumType)
{
var result = new List<IEnumerationItem>();
foreach (var value in Enum.GetValues(enumType))
{
var item = new EnumerationItem { Value = value };
FieldInfo fieldInfo = enumType.GetField(value.ToString());
var obsoleteAttribute = (ObsoleteAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(ObsoleteAttribute));
item.IsEnabled = obsoleteAttribute == null;
var displayAttribute = (DisplayAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(DisplayAttribute));
item.Name = displayAttribute?.Name ?? value.ToString();
item.Description = displayAttribute?.Description ?? value.ToString();
result.Add(item);
}
return result;
}
}
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
public class EnumRadioButton : RadioButton
{
private static readonly Lazy<IValueConverter> ConverterFactory = new Lazy<IValueConverter>(() => new EnumToBooleanConverter());
protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.Property == DataContextProperty)
{
SetupBindings();
}
}
/// <summary>
/// This entire method would not be necessary if I could have used a Binding for "ConverterParameter" - I could have done it all in XAML.
/// </summary>
private void SetupBindings()
{
var enumerationItem = DataContext as IEnumerationItem;
if (enumerationItem != null)
{
// I'm making the assumption that the properties of an IEnumerationItem won't change after this point
Content = enumerationItem.Name;
IsEnabled = enumerationItem.IsEnabled;
ToolTip = enumerationItem.Description;
//// Note to self, I used to expose GroupName on IEnumerationItem, so that I could set that property here; but there is actually no need...
//// You can have two EnumRadioButtons controls next to each other, bound to the same collection of values, each with SelectedItem bound
//// to different properties, and they work independently without setting GroupName.
var binding = new Binding
{
Mode = BindingMode.TwoWay,
RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(EnumRadioButtons), 1),
Path = new PropertyPath(nameof(EnumRadioButtons.SelectedValue)),
Converter = ConverterFactory.Value, // because we can reuse the same instance for everything rather than having one for each individual value
ConverterParameter = enumerationItem.Value,
};
SetBinding(IsCheckedProperty, binding);
}
}
}
using System;
using System.Globalization;
using System.Windows.Data;
public class EnumToBooleanConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return value?.Equals(parameter);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value.Equals(true) ? parameter : Binding.DoNothing;
}
}
<DataTemplate x:Key="EnumRadioButtonItem" DataType="{x:Type local:EnumerationItem}">
<local:EnumRadioButton/>
</DataTemplate>
<Style TargetType="local:EnumRadioButton">
<!-- Put your preferred stylings in here -->
</Style>
<Style TargetType="local:EnumRadioButtons">
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="ItemTemplate" Value="{StaticResource EnumRadioButtonItem}"/>
<!-- Put your preferred stylings in here -->
</Style>