2012-02-13 70 views
30

我有一個枚舉標誌的網格,其中每個記錄是一行復選框,以確定該記錄的標誌值。這是通知,該系統提供,用戶可以挑選(每一個人)的名單,他們是如何希望他們提供:模型綁定枚舉標誌列表

[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模型活頁夾可以處理標誌。謝謝!

回答

69

一般來說,我在設計我的視圖模型時避免使用枚舉,因爲它們不使用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):爲NotificationDeliveryType~/Views/Shared/EditorTemplates/NotificationDeliveryType.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> 
} 

自定義編輯器模板:

@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> 

其產生的表:

enter image description here

現在顯然它應該是nic e如果我們可以爲這些複選框提供更友好的標籤。例如像:

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

這給了我們一個更好的結果:

enter image description here

+0

是否有特殊原因的自定義需要ModelBinder的?到目前爲止,我還沒有接受可以爲null的枚舉或枚舉列表並正確解析它的ViewModel的問題。 – 2012-02-13 20:55:01

+0

@ Splash-X,它是一個標記的枚舉(標有'[Flags]')屬性的枚舉。所以你需要能夠對它進行按位操作。我不確定默認模型聯編程序是否可以處理此問題。無論如何,我不在我的視圖模型中使用枚舉。 – 2012-02-13 20:57:03

+0

哇...謝謝!太棒了!關於使用枚舉的觀點,我同意......這只是一個典型的標誌,並且確定我缺少一些默認的模型綁定器... – Rikon 2012-02-13 21:23:27

13

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

+0

一個例子會很好。特別是因爲MVC視圖標記似乎不能識別泛型HTML助手,比如CheckboxesForEnumFlagsFor <>。 – 2012-10-04 16:25:07

+0

表達對我來說也不是很清楚,你會如何使用這樣的東西?請原諒我不是一名MVC高級用戶。 – 2012-10-04 16:49:40

+2

如果你在ViewModel類中使用了enum屬性的幫助器,比如'@ Html.CheckBoxesForEnumFlagsFor(model => model.MyEnum)'(我想lambda表達式應該支持這個),你必須替換'var model = htmlHelper .ViewData.Model as Enum;'by'var model = metadata.Model as Enum;'因爲'htmlHelper.ViewData.Model'不是**枚舉,它是ViewModel類。 – Slauma 2013-04-13 16:32:42

2

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

你可以嘗試MVC Enum Flags包(通過nuget可用)。它會自動跳過零值枚舉選擇,這是一個很好的接觸。

[以下內容來自Documentation及其評論;看到有如果這不適合你]

安裝後正常結合,添加以下的Global.asax.cs \的Application_Start:

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

然後在視圖中,把@using MvcEnumFlags向上頂和@Html.CheckBoxesForEnumFlagsFor(model => model.MyEnumTypeProperty)爲實際的代碼。

+2

'MVC Enum Flags'已經移動到https://github.com/Bitmapped/MvcEnumFlags – Korayem 2016-08-13 00:03:25

1

位圖,你問重要的問題,我可以提出以下解決方案:你應該重寫你的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); 
} 
1

使用達林和位圖的代碼,我寫答案,但沒有爲我工作,所以首先我修復可空的東西,然後我仍然有問題,發現有什麼問題的HTML,所以我loos信仰這個答案,尋找另一個,我發現在我的國家的一個論壇上的東西,它使用了與這裏相同的代碼,但是隻做了很少的改變,所以我將它與我的代碼合併,並且一切進展順利,我的項目使用了空值,所以我不知道它是如何實現的要在o上工作其他地方,可能需要一點修復,但我試圖考慮可空和模型作爲枚舉本身。

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