I'm done tearing my hair out, so I'm asking for help. I've build a short recursive HTML Helper to display a list of lists. I want to return any edits back to the controller. The child of a child keeps coming back null. When I remove the recursion and just force through a 2nd layer of a "for loop", it seems to work. Any clue? I'm about to throw in the towel on RAZOR and just learn to do all this in Jquery...
Based on my code, the problem resides here when the model is posted back (you'll see this in the comments in the post ActionResult method):
Node.Name is ok
Node.Children[0].Name is ok
Node.Children[1].Name is ok
Node.Children[1].Children = null (HERE LIES THE PROBLEM!)
CONTROLLER CODE
public ActionResult Y()
{
Node driverTree = new Node() { Name="Level1", Children = new List<Node>() };
driverTree.Children.Add(new Node() { Children = null, Name = "Level2A" });
driverTree.Children.Add(new Node() {Name ="Level2B", Children = new List<Node> {new Node{Name="Level3A", Children=null},
new Node{Name="Level3B", Children=null}}
});
return View(driverTree);
}
[HttpPost]
public ActionResult Y(Node Node)
{
//Keeps returning:
// Node.Name is ok
// Node.Children[0].Name is ok
// Node.Children[1].Name is ok
// Node.Children[1].Children = null (HERE LIES THE PROBLEM!)
int x = 5; //
return View();
}
}
public class Node
{
public string Name { get; set; }
public List<Node> Children { get; set; }
public bool IsChecked { get; set; }
}
VIEW CODE
#model JqueryUITests.Controllers.Node
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
<script src="../../Scripts/jquery-ui-1.10.3.min.js" type="text/javascript"></script>
<script src="../../Scripts/jquery-ui.unobtrusive-2.1.0.min.js" type="text/javascript"></script>
}
#helper ShowTree(List<JqueryUITests.Controllers.Node> children)
{
<ul>
#for (int i = 0; i < children.Count;i++ )
{
<li>
#Html.EditorFor(x => children[i].Name)
#if (children[i].Children != null)
{
#ShowTree(children[i].Children)
}
</li>
}
</ul>
}
#using (Html.BeginForm())
{
<ul id="tree">
<li>
#Html.EditorFor(x=>Model.Name)
#ShowTree(Model.Children)
</li>
</ul>
<p>
<input type="submit" value="Create" />
</p>
}
HTML CODE
<form action="/X/Y" method="post"> <ul id="tree">
<li>
<input class="text-box single-line" id="Name" name="Name" type="text" value="Level1" />
<ul>
<li>
<input class="text-box single-line" id="children_0__Name" name="children[0].Name" type="text" value="Level2A" />
</li>
<li>
<input class="text-box single-line" id="children_1__Name" name="children[1].Name" type="text" value="Level2B" />
<ul>
<li>
<input class="text-box single-line" id="children_0__Name" name="children[0].Name" type="text" value="Level3A" />
</li>
<li>
<input class="text-box single-line" id="children_1__Name" name="children[1].Name" type="text" value="Level3B" />
</li>
</ul>
</li>
</ul>
</li>
</ul>
<p>
<input type="submit" value="Create" />
</p>
The list is rendering an item for each node, but it is not rendering the name attribute for each node properly.
Razor handles lists by using the name attribute to create arrays when the inputs are posted back and the model re-created. Eg
List<Foo> AllMyFoos { new Foo { Id = 1 }, new Foo { Id = 2 }, new Foo { Id = 3 }}
Would be rendered (as per your example above) like this:
<input type="text" name="AllMyFoos[0].Id" />
<input type="text" name="AllMyFoos[1].Id" />
<input type="text" name="AllMyFoos[2].Id" />
Under the hood the model binding in Razor and MVC then recreates the list of Foo objects and passes it to your controller method.
The issue with your recursive method is that when you call it with the list of child nodes, the index of each child node is being rendered and you are losing the info that is defining it as a child of another node.
Does that make sense?
Have a look at this SO question and read the answer about display templates for collection types - it's a better way of achieving what you're after.
SO Display Templates Answer
Problem is clear:
#Html.EditorFor(x => children[i].Name)
Normally x represents model and then properties are of format x.children[i].Name. Next level would be x.children[i].children[j].Name. EditorFor derives id and name of <input /> field from that expression. In your case expression is always children[i].Name, so it breaks mapping of id/name relative to your root Model.
I'm not sure if there is a good way to make recursive rendering like you want. Perhaps using non-lambda version Html.Editor(string expression), where you would construct expression as a string, taking care of nesting level in your code (ex: #Html.Editor("Children[1].Children[2].Name")).
You would be responsible to generate proper value "Children[1].Children[2].Name".
Expanding on dav83's remark about a display template. It should look something like this.
File
Views\DisplayTemplates\Node.cshtml
#model JqueryUITests.Controllers.Node
<li>
#Html.EditorFor(x => x.Name)
#if (Model.Children != null)
{
<ul>
#Html.Editor(x => x.Children)
</ul>
}
</li>
Now in your view put.
#model JqueryUITests.Controllers.Node
#section Scripts
{
#Scripts.Render("~/bundles/jqueryval")
<script src="../../Scripts/jquery-ui-1.10.3.min.js" type="text/javascript"></script>
<script src="../../Scripts/jquery-ui.unobtrusive-2.1.0.min.js" type="text/javascript"></script>
}
#using (Html.BeginForm())
{
<ul id="tree">
#Html.EditorForModel()
</ul>
<p>
<input type="submit" value="Create" />
</p>
}
MVC will render the HTML using the Node DisplayTemplate for your model. Additionally, when you call #Html.EditorFor(x => x.Children), it will use the display template to render each item in the collection. This is where you get your recursion to build the entire tree.
Additionally, it will keep track of where it is in the tree and name the HTML elements correctly, allowing your tree to post as you would expect.
Related
I have the following model:
public class Card
{
[DataType(DataType.Date)]
[BindProperty]
public DateTime Day { get; set; }
[BindProperty]
public string Field { get; set; }
}
The following Controller:
// GET: Card
public async Task<IActionResult> Index(DateTime? day)
{
return View(model);
}
public async Task<IActionResult> Refresh(DateTime? Day, string Field)
{
return RedirectToAction("Index", Day);
}
The following View:
#model Card
<h1>Cards</h1>
<div class="text-center">
<label asp-for="Day" class="control-label"></label>
<input asp-for="Day" class="form-control" />
</div>
<div class="text-center">
<label asp-for="Field" class="control-label"></label>
<select asp-for="Field" class="form-control" asp-items="ViewBag.Fields"></select>
</div>
<form asp-action="Refresh">
#Html.HiddenFor(x => x.Day)
#Html.HiddenFor(y => y.Field)
<input type="submit" value="Refresh" class="btn btn-default" />
</form>
No matter what I change, I always get the initial Day value back and null as the Field, like the Model has never been changed…
So how can I post back the modified model to my controller?
your form is submitting the values from the hidden fields which are rendered on the page when the view first loads and then they are never modified (which is why you are seeing the default initialization for Day and for Field). Your editable fields are outside of the form and are what you're editing but they never get submitted to the server. I think the main takeaway here for you is that forms only know about inputs that exist inside of them (unless you write some javascript magic to handle this but there is no reason to do so in this case)
You need to remove the hidden fields and put your editable fields inside the form as follows:
#model Card
<h1>Cards</h1>
<form method="post" asp-action="Refresh">
<div class="text-center">
<label asp-for="Day" class="control-label"></label>
<input asp-for="Day" class="form-control" />
</div>
<div class="text-center">
<label asp-for="Field" class="control-label"></label>
<select asp-for="Field" class="form-control" asp-items="ViewBag.Fields"></select>
</div>
<input type="submit" value="Refresh" class="btn btn-default" />
</form>
you can also change your controller action to:
[HttpPost]
public async Task<IActionResult> Refresh(Card card)
{
return RedirectToAction("Index", card.Day);
}
I'm having a weird issue with a form in my View not returning the model's correct Id property value. You'll noticed in the code below I have the script section logging the model's Id. Doing this shows the correct Id on the console, but when the Id is passed to the Controller's action method it is always 0, which is incorrect.
Here's the view:
#model EotE_Encounter.Models.Encounter
<div>
<h4>#Model.Name</h4>
<div>
<form asp-action="CreateCharacter" asp-controller="Encounter" data-bind="" data-ajax="true" data-ajax-mode="replace" data-ajax-update="#character-container">
<input id="encounterId" type="hidden" value="#Model.Id" />
<button class="btn btn-default" type="submit">Create Character</button>
</form>
</div>
<hr />
<div>
<ul>
#{
if(Model.CharactersInEncounter != null)
{
foreach(Character character in Model.CharactersInEncounter)
{
<li>#character.Name</li>
}
}
}
</ul>
</div>
</div>
<script>
console.log(#Model.Id);
</script>
Related Action Method:
public ActionResult CreateCharacter(int encounterID)
{
return RedirectToAction("CreateCharacter", "Character", encounterID);
}
And the Encounter model:
public class Encounter
{
//these first three properties may not be used just yet.
public int Id { get; set; }
public string Name { get; set; }
public byte Round { get; set; }
public List<Character> CharactersInEncounter { get; set; }
[StringLength(2000)]
public string Notes { get; set; }
}
Only form elements with a name attribute will have their values passed when submitting a form. So, add the name attribute to your hidden element. Id and the name are not the same.
#model EotE_Encounter.Models.Encounter
<div>
<h4>#Model.Name</h4>
<div>
<form asp-action="CreateCharacter" asp-controller="Encounter" data-bind="" data-ajax="true" data-ajax-mode="replace" data-ajax-update="#character-container">
<input id="encounterId" name="encounterID" type="hidden" value="#Model.Id" />
<button class="btn btn-default" type="submit">Create Character</button>
</form>
</div>
<hr />
<div>
<ul>
#{
if(Model.CharactersInEncounter != null)
{
foreach(Character character in Modelsel.CharactersInEncounter)
{
<li>#character.Name</li>
}
}
}
</ul>
</div>
</div>
<script>
console.log(#Model.Id);
</script>
Notice the name attribute of the <input id="encounterId" name="encounterID" type="hidden" value="#Model.Id" /> element. It has to be the same name as the action parameter (int encounterID). If it's not the same then the parameter binding will not work.
My View has list of guests (i got entire list) and on their arrival, I have to mark them as arrived.
I wait for 30 min and submit all details to servers by clicking on submit button.
i will submit, guest ID and arrived status to server
I donot know how to pass last of the active guests from View to server.
My Controller
[HttpPost]
public ActionResult GuestList(GuestListForModel collection)
{
var temp = collection;
return View();
}
MyView
#using (Html.BeginForm("GuestList", "Guest"))
{
#foreach (var saiList in Model.GuestArrivalInfoList)
{
<li class="group">
<div class="IB left">
<img src="images/user-1.png" alt="" class="stdImg rds50" width="100" height="100" />
</div>
<div class="left brRgt nameAge">
<div class="group">
#saiList.FirstName #saiList.LastName
<div class="age">#saiList.Age<span class="font9">yrs</span></div>
</div>
</div>
<footer class="group">
<div class="sheet brRgt left">
<span class="font9 dblock">present</span>
<div>#Model.Present</div>
</div>
<div class="sheet brRgt left">
<span class="font9 dblock">#Model.Absent</span>
<div>1</div>
</div>
<div class="sheet brRgt left">
<span class="font9 dblock">Model.NoAttendance</span>
<div>1</div>
</div>
<input type="submit" value="Save" id="Save" />
</footer>
Model
public class GuestListForModel
{
public bool Status {get;set;} // arrived or not arrived.
public Int GuestID {get;set;}
}
P.S : I could not get satisfactory answer from googling.
Thanks in Advance
There are two changes that you will need to make:
Change the GuestList action to accept a collection:
[HttpPost]
public ActionResult GuestList(ICollection<GuestListForModel> collection)
{
var temp = collection;
//do your processing
return View();
}
Use a for loop to render the guest list
#for(var i = 0; i < Model.GuestArrivalInfoList.Count; i++)
{
#Html.TextBoxFor(m => Model.GuestArrivalInfoList[i].GuestID)
}
Razor/ASP.NET will take care of both setting the correct id in the view model when rendered (e.g. GuestID would have an id of "GuestArrivalInfoList[1].GuestID"), and will also handle the model binding when hitting the action, making sure that the collection of fields are mapped into the collection.
I presume that there are missing / additional details in your view model and GuestListForModel that ensure that you have the correct data going back and forth (e.g. I don't see the GuestID in the view).
I am not able to create labels and textboxes using model in my view with html helpers.
My View Syntax:
#using (Ajax.BeginForm("RoomsAvail",new AjaxOptions
{
UpdateTargetId = "RoomsAvailData",
InsertionMode ="InsertionMode.Replace"
}))
{
<input type='text' id="Checkin" class="form-control datepicker" />
<input type='text' id="Checkout" class="form-control datepicker" />
<button type="submit" value="Search"></button>
}
#model IEnumerable<RoomAvailabilitySummary>
<div>
#Html.LabelFor(model=>model.ARRDAT)
</div>
<div>
#Html.EditorFor(model=>model.Name)
</div>
My Model
public class RoomAvailabilitySummary
{
public string ARRDAT{get;set;}
public string Name{get;set;}
}
After doing like this, my view is not rendering the model properties into my view to display text for labels and getting the error as "Syste.Collection.Generic.IEnumerable does not contain the definiton for ARRDAT"
Why i am not able to render model property in my html helper to display the name with the help of html helper?
And also i want to access my model values in controller. How can i do this?
Your model is:
#model IEnumerable<RoomAvailabilitySummary>
And you trying to access Name in IEnumerable:
<div>
#Html.EditorFor(model=>model.Name)
</div>
You should use foreach if it is IEnumerable OR just #model RoomAvailabilitySummary if it is not IEnumerable
Added:
Something like this should work:
#model IEnumerable<RoomAvailabilitySummary>
#if(Model.Count() > 0)
{
<div>
#Html.LabelFor(model=>Model.First().Name)
</div>
}
#foreach (var item in Model)
{
<div>
#Html.EditorFor(model=>item.Name)
</div>
}
I think u are trying to iterate over the model (list):
#model IEnumerable<RoomAvailabilitySummary>
#Html.LabelFor(model => model[0].Name)
#foreach(var room in Model)
{
<br />
#Html.EditorFor(x => room.Name)
}
I have a view model with a from that includes a set of checkboxes. I need the check boxes to map to an array when binding in the post back method of my controller.
Here's the view model.
#model TMDM.Models.TestSeriesCreateViewModel
#{
ViewBag.Title = "Create";
}
<h2>Create a Test Series</h2>
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<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>
<h3>Which Test Collections are in this Test Series?</h3>
<div class="editor-field">
#{
var i = 0;
foreach (var testCollection in Model.TestCollections)
{
<input type="checkbox" id="ChosenTestCollectionIds[#i]" name="ChosenTestCollectionIds[#i]" value="#testCollection.Id" />
<span>#testCollection.Title</span>
<br />
i++;
}
}
</div>
<p>
<input type="submit" value="Save" class="medium green awesome" />
#Html.ActionLink("Cancel", "Index", "TestSeries", null, new { #class = "medium black awesome" })
</p>
</fieldset>
The form is rendering fine, I've checked the source and each output check box has a different number for their id and name fields.
<input type="checkbox" id="ChosenTestCollectionIds[0]" name="ChosenTestCollectionIds[0]" value="5" />
<input type="checkbox" id="ChosenTestCollectionIds[1]" name="ChosenTestCollectionIds[1]" value="6" />
//etc...
Here is the view model.
public class TestSeriesModel
{
public int Id { get; set; }
public string Title { get; set; }
}
public class TestSeriesCreateViewModel : TestSeriesModel
{
public List<ITestCollectionDataObject> TestCollections { get; set; }
public int[] ChosenTestCollectionIds { get; set; }
}
Problem I'm having is that when the form posts back the ChosenTestCollectionIds array comes back null. What am I doing wrong here?
ANSWER
I've worked out how to do it:
<input type="checkbox" id="[#i]" name="ChosenTestCollectionIds" value="#testCollection.Id" />
<input type="checkbox" id="[#i]" name="ChosenTestCollectionIds" value="#testCollection.Id" />
I always come back to Phil Haack's post about model binding a list. In addition, I always define my own index because my user's will alter the list on the client side then post back the changes.
http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx
Set the name of the input types to all be the same. You can also create a custom model binder if you are trying to bind a more complex model than just a list. Here is an excellent article on the different ways to bind to your models
Various Model Binding techniques