我希望在我的ViewModel上有一个Enum,让我们说代表一个人的性别。表示ViewModel的View应该能够提供一种提供该值的方法;这是一组无线电按钮还是一个组合框(如果有很多)。并且有很多例子可以在XAML中硬编码单选按钮,每个单元都说明它代表的值。而较好的也将使用显示属性的名称来提供单选按钮的文本。
我希望更进一步。我希望根据Enum的值以及DisplayAttribute的名称和描述等动态生成 RadioButtons。理想情况下,我喜欢选择创建一个ComboBox(而不是RadioButtons),如果它超过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>
EnumDescriptionProvider:
public static class EnumDescriptionProvider
{
public static IList<EnumerationItem> GetValues(Type enumType)
{
string typeName = enumType.Name;
var typeList = new List<EnumerationItem>();
foreach (var value in Enum.GetValues(enumType))
{
FieldInfo fieldInfo = enumType.GetField(value.ToString());
var displayAttribute = (DisplayAttribute)Attribute.GetCustomAttribute(fieldInfo, typeof(DisplayAttribute));
if (displayAttribute == null)
{
typeList.Add(new EnumerationItem
{
GroupName = typeName,
Value = value,
Name = value.ToString(),
Description = value.ToString()
});
}
else
{
typeList.Add(new EnumerationItem
{
GroupName = typeName,
Value = value,
Name = displayAttribute.Name,
Description = displayAttribute.Description
});
}
}
return typeList;
}
}
EnumerationItem:
public class EnumerationItem
{
public object GroupName { get; set; }
public object Value { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
MultiConverter(因为IValueConverter无法对ConverterParameter进行绑定):
public class EnumMultiConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values[0].Equals(values[1]);
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
}
所以我唯一遇到的问题是我无法进行转换。但也许有人有一个出色的解决方案。正如我所说,理想情况下,我只想要一些神奇的控件,我可以在我的ViewModel上绑定到Enum,并为它动态创建RadioButtons用于该枚举的每个值。但是我会接受任何我能得到的建议。
答案 0 :(得分:1)
我建议您使用自定义行为,这样您就可以将所有Enum to ViewModel逻辑放入一个可重复使用的代码段中。这样你就不必纠缠复杂的ValueConverters
有一篇很棒的文章和GitHub示例演示了解决这个问题的方法,请参阅下面的链接
WPF – Enum ItemsSource With Custom Behavior - Article
GitHub repository for sample code
我希望能为您提供所需的内容
答案 1 :(得分:0)
你几乎就在那里,关键是意识到当用户点击它时,RadioButton的Command
事件总是会被触发,即使绑定了IsChecked
属性也是如此。您需要做的就是创建IsChecked
多值绑定OneWay
并添加一个命令处理程序,在用户选中单选按钮时调用该命令处理程序,例如:
<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>
然后回到视图模型中,为命令提供一个处理程序,它手动设置Gender的值,而不是依靠单选按钮将值传回给自己:
public ICommand CheckedCommand { get { return new RelayCommand<Gender>(value => this.Gender = value); } }
请注意,您甚至不需要GroupName,它们都会根据您在视图模型中绑定的属性和命令自动处理(无论如何都更适合测试目的)。
答案 2 :(得分:0)
我最终发现了这篇文章:How to bind RadioButtons to an enum? 如果你看一下artiom的答案还有很长的路要走,他提出了一个解决方案,然后给予一个链接(现在已经破了),然后再被指责给出一个可能会破坏的链接:)我联系了他,他立即给我发了信息。例如,它允许我在XAML中使用它:
<local:EnumRadioButton
SelectedItem="{Binding Path=Gender, Mode=TwoWay}"
EnumType="{x:Type local:Gender}"
RadioButtonStyle="{StaticResource MyStyle}"/>
所以,不是我在原帖中提到的MultiConverter,你需要:
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;
}
}
这是神奇的一点:
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);
}
}
答案 3 :(得分:0)
距离我发布其他答案已经有好几年了,所以我认为我应该把我从采用这种方法的经验中受益,以及一个更新更好的解决方案。
我绝对有一个正确的想法,就是希望有一个控件来表示RadioButton
的集合(例如,以便您可以在具有一组单选按钮或一个单选按钮之间来回切换。 ComboBox
。但是,在我的另一个答案中,将项目的生成填充到该控件中是一个错误。允许控件的用户将所需的任何内容绑定到控件中,要用的是WPF-y。当我想修改特定时间显示的值时,也会引起线程问题。
这个新解决方案看起来更干净,尽管它(根据需要)由很多部分组成;但是它确实实现了具有单个控件来表示单选按钮集合的目标。例如,您将能够做到:
<local:EnumRadioButtons SelectedValue="{Binding Gender, Mode=TwoWay}" ItemsSource="{Binding Genders}"/>
ViewModel所在的位置...
public ObservableCollection<IEnumerationItem> Genders { get; }
public Gender? Gender
{
get => _gender;
set => SetProperty(ref _gender, value); // common implementation of INotifyPropertyChanged, as seen on ViewModels.
}
所以安顿下来,如果我教你吸鸡蛋,我会带你经过...和道歉。
控件本身基本上是ItemsControl
的扩展,这使它能够包含其他控件的集合。它允许您以与使用ItemsControl一样的方式(通过ItemsPanel
)来控制单个项目的整体布局(例如,如果要使其横向而不是垂直)。
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); }
}
}
我们将需要设置其默认样式;但我稍后再讲。让我们看一下单独的EnumRadioButton
控件。这里最大的问题是我最初提出的问题……转换器无法通过ConverterParameter
来获取Binding
。这意味着我不能将其留给调用者,因此我需要知道项目集合的类型。所以我已经定义了此接口来表示每个项目...
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;
}
}
这个想法是,这将为您提供一个起点,您可以修改项目及其属性(如果需要)。之前,将它们放入ObservableCollection
,然后将其绑定到EnumRadioButtons.ItemsSource
。之后,您可以向集合中添加/从集合中删除项目;但是更改属性不会反映出来(因为我没有实现INotifyPropertyChanged
,因为我不希望在那之后更改它们)。我认为这是合理的;但是如果您不同意,则可以更改实现。
因此,返回到单独的EnumRadioButton
。基本上,它只是一个RadioButton
,它将在设置DataContext
时设置绑定。正如我之前提到的,我们必须以这种方式进行操作,因为ConverterParameter
不能是Binding
,而MultiConverter
不能ConvertBack
与其来源之一。
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);
}
}
}
正如您在上面看到的,我们仍然需要一个Converter,并且您可能已经有一个类似的转换器了;但是为了完整性,这里是...
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
子句。)
<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>
希望这会有所帮助。