WPF ComboBox/ListBox with MultiSelect based on Enum with Flags
Asked Answered
D

2

6

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!

Debbi answered 10/10, 2009 at 5:14 Comment(0)
D
6

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).

Depute answered 10/10, 2009 at 6:39 Comment(7)
Cheers, that appears like it would work perfectly! I'll go ahead and try and implement it into my application.Debbi
Egor, I was able to get it working perfectly. I was wondering if I could signal the DataGrid that a row has been modified when a checkbox state has been changed? Right now I'm relying on an IEditableInterface implementation on the bound data to write updates to a backend DB. However, editing the checkboxes doesn't fire this behavior.Debbi
Also, is it possible to change the value shown in the combo box? I noticed the property SelectionBoxItem but it appears this is read-only.Debbi
I believe the value shown in the combobox is the currently selected item (in this case, out of a collection of checkboxes). You might be better served by a template with a header and an expander (you can probably template it to look like a combobox) - that way you don't need to worry about the selected item semantics, and can provide a custom value converter for the flag property to show as your "current" value.Depute
As for your other question - you have to make sure that the correct events get fired off in the correct sequence. If you have access to the source put some break points in the relevant event calling methods, and see what happens when you edit one of the other properties. There are only three events in that interface, and you have to make sure somehow that clicking a checkbox fires them off. I know next to nothing about IEditable, sorry.Depute
I figured I'd probably have to abandon the combo-box. I'll play around with it. Thanks!Debbi
Cheers again, Egor. I managed to get it working with the Expander. The events are still causing confusion. I've asked another question to deal with that. Thanks for your help!Debbi
N
1

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();
    }
}
Niphablepsia answered 7/1, 2020 at 23:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.