I'm currently working on a ASP.NET MVC 4 project as a trainee and I'm trying to implement an admin panel. The goal is to show all the users on a grid (MVC.GRID) and edit them on the same page.
I've managed to show all the users on the grid and once a user is selected it shows the info below the grid and puts it in a form (via ajax/jquery).
The problem is: the form validation is being displayed on a new page and not on the page where the grid is at. And I've no idea why..
Below is my code.
This is where the form is placed:
<div id="order-content">
<p class="muted">
Select a user to see his or her information
</p>
</div>
The form itself (partial view "_UserInfo):
#using (Ajax.BeginForm("EditUser", "Admin", FormMethod.Post,
new AjaxOptions
{
InsertionMode = InsertionMode.Replace,
HttpMethod = "POST",
UpdateTargetId = "order-content"
}))
{
#Html.Bootstrap().ValidationSummary()
#Html.Bootstrap().ControlGroup().TextBoxFor(x => x.Id)
#Html.Bootstrap().ControlGroup().TextBoxFor(x => x.Name)
#Html.Bootstrap().ControlGroup().TextBoxFor(x => x.Password)
#Html.Bootstrap().SubmitButton().Text("Opslaan").Style(ButtonStyle.Primary)
}
JQuery to show the user info once a row is selected:
$(function () {
pageGrids.usersGrid.onRowSelect(function (e) {
$.post("/Admin/GetUser?id=" + e.row.Id, function (data) {
if (data.Status <= 0) {
alert(data.Message);
return;
}
$("#order-content").html(data.Content);
});
});
});
My AdminController:
[HttpPost]
public JsonResult GetUser(int id)
{
var user = _UserService.Get(id);
var input = _EditInputMapper.MapToInput(user);
if (user == null)
return Json(new { Status = 0, Message = "Not found" });
return Json(new { Content = RenderPartialViewToString("_UserInfo", input) });
}
[HttpPost]
public ActionResult EditUser(AdminUserEditInput input)
{
if (ModelState.IsValid)
{
// todo: update the user
return View();
}
// This is where it probably goes wrong..
return PartialView("_UserInfo",input);
}
Can anyone see what is wrong with my code?
Thank you.
When the ModelState is valid and you return View(), does this full view gets embedded in order-content? I suspect not, and if so it would be because the ajax request is not being sent . You may not have included the jquery.unobtrusive-ajax js file
I've got it working now.. With the use of an jsonvalidator. I don't know if it's a good solution but it does the job for now..
This is what I've changed in my AdminController
[HttpPost]
public ActionResult EditUser(AdminUserEditInput input)
{
int id = (int)TempData["UserID"];
if (ModelState.IsValid)
{
_UserService.ChangeMail(id, input.Mail);
_UserService.ChangeName(id, input.Firstname, input.Name);
return new RedirectResult(Url.Action("Users", "Admin") + "#id=" + id);
}
else
{
return new JsonResult { Data = new { Valid = false, Errors = Validator.CheckModelErrors(ModelState) } };
}
}
Added a JsonValidator class:
public static class Validator // todo: doesn't belong in the controllers directory ?
{
public static List<JsonError> CheckModelErrors(System.Web.Mvc.ModelStateDictionary modelstate)
{
List<JsonError> errorlist = new List<JsonError>();
if (modelstate != null && modelstate.Values != null)
{
foreach (var m in modelstate.Values)
{
if (m.Errors != null)
{
foreach (var e in m.Errors)
{
errorlist.Add(new JsonError() { ErrorMessage = e.ErrorMessage ?? "" });
}
}
}
}
return errorlist;
}
}
And a JsonError class..
public class JsonError // todo: doesn't belong in the controllers directory ?
{
public string ErrorMessage { get; set; }
public bool HasError { get; set; }
public bool CanPerform { get; set; }
}
Last but not least, js:
$(document).on('submit', '#UserForm', function (e) {
e.defaultPrevented();
$('#UserForm').validate();
$.post($(this).attr("action"), $(this).serialize(), function (json) {
if (json.Valid) {
$("#order-content").html('<p class="muted"> Select a user.</p>');
}
else {
$("#ShowValidation").empty();
var list = $("#ShowValidation").append('<ul></ul>').find('ul');
$.each(json.Errors, function (index, optionData) {
list.append('<li>' + optionData.ErrorMessage + '</li>');
})
}
}, "json");
});
I was thinking about another way to manage this because this is just temporary..
Should it be a good idea to store the input with the validation messages in a session and let js put it in the _UserInfo view?
Related
I am calling a JavaScript function which relies on the case Id being set this case Id is being set through the edit function. Obviously I need some way of getting the case Id into jQuery function I'm thinking maybe I should parse the query string for that as you see the pattern for that is MISObjects/Edit/5 where 5 would be the case Id?
I am using asp.net core 3.1 and Entity Framework Core. I have this code in a component view so that I can pass a different model to it.
But of course my jquery is being called before the edit action is completed their for the variables I am using becomes null
// GET: MISObjects/Edit/5
public async Task<IActionResult> Edit(int id) {
if (id == null) {
return NotFound();
}
SetupViewBags();
var list = _userManager.Users.ToListAsync();
ViewBag.Users = list;
var mISObject = await _context.MISobject.FindAsync(id);
HttpContext.Session.SetString("CaseId", id.ToString());
//Using temp data to store
TempData["CaseId"] = id.ToString();
SetupUploadsViewBags();
if (mISObject == null) {
return NotFound();
}
return View(mISObject);
}
But when I debug the Value is blank because the GetAuditTrailData gets called before the edit function has fired at least it does when I place the brake points.
[HttpGet]
public ActionResult<DataTableWrapper<List<MISAuditTrail>>> GetAuditTrailData() {
Int32.TryParse(TempData.Peek["CaseId"], out int resultCaseId);
var auditTrailsHistory = _context.MisAuditTrail.Where(w => w.isActive == true && w.isDeleted == false && w.MISObjectId== resultCaseId).ToList();
string output = JsonConvert.SerializeObject(auditTrailsHistory);
DataTableWrapper<List<MISAuditTrail>> data = new DataTableWrapper<List<MISAuditTrail>>() {
data = auditTrailsHistory
};
return data;
}
This is the jQuery which resides in my components view
#section scripts {
<script>
$(document).ready(function () {
$("#audTrailTable").DataTable({
"ajax": {
url: "/MISObjects/GetAuditTrailData/" ,
type: "get",
database: "json"
},
"columns": [
{ "data": "createdDate" },
{ "data": "createdBy" },
{ "data": "action" }
],
"scrollY": "200px",
"scrollCollapse": true,
"paging": false,
"processing": true, // for show progress bar
"filter": true, // this is for disable filter (search box)
"orderMulti": false // for disable multiple column at once
})
});
alert(caseId);
</script>
}
My View Component
namespace MISSystem.Web.ViewComponents {
[ViewComponent(Name = "AudiTrailList")]
public class AuditTrailViewComponent : ViewComponent {
private readonly IHttpContextAccessor _contextAccessor;
private readonly MISDBContext db;
public int CaseId { get; set; }
public AuditTrailViewComponent(MISDBContext context , IHttpContextAccessor contextAccessor) {
db = context;
_contextAccessor = contextAccessor;
}
public async Task<IViewComponentResult> InvokeAsync(
int caseId) {
CaseId = caseId;
var items = await GetItemsAsync(caseId);
_contextAccessor.HttpContext.Session.SetString("CaseId",CaseId.ToString());
return View(items);
}
private Task<List<MISAuditTrail>> GetItemsAsync(int caseId) {
return db.MisAuditTrail.Where(x => x.isActive ==true && x.MISObjectId == caseId).ToListAsync();
}
}
}
The component gets called as such which is fine when the edit mode fires it finds the id of the case but when the ajax calls thats functions its gone and is null.
#await Component.InvokeAsync("AudiTrailList", new { caseId = Model.Id })
This script is supposed to send a ProductId to the home controller's Delete-method, and the controller should make the appropriate Remove-operation:
$('[name="DeleteItem"]').click(function (e) {
$.ajax({
type: "DELETE",
url: "#Url.Action('Delete','Home')",
data: { id: $('DeleteItem#data-id').val() },
success: function () {
alert("success!");
window.location.replace("#Url.Action('Index', 'Home')");
},
error: function (data) {
alert("Error: " + data.id);
}
});
});
This is the form:
<form asp-action="Update">
#foreach (var item in Model.ShoppingCartItems)
{
#item.ProductTitle
<input asp-for="#item.Quantity" />
<button name="DeleteItem" data-id="#item.ProductId">DELETE</button>
}
<button type="submit">Update quantity</button>
</form>
This is the controller's Delete-method (I don't have the ShoppingCartId, so I'm getting it based on SessionId, which is stored in the ShoppingCarts-table):
[HttpDelete]
//[ValidateAntiForgeryToken] // <-- Do I need this in this case?
public async Task<IActionResult> Delete(
[Bind("ShoppingCartItemProductId")]
ViewModelAddToCart model)
{
// Initialize session to enable SessionId
HttpContext.Session.SetString("_Name", "MyStore");
string SessionId = HttpContext.Session.Id;
var ShoppingCart = new ShoppingCart()
{
SessionId = SessionId
};
var ShoppingCartItem = new ShoppingCartItem()
{
ProductId = model.ShoppingCartItemProductId,
};
if (ModelState.IsValid)
{
// Find ShoppingCart containing current SessionId.
var cartInfo =
(from Cart in _context.ShoppingCarts
where Cart.SessionId == SessionId
select new { TempId = Cart.Id })
.SingleOrDefault();
if (cartInfo != null)
{
ShoppingCartItem.ShoppingCartId = cartInfo.TempId;
}
// Find ShoppingCartItem containing current ProductId:
var cartItemInfo =
(from CartItem in _context.ShoppingCartItems
where (CartItem.ShoppingCartId == ShoppingCartItem.ShoppingCartId &&
CartItem.ProductId == model.ShoppingCartItemProductId)
select new { TempId = CartItem.Id })
.FirstOrDefault();
if (cartItemInfo != null)
{
// Delete ShoppingCartItem
ShoppingCartItem.Id = cartItemInfo.TempId;
_context.ShoppingCartItems.Remove(ShoppingCartItem);
}
await _context.SaveChangesAsync();
return RedirectToAction("Index", "Home");
}
else
{
return View("Index", "Home");
}
}
Edit I have made some changes to my code, and now I receive "Error: undefined" in an alert. That is because the error: in the ajax is triggered, and the data-object is not defined. Why is that? And a second question is what is the controller supposed to return? As I understand, not a RedirectToAction.
what is "deleteitem"
you should have some id or class for the button in your case class should be easy
<button name="DeleteItem" class = "deleteitemevent" data-id="#item.ProductId">DELETE</button>
$(".deleteitemevent").click(function (e) {
}
[HttpPost]
[ValidateAntiForgeryToken]
//^^yes you should for any post... but since you insist on
//doing ajax calls...
//you will have to research how to build this up... from JS and inject with the ajax call..
public async Task<IActionResult> Delete(
[Bind("ShoppingCartItemProductId")]
ViewModelAddToCart model)
{
//...
}
$('[name="DeleteItem"]').click(function (e) {
var dataid = $(this).attr('data-id'); // because name was used for control not id
$.ajax({
type: "POST",
url: "#Url.Action('Delete','Home')",
data: { id: dataid },
success: function () {
alert("success!");
window.location.replace("#Url.Action('Index', 'Home')");
},
error: function (data) {
alert("Error: " + data.id);
}
});
});
I think you have a long way to go... There are easier ways of doing this without needing ajax calls...
I have this controller:
[Authorize]
public class CheckoutController : Controller
{
ShoppingCartContext storeDB = new ShoppingCartContext();
const string PromoCode = "FREE";
[HttpPost]
public ActionResult AddressAndPayment(FormCollection values)
{
var order = new Order();
TryUpdateModel(order);
try
{
if (string.Equals(values["PromoCode"], PromoCode,
StringComparison.OrdinalIgnoreCase) == false)
{
return View(order);
}
else
{
order.Username = User.Identity.Name;
order.OrderDate = DateTime.Now;
//Save Order
storeDB.Orders.Add(order);
storeDB.SaveChanges();
//Process the order
var cart = Models.ShoppingCart.GetCart(this.HttpContext);
cart.CreateOrder(order);
return RedirectToAction("Complete",
new { id = order.OrderId });
}
}
catch
{
//Invalid - redisplay with errors
return View(order);
}
}
public ActionResult Complete(int id)
{
// Validate customer owns this order
bool isValid = storeDB.Orders.Any(
o => o.OrderId == id &&
o.Username == User.Identity.Name);
if (isValid)
{
return View(id);
}
else
{
return View("Error");
}
}
}
And I have created a View called AddressAndPayment under Checkout, so it goes to localhost/Checkout/AddressAndPayment but I only get a 404 error, even if I right click on the View and click on view in Page Inspector. I don't know why its not even showing the view when it is created.
You need a corresponding HttpGet method, as your current one only accepts a HttpPost request. Add the following:
[HttpGet]
public ActionResult AddressAndPayment()
{
return View();
}
Currently I'm using a standard Manage page which looks like:
public ActionResult Manage(string type = "")
{
ViewBag.Type = type;
switch (type)
{
case "EmailAddress":
ViewBag.EmailAddress = lol.UserProfiles.Find((int)Membership.GetUser().ProviderUserKey).EmailAddress;
break;
case "Password":
break;
default:
break;
}
//ViewBag.ReturnUrl = Url.Action("Manage/" + type);
return View();
}
Now my Manage Model page looks like:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Manage(ManageViewModel model)
{
if (ModelState.IsValid)
{
if (model.EmailAddressModel != null)
{
try
{
int userID = (int)Membership.GetUser().ProviderUserKey;
var User = lol.UserProfiles.First(f => f.UserId == userID);
User.EmailAddress = model.EmailAddressModel.EmailAddress;
int saveStatus = lol.SaveChanges();
if (saveStatus == 1)
{
ViewBag.StatusMessage = MessagesEnum.ChangeEmailSuccess;
return RedirectToAction("Manage/EmailAddress", "Account");
}
else
{
ModelState.AddModelError("", MessagesEnum.ChangeEmailFailed);
}
}
catch { }
}
else
{
// ChangePassword will throw an exception rather than return false in certain failure scenarios.
bool changePasswordSucceeded;
try
{
changePasswordSucceeded = WebSecurity.ChangePassword(User.Identity.Name, model.PasswordModel.OldPassword, model.PasswordModel.NewPassword);
}
catch (Exception)
{
changePasswordSucceeded = false;
}
if (changePasswordSucceeded)
{
ViewBag.StatusMessage = MessagesEnum.ChangePasswordSuccess;
return RedirectToAction("Manage/Password", "Account");
}
else
{
ModelState.AddModelError("", MessagesEnum.ChangePasswordFailed);
}
}
}
else
{
}
return View(model);
}
My ManageViewModel class:
public class ManageViewModel
{
public LocalPasswordModel PasswordModel { get; set; }
public LocalEmailAddressModel EmailAddressModel { get; set; }
}
What I was trying to do is make it where if they access multiple pages on Manage such as /Manage/Password and /Manage/EmailAddress and according to those links, it will post a different page. Now first of all, is this the proper way of doing it / is it fine? Second, if it is, I was trying to pass a Sucess message after they change their email to the /Manage/EmailAddress page, but the ViewBag.StatusMessage is not outputting anything on my HTML page. Why is that?
I did a little more research and here are my findings (tell me if it's correct or not). So I should edit the route settings and so something like:
routes.MapRoute(
name: "Manage",
url: "Account/{controller}/{action}/{id}",
defaults: new { controller = "Manage", action = "Index", id = UrlParameter.Optional}
);
And just make a new controller called Manage and create new Functions inside the controller for different pages like ChangeEmail and ChangePassword?
ViewBag and ViewData are only valid for the current request, they wont survive if you do a redirect RedirectToAction. Use TempData instead.
Check the following: ViewBag, ViewData and TempData
If you want your URLs to be /Manage/Password and /Manage/EmailAddress, you dont need to add a new MapRoute, the default one will work. Just use ManageController for you controller class name and "Password" and "EmailAddress" for your function names:
class ManageController
{
public ActionResult Password(...)
{
I have a view that displays a list of user roles (e.g., administrator, operator, etc) and an "Add" button that pops up a modal window, allowing the user to add a new role.
In my controller, I have this as my HttpPost method
[HttpPost]
public ActionResult Create(RoleModel model)
{
if (ModelState.IsValid)
{
var role = new RoleDto
{
Name = model.Name,
Description = model.Description
};
var roleAdded = _rolePermissionsRepository.AddRole(role);
if (roleAdded != null)
{
//CLOSE WINDOW
}
else
{
//PRINT ERROR MSG TO WINDOW
}
}
return View();
}
If the add to the DB is successful, I would like to close the modal window, and then refresh the list on my main index page.
If there's some error while persisting to the DB, the modal window should remain open, and show some error.
How do I achieve that?
This is what I'm using on my Index page to pop up the window
$("#open").click(function (e) {
wnd.center();
wnd.open();
});
You should return a JsonResult to tell the browser what happened.
[HttpPost]
public ActionResult Create(RoleModel model)
{
if (ModelState.IsValid)
{
var role = new RoleDto
{
Name = model.Name,
Description = model.Description
};
var roleAdded = _rolePermissionsRepository.AddRole(role);
if (roleAdded != null)
{
//CLOSE WINDOW
return Json(new { success = true });
}
else
{
return Json(new { error = "Error! Can't Save Data!" });
}
}
return Json(new { error = "Generic Error Message!" });
}
Here is the javascript that should run in your wnd page, you show the error message if there is any, otherwise you close the window.
$('form').submit(function(e) {
e.preventDefault();
$.post(this.action, $(this).serialize(), function(response) {
if(response.error) {
alert(response.error);
}
else {
wnd.close();
}
}, 'json');
});