Koopakiller
Koopakiller

Reputation: 2883

MVC - pass all ListBox items back to Controller

I created a database with the Entity Framework with the model first approach. Among other things, I have a type ContentEntry and a type Tag. Every ContentEntry can have multiple Tags and every Tag can be used by multiple ContentEntry’s. It should be such that no tag exists twice in the db, for that is the n:m relation: Image of the realtion between Tag and ContentEntry

Now, I try to create a controller/view to create a new ContentEntry with Tags. I have no idea how to create a ListBox which give all his items back to the controller. The JavaScript (with jQuery) is no problem for me:

<span class="label">Tags</span>
@Html.ListBoxFor(model => model.Tags, 
                 new MultiSelectList(Model.Tags),
                 new { id = "lbTags" })

<input type="text" id="tbTag" />
<input type="button" value="add" onclick="addTag();" />
<input type="button" value="delete" onclick="delTags();" />

<script>
    function addTag() {
        $('#lbTags').append($('<option>',
            { value: $("#tbTag").val(), text: $("#tbTag").val() }));
    }
    function delTags() {
        $("#lbTags option:selected").remove();
    }
</script>
@Html.ValidationMessageFor(model => model.Tags, "", new { @class = "input-error" })

But my Tags collection stays always empty:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateBlogEntry
  ([Bind(Include = "Id,Published,Title,AccountId,HtmlContent,Tags")]BlogEntry blogEntry)
{
    //blogEntry.Tags is always empty
    if (ModelState.IsValid
    {
        db.ContentEntrySet.Add(blogEntry);
        db.SaveChanges();
        return RedirectToAction("Index");
    }

BlogEntry is a derivate of ContentEntry and ContentEntry.Tags is a ICollection<Tag>.

Has anyone an idea how to solve this task?

EDIT: Here is my GET-method for CreateBlogEntry:

public ActionResult CreateBlogEntry()
{
    //ViewBag.Tags = db.TagSet;
    return View(new BlogEntry()
    {
        Published = DateTime.Now,
    });
}

Upvotes: 2

Views: 5621

Answers (1)

Yogiraj
Yogiraj

Reputation: 1982

The default model binder will bind the selected values from the multiselect to a collection.

Change your view to

 @Html.ListBox("SelectedTags"
 , new MultiSelectList(Model.Tags,"Name","Name")
 , new { id = "lbTags" })

This way even if you load the values from server it will work. Notice that I am not using the ListBoxFor because I want to set the name of the collection.

Your BlogEntry model that comes into CreateBlogEntry method will have a property like below

public IEnumerable<string> SelectedTags{ get; set; }

You can then make use of this SelectedTags property and create a new model that goes into you database.

If you don't want this behavior, your will have to override the default model binder's behavior. You can find all about the binders in this MSDN post.I will try to update the answer with model binder but this is enough to unblock you in the mean time.

One other note, you are using all the properties BlogEntry the bind is unnecessary. You can remove it.

Update - The custom model binder version

You can create your own binder like below:

public class FancyBinder:DefaultModelBinder
{
    protected override object CreateModel(ControllerContext controllerContext
   ,ModelBindingContext bindingContext
   ,Type modelType)
    {
        if (modelType.Name == "BlogEntry")
        {
            return BindBlogEntry(controllerContext, bindingContext, modelType);
        }
        return base.CreateModel(controllerContext, bindingContext, modelType);
    }

    private static object BindBlogEntry(ControllerContext controllerContext
   ,ModelBindingContext bindingContext
   ,Type modelType)
    {
        var tagsOnForm = controllerContext.HttpContext.Request.Form["Tags"];
        return new BlogEntry
        {
            Content = controllerContext.HttpContext.Request.Form["Content"],
            Tags = GetTags(tagsOnForm)
        };
    }

    private static List<Tag> GetTags(string tagsOnForm)
    {
        var tags = new List<Tag>();
        if (tagsOnForm == null)
            return tags;

        tagsOnForm.Split(',').ForEach(t=>tags.Add(new Tag {Name = t}));
        return tags;
    }
}

You can wire up this binder in the global.asax like below:

public class MvcApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        ModelBinders.Binders.DefaultBinder = new FancyBinder();
    }
}

I hope this is clear enough. Let me know if you have questions.

Upvotes: 3

Related Questions