I have a form inside a partial view, it works but in case a server side validation error is raised it displays only the partial view. So I decided to use ajax for the submission (actually it makes sense as it is inserting a contact and there's a list in the main view).
Thing is, with the posted code if one of these errors comes it's properly displayed in the view (I'd need to make the partial visible again, but that's another thing), but if there's no error it'll display the list only in the partial view. I can do the other way round, displaying properly when there's no error but then not achieving the proper displaying of validation errors.
I would like to understand what's the best approach, or at least which are the possibilities: maybe change the code in the controller or do some kind of check in the success callback...
I edit what I had before, because when no error I should return the list, not the whole view as I posted before, but anyway I still have the doubt on how to tell one from another as both are succesfull calls to the post action
Thanks
The view is this one
#model ContactListViewModel
#{
ViewBag.Title = " My Contacts"
}
<div id="ContactList">
<h2>My Contacts</h2>
<hr />
<div id="addContainer">
#{ Html.RenderAction("AddContact"); }
</div>
<div id="editContainer" data-amp-url="#Url.Action("Edit", "Contacts")" class="initiallyHidden"></div>
#foreach (var group in Model.Contacts)
{
<div class="PlanContacts">
<div class="PlanName">#group.Key</div>
#foreach (var contact in group.Values)
{
<div class="Preview">
#Html.DisplayFor(m => contact, "Contact")
</div>
}
</div>
}
</div>
#section PageJavascript
{
<script src="~/Scripts/AMPContacts.js"></script>
}
The controller post action
[HttpPost]
public ActionResult AddContact(AddContactViewModel viewModel)
{
var partyId = (int) Session["PartyId"];
if (ModelState.IsValid)
{
_contactsManager.AddContact(viewModel, partyId);
// Here I should return the updated list
}
var newViewModel = _createBuilder.Rebuild(viewModel, partyId);
return PartialView("_AddContact", newViewModel);
}
And the ajax submission code inside the longer AMPContact.js
$('#addForm').submit(function (e) {
e.preventDefault();
var addContainer = $(document.getElementById('addContainer'));
$.ajax({
url: this.action,
type: this.method,
data: $(this).serialize(),
success: function(result) {
addContainer.html(result);
}
});
});
I understand this answer it's far from being good but maybe it's useful for somebody in terms of narrowing a possible search. As it would ended up being huge I rather post this update as an answer to my own question, not sure if it complies with good manners.
The explained one wasn't the only problem I faced, hard times as well with the reset of the form and specially the successive error submissions (once I got an error trying to submit it with errors again), so I ended up making a mess out of different solutions for different problems. Hopefully I will be able to clean it up
Thanks
In the view I use now
<div id="myContacts">
<h2>My Contacts</h2>
<hr />
<div id="addContainer">
<div class="toggler">
Add Contact
</div>
<div id="addToggling" class="initiallyHidden">
#{ Html.RenderAction("AddContact"); }
</div>
</div>
<div id="editContainer" data-amp-url="#Url.Action("Edit", "Contacts")" class="initiallyHidden"></div>
<div id="list">
#{ Html.RenderPartial("_ContactList", Model); }
</div>
In the .js
$('#addContainer').on('submit', '#addForm', ajaxCall);
function ajaxCall(e) {
e.preventDefault();
var addToggling = $(document.getElementById('addToggling'));
var contactList = $(document.getElementById('contactList'));
$.ajax({
url: this.action,
type: this.method,
data: $(this).serialize(),
success: function (result) {
if (result.passedValidation == true) {
// Json is returned with flag, we get the list from the server and update the list element
$.get(result.action, function (partial) {
contactList.html(partial);
});
// Add some effects and clear the form
$(document).scrollTop(0);
setTimeout(function () {
addToggling.slideUp(300, resetAddForm);
}, 500);
setTimeout(function () {
contactList.effect("highlight", {}, 3000);
}, 1000);
}
else {
// The form partial view is returned and displayed in the same element when there are validation errors
$(document).scrollTop(0);
addToggling.html(result);
$.validator.unobtrusive.parse('#addForm');
}
}
});
}
function resetAddForm() {
var addForm = $(document.getElementById('addForm'));
// Hhide the error messages
addForm.find("span.field-validation-error").hide();
addForm.find("div.validation-summary-errors").hide();
// Removes the class associated to errors
addForm[0].reset();
// Clear the inputs
addForm.find('input:text, input:password, input:file, select, textarea').val('');
addForm.find('input:radio, input:checkbox').removeAttr('checked').removeAttr('selected');
}
Controller with the existing action method slightly changed and a new one
public ActionResult ContactList()
{
var partyId = (int)Session["PartyId"];
var viewModel = _displayBuilder.Build(partyId);
return PartialView("_ContactList", viewModel);
}
[HttpGet]
public ActionResult AddContact()
{
var partyId = (int) Session["PartyId"];
var viewModel = _createBuilder.Build(partyId);
return PartialView("_AddContact", viewModel);
}
[HttpPost]
public ActionResult AddContact(AddContactViewModel viewModel)
{
var partyId = (int) Session["PartyId"];
if (ModelState.IsValid)
{
_contactsManager.AddContact(viewModel, partyId);
if (Request.IsAjaxRequest())
return Json(new { passedValidation = true, action = Url.Action("ContactList")});
return RedirectToAction("Index");
}
var newViewModel = _createBuilder.Rebuild(viewModel, partyId);
return PartialView("_AddContact", newViewModel);
}
Related
I am attempting to implement a Dragula Drag and Drop feature to the part of my application that allows an Admin to assign users to certain roles. The .cshtml is implemented and displaying correctly but am having trouble figuring out how to write the Ajax call so that I can pass parameters to my method in the controller that utilizes a helper class to add and remove. Can parameters be passed into the ajax $.post(url) that are derived from the location where it was dropped? Included below should be all relevant code. I also understand Viewbags aren't the best way to do this, but if i can get it functioning i will write a viewmodel and pass the data through that way instead.
.cshtml Code
there are 4 of these, one for each of the 3 assignable roles, and one for those not assigned to any role. They're populating correctly currently.
<div class="col-3 h-100">
<div class="bg-secondary p-4">
<h6 class="card-title">Developers</h6>
<div id="dragula-developers" class="py-2">
#foreach (var user in ViewBag.Developers)
{
<div class="card rounded mb-2">
<div class="card-body p-3">
<div class="media">
<div class="media-body">
<h6 class="mb-1">#user.FirstName #user.LastName</h6>
<p class="mb-0 text-muted"> #user.Email </p>
</div>
</div>
</div>
</div>
}
Controller Method
public JsonResult ManageRoles(string id, string role)
{
var message = "User Not Assigned To Role";
if (id != null)
{
userRolesHelper.RemoveUserFromRole(id, role);
if (!string.IsNullOrEmpty(role))
{
userRolesHelper.AddUserToRole(id, role);
message = "User Assigned To Role Successfully!";
}
}
return Json(message);
}
and finally the bare bones of my dragula script where my ajax should go i believe, after the .on('drop').
(function ($) {
'use strict';
dragula([document.getElementById("dragula-noRole"), document.getElementById("dragula-submitters"),
document.getElementById("dragula-developers"), document.getElementById("dragula-managers")])
.on('drop', function (el) {
console.log($(el));
})
})(jQuery);
There are 4 boxes, and ideally when a user's card is dropped into a dragula container, their id and the role associated with the dragula would be shot to the post and their role would be changed, returning a message. This isn't a feature that would be used heavily, so i shouldnt need websockets, but i can't figure out how to implement it. Any help would be greatly appreciated!
After much longer than I care to admit, I've finally figured it out. Just in case it helps anyone else in the future, I had to change my controller to accept an Array. It ended up looking like this before Abstraction.
[HttpPost]
public JsonResult ReassignToDev(Array userIds)
{
var message = "";
if (userIds == null)
{
message = "No Users to Change Roles Of";
}
if (userIds != null)
{
foreach (var user in userIds)
{
userRolesHelper.RemoveAllRoles(user.ToString());
userRolesHelper.AddUserToRole(user.ToString(), "Developer");
message = "Role Change Successful";
}
}
return Json(message);
}
and my ajax call finally wound up looking like this:
var developerIds = new Array();
$('.developer .hidden').each(function (i) {
developerIds.push($(this).text());
})
console.log(developerIds);
$.ajax({
type: "POST",
url: '#Url.Action("ReassignToDev", "Admin")',
dataType: 'JSON',
data: { userIds: developerIds },
success: function (response) {
alert(message);
},
error: function (response) {
alert(message);
}
});
4 different methods and 4 different calls, submitted through a button click that fires an ajax request. not the most elegant solution i'm sure, but it is functional and fast.
I read a lot of questions here on stacoverflow, but it is still not clear to me how should I use a partialview within a main view with an action method. What's probably wrong is my aproach in general. With what I have so far I am not sure how to continue with my code.
I will start with the main view :
#{
ViewBag.Title = "getRolesByYear";
}
</script>
<script type="text/javascript">
getRolesForYear(parseInt(#DateTime.Now.Year));
$(function () {
$('#years a').click(function () {
var year = $(this).text();
console.log(year);
getRolesForYear(parseInt(year));
});
})
//console.log(year);
function getRolesForYear(year) {
console.log(year);
$.ajax({
type: "POST",
url: '#Url.Action("getRolesByYear", "WorkRoles")',
dataType: "json",
data: {
year: year
},
success: successFunc,
error: errorFunc
});
function successFunc(data, status) {
console.log('x');
}
}
function errorFunc() {
alert('error');
}
}
</script>
<div id = "years" class="btn-group btn-group-justified timeline">
#DateTime.Now.Year
#DateTime.Now.AddYears(-1).Year
#DateTime.Now.AddYears(-2).Year
</div>
<div id"partial"></div>
In this view I have three buttons with different year for each button. On page load or on button click I make an ajax call to an action method with the an int year as a parameter.
This is a simplified version of my action method :
public ActionResult getRolesByYear(int year)
{
// a couple of queries here
var list = list of RoleViewModel objects;
return PartialView(list);
And here is the partialView :
#model IEnumerable<eksp.Models.RoleViewModel>
#foreach (var item in Model)
{
<div class="jumbotron">
<h2>item.Role.RoleName</h2>
<h1> item.Role.RoleDescription</h1>
<p class="lead">Focus start : item.Role.FocusStart</p>
<p>Focus end : item.Role.FocusStart </p>
</div>
}
Obviously, a lot of thins aren't clear to me. How can I use this partial view with the action method i have and the main view? Do I need a separate method for the partial view? Any tips?
Your ajax call will invoke the action method which returns the partial view result (markup generated by executing the partial view). I guess you simply need to use the response of the ajax call to update your DOM.
If you want to update the content of the div with id partial with the response, you can do that in the success event handler of your ajax call.
success : function(response)
{
$("#partial").html(response);
}
I would also recommend to call the getRolesForYear method on document ready event.
$(function () {
getRolesForYear(parseInt(#DateTime.Now.Year));
$('#years a').click(function () {
var year = $(this).text();
console.log(year);
getRolesForYear(parseInt(year));
});
})
Also, If your main view is also the result of action method getRolesByYear, you probably want to return the partial view result only on the ajax call, the other calls,you can return the partial view
public ActionResult getRolesByYear(int year)
{
var list = list of RoleViewModel objects;
if(Request.IsAjaxRequest())
{
return PartialView(list);
}
else
{
return View(list);
}
}
Here the same action method handles the request for main view and ajax call. It returns the same view for ajax call, but using PartialView call, so layout will be ignored. But If you have a specific view you want to return, you can do that as well.
if(Request.IsAjaxRequest())
{
return PartialView("_yearlyRoles",list);
}
One of the reasons I like using partial views for rendering data via Ajax calls. For example if I want to start searching in order to avoid the server call i Just use an ajax call to the controller which returns the search results through a partial view. In your example yoy need to load the results in partial div.
function successFunc(data, status) {
$("#partial").html(data);
}
View:
#if (Model.IsEventActive)
{
<div id="GameEvent">
//SomeCode
</div>
}
else
{
<div id="GameNonEvent">
//SomeCode
</div>
}
JS file:
$('#btnNextEvent').click(function () {
$.ajax({
type: "POST",
url: "Game/UpdateUserEventInfo"
//success: function () {
// $("#GameEvent").hide();
//}
});
});
Controller:
[HttpPost]
public ActionResult UpdateUserEventInfo()
{
var user = _user;
_instance.Users.UpdateUserEventInfo(user);
return RedirectToAction("Index");
}
public ActionResult Index()
{
if (!_instance.Users.CheckUserSkillsExist(WebSecurity.CurrentUserName))
{
return RedirectToAction("CreateChar");
}
_instance.GameBase.GetBaseData();
var userModel = GetPlayerDisplayStats();
return View(userModel);
}
If my beginning IsEventActive = true;
The FullEvent View at a certain moment calls my JS method which triggers the ajax call to UpdateUserEventInfo.
So, basically what is supposed to happen is when the Controller method UpdateUserEventInfo is fired, it updates the DB and then calls my index view again. Index view rebuilds the model and launches the view.
The view checks for IsEventActive and builds the divs based on that.
The page shows the GameEvent Div in the beginning because IsEventActive is true, but when the index view rebuilds again via the ajax call. The if-else loops follows correctly and goes to the GameNonEvent div and creates it. But I do not see that on the page. The page still shows GamEvent Div. Even though the view didn't go into the if statement.
If i refresh the page, then it shows correctly.
Wrap the part of the view that you showed in another container:
#if (Model.IsEventActive)
{
<div id="container">
<div id="GameEvent">
<div class="col-lg-10 col-lg-offset-1">
<div class="row">
#{ Html.RenderAction("FullEvent", "Game", new { ActiveEventGrpId = Model.ActiveEventGrpId }); }
</div>
</div>
</div>
</div>
}
else
{
...
}
Then, in success handler of ajax call do:
success: function (data) {
$("#container").html(data);
}
if i get your code right it looks like you are not doing anything with result of ajax call to "Game/UpdateUserEventInfo",
so nothing will happen. Where you have "success" commented out you should have code that updates view client side. Which could be a lot of code, it is possible that ajax call may not be even needed there, href to action will do just fine from functional point. The simple way
to fix this (since you say page refresh works) would be to put a page reload on ajax success :
$.ajax({
type: "POST",
url: "Game/UpdateUserEventInfo"
success: function () {
window.location.reload();
}
});
but this will also successfully cancel the whole point of using ajax, you may need review your approach for this.
Ey!
How I could refresh a Partial View with data out of the Model?
First time, when the page loads it's working properly, but not when I call it from the Action.
The structure I've created looks like:
Anywhere in my View:
#{ Html.RenderAction("UpdatePoints");}
My PartialView "UpdatePoints":
<h3>Your points are #ViewBag.points </h3>
At the Controller I have:
public ActionResult UpdatePoints()
{
ViewBag.points = _Repository.Points;
return PartialView("UpdatePoints");
}
Thanks for your help!
UPDATE
Thanks all for your help! Finally I used JQuery/AJAX as you suggested, passing the parameter using model.
So, in JS:
$('#divPoints').load('/Schedule/UpdatePoints', UpdatePointsAction);
var points= $('#newpoints').val();
$element.find('PointsDiv').html("You have" + points+ " points");
In Controller:
var model = _newPoints;
return PartialView(model);
In View
<div id="divPoints"></div>
#Html.Hidden("newpoints", Model)
So, say you have your View with PartialView, which have to be updated by button click:
<div class="target">
#{ Html.RenderAction("UpdatePoints");}
</div>
<input class="button" value="update" />
There are some ways to do it. For example you may use jQuery:
<script type="text/javascript">
$(function(){
$('.button').on("click", function(){
$.post('#Url.Action("PostActionToUpdatePoints", "Home")').always(function(){
$('.target').load('/Home/UpdatePoints');
})
});
});
</script>
PostActionToUpdatePoints is your Action with [HttpPost] attribute, which you use to update points
If you use logic in your action UpdatePoints() to update points, maybe you forgot to add [HttpPost] attribute to it:
[HttpPost]
public ActionResult UpdatePoints()
{
ViewBag.points = _Repository.Points;
return PartialView("UpdatePoints");
}
You can also try this.
$(document).ready(function () {
var url = "#(Html.Raw(Url.Action("ActionName", "ControllerName")))";
$("#PartialViewDivId").load(url);
setInterval(function () {
var url = "#(Html.Raw(Url.Action("ActionName", "ControllerName")))";
$("#PartialViewDivId").load(url);
}, 30000); //Refreshes every 30 seconds
$.ajaxSetup({ cache: false }); //Turn off caching
});
It makes an initial call to load the div, and then subsequent calls are on a 30 second interval.
In the controller section you can update the object and pass the object to the partial view.
public class ControllerName: Controller
{
public ActionResult ActionName()
{
.
. // code for update object
.
return PartialView("PartialViewName", updatedObject);
}
}
Thanks all for your help!
Finally I used JQuery/AJAX as you suggested, passing the parameter using model.
So, in JS:
$('#divPoints').load('/Schedule/UpdatePoints', UpdatePointsAction);
var points= $('#newpoints').val();
$element.find('PointsDiv').html("You have" + points+ " points");
In Controller:
var model = _newPoints;
return PartialView(model);
In View
<div id="divPoints"></div>
#Html.Hidden("newpoints", Model)
Controller :
public ActionResult Refresh(string ID)
{
DetailsViewModel vm = new DetailsViewModel(); // Model
vm.productDetails = _product.GetproductDetails(ID);
/* "productDetails " is a property in "DetailsViewModel"
"GetProductDetails" is a method in "Product" class
"_product" is an interface of "Product" class */
return PartialView("_Details", vm); // Details is a partial view
}
In yore index page you should to have refresh link :
Refresh
This Script should be also in your index page:
<script type="text/javascript">
$(function () {
$('a[id=refreshItem]:last').click(function (e) {
e.preventDefault();
var url = MVC.Url.action('Refresh', 'MyController', { itemId: '#(Model.itemProp.itemId )' }); // Refresh is an Action in controller, MyController is a controller name
$.ajax({
type: 'GET',
url: url,
cache: false,
success: function (grid) {
$('#tabItemDetails').html(grid);
clientBehaviors.applyPlugins($("#tabProductDetails")); // "tabProductDetails" is an id of div in your "Details partial view"
}
});
});
});
In the code below, I have a form with some informations about an employee. I'd like feel in the password and post. If the password is correct the user is redirected to /Dashboard that it's ok.
When the password is wrong, I'd like show the error (_ErrorPartial) in MyError div and still see the form. I'd like see the form and the message in the div (MyError). With the code below, when the password is wrong, I see the error message but on a blank page and not a page with the layout + the form + the error message
Any idea ?
Thanks,
My page :
#model MyApp.EmployeeModel
<script type="text/javascript">
$(document).ready(function () {
$(".").click(function () {
$.ajax({
url: '/Account/Login',
type: "Post",
data: $('#MyForm').serialize(),
success: function (result) {
alert('Im here');
$("#MyError").html(result);
//Here is code to detect error or not but removed
}
});
});
});
</script>
<div id="MyError"></div>
#using (Html.BeginForm("Login", "Account", FormMethod.Post, new { id = "MyForm" }))
{
#Html.AntiForgeryToken()
<div>
#Html.LabelFor( m => m.FirstName)
<div>
#Html.HiddenFor(model => model.Username)
#Html.LabelFor( m => m.Password)
#Html.PasswordFor(m => m.Password)
<input type="submit" class="jqClick" />
</div>
</div>
}
Controller :
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(EmployeeModel employeeModel)
{
var Errors = new List<string>();
var employee = .... //Get employee from DB
if (employee == null)
{
Errors.Add("Employee trouble");
return PartialView("_ErrorPartial", Errors.AsEnumerable());
}
return RedirectToAction("Index", "Dashboard");
}
My _ErrorPartial view :
#model IEnumerable<string>
<div id="DisplayError">
#{ var error = string.Empty; }
#foreach (var err in Model){
error += err + "<br>";
}
#Html.Raw(error)
</div>
You have to make some changes to your javascript code. First of all, replace jQuery selector $(".") with specific one, like $(".jqClick"). Second of all, never hardcode urls like /Account/Login in your code, use UrlHelper for that. And the third thing is, you have to prevent a default behavior of the submit button. See the changes:
$(document).ready(function () {
$(".jqClick").click(function (e) {
e.preventDefault();
$.ajax({
url: '#Url.Action("Login", "Account")',
type: "Post",
data: $('#MyForm').serialize(),
success: function (result) {
alert('Im here');
if (result.redirectUrl)
window.location.href = result.redirectUrl;
else
$("#MyError").html(result);
//Here is code to detect error or not but removed
}
});
});
});
Also you need to change the Login action a little in order to make a redirect if a login is success:
public ActionResult Login(EmployeeModel employeeModel)
{
var Errors = new List<string>();
var employee = .... //Get employee from DB
if (employee == null)
{
Errors.Add("Employee trouble");
return PartialView("_ErrorPartial", Errors.AsEnumerable());
}
return Json(new { redirectUrl = Url.Action("Index", "Dashboard") });
}
The probable error that you throw from the server side, won't appear in success function. Instead you should have below
error:function(response)
{
//your code here
}
Simply throw exception from the controller.