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.
Related
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.
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");
}
I have been working on an MVC ViewModel that contains scalar data values I need to pass to the View (instead of using ViewBags) along with an IEnumerable that contains the tabular data that goes in the table. This was based on a suggestion someone made on an earlier SO question. This way, there is no duplication I would have if I added the scalar values as constants to a table that could have many, many rows.
I am pasting my (simplified) data model and view model definitions and the controller action in question. The data structures are what I need it to be, but Visual Studio won't create a View from this Action (right-click and select "Add View with Model").
My question, is given the Action and Model definitions, what do I need to do so that I can create a View of the View Model?
I'm figuring auto-scaffold is out of the question because the ViewModel has no key, so I'm expecting manual coding. I have no idea how to do this, because all the examples I've seen show ViewModels that are essentially the equivalent of merging two tables into a larger table.
I have tried using #model = [ViewModelName] and #model = IEnumerable, but both failed.
The binary response would be, "Duh, the error is telling you exactly what's going on. You don't have a Key." But that response doesn't help me resolve this. How do I move forward with this definition of the ViewModel and create a View?
View Model
public class StudentRosterViewModel
{
// This list is the "table" shown on the view
public IEnumerable<StudentRoster> StudentRosters { get; set; }
// These are scalar values apply to the view as a whole
public string SelectedCampus { get; set; }
public string SelectedFiscalYear { get; set; }
}
Data Model
[Table("StudentRoster")]
public partial class StudentRoster
{
public int ID { get; set; }
[Required]
[StringLength(3)]
public string Campus { get; set; }
[Required]
[StringLength(4)]
public string FiscalYear { get; set; }
[Required]
[StringLength(50)]
public string StudentName { get; set; }
public int StudentID { get; set; }
}
Controller Action:
public ActionResult FilterableIndex(string campus="MRA",string fiscalYear = "FY16")
{
StudentRosterViewModel vm = new StudentRosterViewModel();
// this is tabular data and goes in the "table"
vm.StudentRosters = db.StudentRosters
.Where(m => m.Campus == campus)
.Where(m => m.FiscalYear == fiscalYear)
.ToList();
// These are scalar values; they are not tabular
// These are needed to supply values to jQuery
vm.SelectedCampus = campus;
vm.SelectedFiscalYear = fiscalYear;
return View(vm);
}
The convention is that in your Views folder there's a folder corresponding to the name of your controller. For example, if the controller is StudentRosterController then the folder would be named StudentRoster.
Then the view would correspond to the name of the action - FilterableIndex.cshtml.
The .cshtml file would begin with
#model MvcApp.Controllers.StudentRosterViewModel
(Where MvcApp.Controllers is the namespace that contains your model.)
Thanks to the help of everyone in the comments (#StephenMuecke, #LinhTuan) and #ScottHannen, I am able to create a working and improved View page of the ViewModel.
Here are the structural improvements I made.
I created a ViewModels folder and placed the ViewModel class there (no longer in the Models folder).
I took out the [Key] attribute and Id property from the ViewModel -- it really wasn't what made it appear to work before.
I right-clicked the Action Name and selected "Create View" (empty without model)
The View was successfully created. The View code you see below is a basic test for ability access the ViewModel properties and display data -- I can add Bootstrap styles and the functionality, now that I have a basic test.
The IEnumerable took a little work to unpack. I later learned how to add a variable var headerMeta = Model.StudentRosters.FirstOrDefault(); so that I could use #Html.DisplayNameFor in the table body section, I picked up another trick (modelItem => item.[column name]).
A potential further step is to create a custom DisplayFor and DisplayNameFor helper so that I can more directly use the Html helpers without having to add the headerMeta variable.
I call this an initial success because I finally have data reaching the view.
#model ViewModelTest.Controllers.ViewModels.StudentRosterViewModel
#{
ViewBag.Title = "FilterableIndex";
#*This enables us to read the StudentRoster column names*#
var headerMeta = Model.StudentRosters.FirstOrDefault();
}
<h2>FilterableIndex</h2>
<p>Test for reception of data</p>
<ul>
<li>Selected campus: #Model.SelectedCampus</li>
<li>Selected fiscal year: #Model.SelectedFiscalYear</li>
</ul>
#*I'll add bootstrap styles later*#
<table>
<tr>
<th>#Html.DisplayNameFor(m => headerMeta.Campus)</th>
<th>#Html.DisplayNameFor(m => headerMeta.FiscalYear)</th>
<th>#Html.DisplayNameFor(m => headerMeta.StudentName)</th>
<th>#Html.DisplayNameFor(m => headerMeta.StudentID)</th>
</tr>
#foreach (var item in Model.StudentRosters)
{
<tr>
<td>#Html.DisplayFor(modelItem => item.Campus)</td>
<td>#Html.DisplayFor(modelItem => item.FiscalYear)</td>
<td>#Html.DisplayFor(modelItem => item.StudentName)</td>
<td>#Html.DisplayFor(modelItem => item.StudentID)</td>
</tr>
}
I have main view and main model.
Inside this view i have such lines
foreach (var loadTask in Model.LoadTasks)
{
Html.RenderPartial("TripUpdateTask", new TripUpdateTaskModel { Task = loadTask });
}
So in main model i have the
public List<OrderTaskRecord> LoadTasks { get; set; }
Submodel is:
public class TripUpdateTaskModel
{
public OrderTaskRecord Task { get; set; }
}
I played and played but still unable to save the data. Here is current and simple look.
<tr>
<td>Actual Time:</td>
<td>#Html.TextBoxFor(m => m.Task.Task.ActualTime)</td>
</tr>
In raw html these time controls have same name and same id. So i dont know what need to do to save that.
I mean the data entered on main view back to controller fine, but not from partial view
I use Html.BeginForm and fieldset in main view like this:
#using (Html.BeginForm("TripUpdate", "SupplierBookingUpdate", FormMethod.Post, new { id = "SupplierBookingUpdateSave" }))
{
<fieldset>
.. table
</fieldset>
}
Rather than trying to name each text box individually, I would recommend to bind your collection of OrderTaskRecord. That way you do not need to worry about a number of text boxes and their names.
There's a good introduction from Scott Hanselman on the subject.
You can also try another example here: http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx/
If you want to pass a custom name to the input being generated use this. It passes the html attributes to the TextBoxFor method:
#Html.TextBoxFor(m => m.Task.Task.ActualTime, new { Name = "yourName" )
Of course, you can create counters, etc in here to keep them unique.
You can extract them later in your Controller by getting the Form data:
Request.Form["yourName"];
I have a display for a collection of "SomeObject"
public List<SomeObject> SomeObjects
#Html.DisplayFor(m => m.SomeObjects)
This all works fine.. Then I had a requirement that meant I would need a second different display template for the same collection..
#Html.DisplayFor(m => m.SomeObjects, "NameOfTemplate")
I get the below message when I do this.. I have used named display templates before, but maybe it only works for primitive types - strings etc?
'System.Collections.Generic.List1[SomeObject]', but this dictionary
requires a model item of type 'SomeObject'.
Adding code as request
"Test" DisplayTemplate
#model ViewModels.SomeObject
<p class="Namme">
<span>#Model.Name</span>
</p>
SimpleViewModel
public class SomeObject
{
public string Name { get; set; }
}
Property in ParentViewModel
public List<SomeObject> SomeObjects { get; set; }
Cshtml file..
#Html.DisplayFor(m => m.SomeObjects, "Test")
To me its a simple question, can a named template be used with a collection or not? Is it another limitation of mvc..
Cheers,
John
Yes you are passing a list of SomeObject to your display template, when it is expecting:
#model ViewModels.SomeObject
You either need to change the template to something like this and accept a list as the view model:
#model IList<ViewModels.SomeObject>
#foreach (var item in Model) {
<p class="Namme">
<span>#item</span>
</p>
}
Or change what you are passing into the Display Template in the parent CSHTML file and make it a single object of SomeObject something like this:
public List<SomeObject> SomeObjects
#foreach (var item in Model.SomeObjects) {
Html.DisplayFor(item, "TemplateName")
}
Its hard to answer because im not sure what your trying to achieve, and what SomeObject is, but this should point you in the right direction.
The type of SomObjects is List<SomeObjects> so the view template must have a matching model:
#model `ICollection<ViewModels.SomeObject>`
I prefer to not restrict the view to the concrete List<> class but rather use an interface.
That also means that you have to adjust your template to use a loop to iterate through the items in the collection.