I'm working on my first ASP.NET MVC 3 application and I've got a View that looks like this:
#model IceCream.ViewModels.Note.NotesViewModel
#using (Html.BeginForm())
{
#Html.ValidationSummary(true)
#Html.TextBoxFor(m => m.Name)
foreach (var item in Model.Notes)
{
#Html.EditorFor(m => item);
}
<input type="submit" value="Submit"/>
}
And I have an EditorTemplate that looks like this:
#model IceCream.ViewModels.Note.NoteViewModel
<div>
#Html.HiddenFor(m => m.NoteID)
#Html.TextBoxFor(m => m.NoteText)
#Html.CheckBoxFor(m => m.IsChecked)
</div>
NotesViewModel looks like so:
public class NotesViewModel
{
public string Name { get; set; }
public IEnumerable<NoteViewModel> Notes { get; set; }
}
NoteViewModel looks like this:
public class NoteViewModel
{
public int NoteID { get; set; }
public System.DateTime Timestamp { get; set; }
public string NoteText { get; set; }
public bool IsChecked { get; set; }
}
The NotesViewModel is populated just fine when it is passed to the view. However when the submit button is clicked, the controller action handling the post has only the value for the Name property of the viewmodel. The Notes property - the list of notes that have been checked/unchecked by the user - is null. I've got a disconnect between the populating of those TextBoxFor and CheckBoxFor elements when the view is displayed and the ViewModel being sent back. Guidance on this?
SOLUTION
Thanks go to Mystere Man for setting me straight on this. As I understand it, essentially by changing my loop to
#Html.EditorFor(m => m.Notes)
changes the underlying HTML, which I understand provides for the proper model binding on the post. Looking at the resulting HTML, I see that I get the following generated for one of the Notes:
<div>
<input id="Notes_0__NoteId" type="hidden" value="1" name="Notes[0].NoteId">
<input id="Notes_0__NoteText" type="text" value="Texture of dessert was good." name="Notes[0].NoteText">
<input id="Notes_0__IsChecked" type="checkbox" value="true" name="Notes[0].IsChecked>
</div>
Which is different than this HTML generated by my original code:
<div>
<input id="item_NoteId" type="hidden" value="1" name="item.NoteId>
<input id="item_NoteText" type="text" value="Texture of dessert was good." name="item.NoteText" >
<input id="item_IsChecked" type="checkbox" value="true" name="item.IsChecked">
</div>
By looping through the Notes, the generated HTML essentially loses any references to the viewmodel's Notes property and while the HTML gets populated correctly, the setting of the checkbox values has no way to communicate their values back to the viewmodel, which I guess is the point of the model binding.
So I learned something, which is good.
You're a smart guy, so look at your view. Then, consider how the HTML gets generated. Then, consider how on postback the Model Binder is supposed to know to re-populate Notes based on the generated HTML.
I think you'll find that your HTML doesn't have enough information in it for the Model Binder to figure it out.
Consider this:
#EditorFor(m => Model.Notes)
Rather than the for loop where you are basically hiding the context from the EditorFor function.
And for those that just want the answer as a for loop:
#for (int x = 0; x < Model.Notes.Count(); x++) {
#Html.HiddenFor(m => m.Notes[x].NoteId)
#Html.EditorFor(m => m.Notes[x].NoteText)
#Html.EditorFor(m => m.Notes[x].IsChecked)
}
Related
I'm new to Razor pages and having trouble with model binding back to the view.
I'm using VS2019 version 16.0.4.
This is my PageModel:
public class IndexModel : PageModel
{
[BindProperty]
public int PageIndex { get; set; }
public IActionResult OnPost()
{
PageIndex++;
return Page();
}
}
And my View:
#page
#model IndexModel
<form method="post">
<div class="form-group">
<label asp-for="PageIndex"></label>
<input asp-for="PageIndex" class="form-control" />
<span class="text-danger" asp-validation-for="PageIndex"></span>
<button type="submit" class="btn btn-primary">Increment</button>
</div>
</form>
I would expect the value displayed in the input control to be incremented on each click - but it remains at zero. The binding into the controller seems to work ok. If I enter a value of "5" and click the button then a breakpoint shows me that a value of 5 is received before being incremented to 6. However, the incremented value does not get reflected back to the view.
Where did I go wrong?
Model binding takes values from the HTTP request and binds them to handler method parameters or PageModel properties. It is not two-way, and does not then bind those values back to the controls where the values originated.
You need to explicitly set the value attribute of the input to see the behaviour that you expect:
<input asp-for="PageIndex" value="#Model.PageIndex" />
This question already has an answer here:
Submit same Partial View called multiple times data to controller?
(1 answer)
Closed 6 years ago.
Here is my scenario:
public class ComplexObject
{
public int SomeProperty {get;set;}
public List<SimpleObject> Object {get;set;}
}
public class SimpleObject
{
public string FirstName {get;set;}
public string LastName {get;set;}
}
I created a strongly typed partial view for SimpleObject
#model SimpleObject
<div>
<input type="button" value="Button" name="btn" />
<div>
<div>
#Html.TextBoxFor(Model => Model.FirstName, new { #class = "", #maxlength = "50" })
</div>
<div>
#Html.TextBoxFor(Model => Model.LastName, new { #class = "", #maxlength = "50" })
</div>
Now I want to Render this partial view inside another view (MainView). The idea is that a user can click SimpleObject partial view button and
generate the same partial view again on the MainView . SO here is how it looks :
MainView
SimpleView -> Add
SimpleView -> Add
I can create an ajax action and generate the simple view and append it to mainview but the problem is that simpleobject is NOT binding to the ComplexObject.
This is how I render partialview in MainView.
#Html.Partial("_PartialView", Model.SimpleObject, new ViewDataDictionary(ViewData)
{
TemplateInfo = new System.Web.Mvc.TemplateInfo
{
HtmlFieldPrefix = "Simple"
}
})
The MainView calls a controller action on submit click and the entire ComplexObject is submitted. Here my List of SimpleObject is always NULL.
public ActionResult CreateComplex(ComplexObject object)
{
// HERE LIST<SIMPLEOBJECT> is always NULL
}
Any ideas what am I doing wrong here ?
The first time you render the partial view you will have something like this:
<div>
<input type="text" name="FirstName" maxlength="50" />
</div>
<div>
<input type="text" name="LasttName" maxlength="50" />
</div>
When you add more than one partial view to the page, at the end only 2 text boxes will be posted to the server because they all have the same name.
To fix this issue, you have to change your partial view to make the input boxes like an array, you can add a property to your model and set it from the action
assuming you added a property called Index to your model, your code should look like this
#model SimpleObject
<div>
<input type="button" value="Button" name="btn" />
<div>
<div>
#Html.TextBox("FirstName["+Model.Index+"]", new { #class = "", #maxlength = "50" })
</div>
<div>
#Html.TextBox("LastName["+Model.Index+"]", new { #class = "", #maxlength = "50" })
</div>
With each button submit, you increment the Index and render the view.
Another solution, is to remove the partial view and add the input boxes using JavaScript, you can use Knockout
have a look at this article that explains how to do it: http://knockoutjs.com/examples/grid.html
Here are the changes that I did to code posted in original question to bind list of SimpleObjects to ComplexObject.
#Html.Partial("_PartialView")
The class looks like this :
public class ComplexObject
{
public int SomeProperty {get;set;}
public List<SimpleObject> Objects {get;set;}
}
and the partialview starts with BeginCollectionItem
#model AZSolutions.Framework.Model.SimpleObject
#using (Html.BeginCollectionItem("Objects")) {
...
}
I have project on ASP MVC 5. I have a model "Article". This model have HashSet and ICollection of Author. Author - second model:
public partial class Article
{
public Article()
{
Authors = new HashSet<Author>();
}
[DisplayName("Авторы")]
public virtual ICollection<Author> Authors { get; set; }
I need to add page of creating Article, on which you can increase the number of authors(using AJAX), and each author to register the fields. I decided to use partial view of Author's model, without "Create" button(Create button used only view of creating Article). I need in unlimited adding new partial views, and after fill them - get all data from them. How make it? I newbie in MVC, and can't imagine how it will works.
http://i.stack.imgur.com/0RHD0.png - an illustration of how it should look
Is there a need to use partials? wouldnt it be easier to write a small script that would instead clone the first author enclosing element and just change the names of the elements involved to create a new author?
<div id="enclosingDiv" data-count="x">
<div class="someClass" data-index='x1' >
Author1 name Aurthor1 Textboxname="CollectionList[Index].Property"...
</div>
Now when creating a new Authouther, you can just create:
<script>
function createNewAuthor()
{
//clone first author
var count = $('encolsingDiv').attr('data-count');
//var count = $('encolsingDiv').children().length;
var author = $('enclosingDiv').first().clone();
//change name and id,etc using data-count
author.find('*[name$='value'])attr('name','ListCollection[count + 1]");
author.find('*[name$='value'])attr('id',....);
author.attr('data-index',count +1)
$('enclosingDiv').append(author);
$('enclosingDiv').attr('data-count',count + 1 to it);//makes life easier
}
function deleteAuthor(authourIndex)
{
//assumes that an author can be removed
$('div[data-index="'+authorIndex+'"]").remove();
$('enclosingDiv').children.each(function()
{
//if delete functionality exists, change the names of the element indices using the count variable
change all indices of element properties concerned
$(this).find('*[name$='value']).attr('name','ListCollection['+count+'].sumproperty");
count++;
});
}
</script>
So you can use that for create and delete methods, you don't need partials for that.
The code might need some work as what I show is the concept
It is not that hard. Your partial views will be posted as a collection.
Suppose that your partial view has 2 values, FirstName and LastName. It should be something like this:
#{
Guid index = Guid.NewGuid();
}
<input type="hidden" name="People.index" value="#index" />
<input type="text" name="People[#index].FirstName" value="" />
<input type="text" name="People[#index].LastName" value="" />
The final output would be:
<input type="hidden" name="People.index" value="B756DAD8-5D5D-449E-A4B4-E61F75C1562C" />
<input type="text" name="People[B756DAD8-5D5D-449E-A4B4-E61F75C1562C].FirstName" value="" />
<input type="text" name="People[B756DAD8-5D5D-449E-A4B4-E61F75C1562C].LastName" value="" />
<input type="hidden" name="People.index" value="B78B7BBC-EB0E-41CB-BE18-C1E3F7526F32" />
<input type="text" name="People[B78B7BBC-EB0E-41CB-BE18-C1E3F7526F32].FirstName" value="" />
<input type="text" name="People[B78B7BBC-EB0E-41CB-BE18-C1E3F7526F32].LastName" value="" />
Your model must have a collection People object.
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class Article
{
//other properties...
public ICollection<Person> People { get; set; }
}
Your Controller:
public ActionResult YourAction (Article model)
{
//...
}
Untested code, but it should work fine.
I've a MVC application, whose SharedLayout view(Master Page) gives user capability to search. They could search their order by Order No or By Bill no. So there are two option buttons the Shared View along with the textbox. Code is somewhat like this
#using (Html.BeginForm("Track", "Tracking", FormMethod.Post))
{
<div style="text-align: center">
<textarea cols="20" id="txtNo" name="txtOrderNo" rows="2" ></textarea>
</div>
<div style="text-align: center">
<input type="radio" name="optOrderNo" checked="checked" value="tracking" />Order No <input type="radio" name="optRefNo" value="tracking" />Ref No
</div>
<div style="text-align: center">
<input type="submit" value="Track" />
</div>
}
So it'll go to TrackingController and Track Method in it and return the view. It works fine for a single search as a View is associated with a controller's methods. It works fine but how could i conditionally return the other view based on the radio button selection.
What i come up with is this
[HttpPost]
public ActionResult Track(FormCollection form)
{
string refNo = null;
if (form["optRefNo"] == null)
{
string OrderNo = form["txtOrderNo"];
var manager = new TrackingManager();
var a = manager.ConsignmentTracking(OrderNo);
var model = new TrackingModel();
if (OrderNo != null)
model.SetModelForConsNo(a, consNo);
return View(model);
}
refNo = form["txtConsNo"];
return TrackByRef(refNo);
}
public ActionResult TrackByRef(string refNo)
{
//what ever i want to do with reference no
return View();
}
Kindly guide.
Thanks
View has an overload where the first parameter is a string. This is the name (or path) to the view you want to use, rather than the default (which is a view that matches the action's name).
public ActionResult TrackByRef(string refNo)
{
//what ever i want to do with reference no
return View("Track");
// or, if you want to supply a model to Track:
// return View("Track", myModel);
}
I am having a trouble while trying to create an entity with a custom view modeled create form. Below is my custom view model for Category Creation form.
public class CategoryFormViewModel
{
public CategoryFormViewModel(Category category, string actionTitle)
{
Category = category;
ActionTitle = actionTitle;
}
public Category Category { get; private set; }
public string ActionTitle { get; private set; }
}
and this is my user control where the UI is
<%# Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<CategoryFormViewModel>" %>
<h2>
<span><%= Html.Encode(Model.ActionTitle) %></span>
</h2>
<%=Html.ValidationSummary() %>
<% using (Html.BeginForm()) {%>
<p>
<span class="bold block">Başlık:</span>
<%=Html.TextBoxFor(model => Model.Category.Title, new { #class = "width80 txt-base" })%>
</p>
<p>
<span class="bold block">Sıra Numarası:</span>
<%=Html.TextBoxFor(model => Model.Category.OrderNo, new { #class = "width10 txt-base" })%>
</p>
<p>
<input type="submit" class="btn-admin cursorPointer" value="Save" />
</p>
<% } %>
When i click on save button, it doesnt bind the category for me because of i am using custom view model and strongly typed html helpers like that
<%=Html.TextBoxFor(model => Model.Category.OrderNo) %>
My html source looks like this
<form action="/Admin/Categories/Create" method="post">
<p>
<span class="bold block">Başlık:</span>
<input class="width80 txt-base" id="Category_Title" name="Category.Title" type="text" value="" />
</p>
<p>
<span class="bold block">Sıra Numarası:</span>
<input class="width10 txt-base" id="Category_OrderNo" name="Category.OrderNo" type="text" value="" />
</p>
<p>
<input type="submit" class="btn-admin cursorPointer" value="Kaydet" />
</p>
</form>
How can i fix this?
Your View Model needs a default constructor without parameters and you need public set methods for each of the properties. The default model binder uses the public setters to populate the object.
The default model binder has some rules it follows. It chooses what data to bind to in the following order:
Form parameters from a post
Url route data defined by your route definitions in global.asax.cs
Query string parameters
The default model binder then uses several strategies to bind to models/parameters in your action methods:
Exact name matches
Matches with prefix.name where prefix is the parent class and name is the subclass/property
Name without prefix (as long as there are no collisions you don't have to worry about providing the prefix)
You can override the behavior with several options from the Bind attribute. These include:
[Bind(Prefix = "someprefix")] -- Forces a map to a specific parent class identified by the prefix.
[Bind(Include = "val1, val2")] -- Whitelist of names to bind to
[Bind(Exclude = "val1, val2")] -- Names to exclude from default behavior
You could use editor templates. Put your ascx control in ~/Views/Shared/EditorTemplates/SomeControl.ascx. Then inside your main View (aspx page) include the template like so (assuming your view is strongly typed to CategoryFormViewModel):
<%= Html.EditorForModel("SomeControl") %>
instead of
<% Html.RenderPartial("SomeControl", Model) %>
Make a default constructor for your viewmodel and initialize the Category there
public CategoryFormViewModel()
{
Category = new Category()
}
And at your controller action receive the viewmodel
public ActionResult ActionName(CategoryFormViewModel model)
{
//here you can access model.Category.Title
}