MVC3 Editor For dynamic property (or workaround required)

I am creating a system that asks questions and gets answers to them. Each question can have its own type of aviver. Let now restrict it to String and DateTime . In the domain, the question is as follows:

 public class Question { public int Id { get; set; } public string Caption { get; set; } public AnswerType { get; set; } } 

where AnswerType is

 enum AnswerType { String, DateTime } 

Please note that I actually have a lot more types of answers.

I came up with the idea of ​​creating an MVC model based on a question and adding the Answer property to it. Therefore, it should be something like this:

 public class QuestionWithAnswer<TAnswer> : Question { public TAnswer Answer { get; set; } } 

And so the problems begin. I want to have a general view to draw any question, so it should be something like this:

 @model QuestionWithAnswer<dynamic> <span>@Model.Caption</span> @Html.EditorFor(m => m.Answer) 

For String I want to have simple input here; for DateTime I'm going to define my own representation. I can transfer a specific model from the controller. But the problem is that at the rendering stage, of course, it cannot determine the type of response, especially if it was initially null (default for String ), so EditorFor does not draw anything for String and the inputs for all properties in DateTime .

I understand the essence of the problem, but is there any elegant solution? Or should I implement my own logic to select the editor view name based on the type of control (big ugly switch )?

+4
source share
2 answers

You can still use Html.EditorFor (..), but specify the second parameter, which is the name of the editor template. You have a property of the Question object, which is a type of AnswerType, so you can do something like ...

 @Html.EditorFor(m => m.Answer, @Model.AnswerType) 

The EditorTemplates folder simply defines the presentation for each of the response types. those. "String", "DateTime", etc.

EDIT: Since the Answer object is null for the String, I would set the placeholder object so that the model in you does not contain the String editor template.

+1
source

Personally, I don't like this:

 enum AnswerType { String, DateTime } 

I prefer to use a system like .NET. Let me offer you an alternative design. As always, we start by defining view models:

 public abstract class AnswerViewModel { public string Type { get { return GetType().FullName; } } } public class StringAnswer : AnswerViewModel { [Required] public string Value { get; set; } } public class DateAnswer : AnswerViewModel { [Required] public DateTime? Value { get; set; } } public class QuestionViewModel { public int Id { get; set; } public string Caption { get; set; } public AnswerViewModel Answer { get; set; } } 

then the controller:

 public class HomeController : Controller { public ActionResult Index() { var model = new[] { new QuestionViewModel { Id = 1, Caption = "What is your favorite color?", Answer = new StringAnswer() }, new QuestionViewModel { Id = 1, Caption = "What is your birth date?", Answer = new DateAnswer() }, }; return View(model); } [HttpPost] public ActionResult Index(IEnumerable<QuestionViewModel> questions) { // process the answers. Thanks to our custom model binder // (see below) here you will get the model properly populated ... } } 

then the main Index.cshtml view:

 @model QuestionViewModel[] @using (Html.BeginForm()) { <ul> @for (int i = 0; i < Model.Length; i++) { @Html.HiddenFor(x => x[i].Answer.Type) @Html.HiddenFor(x => x[i].Id) <li> @Html.DisplayFor(x => x[i].Caption) @Html.EditorFor(x => x[i].Answer) </li> } </ul> <input type="submit" value="OK" /> } 

and now we can have editor templates for our answers:

~/Views/Home/EditorTemplates/StringAnswer.cshtml :

 @model StringAnswer <div>It a string answer</div> @Html.EditorFor(x => x.Value) @Html.ValidationMessageFor(x => x.Value) 

~/Views/Home/EditorTemplates/DateAnswer.cshtml :

 @model DateAnswer <div>It a date answer</div> @Html.EditorFor(x => x.Value) @Html.ValidationMessageFor(x => x.Value) 

and the last part is a binding device for our answers:

 public class AnswerModelBinder : DefaultModelBinder { protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) { var typeValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName + ".Type"); var type = Type.GetType(typeValue.AttemptedValue, true); var model = Activator.CreateInstance(type); bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type); return model; } } 

which will be registered in Application_Start :

 ModelBinders.Binders.Add(typeof(AnswerViewModel), new AnswerModelBinder()); 
+5
source

All Articles