Reputation: 737
I am working on an app where I have a requirement to be able to load object properties at runtime from a database. The customer wants to be able to add attributes to the database and have them show up in the app. I am accomplishing this by giving my model a list of Field objects that contain a name, a type, and a value. This works well for displaying and editing project properties, but I'm having trouble with validation in the editor view. Thanks for your help.
I want to be able to do this in my Edit action:
[HttpPost]
public ActionResult Edit(Movie movie)
{
if (ModelState.IsValid)
{
db.Entry(movie).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
return View(movie);
}
Normal view:
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Movie</legend>
@Html.HiddenFor(model => model.ID)
<div class="editor-label">
@Html.LabelFor(model => model.Title)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Title)
@Html.ValidationMessageFor(model => model.Title)
</div>
</fieldset>
}
What I need to do:
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Movie</legend>
@Html.HiddenFor(model => model.ID)
for(i = 1 to n) {
<div class="editor-label">
@Html.LabelFor(model => model.Fields[i].Name)
</div>
<div class="editor-field">
@Html.EditorFor(model => model.Fields[i].Value)
@Html.ValidationMessageFor(model => model.Fields[i].Value)
</div>
}
</fieldset>
}
Model:
public class Movie
{
public Movie()
{
this.Fields = new List<Field>();
}
public List<Movie> Movies { get; set; }
}
Field Class:
public class Field
{
public string Name { get; set; }
public Type Type { get; set; }
public object Value { get; set; }
}
Upvotes: 2
Views: 1334
Reputation: 156524
I believe something like this should work:
@using (Html.BeginForm()) {
@Html.ValidationSummary(true)
<fieldset>
<legend>Movie</legend>
@Html.HiddenFor(model => model.ID)
@{int i = 0;}
@foreach(var field in model.Fields) {
var htmlFieldName = string.Format("Fields[{0}]", i);
<div class="editor-label">
<label for="@htmlFieldName">@field.Title</label>
</div>
<div class="editor-field">
@Html.EditorFor(model => field, null, htmlFieldName)
@Html.ValidationMessage(htmlFieldName)
</div>
}
</fieldset>
}
(Note that I made up how you're producing your label text, since using the actual value as the label didn't make sense to me).
The POST should end up with values like this:
ID=123
Fields[0]=Jaws
Fields[1]=VeggieTales
...
... and that should automatically bind to your Movie model, provided the model has, for example, a List<string>
named Fields. If your model doesn't look like that, this should at least get you on the right track.
In your comment, you explain that you are trying to produce an editor for an object. There are two major points of difficulty here:
MVC relies on the static type returned in the lambda expression you give to EditorFor to determine which kind of editor it should produce. To override this, you will need to provide a specific template name where my original suggestion shows you providing null
:
@Html.EditorFor(model => field.Value, field.Type.Name, htmlFieldName + ".Value")
You'll probably need to tweak this to make it provide the right template name for types like Integer, but this should give you the general idea.
When posting back, there is no way for the server to know that Field[0]
is an int
, etc. You can either:
Provide hidden values to specify each type, and then use a custom model binder that can consume this information to build each Field based on the combined Type and Value.
Recreate the structure of the Movie object on the server side based on the Movie's ID, and then walk through each of it's Fields calling:
TryUpdateModel((dynamic)field, string.Format("Field[{0}]", i));
There are probably other options, but that's about all the time I'm willing to put into this today.
Upvotes: 2