How to Bind Flags Enums To ListBox In MVVM
I want to bind an enum which has flags attribute to a listbox with a check list box item template in mvvm pattern? How can I do this?
[Flags]
public enum SportTypes
{
None = 0,
Baseball = 1,
Basketball = 2,
Football = 4,
Handball = 8,
Soccer = 16,
Volleyball = 32
}
<ListBox Name="checkboxList2"
ItemsSource="{Binding Sports}"
Margin="0,5"
SelectionMode="Multiple">
<ListBox.ItemTemplate&开发者_Go百科gt;
<DataTemplate>
<CheckBox IsChecked="{Binding Path=SportTypeEnum, Converter={StaticResource enumBooleanConverter}}"
Content="{Binding Item}"/>
</DataTemplate>
</ListBox.ItemTemplate>
You can't easily bind the value directly, because the converter can't build the flag combination from a single flag. So you need to manage a collection of flags, and build the combination based on this collection. The difficulty is that the ListBox.SelectedItems
property is readonly. However, this blog post gives a nice workaround, using an attached property.
Here's a complete example using this solution :
Code-behind
[Flags]
public enum MyEnum
{
Foo = 1,
Bar = 2,
Baz = 4
}
public partial class TestEnum : Window, INotifyPropertyChanged
{
public TestEnum()
{
InitializeComponent();
_flags = (MyEnum[])Enum.GetValues(typeof(MyEnum));
_selectedFlags = new ObservableCollection<MyEnum>();
_selectedFlags.CollectionChanged += new System.Collections.Specialized.NotifyCollectionChangedEventHandler(_selectedFlags_CollectionChanged);
this.DataContext = this;
}
void _selectedFlags_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (_selectedFlags.Count == 0)
Value = default(MyEnum);
else
Value = _selectedFlags.Aggregate((v, acc) => acc | v);
}
private MyEnum[] _flags;
public MyEnum[] Flags
{
get { return _flags; }
set
{
_flags = value;
OnPropertyChanged("Flags");
}
}
private ObservableCollection<MyEnum> _selectedFlags;
public ObservableCollection<MyEnum> SelectedFlags
{
get { return _selectedFlags; }
set
{
_selectedFlags = value;
OnPropertyChanged("SelectedFlags");
}
}
private MyEnum _value;
public MyEnum Value
{
get { return _value; }
set
{
_value = value;
OnPropertyChanged("Value");
var currentFlags = _flags.Where(f => _value.HasFlag(f));
var addedFlags = currentFlags.Except(_selectedFlags).ToArray();
var removedFlags = _selectedFlags.Except(currentFlags).ToArray();
foreach (var f in addedFlags)
{
_selectedFlags.Add(f);
}
foreach (var f in removedFlags)
{
_selectedFlags.Remove(f);
}
}
}
private DelegateCommand<MyEnum> _setValueCommand;
public ICommand SetValueCommand
{
get
{
if (_setValueCommand == null)
{
_setValueCommand = new DelegateCommand<MyEnum>(v => Value = v);
}
return _setValueCommand;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
XAML
<Window x:Class="TestPad.TestEnum"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:my="clr-namespace:TestPad"
Title="TestEnum" Height="300" Width="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Text="{Binding Value}" />
<ListBox Grid.Row="1"
ItemsSource="{Binding Flags}"
SelectionMode="Extended"
my:MultiSelectorBehavior.SynchronizedSelectedItems="{Binding SelectedFlags}">
<ListBox.ItemTemplate>
<DataTemplate>
<CheckBox Content="{Binding}" IsChecked="{Binding IsSelected, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ListBoxItem}}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ItemsControl Grid.Row="2"
ItemsSource="{Binding Flags}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Command="{Binding DataContext.SetValueCommand, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=Window}}"
CommandParameter="{Binding}"
Content="{Binding}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Window>
NOTE: for the sake of simplicity, the ViewModel is the Window class. In a real MVVM application, you would of course use a separate ViewModel class...
I see two solutions: One that is fully dynamic, and one that is static. The dynamic solution is a lot of work and IMO not trivial. The static one should be easy:
Create an Panel within your DataTemplate. There in, place for each Flag-Value a CheckBox. Then use the ConverterParameter to specifiy the flag, the converter should use. This would look something like this:
<StackPanel>
<CheckBox IsChecked="{Binding Path=SportTypeEnum, Converter={StaticResource enumBooleanConverter},ConverterParameter={x:Static local:SportTypes.Baseball}}" Content="Baseball"/>
<CheckBox IsChecked="{Binding Path=SportTypeEnum, Converter={StaticResource enumBooleanConverter},ConverterParameter={x:Static local:SportTypes.Basketball}}" Content="Basketball"/>
<CheckBox IsChecked="{Binding Path=SportTypeEnum, Converter={StaticResource enumBooleanConverter},ConverterParameter={x:Static local:SportTypes.Football}}" Content="Football"/>
<CheckBox IsChecked="{Binding ..../>
</StackPanel/>
In your converter you only have to do some logical AND-comparisons and you will have what you're looking for. If you're interested in the dynamic solution, make a comment, I can give you some ideas where to start. But IMO this will really not be trivial.
Additonal info
If you want to have a list instead of the StackPanel, use a ScrollViewer in a Border or even a ListBox.
To expand on Chris's post, here's a more thorough explanation of how you can do this.
This isn't the most ideal scenario, as the property holding the enum has to be a bit more complex than usual, but it works.
Converter code:
public class EnumFlagConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
var theEnum = value as Enum;
return theEnum.HasFlag(parameter as Enum);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
var theEnum = parameter as Enum;
return theEnum;
}
}
XAML for converter:
<StackPanel>
<StackPanel.Resources>
<local:EnumFlagConverter x:Key="MyConverter" />
</StackPanel.Resources>
<CheckBox IsChecked="{Binding Path=SportTypeEnum, Converter={StaticResource MyConverter},ConverterParameter={x:Static local:SportTypes.Baseball}}" Content="Baseball"/>
<CheckBox IsChecked="{Binding Path=SportTypeEnum, Converter={StaticResource MyConverter},ConverterParameter={x:Static local:SportTypes.Basketball}}" Content="Basketball"/>
<CheckBox IsChecked="{Binding Path=SportTypeEnum, Converter={StaticResource MyConverter},ConverterParameter={x:Static local:SportTypes.Football}}" Content="Football"/>
<CheckBox IsChecked="{Binding ..../>
</StackPanel>
Then, to bind to this, I did a bit of trickery to get the converter to work properly:
private SportTypeEnum _TheSportType;
public SportTypeEnum _TheSportType
{
get { return _TheSportType; }
set
{
if (_TheSportType.HasFlag(value))
_TheSportType &= ~value;
else
_TheSportType |= value;
NotifyPropertyChanged();
}
}
Because of this special setter logic, you probably want to include a method like this to allow you to fully set the value from code:
private void ResetTheSportType()
{
_TheSportType = _TheSportType.None;
NotifyPropertyChanged(() => TheSportType);
}
精彩评论