Reputation: 2883
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 Tag
s 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:
Now, I try to create a controller/view to create a new ContentEntry
with Tag
s. 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
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