ASP.NET MVC – Flag Enum als Checkbox Liste Binden

In ASP.NET gibt es einige nützliche Hilfsmittel, um HTML-Objekte an ein Model zu binden. So wird aus „@Html.TextBoxFor(m => m.MyProperty)“ zum Beispiel eine TextBox, welche die Daten beim Absenden der Form automatisch in die entsprechende Property des Objektes schreibt und auch ausliest. Für was es allerdings keine vorgefertigte Möglichkeit gibt, ist das Binden von FlagEnum’s, sprich Enum Datentypen, die eine Mehrfachauswahl über das Flag-Attribut zulassen. Um eine solche Funktionalität zu bekommen, muss man aktuell selbst tätig werden und zwei Klassen erstellen, die dies erlauben. Hierfür braucht man einen „ModelBinder“ sowie eine Erweiterung für die Html-Klasse. Natürlich sollte man sich aber zuvor bereits darüber im Klaren sein, wie das Flag-Attribute bei einem Enum das Verhalten beeinflusst. 

Implementierung des ModelBinder

Für den ModelBinder benötigen wir Referenzen auf „System“ sowie auf „System.Web.Mvc“. Die Klasse an sich ist recht schlank und benötigt auch nicht sonderlich viel Inhalt.

public class EnumFlagsModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var providerResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (providerResult != null)
        {
            Type valueType = bindingContext.ModelType;
            if (valueType.IsGenericType && valueType.GetGenericTypeDefinition() == typeof(Nullable<>))
                valueType = valueType.GetGenericArguments().First();

            var rawValues = providerResult.RawValue as string[];
            if (rawValues != null)
            {
                try
                {
                    // ReSharper disable once RedundantAssignment
                    var instance = (Enum)Activator.CreateInstance(valueType);
                    instance = (Enum)Enum.Parse(valueType, string.Join(",", rawValues));

                    return instance;
                }
                catch
                {
                    // ignored
                }
            }
        }

        return base.BindModel(controllerContext, bindingContext);
    }
}

Sollte das Enum am Ende im Model nicht wie gewünscht ankommen, trägt die Zeile „instance = (Enum)Enum.Parse(valueType, string.Join(„,“, rawValues));“ gut zum Debugging bei. Hierbei sollten in „rawValues“ alle in den Checkboxen ausgewählten Werte enthalten sein, damit die Enum-Instanz korrekt erstellt wird. Sind in „rawValues“ alle Werte wie gewünscht vorhanden und das resultierende Enum ist dennoch falsch, stimmt vermutlich etwas mit der Werte-Bestückung im Enum nicht.

Implementierung der Html-Erweiterung

Für die Haupt-Implementierung, die Html-Erweiterung, haben wir ein paar Dinge zu berücksichtigen. Etwaige Lokalisierungen könnten hier über das „Display“-Attribut verwendet werden. Auch sollte es möglich sein, CSS-Klassen an der Generierung weitergeben zu können. Oder was ist, wenn das Flag Enum Nullable sein muss (dies mussten wir auch schon im ModelBinder berücksichtigen, siehe „valueType.GetGenericTypeDefinition() == typeof(Nullable<>)“)?

public static class HtmlExtensions
{
    public static IHtmlString CheckBoxesForEnumFlagsFor&lt;TModel, TEnum&gt;(this HtmlHelper&lt;TModel&gt; htmlHelper, Expression&lt;Func&lt;TModel, TEnum&gt;&gt; expression, object htmlAttributesCheckbox = null, object htmlAttributesLabel = null)
    {
        ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
        Type modelType = metadata.ModelType;

        if (modelType.IsGenericType &amp;&amp; modelType.GetGenericTypeDefinition() == typeof (Nullable&lt;&gt;))
            modelType = modelType.GetGenericArguments().First();

        if (!modelType.IsEnum)
            throw new ArgumentException(
<pre wp-pre-tag-1="">

quot;This helper can only be used with enums. Type used was: {modelType.FullName}.“);

var stringBuilder = new StringBuilder();
int counter = 0;

foreach (Enum item in Enum.GetValues(modelType))
{
if (Convert.ToInt32(item) != 0)
{
var templateInfo = htmlHelper.ViewData.TemplateInfo;
var id = templateInfo.GetFullHtmlFieldId(item.ToString());
var name = templateInfo.GetFullHtmlFieldName(metadata.PropertyName);

var label = new TagBuilder(„label“);
if (htmlAttributesLabel != null)
{
var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributesLabel);
label.MergeAttributes(attributes);
}

var field = item.GetType().GetField(item.ToString());

var checkbox = new TagBuilder(„input“);
checkbox.Attributes[„id“] = id;
checkbox.Attributes[„name“] = name;
checkbox.Attributes[„type“] = „checkbox“;
checkbox.Attributes[„value“] = item.ToString();

var model = metadata.Model as Enum;
if (model != null && model.HasFlag(item))
checkbox.Attributes[„checked“] = „checked“;

if (htmlAttributesCheckbox != null)
{
var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributesCheckbox);
checkbox.MergeAttributes(attributes);
}

var displayName = field.GetCustomAttributes(typeof(DisplayNameAttribute), true).FirstOrDefault() as DisplayNameAttribute;
if (displayName != null)
{
label.InnerHtml = checkbox + displayName.DisplayName;
}
else
{
var display = field.GetCustomAttributes(typeof(DisplayAttribute), true).FirstOrDefault() as DisplayAttribute;
label.InnerHtml = checkbox + (display != null ? display.GetName() : item.ToString());
}

stringBuilder.AppendLine(label.ToString());
stringBuilder.AppendLine(„<br />“);
}
}

return new HtmlString(stringBuilder.ToString());
}
}

Viel Quellcode für viel Wirkung. Berücksichtigt wurde hierbei eine Lokalisierung der Enum-Member durch das „Display“-Attribut, aber auch das Prüfen auf Nullable Typen. Die zusätzlichen Html-Attribute können über zwei Parameter getrennt für das Label und für die Checkbox angegeben werden. Die Ausgabe von allem wäre ein Label, welches eine Checkbox beinhaltet (um möglichst nahe an Bootstrap zu kommen).

Ein Beispiel Flag Enum

Hier ist ein Beispiel für ein gültiges Flag Enum zur Orientierung. Als Kommentar stehen immer die Bit-Werte der Hex-Werte hinter dem jeweiligen Wert.

[Flags]
public enum MyEnumType
{
    Value001 = 0x01, // 0000 0001
    Value002 = 0x02, // 0000 0010
    Value003 = 0x04, // 0000 0100
    Value004 = 0x08, // 0000 1000
    Value005 = 0x10, // 0001 0000
    Value006 = 0x20  // 0010 0000
}

Die Verwendung

Um das Ganze zu verwenden, benötigen wir zwei Angaben. Zunächst benötigen wir im „Application_Start“ unserer „Global“-Klasse das Binden zwischen unserem Flag Enum Typen und unserem ModelBinder. Dies geschieht mit folgender Zeile:

ModelBinders.Binders.Add(typeof(MyEnumType), new EnumFlagsModelBinder());

Noch ein kleiner Hinweis: Ist die Property in unserem Model später Nullable, muss dies auch hier als Nullable angegeben werden! Anschließend brauchen wir an der Stelle der Verwendung folgende Zeile:

@Html.CheckBoxesForEnumFlagsFor(m => m.MyProperty, htmlAttributesLabel: new { @class = "checkbox-inline col-sm-4" })

Wobei „m“ hier unser Model ist. Nützliche Erweiterungen sind z.B. die Möglichkeit, dass der „Splitter“, welcher die einzelnen Checkboxen von einander trennt von „<br />“ abweichen können sollte. Dies muss dann natürlich auch in der Html-Erweiterung implementiert werden. Auch wäre es evtl. hilfreich diesen „Splitter“ nur alle X-Einträge zu verwenden. Auch hierfür wäre ein zusätzlicher Parameter sehr hilfreich.

Schlusswort

Ich hoffe dies löst für einige das Problem, welches mich die letzten paar Stunden beschäftigt hat (und an einigen Stellen wurde auch die eigene Intelligenz angezweifelt, als das Ganze wieder neue Fehler hervorbrachte ^^). Im Prinzip sehr nützlich und hilfreich, wenn mehrere Flag Enum Typen verwendet werden.