1

I've been trying to read about this DefaultModelBinder for a couple of days now but I'm still very confused. I am using MVC 4 & EF 5 TablePerHiearchy structure.

My problem is that I have a base class of Resource:

  public class Resource : PocoBaseModel
  {
    private int _resourceID;
    private string _title;
    private string _description;

    //public accessors
  }

that has sub classes (DVD, EBook, Book, etc)

public class DVD : Resource
{
    private string _actors;
    //more fields and public accessors
}

My controller code uses a custom ModelBinder

[HttpPost]
public ActionResult Create([ModelBinder(typeof(ResourceModelBinder))] Resource resource)
{
     //controller code
}

public class ResourceModelBinder : DefaultModelBinder
{

  public override object BindModel(ControllerContext controllerContext,
  ModelBindingContext bindingContext)
  {
      var type = controllerContext.HttpContext.Request.Form["DiscriminatorValue"];
      bindingContext.ModelName = type;
      bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(null, resourceTypeMap[type]);

      return base.BindModel(controllerContext, bindingContext);

    }
static Dictionary<string, Type> resourceTypeMap = new Dictionary<string, Type>
    {
      {"Resource", typeof(Resource)},
      {"Book", typeof(Book)},
      {"DVD", typeof(DVD)},
      {"EBook", typeof(EBook)},
      {"Hardware", typeof(Hardware)},
      {"Software", typeof(Software)}

    };
}

So that I could pass my view a Resource (casted as a DVD, Book, or any other type)

@model Models.Resource

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>

@using (Html.BeginForm("Create", "Admin", null, FormMethod.Post, null))
{
    @Html.ValidationSummary(true)
    <fieldset>
        <legend>Resource</legend>

        @Html.HiddenFor(model => model.ResourceID)
        @Html.HiddenFor(model => model.ResourceTypeID)
        @Html.HiddenFor(model => model.Committed)
        @Html.Partial("_CreateOrEdit", Model)
        <p>
            <input type="submit" value="Create"/>
        </p>
    </fieldset>
}

and bind it based on its derived properties which happens in a switch inside the partialview.

@using Models.ViewModels;
@using Models.ResourceTypes;
@using Helper;
@model Models.Resource
@Html.HiddenFor(model => Model.DiscriminatorValue);
<table cellspacing="2" cellpadding="2" border="0">
    @{
        string type = Model.DiscriminatorValue;
        switch (type)
        {
            case "Book":
                Book book = (Book)Model;
        <tr>
        <td colspan="2">
            <div class="editor-label" style="padding-top: 15px;">
                @Html.LabelFor(model => model.Title)
            </div>

            <div class="editor-field">
                @Html.TextAreaFor(model => model.Title, new { style = "width: 750px; height: 65px;" })
                @Html.ValidationMessageFor(model => model.Title)
            </div>
        </td>
    </tr>
    <tr>
        <td>
            <div class="editor-label">
                @Html.LabelFor(model => book.Edition)
            </div>
            <div class="editor-field">
                @Html.TextBoxFor(model => book.Edition, new { style = "width: 150px;" })
                @Html.ValidationMessageFor(model => book.Edition)
            </div>
        </td>
        <td>
            <div class="editor-label">
                @Html.LabelFor(model => book.Author)
            </div>
            <div class="editor-field">
                @Html.EditorFor(model => book.Author)
                @Html.ValidationMessageFor(model => book.Author)
            </div>
        </td>
    </tr>
    <tr>
        <td> 
            <div class="editor-label">
                @Html.LabelFor(model => book.Pages)
            </div>
            <div class="editor-field">
                @Html.TextBoxFor(model => book.Pages, new { style = "width: 75px;" })
                @Html.ValidationMessageFor(model => book.Pages)
            </div>
        </td>
    </tr>
      <tr>
        <td colspan="2">
            <div class="editor-label">
                @Html.LabelFor(model => model.Description)
            </div>
            <div class="editor-field">
                @Html.TextAreaFor(model => model.Description, new { style = "width: 750px; height: 105px;" })
                @Html.ValidationMessageFor(model => model.Description)
            </div>
        </td>
    </tr>
     <tr>
        <td colspan="2">
            <div class="editor-label">
                @Html.LabelFor(model => model.AdminNote)
            </div>
            <div class="editor-field">
                @Html.TextAreaFor(model => model.AdminNote, new { style = "width: 750px; height: 105px;" })
                @Html.ValidationMessageFor(model => model.AdminNote)
            </div>
        </td>
    </tr>
    <tr>
        <td>
            <div class="editor-label">
                @{ int copies = Model == null ? 1 : Model.Copies; }
                @Html.LabelFor(model => model.Copies)
            </div>
            <div class="editor-field">
                @Html.TextBoxFor(model => model.Copies, new { style = "width: 75px;", @Value = copies.ToString() })
                @Html.ValidationMessageFor(model => model.Copies)
            </div>
        </td>
    </tr>
    <tr>
        <td>
            <div class="editor-label">
                @Html.LabelFor(model => book.ISBN10)
            </div>
            <div class="editor-field">
                @Html.EditorFor(model => book.ISBN10)
                @Html.ValidationMessageFor(model => book.ISBN10)
            </div>
        </td>

        <td>
            <div class="editor-label">
                @Html.LabelFor(model => book.ISBN13)
            </div>
            <div class="editor-field">
                @Html.EditorFor(model => book.ISBN13)
                @Html.ValidationMessageFor(model => book.ISBN13)
            </div>
        </td>

    </tr>

  break;

My first problem was that when I posted the form back, it went back as a resource and not as the casted type (so I was losing all the derived type properties) which is why I created the ResourceModelBinder. Now it correctly binds/postsback the casted type but it does not bind the base class properties of resource like Title, ResourceID, ResourceTypeID..

Can anyone help me understand what I am missing so that it actually binds the base resource class properties as well as the derived type properties.?

1 Answers1

0

So all I had to do was override the BindProperty method as well in my custom ModelBinder class.

protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor)
    {
      if (propertyDescriptor.DisplayName != null)
      {
        var Form = controllerContext.HttpContext.Request.Form;
        string currentPropertyFormValue = string.Empty;
        string formDerivedTypeKey = bindingContext.ModelName.ToLower() + "." + propertyDescriptor.DisplayName;
        string formBaseTypeKey = propertyDescriptor.DisplayName;
        List<string> keywordList = null;
        Type conversionType = propertyDescriptor.PropertyType;

        if (!string.IsNullOrEmpty(Form[formDerivedTypeKey]) || !string.IsNullOrEmpty(Form[formBaseTypeKey]))
        {
          if (!string.IsNullOrEmpty(Form[formDerivedTypeKey]))
          {
            //store current derived type property
            currentPropertyFormValue = Form[formDerivedTypeKey];
          }
          if (!string.IsNullOrEmpty(Form[formBaseTypeKey]))
          {
            //store current base type property
            currentPropertyFormValue = Form[formBaseTypeKey];
          }
        }

        if (conversionType.IsGenericType)
        {
          if (conversionType.GetGenericTypeDefinition() == typeof(List<>))
          {
            if (propertyDescriptor.DisplayName == "KeyWords")
            {
              string[] keywords = currentPropertyFormValue.Split(',');
              if (keywords != null && keywords.Count() > 0)
              {
                //create keyword list
                keywordList = new List<string>();
                foreach (var item in keywords)
                {
                  if (!string.IsNullOrEmpty(item) && !item.Contains(','))
                  {
                    keywordList.Add(item);
                  }
                }
              }
            }
          }
          if (conversionType.GetGenericTypeDefinition() == typeof(Nullable<>))
          {
            // nullable type property.. re-store nullable type to a safe type
            conversionType = Nullable.GetUnderlyingType(conversionType) ?? propertyDescriptor.PropertyType;
          }
        }
        if (!string.IsNullOrEmpty(currentPropertyFormValue))
        {
          //bind property
          if (propertyDescriptor.DisplayName != "KeyWords")
          {
            propertyDescriptor.SetValue(bindingContext.Model, Convert.ChangeType(currentPropertyFormValue, conversionType)); 
          }
          else
            propertyDescriptor.SetValue(bindingContext.Model, Convert.ChangeType(keywordList, conversionType));
        }
      }
      else
        base.BindProperty(controllerContext, bindingContext, propertyDescriptor); //default condition
    }

This method goes through every property that could be assigned. It get gets the type of the property so that you can convert its value from the form into its appropriate type. Some challenges were there with nullable types and a list of string property but it binds successfully so I hope this may be useful for someone.