【问题标题】:Model Bind List of Enum Flags枚举标志的模型绑定列表
【发布时间】:2012-03-05 02:45:36
【问题描述】:

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

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

我找到了这个article,但他得到了一个标志值,并且他像这样将它绑定在控制器中(使用一周中的某一天的概念):

[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[]");
  ...
}

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

【问题讨论】:

    标签: c# asp.net-mvc asp.net-mvc-3 model-binding enum-flags


    【解决方案1】:

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

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

    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);
        }
    }
    

    将在 Application_Start 中注册:

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

    到目前为止一切顺利。现在是标准的东西:

    查看模型:

    [Flags]
    public enum NotificationDeliveryType
    {
        InSystem = 1,
        Email = 2,
        Text = 4
    }
    
    public class MyViewModel
    {
        public IEnumerable<NotificationDeliveryType> Notifications { get; set; }
    }
    

    控制器:

    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);
        }
    }
    

    查看(~/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>
    }
    

    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>
    

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

    所以我们在一个可重用的自定义 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());
        }
    }
    

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

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

    生成表格:

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

    [Flags]
    public enum NotificationDeliveryType
    {
        [Display(Name = "in da system")]
        InSystem = 1,
    
        [Display(Name = "@")]
        Email = 2,
    
        [Display(Name = "txt")]
        Text = 4
    }
    

    我们所要做的就是修改我们之前编写的 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());
    }
    

    这给了我们更好的结果:

    【讨论】:

    • 是否有需要自定义 ModelBinder 的特定原因?到目前为止,我还没有遇到 ViewModel 接受可为空的枚举或枚举列表并正确解析它的问题。
    • @Splash-X,它是一个标记的枚举(用[Flags] 标记的)属性。因此,您需要能够使用它执行按位运算。我不确定默认模型绑定器是否可以处理此问题。无论如何,我不在我的视图模型中使用枚举。
    • 哇...谢谢!太棒了!关于您使用枚举的观点,我同意...这只是一种经典标志的情况,并且确定我在默认模型绑定器中遗漏了一些东西...
    • 是否可以设置一个枚举编辑器,并设置一个隐藏字段来保存枚举标志的整数值,然后使用生成器为每个复选框生成 js/jq 处理程序以修改该隐藏字段中的值,因此当表单被回发时,该枚举字段将与新字段一起正确设置?
    • 不错的解决方案,拯救了我的一天
    【解决方案2】:

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

    在创建框的 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());
    }
    

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

    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);
    }
    

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

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

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

    【讨论】:

    • 举个例子就好了。特别是因为 MVC 视图标记似乎无法识别通用 HTML 助手,例如您的 CheckboxesForEnumFlagsFor.
    • 表达方式我也不太清楚,你怎么用这种东西?请原谅我不是 MVC 高级用户。
    • 如果您在 ViewModel 类中使用枚举属性的帮助器,例如 @Html.CheckBoxesForEnumFlagsFor(model =&gt; model.MyEnum)(我猜 lambda 表达式应该支持这一点),您必须将 var model = htmlHelper.ViewData.Model as Enum; 替换为 var model = metadata.Model as Enum;,因为 @ 987654328@ 不是枚举,它是 ViewModel 类。
    • 继续... 我需要更改以使其正常工作的第二件事是将 var name = ti.GetFullHtmlFieldName(string.Empty); 替换为 var name = ti.GetFullHtmlFieldName(metadata.PropertyName);
    • 是否可以设置一个枚举编辑器,并设置一个隐藏字段来保存枚举标志的整数值,然后使用生成器为每个复选框生成 js/jq 处理程序以修改该隐藏字段中的值,因此当表单被回发时,该枚举字段将与新字段一起正确设置?
    【解决方案3】:

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

    [以下来自Documentation及其cmets;看看这是否不适合你]

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

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

    然后在视图中,将@using MvcEnumFlags 放在最上面,将@Html.CheckBoxesForEnumFlagsFor(model =&gt; model.MyEnumTypeProperty) 放在实际代码中。

    【讨论】:

    【解决方案4】:

    我使用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));
    

    【讨论】:

      【解决方案5】:

      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);
      }
      

      【讨论】:

        【解决方案6】:

        使用 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()
                    );
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2012-04-21
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-02-17
          • 1970-01-01
          相关资源
          最近更新 更多