2

I've been looking into model binding to resolve a specific issue I'm having. I tried some methods described in various blogs and stackoverflow answers but I'm not really getting there.

First I'll present my model:

public class CampaignModel
{
    [Required]
    [StringLength(24)]
    [Display(Name = "CampaignModel_Name", Prompt = "CampaignModel_Name", ResourceType = typeof(CampaignResources))]
    public string Name { get; set; }

    [StringLength(255)]
    [Display(Name = "CampaignModel_Description", Prompt = "CampaignModel_Description", ResourceType = typeof(CampaignResources))]
    public string Description { get; set; }

    [Required]
    [DataType(DataType.Date)]
    [Display(Name = "CampaignModel_StartDate", ResourceType = typeof(CampaignResources))]
    public DateTime StartDate { get; set; }

    [Required]
    [DataType(DataType.Date)]
    [Display(Name = "CampaignModel_EndDate", ResourceType = typeof(CampaignResources))]
    public DateTime EndDate { get; set; }

    [Display(Name = "CampaignModel_Tags", Prompt = "CampaignModel_Tags", ResourceType = typeof(CampaignResources))]
    public TagList Tags { get; set; }
}

So this is a fairly basic model except for the last property TagList. Now in the future allot of my models will be having a TagList so this is fairly important to me. A TagList is just this:

public class TagList : List<Tag>
{
}

I created this class to easily be able to create an EditorTemplate for it without having to place UIHint attributes. Now I'm using the select2.js library for my taglist editor, which handles ajax searching in existing tags and so forth. The issue here is that Select2 binds to a single hidden field where it seperates the various tag values by a , and depending whether it is an existing or a new tag it uses the text or id producing a list like 1,tag,34,my new tag. This input I would like to translate into a TagList.

So the concrete question is: How would I model bind this single hidden input into the TagList property on my Model and be able to reuse this behaviour for all my models easily?

EDIT => Adding the code of the EditorTemplate and The ModelBinder I created based off the answer of Andrei

My EditorTemplate (omitted js)

<div class="form-group">
    @Html.Label(string.Empty, ViewData.ModelMetadata.DisplayName, new { @class = "col-md-3 control-label" })
    <div class="col-md-9">
        @Html.Hidden(string.Empty, ViewData.TemplateInfo.FormattedModelValue, new { @class = "form-control select2" })
    </div>
</div>

My ModelBinder (set as DefaultModelBinder in global.asax)

public class TagListModelBinder : DefaultModelBinder
{
    protected override void BindProperty(
        ControllerContext controllerContext,
        ModelBindingContext bindingContext,
        PropertyDescriptor propertyDescriptor)
    {
        if (propertyDescriptor.PropertyType == typeof(TagList))
        {
            ValueProviderResult value = bindingContext.ValueProvider.GetValue(propertyDescriptor.Name);
            string[] rawTags = value.AttemptedValue.Split(',');
            List<long> tagIds = new List<long>();

            TagList tags = new TagList();
            foreach (string rawTag in rawTags)
            {
                long id;
                if (long.TryParse(rawTag, out id))
                {
                    // Existing tags need to be retrieved from DB
                    tagIds.Add(id);
                }
                else
                {
                    // New tags can simply be added without ID
                    tags.Add(new Tag { Text = rawTag });
                }
            }

            if (tagIds.Count > 0)
            {
                using (TagServiceClient client = new TagServiceClient())
                {
                    List<Services.TagService.Tag> existingTags = client.GetTagsByIds(tagIds);
                    tags.AddRange(existingTags.Select(t => new Tag { Id = t.id, Text = t.text }));
                }
            }

            propertyDescriptor.SetValue(bindingContext.Model, tags);
        }

        base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
    }

3 Answers 3

3

The best way to go here is to inherit from DefaultModelBinder and check what property are you dealing with. If it is of type TagList - go ahead and apply whatever logic you need. Otherwise let default implementation do the work.

public class TagListModelBinder : DefaultModelBinder
{
    protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor )
    {
        if (propertyDescriptor.PropertyType == typeof(TagList))
        {
            ValueProviderResult value = bindingContext.ValueProvider.GetValue(propertyDescriptor.Name);
            string[] rawTags = value.ToString().Split(',');

            TagList tags = new TagList();
            foreach (string rawTag in rawTags)
            { 
                // for numbers - get them from DB
                // for strings - create new and store in DB
                // then add them to tags
            }

            propertyDescriptor.SetValue(bindingContext.Model, tags);
        }
        else
        {
            base.BindProperty(controllerContext, bindingContext, propertyDescriptor)
        }
    }
}
Sign up to request clarification or add additional context in comments.

7 Comments

Should I register this type for DefaultModelBinder or specifically for the TagList type?
@IvanL, since this binder does not interfere with any other types there should be no harm in just replacing the default one: ModelBinders.Binders.DefaultBinder = new TagListModelBinder();
I tried the solution but I'm running into the exception "The parameter conversion from type 'System.String' to type 'ROCME.EventDrive.Backend.Models.Shared.Tag' failed because no type converter can convert between these types." even though I see the binding occuring as expected. Also I needed to handle the ValueProviderResult differently since the .ToString() resulted in the Type name instead of the actual values used.
@IvanL, well, that means that general approach works, but something is wrong with implementation. It's hard to say what without seeing the actual code though. How and where are you doing a conversion that fails?
That's the strange thing, I'm not doing any conversions, the only thing I can think of is that it fails in my view/EditorTemplate, where this is used: ViewData.TemplateInfo.FormattedModelValue. I'll add the code I'm using.
|
0

Check this out: Understanding ASP.NET MVC Model Binding. Hope it will help you!

Comments

0

If you are posting this data back in an ajax call, the you can prepare data such that it binds to your model property TagList.

I am assuming the structure of the Tag class to be

public class Tag
{
    public string Name {get; set;}
    public string Id {get; set;}
}

While making an ajax call you can do

  1. Split the value of the hidden field on comma (,)
  2. Add each tag value as

    Foreach tag in split list

    data.push("name": "TagList[count].Name", "value", "Name to bind");

    data.push("name": "TagList[count].Id", "value", "Id to bind");

    count++

And post this data with the ajax request and hopefully this should bind to your model.

1 Comment

I'm not posting my data using ajax, it's a direct form submit

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.