Here is what I am trying to do:
My goal is to display a list of Trending Opinions (A custom Model) from the page's model when the page loads. If a user clicks the "Show more Trending Opinions" button, it uses ajax to call a method on a controller that will then retrieve an additional number of items, come back to the page and display them. Then it adds say 20 more. Then they can repeat the process and click it again, etc.
Exactly the same as a normal site does when you click "Show More" on a list of items.
If the way I am approaching this is incorrect and you know of any tutorial (or just out of your head) showing the correct way to do this in MVC 4, please let me know. I am not dead-set on the way I am doing it at the moment, this is just the "correctest" way I have found.
I followed the answer to a similar question: How to Update List<Model> with jQuery in MVC 4
However, the data coming through to my controller is incorrect and I can't figure out what the issue is.
Let me put as much info as I can, because I have no idea where the error may be.
Model for page (OpinionModel has a few public properties):
public class IndexModel
{
public IList<OpinionModel> TopTrendingOpinions { get; set; }
}
The View:
<div id="TrendingOpinions">
<p>What is trending at the moment</p>
#using (Html.BeginForm("LoadMoreTrendingOpinions", "AjaxHelper",
method: FormMethod.Post,
htmlAttributes: new { #class = "form-horizontal", id = "LoadTrendingOpinionsForm" }))
{
#Html.EditorFor(x => x.TopTrendingOpinions)
<input type="submit" value="Load More Trending Opinions" />
}
<script type="text/javascript">
$('#LoadTrendingOpinionsForm').submit(function () {
$.ajax({
url: this.action,
type: this.method,
data: {
topTrendingOpinions: $(this).serialize()
},
success: function (result) {
alert(result);
}
});
return false;
});
</script>
</div>
**There is also an EditorTemplate for my model.
The Controller:**
[HttpPost]
public ActionResult LoadMoreTrendingOpinions(IList<MyGoldOpinionMVC.Models.OpinionModel> topTrendingOpinions)
{
var dataHelper = new Data.DataHelper();
var moreTrendingOpinions = dataHelper.LoadMoreTrendingOpinions(topTrendingOpinions.LastOrDefault().Id);
// var partialView = PartialView("../PartialViews/_ListOfPostedOpinion", moreTrendingOpinions);
return View(moreTrendingOpinions);
}
So here is the order of events:
When running the site, the form shows a list of OpinionModels (Using the Editor Template displaying correct data). When I click the SUBMIT button, it goes to the controller (I have a breakpoint) and the data for the "topTrendingOpinions" parameter is a List with one item in it, but that item is null. So in other words, it is not passing through the list that is clearly being used to populate the form.
The only way I have been able to get a list to post back to the controller is to build it manually with jquery. my understanding is this.serialize on a form click is going to try to serialize the whole from which would get very ugly. How I would do this is
<input type="button" class="btnMore" value="Load More Trending Opinions" />
$('.btnMore').on('click', function () {
$.ajax({
url: '#Url.Action("LoadMoreTrendingOpinions", "AjaxHelper")',
type: 'post',
contentType: 'application/json; charset=utf-8',
data: {
Id: '#ViewBag.Id'
},
success: function (result) {
//add results to your table
}
});
});
and set the id of the last record sent through the view bag on your controller so you have a reference to go off of for pulling the next chunk. Let me know if you have any questions
When posting lists you have to be really careful that your inputs are named correctly. If they are not, the default model binder fails to parse them into classes when posted resulting the object being null in the controller.
In your case you are posting a list of models inside a model, but not the whole model. I'd use PartialView instead of editortemplate, just to make working with field names easier. In my example we are posting a list of FooModels contained in IndexModel
Model
public class FooModel
{
public string Foo { get; set; }
public string Bar { get; set; }
}
public class IndexModel
{
public IList<FooModel> Foos { get; set; }
}
View
#using (Html.BeginForm("LoadMoreTrendingOpinions","AjaxHelper",
method: FormMethod.Post,
htmlAttributes: new { #class = "form-horizontal", id = "LoadTrendingOpinionsForm" }))
{
#Html.Partial("FooModelsPartial", Model.Foos)
<input type="submit" value="Load More Trending Opinions" />
}
FooModelsPartial
#model IList<FooModel>
#for (int i = 0; i < Model.Count(); i++)
{
#Html.EditorFor(model => model[i].Foo)
#Html.EditorFor(model => model[i].Bar)
}
Notice how we are using for instead of foreach loop. This is because editors in foreach loop are not named correctly. In this case we want our fields to be [0].Foo, [0].Bar, [1].Foo, [1]. Bar etc.
Controller:
[HttpPost]
public ActionResult LoadMoreTrendingOpinions(IList<FooModel> topTrendingOpinions)
{
// do something with toptrending thingy
var model = new IndexModel();
model.Foos = topTrendingOpinions;
return View("Index", model);
}
Now the real question in my opinion is do you really want to post the whole list of models to get bunch of new ones related to one of them? Wouldn't it be more convenient to post the id of opinion you'd want to read more of, returning partialview containing the requested more trending opinions and appending that to some element in the view with jquery?
Html:
#using (Html.BeginForm("LoadMoreTrendingOpinions","AjaxHelper",
method: FormMethod.Post,
htmlAttributes: new { #class = "form-horizontal", id = "LoadTrendingOpinionsForm" }))
{
<div id="more">#Html.Partial("FooModelsPartial", Model.Foos)</div>
<input type="submit" value="Load More Trending Opinions" />
}
Javascript:
$('#LoadTrendingOpinionsForm').submit(function () {
$.ajax({
url: this.action,
type: this.method,
data: {
id: 1 /* The id of trending item you want to read more of */
},
success: function (result) {
$("#more").html(result)
}
});
return false;
});
Controller:
[HttpPost]
public ActionResult LoadMoreTrendingOpinions(int id)
{
var moreTrendingOpinions = dataHelper.LoadMoreTrendingOpinions(id);
return PartialView("FooModelsPartial", moreTrendingOpinions);
}
Related
I'm creating an ajax-based Quiz in MVC. Below is the Question view. When the form is submitted I save the user selection in the controller then need to send the next question to the view without reloading the page. Is it possible to send/update the model from the controller in the ajax request
#model DataAccess.Question
#{
ViewBag.Title = "Survey";
Layout = "~/Views/Shared/_Layout.cshtml";
}
#using (Ajax.BeginForm("Survey", "Tools", new AjaxOptions { UpdateTargetId = "QuestionContent", HttpMethod = "Post", InsertionMode = InsertionMode.Replace }, new { QuestionId = Model.QuestionId }))
{
<div id="QuestionContent">
<h2>Welcome To Quiz</h2>
<fieldset>
<p>
Question
#Model.QuestionId of #ViewBag.QuestionCount:
</p>
<p>
#Model.Description.
</p>
<ul style="list-style:none;">
#foreach (var item in Model.Answers)
{
<li> #Html.RadioButton("ChoiceList", item.score) #item.AnswerDesc</li>
}
</ul>
<input type="submit" value="Next" id="submitButton" />
</fieldset>
</div>
}
It's much easier to implement an AJAX POST using jQuery and return a JSON object that contains all the next Q&A info. Use js/jQuery to set <div>'s or any other html element. Posting back and reloading is such a pain and becoming an outdated approach.
For example you could have this ViewModel class:
public class Answer
{
public int QuestionId {get; set;}
public string Answer {get; set;}
}
Build a view that has a div & input control for the Q & A.
Implement the Answer Button to POST via AJAX:
$.ajax({
type: "POST",
url: "/Exam/HandleAnswer" ,
data: { QuestionId: _questionId, Answer: $("#txt_answer").val() },
success: function (resp) {
if (resp.Success) {
$("#div_Question").text( resp.NextQuestionMessage);
_questionId = resp.NextQuestionId,
$("#txt_answer").val(''"); //clear
}
else {
alert(resp.Message);
}
}
});
In your ExamController:
[HttpPost]
public ActionResult HandleAnswer(Answer qa)
{
//use qa.QuestionId to load the question from DB...
//compare the qa.Answer to what the DB says...
//if good answer get next Question and send as JSON or send failure message..
if (goodAnswer)
{
return Json(new { Success = true, NextQuestionMessage = "What is the capital of of Texas", NextQuestionId = 123});
}
else{
return Json(new { Success = false, Message = "Invalid response.."});
}
}
I have the following problem when updating a for via AJAX after it is submitted. For some reason some hidden fields that are on the HTML that is returned are not being updated, which is weird because when I run the debugger they appeared to have the correct value.
This is the relevant part of my form
<div id="itemPopUpForm">
#{Html.EnableClientValidation();}
#Html.ValidationSummary()
<div id="formDiv">
#{ Html.RenderPartial("ItemData", Model, new ViewDataDictionary() { { "Machines", ViewBag.Machines }, { "WarehouseList", ViewBag.WarehouseList }, { WebConstants.FORM_ID_KEY, #ViewData[WebConstants.FORM_ID_KEY] } }); }
</div>
</div>
Then the partial view contains hidden fields like these which are the ones not being updated
#using (Html.BeginForm("Index", "Item", FormMethod.Post, new { id = "frmItem", name = "frmItem" }))
{
#Html.AntiForgeryToken()
#Html.HiddenFor(model => model.Item.SodID)
#Html.HiddenFor(model => Model.Item.ItemID) //The itemID needs updating when an item is copied
#Html.HiddenFor(model => model.Item.Delivery.DeliveryAddressID, new { #id = "delAddressID" })
And this is the javascript method that updates the form
function ajaxSave() {
if (!itemValid()) return;
popup('ajaxSplash');
$.ajax({
type: "POST",
url: '#Url.Action("Index")',
data: $("#frmItem").serialize(),
success: function (html) {
console.log(html);
$("#formDiv").html(html);
initItemPage();
alert("Item was saved successfully");
},
error: function () { popup('ajaxSplash'); onFailure(); }
});
}
The action Index returns the Partial View "ItemData" and when I check the Item Model it does have the correct value, but when I see the html returned it is still set to 0.
If you intend to modify a model property in your POST action don't forget to remove it from ModelState first, otherwise HTML helpers will use the originally posted value when rendering:
[HttpPost]
public ActionResult Index(MyViewModel model)
{
// remove the value from modelstate
ModelState.Remove("Item.ItemID");
// update the value
model.Item.ItemID = 2;
return PartialView(model);
}
I'm having the same problem and it seems like the helper HiddenFor evaluates with required unobtrusive validation even if in the model one does not annotate the property with [Required].
The HTML rendered by #Html.HiddenFor(m=>m.Step) is :
<input data-val="true" data-val-number="The field Step must be a number." data-val-required="The Step field is required." id="Step" name="Step" type="hidden" value="2">
Hence, it is why it works if we remove it from the ModelState.
Removing the property from the ModelState seems to me like a hack. I would prefer to use
<input type="hidden" id="Step" name="Step" value="#Model.Step" />
instead of the Html.HiddenFor helper.
You can also implement you own HiddenFor helper.
I have a ViewModel which contains a List of my Model, like so:
public class OrderConfirm
{
public ICollection<DayBookQuoteLines> SalesLines { get; set; }
public ICollection<DayBookQuoteLines> LostLines { get; set; }
public string Currency { get; set; }
}
I then use this ViewModel in my View like so:
#model btn_intranet.Areas.DayBook.Models.ViewModels.OrderConfirm
#{
ViewBag.Title = "Daybook - Order Confirmation";
}
<h6>Sales Lines</h6>
<div id="SalesOrders">
#using (Ajax.BeginForm("ConfirmSalesOrder", new AjaxOptions()
{
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "SalesOrders",
OnBegin = "SalesOrderConfirm"
}))
{
#foreach(var item in Model.SalesLines)
{
<p>#item.ItemName</p>
<p>#item.Qty</p>
#* Other Properties *#
}
<input type="submit" value="Submit Sales Order" />
}
</div>
<h6>Lost Lines</h6>
<div id="LostOrders">
#using (Ajax.BeginForm("ConfirmLostOrder", new AjaxOptions()
{
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "LostOrders",
OnBegin = "LostOrderConfirm"
}))
{
#foreach(var item in Model.SalesLines)
{
<p>#item.ItemName</p>
<p>#item.Qty</p>
#* Other Properties *#
}
<input type="submit" value="Submit Lost Order" />
}
</div>
The problem is, in my [HttpPost] actions, both ConfirmSalesOrder and ConfirmLostOrder. The value of my Model passed as a parameter is null:
[HttpPost]
public ActionResult ConfirmSalesOrder(List<DayBookQuoteLines> quoteLines)
{
// Process order...
return PartialView("Sales/_ConfirmSalesOrder");
}
so quoteLines is null. How can I bind the form to my model?
You don't have any input field in your form that will send the values to the server. You are only displaying them. That's why they are null when you submit the form => nothing is sent to the server.
But if inside this form the user is not supposed to modify any of the values all you need to do is to pass an id to the controller action that will allow you to fetch the model from the exact same location from which you fetched it in your GET action that rendered this form.
In this case your action will look like this:
[HttpPost]
public ActionResult ConfirmSalesOrder(int id)
{
List<DayBookQuoteLines> quoteLines = ... fetch them the same way as in your GET action
// Process order...
return PartialView("Sales/_ConfirmSalesOrder");
}
If on the other hand the user is supposed to modify the values in the form you need to provide him with the necessary input fields: things like textboxes, checkboxes, radio buttons, dropdownlists, textereas, ... And in order to generate proper names for those input fields I would recommend you using editor templates instead of writing foreach loops in your views.
UPDATE:
Seems like the user is not supposed to edit the data so there are no corresponding input fields. In this case in order to preserve the model you could during the AJAX request you could replace the Ajax.BeginForm with a normal Html.BeginForm an then manually wire up the AJAX request with jQuery. The advantage of this approach is that now you have far more control and you could for example send the entire model as a JSON request. To do this you could store the model as a javascript encoded variable inside the view:
<script type="text/javascript">
var model = #Html.Raw(Json.Encode(Model));
</script>
and then AJAXify the form:
$('#formId').submit(function() {
$.ajax({
url: this.action,
type: this.method,
contentType: 'application/json',
data: JSON.stringify({ quoteLines: model }),
success: function(result) {
$('#someTargetIdToUpdate').html(result);
}
});
return false;
});
i know there are a lot questions like this but i really cant get the explanations on the answers... here is my view...
<script type="text/javascript" language="javascript">
$(function () {
$(".datepicker").datepicker({ onSelect: function (dateText, inst) { },
altField: ".alternate"
});
});
</script>
#model IEnumerable<CormanReservation.Models.Reservation>
#{
ViewBag.Title = "Index";
}
<h5>
Select a date and see reservations...</h5>
<div>
<div class="datepicker">
</div>
<input name="dateInput" type="text" class="alternate" />
</div>
i want to get the value of the input text... there's already a value in my input text because the datepicker passes its value on it... what i cant do is to pass it to my controller... here is my controller:
private CormantReservationEntities db = new CormantReservationEntities();
public ActionResult Index(string dateInput )
{
DateTime date = Convert.ToDateTime(dateInput);
var reservations = db.Reservations.Where(r=>r.Date==date).Include(r => r.Employee).Include(r => r.Room).OrderByDescending(r => r.Date);
return View(reservations.ToList());
}
i am trying to list in my home page the reservations made during the date the user selected in my calender in my home page....
I don't see a Form tag in your View...or are you not showing the whole view? hard to tell.. but to post to your controller you should either send the value to the controller via an ajax call or post a model. In your case, your model is an IEnumerable<CormanReservation.Models.Reservation> and your input is a date selector and doesn't look like it is bound to your ViewModel. At what point are you posting the date back to the server? Do you have a form with submit button or do you have an ajax call that you aren't showing?
Here is an example of an Ajax request that could be called to pass in your date
$(function () {
$(".datepicker").onselect(function{
searchByDate();
})
});
});
function searchbyDate() {
var myDate = document.getElementById("myDatePicker");
$.ajax({
url: "/Home/Search/",
dataType: "json",
cache: false,
type: 'POST',
data: { dateInput: myDate.value },
success: function (result) {
if(result.Success) {
// do something with result.Data which is your list of returned records
}
}
});
}
Your datepicker control needs something to reference it by
<input id="myDatePicker" name="dateInput" type="text" class="alternate" />
Your action could then look something like this
private CormantReservationEntities db = new CormantReservationEntities();
public JsonResult Search(string dateInput) {
DateTime date = Convert.ToDateTime(dateInput);
var reservations = db.Reservations.Where(r=>r.Date==date).Include(r => r.Employee).Include(r => r.Room).OrderByDescending(r => r.Date);
return View(reservations.ToList());
return Json(new {Success = true, Data = reservations.ToList()}, JsonRequestBehaviour.AllowGet());
}
Update
If you want to make this a standard post where you post data and return a view then you need to make changes similar to this.
Create a ViewModel
public class ReservationSearchViewModel {
public List<Reservation> Reservations { get; set; }
public DateTime SelectedDate { get; set; }
}
Modify your controller actions to initially load the page and then be able to post data return the View back with data
public ActionResult Index() {
var model = new ReservationSearchViewModel();
model.reservations = new List<Reservation>();
return View(model);
}
[HttpPost]
public ActionResult Index(ReservationSearchViewModel model) {
if(ModelState.IsValid)
var reservations = db.Reservations.Where(r => r.Date = model.SelectedDate).Include(r => r.Employee).Include(r => r.Room).OrderByDescending(r => r.Date);
}
return View(model)
}
Modify your view so that you have a form to post to the Index HttpPost action
#model CormanReservation.Models.ReservationSearchViewModel
<h5>Select a date and see reservations...</h5>
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
#Html.EditorFor(model => model.SelectedDate)
#Html.EditorFor(model => model.Reservations) // this may need to change to a table or grid to accomodate your data
<input type="submit" value="Search" />
}
I'm new to jQueryUI+MVC3 (Razor) and this is probably a trivial question, anyway: I am trying to let autocomplete work so that as soon as an item is selected from the popup my form is submitted back to its Index action.
Here are my steps (this fake sample refers to an index of Person's):
1) I create a PersonFilter wrapper like:
public sealed class PersonFilter
{
public string LastName { get; set; }
public int RoleId { get; set; }
// ... etc.
}
2) I create a PersonList model to hold the list of Person's together with some filters.
3) my Index action is like (it is serving data to a view using MvcContrib data grid, whence page and sort):
public ViewResult Index(int? page, GridSortOptions sort, PersonFilter filter)
{
var persons = _repository.GetPersons(filter);
if (sort.Column != null)
persons = persons.OrderBy(sort.Column, sort.Direction);
ViewBag.Sort = sort;
PersonList list = new PersonList
{
persons = persons.AsPagination(page ?? 1, 10),
LastName = filter.LastName,
RoleId = filter.RoleId,
Roles = _repository.GetRoles(),
// ...
};
ViewBag.Filter = filter;
return View(list);
}
I also have a FindPerson action which gets a LastName parameter and is used to autocomplete on the person name filter.
4) my view relevant code:
...
#model PersonList
...
#using (Html.BeginForm("Index", "Person", FormMethod.Post, new { id = "TheForm" }))
{
...
<input type="text" id="LastName"/>
#Html.DropDownListFor(m => m.RoleId, new SelectList(Model.Roles, "Id", "Title", 0),
new {onchange = "document.getElementById('TheForm').submit();"})
...
}
<script type="text/javascript" language="javascript">
$(function () {
$("#LastName").autocomplete({
source: function (request, response) {
$.ajax({
url: "/Person/FindPerson", type: "POST", dataType: "json",
data: { LastName: request.term },
success: function (data) {
response($.map(data, function (item) {
return { label: item.LastName, value: item.LastName, id: item.Id };
}));
}
});
},
select: function (event, ui) {
$("#LastName").val(ui.item.value);
//alert($("#LastName").val());
$(this).closest("form").submit();
}
});
});
</script>
Now the autocomplete works fine, I can type and get a popup and select an item from it; in this case my select handler is called, and the form is posted to the Index action. Anyway, this action does not get its filter LastName member filled (its name is equal to the autocompleted input), while it regularly gets its RoleId and other members filled as expected.
I tried explicitly setting the LastName input value as shown in the select handler (even if this should be redundant), but nothing changes. Yet, if I uncomment the alert line I can view the correct value shown. If I break into the Index action, the filter LastName is not set and if I add the FormCollection object in its parameters I find NO key named LastName. A key appears only if I add a hidden field like:
#Html.HiddenFor(m => m.LastName)
but this is just a test for finding out what's wrong with my autocompleted input. Could anyone explain what I'm doing wrong here?
You should give a name to your LastName textbox:
<input type="text" id="LastName" name="LastName" />
Also I would recommend you using helpers to generate it. If you used helpers you wouldn't have had such problems:
#Html.TextBoxFor(x => x.LastName)
Without a name nothing will be posted to the server.