views:

763

answers:

1

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!

+3  A: 

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

Egor
Cheers, that appears like it would work perfectly! I'll go ahead and try and implement it into my application.
sohum
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.
sohum
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.
sohum
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.
Egor
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.
Egor
I figured I'd probably have to abandon the combo-box. I'll play around with it. Thanks!
sohum
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!
sohum