C# 枚举标志的模型绑定列表

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/9264927/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-09 06:37:54  来源:igfitidea点击:

Model Bind List of Enum Flags

c#asp.net-mvcasp.net-mvc-3model-bindingenum-flags

提问by Rikon

I have a grid of Enum Flags in which each record is a row of checkboxes to determine that record's flag values. This is a list of notifications that the system offers and the user can pick (for each one) how they want them delivered:

我有一个枚举标志网格,其中每条记录都是一行复选框,以确定该记录的标志值。这是系统提供的通知列表,用户可以选择(针对每个通知)他们希望如何发送:

[Flag]
public enum NotificationDeliveryType
{
  InSystem = 1,
  Email = 2,
  Text = 4
}

I found this articlebut he's getting back a single flag value and he's binding it in the controller like this (with a days of the week concept):

我找到了这篇文章,但他得到了一个单一的标志值,并且他像这样将它绑定到控制器中(使用星期几的概念):

[HttpPost]
public ActionResult MyPostedPage(MyModel model)
{
  //I moved the logic for setting this into a helper 
  //because this could be re-used elsewhere.
  model.WeekDays = Enum<DayOfWeek>.ParseToEnumFlag(Request.Form, "WeekDays[]");
  ...
}

I can't find anywhere that the MVC 3 model binder can handle flags. Thanks!

我找不到 MVC 3 模型绑定器可以处理标志的任何地方。谢谢!

采纳答案by Darin Dimitrov

In general I avoid using enums when designing my view models because they don't play with ASP.NET MVC's helpers and out of the box model binder. They are perfectly fine in your domain models but for view models you could use other types. So I leave my mapping layer which is responsible to convert back and forth between my domain models and view models to worry about those conversions.

一般来说,我在设计我的视图模型时避免使用枚举,因为它们不使用 ASP.NET MVC 的助手和开箱即用的模型绑定器。它们在您的域模型中非常好,但对于视图模型,您可以使用其他类型。所以我离开了我的映射层,它负责在我的域模型和视图模型之间来回转换,以担心这些转换。

This being said, if for some reason you decide to use enums in this situation you could roll a custom model binder:

话虽如此,如果由于某种原因您决定在这种情况下使用枚举,您可以滚动自定义模型绑定器:

public class NotificationDeliveryTypeModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
        if (value != null )
        {
            var rawValues = value.RawValue as string[];
            if (rawValues != null)
            {
                NotificationDeliveryType result;
                if (Enum.TryParse<NotificationDeliveryType>(string.Join(",", rawValues), out result))
                {
                    return result;
                }
            }
        }
        return base.BindModel(controllerContext, bindingContext);
    }
}

which will be registered in Application_Start:

将在 Application_Start 中注册:

ModelBinders.Binders.Add(
    typeof(NotificationDeliveryType), 
    new NotificationDeliveryTypeModelBinder()
);

So far so good. Now the standard stuff:

到现在为止还挺好。现在标准的东西:

View model:

查看型号:

[Flags]
public enum NotificationDeliveryType
{
    InSystem = 1,
    Email = 2,
    Text = 4
}

public class MyViewModel
{
    public IEnumerable<NotificationDeliveryType> Notifications { get; set; }
}

Controller:

控制器:

public class HomeController : Controller
{
    public ActionResult Index()
    {
        var model = new MyViewModel
        {
            Notifications = new[]
            {
                NotificationDeliveryType.Email,
                NotificationDeliveryType.InSystem | NotificationDeliveryType.Text
            }
        };
        return View(model);
    }

    [HttpPost]
    public ActionResult Index(MyViewModel model)
    {
        return View(model);
    }
}

View (~/Views/Home/Index.cshtml):

视图 ( ~/Views/Home/Index.cshtml):

@model MyViewModel
@using (Html.BeginForm())
{
    <table>
        <thead>
            <tr>
                <th>Notification</th>
            </tr>
        </thead>
        <tbody>
            @Html.EditorFor(x => x.Notifications)
        </tbody>
    </table>
    <button type="submit">OK</button>
}

custom editor template for the NotificationDeliveryType(~/Views/Shared/EditorTemplates/NotificationDeliveryType.cshtml):

NotificationDeliveryType( ~/Views/Shared/EditorTemplates/NotificationDeliveryType.cshtml) 的自定义编辑器模板:

@model NotificationDeliveryType

<tr>
    <td>
        @foreach (NotificationDeliveryType item in Enum.GetValues(typeof(NotificationDeliveryType)))
        {
            <label for="@ViewData.TemplateInfo.GetFullHtmlFieldId(item.ToString())">@item</label>
            <input type="checkbox" id="@ViewData.TemplateInfo.GetFullHtmlFieldId(item.ToString())" name="@(ViewData.TemplateInfo.GetFullHtmlFieldName(""))" value="@item" @Html.Raw((Model & item) == item ? "checked=\"checked\"" : "") />
        }
    </td>
</tr>

It's obvious that a software developer (me in this case) writing such code in an editor template shouldn't be very proud of his work. I mean look t it! Even I that wrote this Razor template like 5 minutes ago can no longer understand what it does.

很明显,在编辑器模板中编写此类代码的软件开发人员(在本例中为我)不应该为自己的工作感到非常自豪。我的意思是看它!即使是像 5 分钟前那样编写这个 Razor 模板的我也无法再理解它的作用。

So we refactor this spaghetti code in a reusable custom HTML helper:

所以我们在一个可重用的自定义 HTML 助手中重构这个意大利面代码:

public static class HtmlExtensions
{
    public static IHtmlString CheckBoxesForEnumModel<TModel>(this HtmlHelper<TModel> htmlHelper)
    {
        if (!typeof(TModel).IsEnum)
        {
            throw new ArgumentException("this helper can only be used with enums");
        }
        var sb = new StringBuilder();
        foreach (Enum item in Enum.GetValues(typeof(TModel)))
        {
            var ti = htmlHelper.ViewData.TemplateInfo;
            var id = ti.GetFullHtmlFieldId(item.ToString());
            var name = ti.GetFullHtmlFieldName(string.Empty);
            var label = new TagBuilder("label");
            label.Attributes["for"] = id;
            label.SetInnerText(item.ToString());
            sb.AppendLine(label.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 = htmlHelper.ViewData.Model as Enum;
            if (model.HasFlag(item))
            {
                checkbox.Attributes["checked"] = "checked";
            }
            sb.AppendLine(checkbox.ToString());
        }

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

and we clean the mess in our editor template:

我们清理编辑器模板中的烂摊子:

@model NotificationDeliveryType
<tr>
    <td>
        @Html.CheckBoxesForEnumModel()
    </td>
</tr>

which yields the table:

这产生了表:

enter image description here

在此处输入图片说明

Now obviously it would have been nice if we could provide friendlier labels for those checkboxes. Like for example:

现在显然,如果我们可以为这些复选框提供更友好的标签,那就太好了。例如:

[Flags]
public enum NotificationDeliveryType
{
    [Display(Name = "in da system")]
    InSystem = 1,

    [Display(Name = "@")]
    Email = 2,

    [Display(Name = "txt")]
    Text = 4
}

All we have to do is adapt the HTML helper we wrote earlier:

我们所要做的就是调整我们之前编写的 HTML 帮助程序:

var field = item.GetType().GetField(item.ToString());
var display = field
    .GetCustomAttributes(typeof(DisplayAttribute), true)
    .FirstOrDefault() as DisplayAttribute;
if (display != null)
{
    label.SetInnerText(display.Name);
}
else
{
    label.SetInnerText(item.ToString());
}

which gives us a better result:

这给了我们更好的结果:

enter image description here

在此处输入图片说明

回答by Bitmapped

Darin's code was great but I encountered some trouble using it with MVC4.

Darin 的代码很棒,但我在使用 MVC4 时遇到了一些问题。

In the HtmlHelper extension to create the boxes, I kept getting run-time errors that the model was not an enum (specifically, saying System.Object). I reworked the code to take a Lambda expression and cleaned up this issue using the ModelMetadata class:

在创建框的 HtmlHelper 扩展中,我不断收到模型不是枚举的运行时错误(特别是 System.Object)。我重新编写了代码以获取 Lambda 表达式并使用 ModelMetadata 类解决了这个问题:

public static IHtmlString CheckBoxesForEnumFlagsFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression)
{
    ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
    Type enumModelType = metadata.ModelType;

    // Check to make sure this is an enum.
    if (!enumModelType.IsEnum)
    {
        throw new ArgumentException("This helper can only be used with enums. Type used was: " + enumModelType.FullName.ToString() + ".");
    }

    // Create string for Element.
    var sb = new StringBuilder();
    foreach (Enum item in Enum.GetValues(enumModelType))
    {
        if (Convert.ToInt32(item) != 0)
        {
            var ti = htmlHelper.ViewData.TemplateInfo;
            var id = ti.GetFullHtmlFieldId(item.ToString());
            var name = ti.GetFullHtmlFieldName(string.Empty);
            var label = new TagBuilder("label");
            label.Attributes["for"] = id;
            var field = item.GetType().GetField(item.ToString());

            // Add checkbox.
            var checkbox = new TagBuilder("input");
            checkbox.Attributes["id"] = id;
            checkbox.Attributes["name"] = name;
            checkbox.Attributes["type"] = "checkbox";
            checkbox.Attributes["value"] = item.ToString();
            var model = htmlHelper.ViewData.Model as Enum;
            if (model.HasFlag(item))
            {
                checkbox.Attributes["checked"] = "checked";
            }
            sb.AppendLine(checkbox.ToString());

            // Check to see if DisplayName attribute has been set for item.
            var displayName = field.GetCustomAttributes(typeof(DisplayNameAttribute), true)
                .FirstOrDefault() as DisplayNameAttribute;
            if (displayName != null)
            {
                // Display name specified.  Use it.
                label.SetInnerText(displayName.DisplayName);
            }
            else
            {
                // Check to see if Display attribute has been set for item.
                var display = field.GetCustomAttributes(typeof(DisplayAttribute), true)
                    .FirstOrDefault() as DisplayAttribute;
                if (display != null)
                {
                    label.SetInnerText(display.Name);
                }
                else
                {
                    label.SetInnerText(item.ToString());
                }
            }
            sb.AppendLine(label.ToString());

            // Add line break.
            sb.AppendLine("<br />");
        }                
    }

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

I also extended the model binder so it works with any generic enum type.

我还扩展了模型绑定器,使其适用于任何通用枚举类型。

public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
{
    // Fetch value to bind.
    var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
    if (value != null)
    {
        // Get type of value.
        Type valueType = bindingContext.ModelType;

        var rawValues = value.RawValue as string[];
        if (rawValues != null)
        {
            // Create instance of result object.
            var result = (Enum)Activator.CreateInstance(valueType);

            try
            {
                // Parse.
                result = (Enum)Enum.Parse(valueType, string.Join(",", rawValues));
                return result;
            }
            catch
            {
                return base.BindModel(controllerContext, bindingContext);
            }
        }
    }
    return base.BindModel(controllerContext, bindingContext);
}

You still need to register each enum type in Application_Start but at least this eliminates the need for separate binder classes. You can register it using:

您仍然需要在 Application_Start 中注册每个枚举类型,但至少这消除了对单独的绑定类的需要。您可以使用以下方法注册它:

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

I posted my code on Github at https://github.com/Bitmapped/MvcEnumFlags.

我在 Github 上发布了我的代码https://github.com/Bitmapped/MvcEnumFlags

回答by Dmitry

I use approach described in MVVM Framework.

我使用MVVM Framework 中描述的方法。

 enum ActiveFlags
{
    None = 0,
    Active = 1,
    Inactive = 2,
}

class ActiveFlagInfo : EnumInfo<ActiveFlags>
{
    public ActiveFlagInfo(ActiveFlags value)
        : base(value)
    {
        // here you can localize or set user friendly name of the enum value
        if (value == ActiveFlags.Active)
            this.Name = "Active";
        else if (value == ActiveFlags.Inactive)
            this.Name = "Inactive";
        else if (value == ActiveFlags.None)
            this.Name = "(not set)";
    }
}

   // Usage of ActiveFlagInfo class:
   // you can use collection of ActiveFlagInfo for binding in your own view models
   // also you can use this ActiveFlagInfo as property for your  classes to wrap enum properties

   IEnumerable<ActiveFlagInfo> activeFlags = ActiveFlagInfo.GetEnumInfos(e => 
                    e == ActiveFlags.None ? null : new ActiveFlagInfo(e));

回答by Arithmomaniac

You can try the MVC Enum Flagspackage (available via nuget). It automatically skips zero-valued enum choices, which is a nice touch.

您可以尝试MVC Enum Flags包(可通过nuget 获得)。它会自动跳过零值枚举选项,这是一个不错的选择。

[The following is from the Documentationand its comments; see there if this is not binding properly for you]

[以下来自文档及其评论;如果这对您没有正确约束,请查看那里]

After installation, add the following to Global.asax.cs\Application_Start:

安装后,将以下内容添加到 Global.asax.cs\Application_Start:

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

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

Then in the view, put @using MvcEnumFlagsup top and @Html.CheckBoxesForEnumFlagsFor(model => model.MyEnumTypeProperty)for the actual code.

然后在view中,放上@using MvcEnumFlagstop和@Html.CheckBoxesForEnumFlagsFor(model => model.MyEnumTypeProperty)for的实际代码。

回答by JMS

Bitmapped, you have asked important questions and i can suggest following solution: you should override BindProperty method of your ModelBinder and next need to override model property value:

Bitmapped,您提出了重要问题,我可以建议以下解决方案:您应该覆盖 ModelBinder 的 BindProperty 方法,然后需要覆盖模型属性值:

protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
{
    if (propertyDescriptor.PropertyType.IsEnum && propertyDescriptor.PropertyType.GetCustomAttributes(typeof(FlagsAttribute), false).Any())
    {
        var value = bindingContext.ValueProvider.GetValue(propertyDescriptor.Name);
        if (value != null)
        {
            // Get type of value.
            var rawValues = value.RawValue as string[];
            if (rawValues != null)
            {
                // Create instance of result object.
                var result = (Enum)Activator.CreateInstance(propertyDescriptor.PropertyType);
                try
                {
                    // Try parse enum
                    result = (Enum)Enum.Parse(propertyDescriptor.PropertyType, string.Join(",", rawValues));
                    // Override property with flags value
                    propertyDescriptor.SetValue(bindingContext.Model, result);
                    return;
                }
                catch
                {                               
                }
            }
        }
        base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
    }
    else
        base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}

回答by deadManN

Using Darin and bitmapped code, i write the answer, but didn't worked for me, so first i fixed nullable stuff, then still i had bining issue, found out that there's something wrong with the html, so i loos faith on this answer, search for another, that i found something in a forum of my country, that used the same code as here, but with a very little change, so i merge that with my code, and it all went well, my project use nullable, so i don't know how it gonna work on other places, may need a little fix, but i tried to think about nullable and model being the enum itself.

使用 Darin 和位图代码,我写了答案,但对我没有用,所以首先我修复了可以为空的东西,然后我仍然遇到 bining 问题,发现 html 有问题,所以我对这个答案失去信心, 搜索另一个,我在我所在国家/地区的论坛中找到的东西,使用与此处相同的代码,但更改很少,因此我将其与我的代码合并,一切顺利,我的项目使用可空,所以我不知道它会如何在其他地方工作,可能需要一些修复,但我试图将可空和模型视为枚举本身。

public static class Extensions
{
    public static IHtmlString CheckBoxesForEnumFlagsFor<TModel, TEnum>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TEnum>> expression)
    {
        ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
        Type enumModelType = metadata.ModelType;

        var isEnum = enumModelType.IsEnum;
        var isNullableEnum = enumModelType.IsGenericType &&
                             enumModelType.GetGenericTypeDefinition() == typeof (Nullable<>) &&
                             enumModelType.GenericTypeArguments[0].IsEnum;

        // Check to make sure this is an enum.
        if (!isEnum && !isNullableEnum)
        {
            throw new ArgumentException("This helper can only be used with enums. Type used was: " + enumModelType.FullName.ToString() + ".");
        }

        // Create string for Element.
        var sb = new StringBuilder();

        Type enumType = null;
        if (isEnum)
        {
            enumType = enumModelType;
        }
        else if (isNullableEnum)
        {
            enumType = enumModelType.GenericTypeArguments[0];
        }

        foreach (Enum item in Enum.GetValues(enumType))
        {
            if (Convert.ToInt32(item) != 0)
            {
                var ti = htmlHelper.ViewData.TemplateInfo;
                var id = ti.GetFullHtmlFieldId(item.ToString());

                //Derive property name for checkbox name
                var body = expression.Body as MemberExpression;
                var propertyName = body.Member.Name;
                var name = ti.GetFullHtmlFieldName(propertyName);

                //Get currently select values from the ViewData model
                //TEnum selectedValues = expression.Compile().Invoke(htmlHelper.ViewData.Model);

                var label = new TagBuilder("label");
                label.Attributes["for"] = id;
                label.Attributes["style"] = "display: inline-block;";
                var field = item.GetType().GetField(item.ToString());

                // Add checkbox.
                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);

                //var model = htmlHelper.ViewData.Model as Enum; //Old Code
                if (model != null && model.HasFlag(item))
                {
                    checkbox.Attributes["checked"] = "checked";
                }
                sb.AppendLine(checkbox.ToString());

                // Check to see if DisplayName attribute has been set for item.
                var displayName = field.GetCustomAttributes(typeof(DisplayNameAttribute), true)
                    .FirstOrDefault() as DisplayNameAttribute;
                if (displayName != null)
                {
                    // Display name specified.  Use it.
                    label.SetInnerText(displayName.DisplayName);
                }
                else
                {
                    // Check to see if Display attribute has been set for item.
                    var display = field.GetCustomAttributes(typeof(DisplayAttribute), true)
                        .FirstOrDefault() as DisplayAttribute;
                    if (display != null)
                    {
                        label.SetInnerText(display.Name);
                    }
                    else
                    {
                        label.SetInnerText(item.ToString());
                    }
                }
                sb.AppendLine(label.ToString());

                // Add line break.
                sb.AppendLine("<br />");
            }
        }

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

?

?

public class FlagEnumerationModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        if (bindingContext == null) throw new ArgumentNullException("bindingContext");

        if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName))
        {
            var values = GetValue<string[]>(bindingContext, bindingContext.ModelName);

            if (values.Length > 1 && (bindingContext.ModelType.IsEnum && bindingContext.ModelType.IsDefined(typeof(FlagsAttribute), false)))
            {
                long byteValue = 0;
                foreach (var value in values.Where(v => Enum.IsDefined(bindingContext.ModelType, v)))
                {
                    byteValue |= (int)Enum.Parse(bindingContext.ModelType, value);
                }

                return Enum.Parse(bindingContext.ModelType, byteValue.ToString());
            }
            else
            {
                return base.BindModel(controllerContext, bindingContext);
            }
        }

        return base.BindModel(controllerContext, bindingContext);
    }

    private static T GetValue<T>(ModelBindingContext bindingContext, string key)
    {
        if (bindingContext.ValueProvider.ContainsPrefix(key))
        {
            ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(key);
            if (valueResult != null)
            {
                bindingContext.ModelState.SetModelValue(key, valueResult);
                return (T)valueResult.ConvertTo(typeof(T));
            }
        }
        return default(T);
    }
}

?

?

ModelBinders.Binders.Add(
            typeof (SellTypes),
            new FlagEnumerationModelBinder()
            );
ModelBinders.Binders.Add(
            typeof(SellTypes?),
            new FlagEnumerationModelBinder()
            );