MVC EditorFor and Generics - c#

I have a model like this
class MyClass
{
}
class MyModel
{
public EditorModel<MyClass> EditorProperty { get; set; }
}
class EditorModel<T>
{
public int MyProp { get; set; }
}
and razor view template under EditorTemplates folder like this
#model Project.EditorModel<MyClass>
#Html.DisplayFor(x => x.MyProp)
and Index razor view
#model Project.MyModel
#Html.EditorFor(x => x.EditorProperty)
But when I run the code to render the template, the template is not rendered, instead what is rendered is a string with the full name of the type used as a model in the editor template view.
Is the problem that EditorFor method doesn't support generic models? is there a way around this?

I think the problem starts with the template name, which cannot really satisfy the generic class name pattern. So the Template is not being picked automatically.
Method EditorFor also has an override where you can pass the Template Name.
What you could do is to update the usage of EditorFor, while providing the Template Name, now the trick is to make that part dynamic, and it could also be part of your ViewModel class:
#model Project.MyModel
#Html.EditorFor(x => x.EditorProperty, Model.TemplateName)
You need to extend MyModel to provide the TemplateName property. It's now up to you how you determine the correct name. You could make it a named pattern convention:
public string TemplateName
{
get
{
// EditorProperty must not be null... mind this is C#6 but its easy to downgrade it
var genericType = EditorProperty?.GetType().GetGenericArguments().FirstOrDefault()?.Name;
return $"EditorModelOf{genericType}";
}
}
And TemplateName will for example return
EditorModelOfMyClass
So you should name your EditorTemplate EditorModelOfMyClass.cshtml. This will only work if your expected amount of generic classes is limited.

Related

MVC 5 Generic List Binding to Partial View

I'm having an issue with the bindings of my model to a partial view and feel that there must be a way to do what I want. I wonder if my design is flawed and a small refactoring might be necessary.
A very simplified (and abstract) version of my models would be like so:
Public Class BaseClass
{
Public string Name { get; set; }
Public List<SomeClass> Things { get; set; }
}
Public Class DerivedClass : BaseClass
{
Public List<LineItem> Items { get; set; }
}
Public Class Library
{
Public List<LineItem> Items { get; set; }
}
Public Class LineItem
{
Public string Name { get; set; }
Public string Value { get; set; }
}
I have Editor Templates for the BaseClass, SomeClass, and LineItem. These are shown in the view for DerivedClass and work as intended by submitting changes to the controller. The LineItem template is wrapped in a LineItemList partial view because I intend to use it for a view for Library and don't want to repeat all of that layout and javascript. The LineItemList partial view is included on the DerivedClass view by Html.PartialView since there doesn't seem to be a way to create an Editor Template for the List type. So my views look like this:
DerivedClassView
BaseClassPartialView
SomeClassPartialView
LineItemListPartialView
LineItemParialView
When I submit my form, the controller gets all of the data for the BaseClass and SomeClass list but none for the LineItem list. The difference of course being that one is rendered using Html.EditorFor and the other Html.PartialView.
Refactoring the classes will be tough as they have to be backwards compatible with an old XML format for serialization, but I'm sure I can work some magic if necessary.
As Chris Pratt mentioned, I forgot to include my controller methods:
Public ActionResult DerivedClassEditor()
{
Return View(New DerivedClass());
}
[HttpPost]
Public ActionResult DerivedClassEditor(DerivedClass dc)
{
// Do Stuff
}
I just noticed in the rendered Html, the SomeClass controls are named SomeClass.[0].Name while those of the LineItem are [0].Name. I have a feeling that might be a symptom of the issue.
And my views look similar to this:
DerivedClassEditor
#model DerivedClass
#using (Html.BeginForm())
{
#Html.EditorFor(model => model)
#Html.Partial("LineItemListPartialView")
<input type="submit" />
}
LineItemListPartialView
#model List<LineItem>
<div name="Items">
#Html.EditorFor(model => model)
</div>
LineItemPartialView
#model LineItem
<div name="LineItem">
#Html.LabelFor(model => model.Name)
#Html.TextEditorFor(model => model.Name)
</div>
Edit:
A link to my view: https://github.com/melance/TheRandomizer/blob/Initial/TheRandomizer.WebApp/Views/UserContent/AssignmentEditor.cshtml
I've narrowed down the issue to the fact that when I load one of the lists using #Html.EditorFor it names the inputs Collection[index].Property yet when I add one dynamically using the same call it simply names the input Property. Is there an easy and reusable way to have the addition of new items have the same naming structure?
Crucially, you failed to post your controller code, so I'm stabbing in the dark, but I think I can guess pretty well what's happening. You most likely have something like:
[HttpPost]
public ActionResult MyAwesomeAction(BaseClass model)
{
...
}
And you're assuming that since your view is working with DerivedClass and posting DerivedClass that you should end up with an instance of DerivedClass rather than BaseClass in your action. That assumption is incorrect.
All that exists on post is a set of string key-value pairs. The modelbinder looks at the action signature and attempts to bind the posted data to an instance of the parameter(s), newing up classes as necessary. Given this, the only information the modelbinder has in this scenario is that it's expected to bind values to an instance of BaseClass and a set of data to attempt to do that with. As a result, it will create an instance of BaseClass and bind what data it can, dropping anything it can't. Importantly, since BaseClass doesn't include stuff like your Items property, all of that data will be discarded.
Long and short, polymorphism isn't supported with action parameters. The type of your parameter must be the type you want. Period.
For what it's worth, you can use editor templates for list properties. EditorFor will simply render the editor template for the contained type for each item in the list. For example, calling:
#Html.EditorFor(m => m.Items)
Is essentially the same as:
#for (var i = 0; i < Model.Items.Count(); i++)
{
#Html.EditorFor(m => m.Items[i])
}
So after much research, trial and error and help from Erik, I was able to solve my problem. The issue turned out to be the naming of the form elements in my partial view. When added by the model they are indexed as such: name = "ListProperty[Index].PropertyName". When I was adding my partial views using ajax, the were named for just the Property Name. In order to fix this, I had to handle my ajax calls for the partial view like this:
public ActionResult CreateParameter(Int32 index)
{
ViewData.TemplateInfo.HtmlFieldPrefix = $"Parameters[{index}]";
return PartialView("~/Views/Shared/EditorTemplates/Configuration.cshtml");
}

MVC Nested Model Collection

I am new to MVC and I am trying to display information from a nested model collection on the page.
so my model is as follow:
public partial class Parent
{
public Parent()
{
this.Childs = new HashSet<Child>();
}
public int ParentID { get; set; }
public string Name { get; set; }
public virtual ICollection<Child> Childs { get; set; }
}
To display the information on the Parent view i used :
#foreach (Child c in Model.Childs)
{
#c.Name
}
The above works but i would like to use a different view for the childs so i have tried the following:
#Html.DisplayFor(model => model.Childs)
and defined the following view:
#model WebApplication1.Models.Child
<div>
#Html.DisplayFor(model => model.Name)
</div>
This doesn't work and what I am getting is a System.Data.Entity.DynamicProxies displayed instead of the list of child names. I have read that this is because MVC5 doesn't know what view to use. I have tried specifying the view in the Display for as #Html.DisplayFor(model => model.Childs, "ViewName1.cshtml") but that didn't help at all.
In addition to the above I would like to use something similar for #Html.EditorFor(model => model.Childs).
Start by creating a DisplayTemplates sub-folder in your Parent view folder. Then create a view in that folder called Child.cshtml.
Child.cshtml
#model WebApplication1.Models.Child
#Model.Name
...more HTML markup if needed
Then all you do is call #Html.DisplayFor(m => m.Childs) and the framework will do the rest for you. Note that if you use the overload that lets you specify which view to use, the loop will not be done automatically for you.
Repeat the same process for editor templates, with a EditorTemplates folder and following the same conventions (view name = type name) for naming your views.
You need to tell Razor which view to use for each child object. Something like this (obviously replace ChildViewName with the name of your child view):
#foreach (Child c in Model.Childs)
{
#Html.Partial("ChildViewName", c)
}
Adding my comments as an extension to the answers above because I think many people who are new to asp.net mvc miss this. #panais mentioned that my comment helped him\her. So, I think it might be helpful for others as well.
The editor template views should be created in EditorTemplates folder.
The display template views should be created in DisplayTemplates folder.
Name of the template view should be same as view model name.

How do I post an individual element of a model's collection to an action?

I have a model that contains a collection. I want to wrap each individual EditorFor in a form that posts to an action with a single element as the parameter. i.e:
class ModelWithCollection
{
List<ElementModel> Elements { get; set; }
}
class ElementModel
{
int Field { get; set; }
}
public ActionResult ElementAction(ElementModel viewModel)
{
}
However, my viewModel doesn't have the properties populated. I can understand this, as the editor renders the ids as things like Elements[0].Field. Rather where the action would like just Field.
How can I get around this?
I don't think I can manually build the post using jquery or something as it include uploading a file.
There is an overload of EditorFor that takes three arguments: expression, templateName, and htmlFieldName. If you supply the expression, null for templateName (thereby telling the helper to resolve the editor template the usual way), and an empty string for htmlFieldName, this should render the fields without the Elements[0]. prefix.
<%=Html.EditorFor(m => m.Elements[i], null, string.Empty)%>
You can workaround this using the partial view. If you move your EditorFor to partial view (with ElementModel as model) there won't be generated [0] brackets.

Why does TryUpdateModel return false even though properties are set and valid?

I have web application based on ASP.Net MVC3. I have a need for a "Create" view which will not know the model type until the user picks a sub-type to create from a drop-down. To attempt to solve this problem, I have created an editor template under Shared/EditorTemplates for each derived model type. This allows me to create a single "Create.cs" which is strongly-typed to a view model. The view model only has two members, an enum and a complex type. The idea is that the view will initially show only a drop-down (editor for the enum member) then when the user initially submits the specified "model type" (drop-down selected value), the POST action can check the "model type" specified and instantiate the correct derived model type for the view model's single complex member who's type is the base type for all possible "model types".
The abstract + derived type model objects...
public abstract class MyModelBase
{
public MyModelType_e ModelType {get; set; }
[Required]
public string Name { get; set; }
}
public class DerivedType1 : MyModelBase
{ ... }
public class DerivedType2 : MyModelBase
{ ... }
public class DerivedType3 : MyModelBase
{ ... }
I have a complex view model as follows...
public enum MyModelType_e
{
DerivedType1 = 0,
DerivedType2 = 1,
DerivedType3 = 2
}
public class MyModelCreate
{
public MyModelType_e ModelTypeForSelectList { get; set; }
public MyModelBase ModelBase { get; set; }
}
My GET controller action instantiates the above view model for the view i.e., only a drop-down list displayed with items based on the MyModelType_e enum + the value of the model's "ModelBase" property is initially null. So the GET action method looks like this...
[HttpGet]
public ActionResult Create()
{
return View(new MyModelCreate());
}
Note the comment in caps bellow regarding the crux of my issue which is that TryUpdateModel fails (see below) even though it sets the properties of the ModelBase (derived-type) member to the corresponding form values as expected...
[HttpPost]
public ActionResult Create(MyModelCreate model)
{
if (model.ModelBase == null ||
(int)model.ModelTypeForSelectList != model.ModelBase.ModelType)
{
switch (model.ModelType)
{
case MyModelType_e.DerivedType1:
model.ModelBase = new DerivedType1();
break;
case MyModelType_e.DerivedType2:
model.ModelBase = new DerivedType2();
break;
case MyModelType_e.DerivedType3:
model.ModelBase = new DerivedType3();
break;
}
return View(model);
}
if (!TryUpdateModel(model.ModelBase))
return View(model); // <<< THIS HAPPENS EVEN THOUGH ModelBase APPEARS TO BE UPDATED PROPERLY...
// For instance, I can see right here with intellisense that model.ModelBase.Name
// is NOT null or empty but rather is truly updated with the correct form value(s)...
// TODO: Insert the record, etc... (currently we never get here...)
}
So the above section is where the problem stems from but here is my view to help understand...
#model MyNamespace.MyModelCreate
<h2>Create</h2>
...
#using (Html.BeginForm())
{
#Html.ValidationSummary(false)
<fieldset>
<legend>Input</legend>
<div class="editor-label">
#Html.Label("Select Model Type")
</div>
<div>
#Html.EnumDropDownListFor(model => model.ModelType)
#Html.ValidationMessageFor(model => model.ModelType)
</div>
#*
Conditionally show the correct editor template...
There is one existing under ../Shared/EditorTemplates for each
derived type (DerivedType1, DerivedType2, DerivedType3, etc...)
This much is working in the sense that the correct editor fields
are displayed based on what the user selects in the above drop-down.
*#
#if (Model.InputModel != null)
{
#Html.EditorFor(model => model.ModelBase);
}
<p>
<input type="submit" value="Continue" />
</p>
</fieldset>
}
So once with the initial POST (model type is selected), my POST action method falls through to the TryUpdateModel line by design but for some reason the validation fails. The part I really don't understand is that the validation summary reports "Name is required" even though I can clearly watch TryUpdateModel set the Name propery properly on the ModelBase member of the view model.
I'd greatly appreciate any help or guidance here... I am fairly new to ASP.NET MVC and I am open to doing things differently since I understand there are probably other ways I could design my requests/actions + views to accomplish this "multi-step" problem but I really am just going for the simplest possible thing which I why I like the EditorTemplate approach for handling the derived model types, etc.
Thanks in advance...
Check ModelState. There should be errors set after TryUpdateModel runs.
I had to remove a Required attribute from some properties.
My solution basically involved growing two additional controller actions (GET and POST). I created separate views under ../Shared/EditorTemplates which are strongly typed (one for each derived model type). This way the initial "Create" POST action only receives the selected type (model is just an enum value specifying the desired type) and then redirects to another controller action like "CreateByType" which gets a new instance of the requested concrete type and returns the correct edit view. The additional "CreateByType" controller actions (GET and POST) only needs to deal with the abstract base because it requests the instance from the service layer (passing the enum value). Then all I had to do was create a view under EditorTemplates for each concrete type.

How to Post Form from View strongly Typed to ViewModel?

I have a view model as such:
public class MyViewModel
{
public MyObject myObject{ get; set; }
public List<MyList> myList{ get; set; }
}
I have a view with a form strongly typed to MyViewModel
This view allows you to enter values for the properties of MyObject, as well as create a list of MyList objects. The List part works fine although I thought that would be the more difficult of the two.
Assuming MyObject has a property Description I create a textbox to enter the value as such:
#Html.EditorFor(x => x.myObject.Description);
The text box renders with an id of MyObject_Description...The problem is when I post this to my controller action, MyObject does not get bound at all(althought the list items do as they recieve the appropriate IDs of "MyViewModel_MyList[guid].myListValue")
What am I doing wrong here??
EDIT: more info
The first line of the view is:
#model MyApp.ViewModels.MyViewModel
And the Action method:
[HttpPost]
public ActionResult Create(MyViewModel myViewModel)
{
}
I am passing a new MyViewModel into the partial view to begin...
public ActionResult Create()
{
MyViewModel model = new MyViewModel();
return PartialView(model);
}
EDIT 2
Ok When I render my partial view that contains the forms I call :
#{Html.RenderAction("Create", "MyController");}
this is called from within a View of type #model IEnumerable<MyApp.Models.MyObject>
(this view displays a list of currently existing MyOjects, and at the bottom the partial is rendered to allow the user to add another MyObject to the DB)
If you are not already doing so, try creating a editor template (e.g., Views->Shared->EditorTemplates) called MyObject.cshtml. Move your partial view content to this view and then call
#Html.Editor("myObject").
from your parent partial view.
Change your ViewModel to have the Description directly
public class MyViewModel
{
public string Description { get; set; }
public List<MyList> myList{ get; set; }
}
then bind accordingly
#Html.EditorFor(x => x.Description);
I would expect the top line of your view to look something like this:
<%# Page Title="" Language="C#" Inherits="System.Web.Mvc.ViewPage<MyViewModel>" %>
This tells the view that the model it is supplied is of type MyViewModel (a la <T> style).
I don't think the out of the box model binding knows how to bind to complex objects. You're probably going to have to write up some sort of custom model binder.
I'm afraid it's not something I've done since MVC1 so I'm a bit hesitant to give you any sample code because the mechanism may well have changed completely since then. A quick google did turn up this article http://www.learnxpress.com/asp-net-mvc-hosting-6-tips-for-asp-net-mvc-model-binding-2.html and this article http://bradwilson.typepad.com/blog/2010/10/service-location-pt9-model-binders.html.
Edit: I've just seen this answer which might help Retrieving data from view, should I use model binder?

Categories