Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF ComboBox/ListBox with MultiSelect based on Enum with Flags

So I may be pushing the boundaries just a bit...

Basically I have the following enum, declared in C# code:

[Flags]
public enum FlaggedEnum : int
{
    Option1 = 1,
    Option2 = 2,
    Option3 = 4,
    Option4 = 8,
    ...
    Option16 = 32768,
    None = 0
}

This enum is a member of an object which I have successfully bound to a DataGrid object. Successfully meaning that I have bound all the other fields successfully. :)

What I want to achieve here is a control where all the appropriate options above are checked, that behaves and acts like a ComboBox/ListBox. So you click on the field and a drop-down menu pops up with the ability to "check" whichever options are required.

The control will also have to be able to read from the enum and write an enum.

I'm a WPF novice so I have no idea where to go apart from creating a ComboBox and binding to the column... Any help would be appreciated!

like image 980
sohum Avatar asked Oct 10 '09 05:10

sohum


2 Answers

I have a way that might work. I take no credit for this - I found this method on the web, and forgot to save the address.

In my project I needed to bind a few checkboxes to a flag enum. To help, I found an implementation of a simple value converter to facilitate two way binding. It's not generic, and a single instance of a converter can only work with one target (meaning one instance of a value and its group of checkboxes) at a time. The converter uses a stored reference to the value as a way to convert back, so if you try to reuse it between separate object instances it won't work. That said, this is the only use i had for something like this and it worked like a charm.

The converter:

/// <summary>
/// Provides for two way binding between a TestErrors Flag Enum property and a boolean value.
/// TODO: make this more generic and add it to the converter dictionary if possible
/// </summary>
public class TestActionFlagValueConverter : IValueConverter {
    private TestErrors target;

    public TestActionFlagValueConverter() {

    }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
        TestErrors mask = (TestErrors)parameter;
        this.target = (TestErrors)value;
        return ((mask & this.target) != 0);
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
        this.target ^= (TestErrors)parameter;
        return this.target;
    }
}

In xaml it is used thusly:

<StackPanel.Resources>
    <local:TestActionFlagValueConverter x:Key="TestActionFlagValueConverter"/>
</StackPanel.Resources>

<CheckBox IsChecked="{Binding Errors, Converter={StaticResource TestActionFlagValueConverter}, ConverterParameter={x:Static local:TestErrors.PowerFailure}...
<CheckBox IsChecked="{Binding Errors, Converter={StaticResource TestActionFlagValueConverter}, ConverterParameter={x:Static local:TestErrors.OpenCondition}...

In your case you might place this into your datacell template (though obviously you probably prefer to use a combobox ragther than a simple stackpanel. Make sure to instantiate the converter close to your checkbox group container to make sure they have their own instance of the converter.

Edit:

Here, I made a little test project to demonstrate using this in a combobox with a datagrid, it's based off the default WPF application - just make sure to reference the WPF toolkit.

Here is the Window1.xaml file:

<Window 
    x:Class="FlagEnumTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:Controls="clr-namespace:Microsoft.Windows.Controls;assembly=WPFToolkit"
    xmlns:FlagEnumTest="clr-namespace:FlagEnumTest"
    Title="Window1" Height="300" Width="300">

    <Window.Resources>
        <x:Array Type="{x:Type FlagEnumTest:TestObject}" x:Key="TestArray">
            <FlagEnumTest:TestObject Errors="OpenCondition" />
            <FlagEnumTest:TestObject />
        </x:Array>
    </Window.Resources>

    <StackPanel>

        <Controls:DataGrid ItemsSource="{Binding Source={StaticResource TestArray}}">
            <Controls:DataGrid.Columns>
                <Controls:DataGridTemplateColumn Header="Errors">
                    <Controls:DataGridTemplateColumn.CellTemplate>
                        <DataTemplate>
                            <ComboBox>
                                <ComboBox.Resources>
                                    <FlagEnumTest:TestErrorConverter x:Key="ErrorConverter" />
                                </ComboBox.Resources>
                                <CheckBox Content="PowerFailure" IsChecked="{Binding Path=Errors, Converter={StaticResource ErrorConverter}, ConverterParameter={x:Static FlagEnumTest:TestErrors.PowerFailure}}" />
                                <CheckBox Content="OpenCondition" IsChecked="{Binding Path=Errors, Converter={StaticResource ErrorConverter}, ConverterParameter={x:Static FlagEnumTest:TestErrors.OpenCondition}}" />
                            </ComboBox>
                        </DataTemplate>
                    </Controls:DataGridTemplateColumn.CellTemplate>
                </Controls:DataGridTemplateColumn>
            </Controls:DataGrid.Columns>
        </Controls:DataGrid>

    </StackPanel>
</Window>

And here is the Window1.xaml.cs file codebehind.

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace FlagEnumTest {
    /// <summary>
    /// Interaction logic for Window1.xaml
    /// </summary>
    public partial class Window1 : Window {
        public Window1() {
            InitializeComponent();
        }
    }

    [Flags]
    public enum TestErrors {
        NoError = 0x0,
        PowerFailure = 0x1,
        OpenCondition = 0x2,
    }

    public class TestObject {
        public TestErrors Errors { get; set; }
    } 

    /// <summary>
    /// Provides for two way binding between a TestErrors Flag Enum property and a boolean value.
    /// TODO: make this more generic and add it to the converter dictionary if possible
    /// </summary>
    public class TestErrorConverter : IValueConverter {
        private TestErrors target;

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
            TestErrors mask = (TestErrors)parameter;
            this.target = (TestErrors)value;
            return ((mask & this.target) != 0);
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) {
            this.target ^= (TestErrors)parameter;
            return this.target;
        }
    }

}

By default the datagrid will create its own representation of the column as well as my mandated templated one, so you can see the text representation as well as the checkbox one. The flag enum confuses the default text representation, but you can still see that the binding is working correctly (check both, then uncheck the one you checked last - string value changes to the other one, not 0).

like image 190
Egor Avatar answered Nov 04 '22 05:11

Egor


I have created an IValueConverter that supports binding to the enum directly without codebehind or helper classes. It has only two restrictions:

  • One converter instance has to be used for each source property. If the model contains more properties of the same enum type, each of them needs to use a separate converter instance. This can be done by instantiating the converters in the XAML.
  • When the enum type is a flags enum it has to be an Integer.

The solution is based on the empirical fact that there is always a Convert first before a ConvertBack. And there is always a Convert if a ConvertBack has changed the value. This one only works when the INotifyPropertyChanged is properly implemented on the model. So between the two calls the last known value can be stored in the converter and used in the ConvertBack method.

The converter instance should get the Type of the enum and whether it is a Flags enum or not.

 <EnumToCheckedConverter x:Key="InstanceName" Type="{x:Type MyEnum}" Flags="True" />

Then the checkboxes can be bound using this converter.

 <CheckBox Content="ValueText" IsChecked="{Binding Source, Converter={StaticResource InstanceName}, ConverterParameter=Value}"/>

Radio buttons can be bound by the same mechanism using an instance with Flags="False"

The source code of the converter

public class EnumToCheckedConverter : IValueConverter
{
    public Type Type { get; set; }
    public int? LastValue { get; private set; }
    public bool Flags { get; set; }

    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value != null && value.GetType() == Type)
        {
            try
            {
                var parameterValue = Enum.Parse(Type, parameter as string);

                if (Flags == true)
                {
                    var intParameter = (int)parameterValue;
                    var intValue = (int)value;
                    LastValue = intValue;

                    return (intValue & intParameter) == intParameter;
                }
                else
                {
                    return Equals(parameterValue, value);
                }
            }
            catch (ArgumentNullException)
            {
                return false;
            }
            catch (ArgumentException)
            {
                throw new NotSupportedException();
            }
        }
        else if (value == null)
        {
            return false;
        }

        throw new NotSupportedException();
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value != null && value is bool check)
        {
            if (check)
            {
                try
                {
                    if (Flags == true && LastValue.HasValue)
                    {
                        var parameterValue = Enum.Parse(Type, parameter as string);
                        var intParameter = (int)parameterValue;

                        return Enum.ToObject(Type, LastValue | intParameter);
                    }
                    else
                    {
                        return Enum.Parse(Type, parameter as string);
                    }
                }
                catch (ArgumentNullException)
                {
                    return Binding.DoNothing;
                }
                catch (ArgumentException)
                {
                    return Binding.DoNothing;
                }
            }
            else
            {
                try
                {
                    if (Flags == true && LastValue.HasValue)
                    {
                        var parameterValue = Enum.Parse(Type, parameter as string);
                        var intParameter = (int)parameterValue;

                        return Enum.ToObject(Type, LastValue ^ intParameter);
                    }
                    else
                    {
                        return Binding.DoNothing;
                    }
                }
                catch (ArgumentNullException)
                {
                    return Binding.DoNothing;
                }
                catch (ArgumentException)
                {
                    return Binding.DoNothing;
                }
            }
        }

        throw new NotSupportedException();
    }
}
like image 27
Daniel Leiszen Avatar answered Nov 04 '22 05:11

Daniel Leiszen