3

I've got an enum of types of data that might be shown in a .NET Forms control and I want to provide an interface for consumers of the control to filter some of the types (set some of the flags). A bit field seems the logical way to do this, unfortunately, the enum starts at 0 rather than 1 (0, 1, 2, 4, 8, ...) and can't be changed.

How can I expose this set of flags so that it can be easily configured programmatically or through the Visual Studio designer?

dmo
  • 3,993
  • 6
  • 35
  • 39

2 Answers2

4

You would need to write a UITypeEditor to do the work, and associate it with the property via [EditorAttribute].

edit now with example - a fairly long one, I'm afraid - but most of the code can be shared between types, fortunately.

You can't use a single composite enum value because of the zero - so here I'm using a HashSet<T> to hold the selected enums - fairly easy to re-work to List<T> if you have .NET 2.0/3.0, though.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing.Design;
using System.Text;
using System.Windows.Forms;
using System.Windows.Forms.Design;

public class MyControl : UserControl
{
    public MyControl()
    {
        Values = new HashSet<MyEnum>();
    }
    [Editor(typeof(MyEnumSetEditor), typeof(UITypeEditor))]
    [TypeConverter(typeof(MyEnumSetConverter))]
    public HashSet<MyEnum> Values { get; set; }
}

public enum MyEnum
{  // numbers as per the question...
    A = 0, B = 1, C = 2, D = 4, E = 8
}
class MyEnumSetEditor : EnumSetEditor<MyEnum> { }
class MyEnumSetConverter : EnumSetConverter<MyEnum> { }

// from here down is shared between types
abstract class EnumSetConverter<T> : TypeConverter where T : struct
{
    public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
    {
        return destinationType == typeof(string) || base.CanConvertTo(context, destinationType);
    }
    public override object ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value, Type destinationType)
    {
        if(destinationType == typeof(string))
        {
            HashSet<T> set = (HashSet<T>)value;
            if (set == null) return "(null)";

            StringBuilder sb = new StringBuilder();
            foreach (T item in Enum.GetValues(typeof(T)))
            {
                if (set.Contains(item))
                {
                    if (sb.Length > 0) sb.Append(", ");
                    sb.Append(item);
                }
            }
            return sb.ToString();
        }
        return base.ConvertTo(context, culture, value, destinationType);
    }
}

public abstract class EnumSetEditor<T> : UITypeEditor where T : struct
{
    public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
    {
        return UITypeEditorEditStyle.DropDown;
    }
    public override bool IsDropDownResizable
    {
        get { return true; }
    }
    public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
    {
        IWindowsFormsEditorService svc = (IWindowsFormsEditorService)
            provider.GetService(typeof(IWindowsFormsEditorService));
        HashSet<T> set = value as HashSet<T>;
        if (svc != null && set != null)
        {
            UserControl ctrl = new UserControl();
            CheckedListBox clb = new CheckedListBox();
            clb.Dock = DockStyle.Fill;
            Button btn = new Button();
            btn.Dock = DockStyle.Bottom;
            foreach (T item in Enum.GetValues(typeof(T)))
            {
                clb.Items.Add(item, set.Contains(item));
            }
            ctrl.Controls.Add(clb);
            ctrl.Controls.Add(btn);
            btn.Text = "OK";
            btn.Click += delegate
            {
                set.Clear();
                foreach (T item in clb.CheckedItems)
                {
                    set.Add(item);
                }
                svc.CloseDropDown();
            };
            svc.DropDownControl(ctrl);
        }

        return value;
    }
}
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
0

I had the same problem: the editor control appeared to work, but the values wouldn't persist. Building on Marc's answer and some further research I've now got it running. Although I needed this feature for editing my own controls at DesignTime, I tested it using a demo project with a PropertyGrid control after reading the following quote from Microsoft:

When you develop your custom UITypeEditor, it is recommended that you set the build number to increment with each build. This prevents older, cached versions of your UITypeEditor from being created in the design environment.

I think that was actually an issue that caused problems while trying to implement Marc's solution. Testing in a separate project is also helpful because you can use Step-By-Step-Debugging to see what happens in the control, editor and converter.

The basic steps are:

  • Create the type (here: MyFlags) and the property (here: MyProperty), adding some extra attributes to the latter.
  • Implement the control (here: EnumEditorControl) to be used for editing.
  • Implement an UITypeEditor class (here: EnumEditor); this is connected to the property using the Editor attribute. It is also creates and manages the instance of our EnumEditorControl.
  • Implement a TypeConverter class (here: EnumConverter); this is also connected to the property using an attribute (TypeConverter) and handles converting the value into a string for display in the property grid.

And now a walkthrough with the relevant code snippets:

  1. Define an Enum using the Flags attribute.

    <Flags>
    Public Enum MyFlags
        Flag1 = 2 ^ 0
        Flag2 = 2 ^ 1
        Flag3 = 2 ^ 2
    End Enum
    
  2. Define the property for the custom control.

    Public Property MyProperty() As MyFlags
        ...
    End Property
    

This property will later have attributes added to it once we've created the other components we need (see #6).

  1. Create the desired control that is to be used for editing the property.

    Imports System.Windows.Forms.Design
    Public Class EnumEditorControl(Of T As Structure)
        Public Sub New(value As Long, editorService As IWindowsFormsEditorService)
            'This call is required by the Windows.Forms Form Designer.
            InitializeComponent()
    
            _value = value
            _editorService = editorService
    
            For Each item As Long In [Enum].GetValues(GetType(T))
                Me.CheckedListBox1.Items.Add([Enum].GetName(GetType(T), item), (_value And item) = item)
            Next
    
        End Sub
    
        Private _value As Long
        Public Property Value As Long
            Get
                Return _value
            End Get
            Set(value As Long)
                _value = value
            End Set
        End Property
        Private _editorService As IWindowsFormsEditorService
    
        Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
            Dim v As Long = 0
            For Each item As String In Me.CheckedListBox1.CheckedItems
                v = (v Or [Enum].Parse(GetType(T), item))
            Next
            _value = v
            Me._editorService.CloseDropDown()
        End Sub
    End Class
    

The above is the class' code without the designer.vb portions. The important parts are: a constructor with value and editorService parameters, filling the control with the currently selected values (in the constructor) and implementing a "commit action" where the Value property of the control is updated (depending on the selection made) and finally calling Me._editorService.CloseDropDown().

  1. Define a generic UITypeEditor.

    Imports System.ComponentModel
    Imports System.Drawing.Design
    Imports System.Windows.Forms.Design
    
    Public Class EnumEditor(Of T As Structure)
        Inherits UITypeEditor
    
        Public Overrides Function GetEditStyle(context As ITypeDescriptorContext) As UITypeEditorEditStyle
            Return UITypeEditorEditStyle.DropDown
        End Function
        Public Overrides ReadOnly Property IsDropDownResizable() As Boolean
            Get
                Return True
            End Get
        End Property
        Public Overrides Function EditValue(context As ITypeDescriptorContext, provider As IServiceProvider, value As Object) As Object
            Dim svc As IWindowsFormsEditorService = DirectCast(provider.GetService(GetType(IWindowsFormsEditorService)), IWindowsFormsEditorService)
            Dim items As Long = CLng(value)
            If svc IsNot Nothing Then
                Dim c As New EnumEditorControl(Of T)(value, svc)
    
                svc.DropDownControl(c)
    
                value = c.Value
            End If
    
            Return CType(value, T)
        End Function
    End Class
    

The core part here is the override of EditValue where an instance of our editor control (see #3) is created and shown using svc.DropDownControl(c). And, finally, retrieving the selected value from our control's property and returning it.

  1. Define a generic TypeConverter used for converting the actual value into a string representation to be used in the property explorer. Imports System.ComponentModel Imports System.Drawing.Design Imports System.Text Imports System.Windows.Forms.Design

    Class EnumConverter(Of T As Structure)
        Inherits TypeConverter
    
        Public Overrides Function CanConvertTo(context As ITypeDescriptorContext, destinationType As Type) As Boolean
            Return destinationType = GetType(String) OrElse MyBase.CanConvertTo(context, destinationType)
        End Function
        Public Overrides Function ConvertTo(context As ITypeDescriptorContext, culture As System.Globalization.CultureInfo, value As Object, destinationType As Type) As Object
            If destinationType = GetType(String) Then
                Dim items As Integer = CLng(value)
    
                If items = 0 Then
                    Return ""
                End If
    
                Dim values As New List(Of String)
    
                For Each item As Integer In [Enum].GetValues(GetType(T))
                    If (items And item) = item Then
                        values.Add([Enum].GetName(GetType(T), item))
                    End If
                Next
                Return String.Join(", ", values)
            End If
    
            Return MyBase.ConvertTo(context, culture, value, destinationType)
        End Function
    End Class
    
  2. Tie it all together by adding the following attributes to the defined property (see #2).

    <Browsable(True),
    DefaultValue(MyFlags.Flag1),
    Editor(GetType(EnumEditor(Of MyFlags)), 
    GetType(System.Drawing.Design.UITypeEditor)),
    TypeConverter(GetType(EnumConverter(Of MyFlags)))
    >
    Public Property MyProperty() As MyFlags
        ...
    End Property
    

Note: Referring to the "zero value" mentioned in the question: this solution does not take that into account, but it could easily be modified to do so.

mike
  • 1,627
  • 1
  • 14
  • 37