Better Enum support

Topics: Feature requests
Aug 17, 2011 at 11:29 AM
Edited Aug 17, 2011 at 11:33 AM

There is missing a Widget to make a selection based on a Enum type. To make this we would need to create to functions, one that can return an IEnumerable<XElement> based on an enum-type, and the widget itself that renderes a dropdownlist with the options from the enum.

A quick and dirty implementation is as folliows, but would of course be nicer when supported by the core:

The widget

 

public WidgetFunctionProvider getEnumSelector<T>()
        {
            string xml = "<f:widgetfunction xmlns:f=\"http://www.composite.net/ns/function/1.0\" name=\"Composite.Widgets.Selector\" label=\"\" bindingsourcename=\"\">";
            xml += "    <f:helpdefinition xmlns:f=\"http://www.composite.net/ns/function/1.0\" helptext=\"\" />";
            xml += "    <f:param xmlns:f=\"http://www.composite.net/ns/function/1.0\" name=\"Options\">";
            xml += "        <f:function xmlns:f=\"http://www.composite.net/ns/function/1.0\" name=\"Acto.EnumEnumerator\">";
            xml += "            <f:param name=\"type\" value=\""+ typeof(T).FullName +"\" />";
            xml += "        </f:function>";
            xml += "    </f:param>";
            xml += "    <f:param name=\"KeyFieldName\" value=\"name\" />";
            xml += "    <f:param name=\"LabelFieldName\" value=\"name\" />";
            xml += "</f:widgetfunction>";

            return new WidgetFunctionProvider(XElement.Parse(xml));
        }

 

The function, exposing a single parameter as TextBox

 

public override IEnumerable<ParameterProfile> ParameterProfiles
        {
            get
            {
                return new List<ParameterProfile>()
                {
                    new ParameterProfile("type", typeof(string), true, new NoValueValueProvider(), StandardWidgetFunctions.TextBoxWidget, "Enum type", new HelpDefinition("Type of the enum")),
                };
            }
        }

 

public override object Execute(ParameterList parameters, FunctionContextContainer context)
        {
            var type = parameters.GetParameter<string>("type");
            var enumType = Type.GetType(type);

            var list = new List<XElement>();

            foreach (var name in Enum.GetNames(enumType))
            {
                list.Add(new XElement("enum", new XAttribute("name", name)));
            }

            return list;
        }

Now i can have a function which uses this new Enum-widget in a parameter
public override IEnumerable<ParameterProfile> ParameterProfiles
        {
            get
            {
                return new List<ParameterProfile>()
                {
                    new ParameterProfile("mode", typeof(MyEnum), false, new ConstantValueProvider(MyEnum.AnEnumValue), getEnumSelector<MyEnum>(), "Mode", new HelpDefinition("Mode help"))
                };
            }
        }

Aug 29, 2011 at 1:09 PM

Just out of curiosity, what strings do your enum contain and are the drop down going to be used by developers or users?

Aug 29, 2011 at 1:27 PM
Edited Aug 29, 2011 at 1:28 PM

I've seen (too) many functions in implemented sites, where the source of ie. a dropdownlist is just a comma-separated string. And then, when the function is executed, this parameter is passed through a simple switch-case checking for simple strings, throwing type-safety out the window. Plus, adding new values, requires changes to both the function itself and the parameter, introducing margin for misspellings etc.

The purpose of enums are in a type safe manner to select and pass predefined values, instead of using "magic strings" or "magic numbers". This should be encouraged and be as easy as possible to use.

Oct 2, 2012 at 9:31 AM
Edited Oct 3, 2012 at 3:08 AM

I tried immensely to get this code sample to work to no avail, i think enums are very useful not just for preventing typos but also helping to see what the options are.

 

Because im doing master page based work, i got pointed to v4 so i tried to get enums in v4 and this is the result of that work, a generic enum provider, the only changes needed are to the attribute on the property required, to test it out put in an ascx file in the App_Data\UserControls folder and add the function to a page.

 

I'm not sure if there is a better way perhaps one day enums will be supported natively.

Sorry the code is in VB.Net, thats my preferred language, and this is still only proof of concept, there are probably some easy enhancements but since i just threw it together into a ascx file some functionality wasnt available (like extension methods etc).

If the Shared method for getting the data for the provider supported parameters (such as the propertyInfo) like the WidgetFactoryMethod did, the CompositeProviderHelper wouldn't have to be a generic and the enum type wouldn't have to be set on the attribute itself. 

Even though its noted in the code comment the EnumHelper code is highly based off http://codereview.stackexchange.com/a/12178  i just made the ToList and the EnumItem struct to enable the binding to the DDL.

 

Hopefully other people find this helpful and maybe this functionality can be migrated into the core in the future (though hopefully better written than this :P )


EDIT: Fixed build error
<%@ Control Language="VB" Inherits="Composite.AspNet.UserControlFunction" %>

<script runat="server">
    'No default value, use the default enum value
    <Composite.Functions.FunctionParameter(DefaultValue:=Nothing, Help:="Help Value", Label:="Label Value", Name:="Name Value", WidgetFactoryClass:=GetType(CompositeProviderHelper(Of TestEnum)), WidgetFactoryMethod:="GetEnumProvider")>
    Public Property TestProperty() As TestEnum
    'Use Val3 as the default enum value
    <Composite.Functions.FunctionParameter(DefaultValue:="Val3", Help:="Help Value 2", Label:="Label Value 2", Name:="Name Value 2", WidgetFactoryClass:=GetType(CompositeProviderHelper(Of TestEnum)), WidgetFactoryMethod:="GetEnumProvider")>
    Public Property TestProperty2() As TestEnum
    Public Enum TestEnum
        <System.ComponentModel.Description("Value 1")>
        Val1
        <EnumHelper.IgnoreEnumAttribute>
        Val2 'we dont want this showing up in the list for some reason
        Val3
    End Enum

    'Public Property EnumProp() As TestEnum
    Public Overrides ReadOnly Property FunctionDescription As String
        Get
            Return "This is a test function"
        End Get
    End Property
      
    Sub loaded(sender As Object, e As EventArgs) Handles MyBase.Load
        Label1.Text = Me.TestProperty.ToString
    End Sub
    
    ''' <summary>
    ''' Helpers for Composite C1 Providers
    ''' </summary>
    ''' <typeparam name="T"></typeparam>
    ''' <remarks></remarks>
    Public Class CompositeProviderHelper(Of T)
        Public Shared Function GetEnums() As IEnumerable
            Dim typ As Type = GetType(T)
            If typ.IsEnum Then
                Return EnumHelper.ToEnumItemList(typ)
            Else
                Return New String() {}
            End If
        End Function
            
        Public Shared Function GetEnumProvider(prop As System.Reflection.PropertyInfo) As Composite.Functions.WidgetFunctionProvider
            'Create DDL of Enums Name as values and Display Names (or regular name if not available) as display values
            Return Composite.Functions.StandardWidgetFunctions.DropDownList(GetType(CompositeProviderHelper(Of T)), "GetEnums", "Name", "Display", False, True)
        End Function
    End Class
    
    ''' <summary>
    ''' Adapted From: http://codereview.stackexchange.com/a/12178
    ''' </summary>
    ''' <remarks></remarks>
    Public Class EnumHelper
        Private Sub New()
        End Sub
        ''' <summary>
        ''' Makes the <see cref="ToDictionary"/> and <see cref="ToEnumItemList" /> ignore the enum.
        ''' </summary>
        ''' <remarks></remarks>
        Public Class IgnoreEnumAttribute
            Inherits Attribute
        End Class
        ''' <summary>
        '''Returns the Description attribute value if available or the enum name if no description is available.
        ''' </summary>
        ''' <param name="value">The enum value to get the display name of.</param>
        Public Shared Function GetDisplayName(value As [Enum]) As String
            If value Is Nothing Then
                Throw New ArgumentNullException("value")
            End If

            Dim name As String = value.ToString()
            Dim fieldInfo = value.[GetType]().GetField(name)
            If fieldInfo Is Nothing Then
                Return ""
            End If
            Dim attributes = DirectCast(fieldInfo.GetCustomAttributes(GetType(System.ComponentModel.DescriptionAttribute), False), System.ComponentModel.DescriptionAttribute())

            If attributes.Length > 0 Then
                name = attributes(0).Description
            End If

            Return name
        End Function
        ''' <summary>
        ''' Checks to see if the enum shoudl be ignored.
        ''' </summary>
        ''' <param name="value">The enum value to check.</param>
        Public Shared Function IsIgnored(value As [Enum]) As Boolean
            If value Is Nothing Then
                Throw New ArgumentNullException("value")
            End If

            Dim name As String = value.ToString()
            Dim fieldInfo = value.[GetType]().GetField(name)

            Return fieldInfo Is Nothing OrElse fieldInfo.IsDefined(GetType(IgnoreEnumAttribute), True)
        End Function

        Private Shared Function GetEnums(enumType As Type) As IEnumerable(Of Object)
            If enumType Is Nothing Then
                Throw New ArgumentNullException("enumType")
            ElseIf Not enumType.IsEnum Then
                Throw New ArgumentException("Type must be an enum", "enumType")
            End If

            Return [Enum].GetValues(enumType).Cast(Of Object)().Where(Function(f) Not IsIgnored(DirectCast(f, [Enum])))
        End Function
        ''' <summary>
        ''' Converts the enum into a dictionary.
        ''' </summary>
        Public Shared Function ToDictionary(enumType As Type) As Dictionary(Of Integer, String)
            Return GetEnums(enumType).ToDictionary(Function(k) CInt(k), Function(v) GetDisplayName(DirectCast(v, [Enum])))
        End Function
        Public Shared Function ToEnumItemList(enumType As Type) As Generic.List(Of EnumItem)
            Return GetEnums(enumType).Select(Function(en) New EnumItem() With {.Display = GetDisplayName(DirectCast(en, [Enum])), .Value = CInt(en), .Name = en.ToString}).ToList
        End Function
        Public Structure EnumItem
            ''' <summary>
            ''' The display name of the enum.
            ''' </summary>
            Property Display As String
            ''' <summary>
            ''' The actual name of the enum
            ''' </summary>
            Property Name As String
            ''' <summary>
            ''' The integer value of the enum
            ''' </summary>
            Property Value As Integer
        End Structure
        
    End Class
    
</script>
<asp:Label ID="Label1" runat="server" Text="Label"></asp:Label>