Why does Unobtrusive Ajax require a layout page (returning a partial view doesn't work)? - ajax

While creating a custom plugin, that Ajaxifies any forms that are not already Ajaxified by unobtrusive Ajax, I noticed that my partial/layout code failed to display on post-back with a normal Unobtrusive Ajax form:
For my other partial-update plugins, I conditionally render pages as partial (for ajax requests) or full with layout (for normal requests) like this:
public ActionResult AjaxForm()
{
if (!Request.IsAjaxRequest())
{
ViewBag.Layout = "_Layout.cshtml";
}
return PartialView();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AjaxForm(AjaxFormModel model)
{
if (!Request.IsAjaxRequest())
{
ViewBag.Layout = "_Layout.cshtml";
}
if (ModelState.IsValid)
{
return PartialView("Success", model);
}
return PartialView(model);
}
When it came to testing a normal unobtrusive Ajax form, I used this test view, which updates just its own element UpdateTargetId="FormPanel":
#using (Ajax.BeginForm("AjaxForm", null, new AjaxOptions() { UpdateTargetId = "FormPanel" }, new { id = "FormPanel" }))
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<div class="form-group">
#Html.LabelFor(model => model.Name, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Name, new { htmlAttributes = new { #class = "form-control" } })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.AddressLine, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.AddressLine, new { htmlAttributes = new { #class = "form-control" } })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
If I remove the test if (!Request.IsAjaxRequest()), and return the full page with master page/layout, it renders the result just fine where the form was.
Fiddler shows the "success" view is returned in either case, but it does not do anything without the layout present.
Q. What is it in a masterpage/layout that unobtrusive Ajax requires in order to extract content and replace the specified element
Update:
To make things worse it actually works correctly the first time, but not if I ajax load the same form again.

A: Forms must have unique IDs!
There are loads of issues about unique IDs on SO, which browsers generally cope with, but the one you cannot ever break is having two forms loaded with the same id. Only one is visible to JavaScript/jQuery!
My problem was caused by a duplicate form ID. You should not insert the result page back in to the form itself (leaving the form element intact).
When I reloaded the original form, dynamically in a slideshow-style control, I wound up with duplicate form ID's.
This meant Unobtrusive Ajax could only see the first form in the DOM!
Solution (do not set Ajax forms to update themselves):
Set UpdateTargetId to an element above the form, never the form itself if you load panels dynamically.
Fix to my example view (surround form with a target panel):
<div id="FormPanel">
#using (Ajax.BeginForm("AjaxForm", new AjaxOptions() { UpdateTargetId = "FormPanel" }))

Related

Should I be using Ajax forms in MVC partial views for adding new data in real time?

I want to know if there is a better way to accomplish what I am doing or if my current approach is ok. My current approach is giving me a little trouble so I need to see what I can do about it all.
In my MVC web application, I have dropdown lists which are searchable (handled by KendoUI Widgets) and I allow the user to add missing data to the database table which serves those dropdown lists.
Example of Widget
#(Html.Kendo().DropDownList()
.Name("Manufacturer")
.Filter("contains")
.OptionLabel("-- Select --")
.DataTextField("ManufacturerName")
.DataValueField("ManufacturerName")
.NoDataTemplate("No Data Add Manufacturer")
.DataSource(source => {
source.Read(read => {
read.Action("GetManufacturers", "Cars");
}).ServerFiltering(false);
}))
The data is added via an ajax form in a partial view which is displayed using a bootstrap 4.0 modal trigger by a button in the no data template (see above example).
These dropdown lists appear in both Create and Edit views, in the Create view they work fine and as expected but within the Edit views, they have dictionary errors.
Create View
#model MyProject.Models.Cars
#using (Ajax.BeginForm("Create", "Cars", new AjaxOptions
{
HttpMethod = "POST",
OnSuccess = "NotifySuccess",
OnFailure = "NotifyFailure"
},
new { #class = "form-horizontal", role = "form" }))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
...Form goes here with widgets etc
}
#Html.Partial("_Manufacturer")
Popup form partial view
#model MyProject.Models.Manufacturer
<div class="modal fade" id="modal_addManufacturer">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-body">
#using (Ajax.BeginForm("AddManufacturer", "Cars", new AjaxOptions
{
HttpMethod = "POST",
OnSuccess = "Success",
OnFailure = "Fail"
}, new { #class = "form-horizontal", role = "form" }))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary()
<div class="form-group">
#Html.LabelFor(l => l.ManufacturerName, new { #Class = "form-label" })
#Html.TextBoxFor(i => i.ManufacturerName, new { #Class = "form-control" })
<input type="submit" value="Add" />
<input type="reset" value="Reset" />
</div>
}
</div>
</div>
</div>
</div>
These errors seem to stem from the fact that the Edit method in the controller passes an Id parameter. These leads me to the most obvious question, is this approach correct? Should I be using ajax forms in modals to create data on the fly like this especially when the models they use are causing problems when the Id parameter is used on views such as edit.
Can anyone offer some guidance on what I can do about this?

Ajax form not submitting to controller action

Ajax form is not submitting to controller action. Here is the code
#using (Ajax.BeginForm("searchCustomers", "Transaction", new { phoneNumber = Model.CustomerMobile }, new AjaxOptions
{
UpdateTargetId = "custList",
InsertionMode = InsertionMode.Replace
}))
{
<div class="col-md-6">
<div class="form-group">
<label>Customer Mobile No:</label>
#Html.TextBoxFor(x => x.CustomerMobile, new { #class = "form-control", id = "custMobile" })
</div>
#*<div class="form-group">
<label>Customer Name</label>
#Html.TextBoxFor(x => x.CustomerName, new { #class = "form-control", id = "custName" })
</div>*#
<input type="submit" class="btn btn-default" value="Get Customer Details" >
</div>
}
Here is the controller action
public ActionResult searchCustomers(string phoneNumber)
{
if (phoneNumber==null)
{
return PartialView(new List<Models.Customer>());
}
var c = Database.Session.Query<Models.Customer>()
.Where(x => x.MobileNumber.Like(phoneNumber) )
.ToList();
return PartialView(c);
}
but the ajax form is not submitting. I've added the JavaScript files as bundles. I've another #Html.Action("searchCustomers", new { phoneNumber = Model.CustomerMobile }) this one calls the controller action.
Everything is fine in your code. There are two javascript files that are needed for Ajax.Beginform to work.
jquery-{Vaersion}.js
jquery.unobtrusive-ajax.js
Check whether you have included those files to your view or not. Or if your view has any LayOut if those javascript files are included in your LayOut or not.

Saving multiple partial views from one main page

Here is my requirement :
I am designing a page to add a vehicle to the database :
Normal vehicle information [Model - Inventory]
Some other features [Model - IList]
Here is my index.cshtml page
#model Model.ViewModel.VehicleViewModel
<div>
<div class='col-md-12'>
<div class="form-group">
<input id="mainFormSubmit" type="button" value="Save" class="btn btn-default" />
</div>
</div>
#{Html.RenderPartial("~/Views/Shared/_InventoryPartial.cshtml", Model.InventoryVM);}
#{Html.RenderPartial("~/Views/Shared/_StandardFeaturePartial.cshtml", Model.StandardFeatures);}
</div>
<script type="text/javascript">
$('#mainFormSubmit').click(function () {
$('#InventoryForm').submit();
$("#StandardFeatureForm").submit();
});
</script>
This is my view model class
public class VehicleViewModel
{
public InventoryViewModel InventoryVM { get; set; }
public IList<StandardFeature> StandardFeatures { get; set; }
}
The Inventory partial view [_InventoryPartial.cshtml]
#model Model.ViewModel.InventoryViewModel
#{
var options = new AjaxOptions() { HttpMethod = "Post" };
}
<div class="container">
<div class="row">
<div class="col-md-12">
#using (Ajax.BeginForm("InventorySave", "AddVehicle", options, new { id = "InventoryForm" }))
{
<fieldset>
<legend>Inventory Info</legend>
<div class='col-md-6'>
<!-- VIN input-->
<div class="form-group">
#Html.LabelFor(x => x.VIN, new { #class = "col-md-4 control-label" })
<div class="col-md-7">
#Html.TextBoxFor(x => x.VIN, new { #class = "form-control", #placeholder = "VIN" })
</div>
</div>
</div>
</fieldset>
}
The standard feature partial view [_StandardFeaturePartial.cshtml]
==
#model IEnumerable<Model.DomainModel.StandardFeature>
#{
var options = new AjaxOptions() { HttpMethod = "Post" };
}
<div class="container">
<div class="row">
<div class="col-md-12">
#using (Ajax.BeginForm("StandardFeatureSave", "AddVehicle", options, new { id = "StandardFeatureForm" }))
{
When I am clicking on index page SAVE button, only
$('#InventoryForm').submit();
$("#StandardFeatureForm").submit();
last one(StandardFeatureForm) is executing.
Please let me know if this process is correct, and what could be the reason of this issue.
You should not call the submit method twice. Depending of the browser you can face different issues :
the form submission causes the browser to navigate to the form action and the submission
of the first may prevent the submission of the second
The browser could detected there are two requests and discards the
first submit.
In your case it will be easier to wrap your two partial views inside a unique form.
#using (Ajax.BeginForm("InventorySave", "AddVehicle", FormMethod.Post, new { id = "InventoryForm" }))
{
#{Html.RenderPartial("~/Views/Shared/_InventoryPartial.cshtml", Model.InventoryVM);}
#{Html.RenderPartial("~/Views/Shared/_StandardFeaturePartial.cshtml", Model.StandardFeatures);}
}
However when the partial views render they are not generating the correct name attributes for the larger modelModel.ViewModel.VehicleViewModel you want to use :
public void InventorySave(VehicleViewModel vehicleViewModel) {}
In this case you should use EditorTempmlate instead of partial views. It's simple to do from your partial views and this post should help you :Post a form with multiple partial views
Basically, drag your partials to the folder ~/Shared/EditorTemplates/
and rename them to match the model name they are the editor templates
for.
Finally something like :
#model Model.ViewModel.VehicleViewModel
#using (Html.BeginForm("InventorySave", "AddVehicle", FormMethod.Post, new { id = "InventoryForm" }))
{
#Html.EditorFor(m => m.InventoryVM);
#Html.EditorFor(m => m.StandardFeatures});
}
The Ajax.BeginForm helper already has a submit event associated to it which creates an Ajax POST request. When you are manually submitting your form using $('#InventoryForm').submit();, you're calling both and the submit events which can have strange side effects.
There are a few ways around this. Here is one solution
Change your forms to a regular HTML form using the Html.BeingForm helper.
Amend your script to create ajax requests and use the form data
$('#InventoryForm').submit(function(e) {
e.preventDefault();
$.post($(this).attr("action"), $(this).serialize(), function(r) {
//Do something
});
});
$('#StandardFeatureForm').submit(function(e) {
e.preventDefault();
$.post($(this).attr("action"), $(this).serialize(), function(r) {
//Do something
});
});
Hope this helps

How i can submit multiple objects inside the form post values to be added

i have a create view to add answers to a question, currently the user can only add one answer at the same time when he clicks on the submit button, instead of this i want the user to be able to insert multiple answers objects into the same view and then the system to add all these new answer objects to the database after the user click on the submit button, my current view looks as the follow:-
#model Elearning.Models.Answer
#{
ViewBag.Title = "Create";
}
<script src="#Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
<script src="#Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>
<div id = "remove">
#using (Ajax.BeginForm("Create", "Answer", new AjaxOptions
{
HttpMethod = "Post",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "remove"
}))
{
<div id = "returnedquestion">
#Html.ValidationSummary(true)
<fieldset>
<legend>Answer</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Description)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Description)
#Html.ValidationMessageFor(model => model.Description)
</div>
</fieldset>
<input type= "hidden" name = "questionid" value = #ViewBag.questionid>
<input type= "hidden" name = "assessmentid" value = #ViewBag.assessmentid>
<input type="submit" value="Add answer" />
</div>
}
</div>
and the action methods look as the follow:-
public ActionResult Create(int questionid)//, int assessmentid)
{
ViewBag.questionid = questionid;
Answer answer = new Answer();
return PartialView("_answer",answer);
}
//
// POST: /Answer/Create
[HttpPost]
public ActionResult Create(int questionid, Answer a)
{
if (ModelState.IsValid)
{
repository.AddAnswer(a);
repository.Save();
return PartialView("_details",a);
}
return View(a);}
so how i can modify the above code to be able to insert multiple answer objects at the same view and then submit these answers objects at the same time when the user click on the submit button?
Try Post a List
Add input by javascript when user click "Add Answer".
And when submit the form ,it will post all answer data to binding to List
<script>
$(document).ready(function () {
var anwserCount = 1;
$("#addbutton").click(function () {
$("#AnwsersDiv")
.append("<input type='text' name='Anwsers[" + anwserCount + "]'/>");
anwserCount += 1;
});
});
</script>
#using (Html.BeginForm())
{
<div id="AnwsersDiv">
<input type="text" name="Anwsers[0]" />
</div>
<input id="addbutton" type="button" value="Add answer" />
<input type="submit" value="submit" />
}
Model
public class Answer
{
public List<String> Anwsers { get; set; }
}
When submit the form
I think this is what you are looking for
http://haacked.com/archive/2008/10/23/model-binding-to-a-list.aspx
Conclusion: you should make the post action with ICollection<Answer> Parameter, then it will be easy to get them when you post your main form, and create the appropriate QUESTION object, then save them all with only one submit.

MVC 3 + $.ajax - response seems to be caching output from partial view

I must be missing something, silly, but here is the problem.
I have a Create action on the Transactions controller. The Create.cshtml uses jQuery to post the form to the server using a call to $.ajax. Debugging shows that everything arrives on the server as expected. I use the form data to update a record: this works fine too. I then return a partial view, passing a model to the view with default data. I can debug and verify that the model is passing nulls and 0s, ie, the default data for my model.
Problem is, what gets sent back to the browser in the response is the old data...!
I can see no reason why. Hopefully, you can...
Note: I am not using any form of output cache.
EDIT 1:
The caching is not happening in the browser. The reason I say that is that I can see in Firebug the response of the call to the AjaxCreate Action. I can also see this in Fiddler.
EDIT 2:
If you look at the code for the Partial View, you will see that each dropdownlist or textbox has the value of #Model.Transaction.[Property] printed out beside it. This, bizarrely, shows the correct value, ie, the defaults for my Transaction object, but the dropdownlists and text boxes stick with the values that were posted to the server rather than the default values for the property each one is supposed to render.
EDIT 3:
I have included the following image, so you can see the values printed to the right of each control that are being passed in. And yet the controls reflect the old data posted to the server in the previous $.ajax call. (The comment shows a date time at the moment of creating the view model, that way I could see things updating).
EDIT 4:
I have found that replacing #Html.EditorFor(...) (see view code below) with #Html.TextBox helpers removes the problem. So, what seems to be happening is that the EditorFor helpers are causing the problem. Why? I have no idea, but will post another, more specific question.
Code and markup as follows:
jQuery:
$(document).ready(function () {
$('input[name="nextRecord"]').live('click', function () {
var theForm = $(this).closest('form');
if ((theForm).valid()) {
var buttonText = $(this).val();
var action = "/input/Transactions/AjaxCreate/";
if (buttonText === "Reset") {
clearForm(theForm);
}
else {
var targetElement = $('#CreateDiv');
var _data = theForm.serialize() + '&nextRecord=' + $(this).val();
$.ajax({
url: action,
data: _data,
cache: 'false',
type: 'POST',
dataType: 'html',
success: function (html) {
$(targetElement).html(html);
createDatePickers(targetElement);
jQuery.validator.unobtrusive.parse(targetElement);
}
});
}
}
return false;
});
});
Partial View:
#model FlatAdmin.Domain.ViewModels.TransactionViewModel
#* This partial view defines form fields that will appear when creating and editing entities *#
<div class="editor-label">
Fecha
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Transaction.TransactionDate, new { #class = "date-picker" })
#Html.ValidationMessageFor(model => model.Transaction.TransactionDate) #Model.Transaction.TransactionDate.ToString()
</div>
<div class="editor-label">
Origen:
</div>
<div class="editor-field">
#Html.DropDownListFor(model => model.Transaction.IdFrom, ((IEnumerable<FlatAdmin.Domain.Entities.Account>)Model.FromAccounts).Select(option => new SelectListItem
{
Text = (option == null ? "None" : option.AccountName),
Value = option.AccountId.ToString(),
Selected = (Model != null) && (option.AccountId == Model.Transaction.IdFrom)
}), "Choose...")
#Html.ValidationMessageFor(model => model.Transaction.IdFrom)#Model.Transaction.IdFrom
</div>
<div class="editor-label">
Destino:
</div>
<div class="editor-field">
#Html.DropDownListFor(model => model.Transaction.IdTo, ((IEnumerable<FlatAdmin.Domain.Entities.Account>)Model.ToAccounts).Select(option => new SelectListItem
{
Text = (option == null ? "None" : option.AccountName),
Value = option.AccountId.ToString(),
Selected = (Model != null) && (option.AccountId == Model.Transaction.IdTo)
}), "Choose...")
#Html.ValidationMessageFor(model => model.Transaction.IdTo)#Model.Transaction.IdTo
</div>
<div class="editor-label">
Monto
</div>
<div class="editor-field">
#Html.DropDownListFor(model => model.Transaction.IdCurrency, ((IEnumerable<FlatAdmin.Domain.Entities.Currency>)Model.AllCurrencies).Select(option => new SelectListItem
{
Text = (option == null ? "None" : option.CurrencyName),
Value = option.CurrencyId.ToString(),
Selected = (Model != null) && (option.CurrencyId == Model.Transaction.IdCurrency)
}))
#Html.EditorFor(model => model.Transaction.Amount)
#Html.ValidationMessageFor(model => model.Transaction.Amount) #Model.Transaction.Amount
</div>
<div class="editor-label">
Comentario
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Transaction.Comment)
#Html.ValidationMessageFor(model => model.Transaction.Comment)#Model.Transaction.Comment
</div>
View:
#model FlatAdmin.Domain.ViewModels.TransactionViewModel
#using FlatAdmin.Domain.Entities
#{
ViewBag.Title = "Nueva Transaccion";
}
<h2>#ViewBag.Title</h2>
<div>
#Html.ActionLink("<< Lista de Transacciones", "Index")
</div>
<br />
<div id="InputPanel">
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>Elegir Actividad</legend>
<div class="editor-field">
#Html.DropDownListFor(model => model.Transaction.IdCostCentre, ((IEnumerable<FlatAdmin.Domain.Entities.CostCentre>)Model.AllCostCentres).Select(option => new SelectListItem
{
Text = (option == null ? "None" : option.Name),
Value = option.CostCentreId.ToString(),
Selected = (Model != null) && (option.CostCentreId == Model.Transaction.IdFrom)
}), "Actividades...")
</div>
</fieldset>
<fieldset>
<legend>Transaccion</legend>
<div id="CreateDiv">
#Html.Partial("_Create", Model)
</div>
<p>
<input type="submit" name="nextRecord" value="Proxima Transaccion >>" />
</p>
<p>
...o sino, para guardar y volver a la lista de transacciones:<br /><input type="submit" value="Guardar" />
</p>
</fieldset>
}
</div>
Controller Action:
[HttpPost]
public virtual ActionResult AjaxCreate(Transaction transaction)
{
if (ModelState.IsValid)
{
service.InsertOrUpdate(transaction);
service.Save();
}
service.ChosenCostCentreId = transaction.IdCostCentre;
TransactionViewModel viewModel = new TransactionViewModel();
viewModel.Transaction = new Transaction();
viewModel.CostCentre = service.ChosenCostCentre;
viewModel.AllCostCentres = service.AllCostCentres;
viewModel.AllCurrencies = service.AllCurrencies;
viewModel.FromAccounts = service.FromAccounts;
viewModel.ToAccounts = service.ToAccounts;
return PartialView("_Create", viewModel);
}
#Darin Dimitrov came up with the answer in a related thread.
Essentially, the HtmlHelpers such as Html.EditorFor, Html.TextBoxFor, etc, check first in the ModelState for existing values, and ONLY then in the Model.
As a result, I needed a call to:
ModelState.Clear();
Ignorance is so painful.
Try explicitly setting the outputcache duration to 0 on your controller action.
I think the browser isn't supposed to cache POSTs but it seems to still do that sometimes.

Resources