Handling concurrency access for deleting an object using Ajax.actionlink - asp.net-mvc-3

i have the following view inside my asp.net MVC application that contains an ajax.actionlink for deleting objects:-
<table id="incrementanswer">
<tr>
<th>
Description
</th>
<th>
Answer
</th>
<th></th>
</tr>
#foreach (var answer in Model.Answers.OrderBy(a => a.IsRight))
{
<tr id = #answer.AnswersID>
<td>
#Html.DisplayFor(modelItem => answer.Description)
</td>
<td>
#Html.DisplayFor(modelItem => answer.Answer_Description.description)
</td>
<td>
#{ string i = "Are uou sure you want to delete " + #answer.Description.ToString() + " ?";}
#Ajax.ActionLink("Delete", "Delete", "Answer",
new { id = answer.AnswersID },
new AjaxOptions
{
Confirm = i,
HttpMethod = "Post",
OnBegin = string.Format(
"disablelink({0})",
Json.Encode(answer.AnswersID)),
OnSuccess = string.Format(
"deleteconfirmation3({0},{1})",
Json.Encode(answer.AnswersID), Json.Encode(answer.Description))
}) </td>
</tr>}</table>
and the following post delete action method that will be called by the above ajax link:-
[HttpPost]
public void Delete(int id)
{ var a = repository.FindAnswer(id);
repository.DeleteAnswer(a);
repository.Save();}
and the folloiwng OnSuccess script:-
function deleteconfirmation3(rid, rname) {
$('#' + rid).remove();
jAlert(rname + ' Was Deleted Succsfully succsfully', 'Deletion Confirmation');}
currently if two user access the same view and then they both click on the delete link which is associated with the same object, then a null exception will be raised on one of the requests; so how i can handle this issue both on the action method side and on the view side to display a friendly message to the user; in these two cases:-
either when the var a = repository.FindAnswer(id); returns a null
exception?
Or when the repository.Save(); does not delete any record?
BR
Edit:-
i updated the post action method to the following:-
[HttpPost]
public ActionResult Delete(int id)
{
try
{
Thread.Sleep(1000);
var a = repository.FindAnswer(id);
repository.DeleteAnswer(a);
repository.Save();
return Json(new { IsSuccess = "True", id = id, description = a.Description }, JsonRequestBehavior.AllowGet);
}
catch (ArgumentNullException) {
return Json(new { IsSuccess = "False" }, JsonRequestBehavior.AllowGet); }}
and on the view i update ajax.actionlink to the folloiwng:-
#{ string i = "Are uou sure you want to delete " + #answer.Description.ToString() + " ?";}
#Ajax.ActionLink("Delete", "Delete", "Answer",
new { id = answer.AnswersID },
new AjaxOptions
{
//OnBegin = "deleteconfirmation1",
Confirm = i,
HttpMethod = "Post",
OnBegin = string.Format(
"disablelink({0})",
Json.Encode(answer.AnswersID)),
OnSuccess = "myresponse"
})
and the Onsuccess script:-
function myresponse(data) {
if (data.IsSuccess == "True")
{
$('#' + data.id ).remove();
jAlert(data.description + ' Was Deleted Succsfully succsfully', 'Deletion Confirmation');
}
else {
$('#' + data.id).remove();
jAlert(data.description + 'record has already been deleted', 'aaaa');
}
}
The above code worked fine when i test it ,, but does this approach sound acceptable as i have not written something similar to this before?
BR

Add a check such that if the record exists delete it or else show a message specifying that the record has already been deleted...
[HttpPost]
public ActionResult Delete(int id)
{ var a = repository.FindAnswer(id);
if(/*check if a is not null*/){
repository.DeleteAnswer(a);
repository.Save();
return Json(new{ IsSuccess="True", id=id,description=a.Description });
}else{
// display a message record already been deleted
return Json(new{ IsSuccess="False" });
}
}
apparantly you will need to change the return type from void to ActionResult or JsonResult
#Ajax.ActionLink("Delete", "Delete", "Answer",
new { id = answer.AnswersID },
new AjaxOptions
{
Confirm = i,
HttpMethod = "Post",
OnBegin = string.Format(
"disablelink({0})",
Json.Encode(answer.AnswersID)),
OnSuccess = "myResponse"
})
the success handler will be like
function myResponse(data){
if(data.IsSuccess=="True")
jAlert(data.description + ' Was Deleted Succsfully succsfully', 'Deletion Confirmation');
else {
alert("record has already been deleted");
}
}

Related

Leverage MVC Client-Side Validation when using Ajax Post

It's my first asp.net mvc project and I am trying to build a view that displays a list of countries. on double click it changes the input from readonly to false to enable editing and on keypress or blur it saves the changes.
Now, server-side validation is working but client-side is not. I am not sure is it because I don't have a form tag or it can't be done or needs manual hook-ups.
CountryModel:
public class Country
{
[Key]
public int CountryId { get; set; }
[Required]
[MinLength(4)]
[MaxLength(60)]
[RegularExpression(#"^[a-zA-Z]+$", ErrorMessage = "Use letters only please")]
public string Name
{ get; set; }
}
Country Controller Action:
[HttpPost]
public ActionResult Save(Country country)
{
if (ModelState.IsValid)
{
db.Entry(country).State = EntityState.Modified;
db.SaveChanges();
//return
return Json("Success");
}
else
{
//return
return Json("Failed");
}
}
Country View:
#model IEnumerable<Models.Country>
<h2>Manage Countries</h2>
<table class="table">
<tr>
<th>
#Html.DisplayNameFor(model => model.Name)
</th>
<th>
Options
</th>
<th></th>
</tr>
#foreach (var item in Model)
{
<tr>
<td>
#Html.EditorFor(modelItem => item.Name, new { htmlAttributes = new { #class = "grid-input", data_input_mode = "read", data_model_id = item.CountryId } })
#Html.ValidationMessageFor(modelItem => item.Name, "", new { #class = "text-danger" })
</td>
</tr>
}
</table>
Scripts.js
$(document).ready(function () {
$(".grid-input").attr("readonly", true);
$(".grid-input").dblclick(function () {
var elem = $(this);
if (elem.attr("data-input-mode") == "read") {
elem.attr("data-input-mode", "edit");
elem.removeAttr("readonly");
//Remove selection when mode changes from read to edit only
var sel = window.getSelection();
sel.removeAllRanges();
//bind blur and keypress to attempt saving data on focusout or enter press
elem.on("blur keypress", function (e) {
if (e.keyCode == 13 || e.type == "blur") {
//Stop Bubbling
e.stopImmediatePropagation();
//Enable Readonly To Avoid Edits While Saving
$(this).attr("data-input-mode", "read");
$(this).attr("readonly", "true");
//Get Country ID & Name
var _countryId = $(this).attr("data-model-id");
var _countryName = $(this).val();
//Validate
$.ajax({
url: '/Country/Save',
data: JSON.stringify({
country: {
CountryId: _countryId,
Name: _countryName
}
}),
type: 'POST',
contentType: 'application/json; charset=utf-8',
success: function (data) {
if (data == "Success") {
console.log("Saved!");
} else {
elem.attr("data-input-mode", "edit");
elem.removeAttr("readonly");
console.log("Error occurs on the Database level!");
}
},
error: function () {
console.log("An error has occured!!!");
}
});
}
});
}
});
});

ActionLink to submit Model value

I want my Ajax.ActionLink to pass a viewModel property to action.
Here is my ViewModel
public class ViewModel
{
public string Searchtext { get; set; }
}
My .cshtml
#Ajax.ActionLink("Bottom3", "Bottom3",new { name = Model.Searchtext}, new AjaxOptions
{
HttpMethod = "POST",
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "pointsDiv"
})
using(Html.BeginForm("Bottom3", "Home", FormMethod.Get))
{
#Html.TextBoxFor(x => x.Searchtext)
<button type="submit">Search</button>
}
<div id="pointsDiv"></div>
}
My Controller action:
public PartialViewResult Bottom3(string name)
{
var model = db.XLBDataPoints.OrderBy(x => x.DataPointID).Take(3).ToList();
return PartialView("Partial1", model);
}
But the name parameter passed to the action is always null. How do I solve this?
In your code... you have 2 different ways of posting to the server: the link and the form button.
The problem is that the ActionLink has no way to get the value from the input in client side... just the original value.
If you press the Search button, you will see a value posted.
Now, you can use some jQuery to modify a standard ActionLink (not the Ajax.ActionLink):
https://stackoverflow.com/a/1148468/7720
Or... you can transform your Form in order to do a Ajax post instead of a normal one:
https://stackoverflow.com/a/9051612/7720
I did this for a model of mine like so. I ONLY supported the HttpPost method. So add the HttpMethod="POST" to your Ajax.ActionLink
[HttpPost]
public ActionResult Accounts(ParametricAccountsModel model)
{
if (model.Accounts == null)
{
GetAccountsForModel(model);
}
if (model.AccountIds == null)
{
model.AccountIds = new List<int>();
}
return View(model);
}
On the razor view
#Ajax.ActionLink(
"Add Account to Order", "Accounts", "Parametric", null,
new AjaxOptions() { InsertionMode = InsertionMode.Replace, UpdateTargetId = "...", HttpMethod = "POST" },
new { #id = "AddParametricAccountLink" })
The model has a list of selected account ids. So in javascript, I modified the href of the action link dynamically.
function UpdateParametricAccountAction() {
var originalLink = '/TradeNCashMgmt/Parametric/Accounts';
var append = '';
var numberOfRows = $('#ParametricAccounts').find('.parametric-account- row').size();
for (var i = 0; i < numberOfRows; i++) {
if (i != 0) {
append += '&';
}
else {
append = '?';
}
var idValue = $('#NotionalTransactionsAccountId_' + i).val();
append += 'AccountIds%5B' + i + '%5D=' + idValue;
}
$('#AddParametricAccountLink').attr('href', originalLink + append);
}
Since the model binder looks for parameter names in the query string and form submission, it will pick up values using the href. So I posted a model object using the querystring on my Ajax.ActionLink. Not the cleanest method, but it works.

How to include the #Html.AntiForgeryToken() when deleting an object using a Delete link

i have the following ajax.actionlink which calls a Delete action method for deleting an object:-
#if (!item.IsAlreadyAssigned(item.LabTestID))
{
string i = "Are You sure You want to delete (" + #item.Description.ToString() + ") ?";
#Ajax.ActionLink("Delete",
"Delete", "LabTest",
new { id = item.LabTestID },
new AjaxOptions
{ Confirm = i,
HttpMethod = "Post",
OnSuccess = "deletionconfirmation",
OnFailure = "deletionerror"
})
}
but is there a way to include #Html.AntiForgeryToken() with the Ajax.actionlink deletion call to make sure that no attacker can send a false deletion request?
BR
You need to use the Html.AntiForgeryToken helper which sets a cookie and emits a hidden field with the same value. When sending the AJAX request you need to add this value to the POST data as well.
So I would use a normal link instead of an Ajax link:
#Html.ActionLink(
"Delete",
"Delete",
"LabTest",
new {
id = item.LabTestID
},
new {
#class = "delete",
data_confirm = "Are You sure You want to delete (" + item.Description.ToString() + ") ?"
}
)
and then put the hidden field somewhere in the DOM (for example before the closing body tag):
#Html.AntiForgeryToken()
and finally unobtrusively AJAXify the delete anchor:
$(function () {
$('.delete').click(function () {
if (!confirm($(this).data('confirm'))) {
return false;
}
var token = $(':input:hidden[name*="RequestVerificationToken"]');
var data = { };
data[token.attr('name')] = token.val();
$.ajax({
url: this.href,
type: 'POST',
data: data,
success: function (result) {
},
error: function () {
}
});
return false;
});
});
Now you could decorate your Delete action with the ValidateAntiForgeryToken attribute:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(int id)
{
...
}
Modifying the answer by Bronx:
$.ajaxPrefilter(function (options, localOptions, jqXHR) {
var token, tokenQuery;
if (options.type.toLowerCase() !== 'get') {
token = GetAntiForgeryToken();
if (options.data.indexOf(token.name)===-1) {
tokenQuery = token.name + '=' + token.value;
options.data = options.data ? (options.data + '&' + tokenQuery)
: tokenQuery;
}
}
});
combined with this answer by Jon White
function GetAntiForgeryToken() {
var tokenField = $("input[type='hidden'][name$='RequestVerificationToken']");
if (tokenField.length == 0) { return null;
} else {
return {
name: tokenField[0].name,
value: tokenField[0].value
};
}
Edit
sorry - realised I am re-inventing the wheel here SO asp-net-mvc-antiforgerytoken-over-ajax/16495855#16495855

ASP.NET MVC - Current Selected value doesnt get selected in IE

In my Action for Editing an item in my model I have:
ViewBag.PossibleSource = context.Source.ToList();
In my View I have:
#Html.DropDownListFor(model => model.SourceID, ((IEnumerable<btn_intranet.Areas.DayBook.Models.DayBookSource>)ViewBag.PossibleSource).Select(option => new SelectListItem
{
Text = (option == null ? "None" : option.SourceName),
Value = option.SourceID.ToString(),
Selected = (Model != null) && (option.SourceID == Model.SourceID)
}))
In Chrome this works as expected. When I pass a model to my view, the current value that's set in my model is the selected value in the list. But in IE8 and 9 it's selected value is the ORIGINAL value my model was set as even though the update does work. So if I selected "hello" originally and then edited to "world". In chrome when i reload the page it will be set to "world" but in IE "hello" is selected in the dropdown even tho "world" is set in my database for my model. It is worth noting these are updated via AJAX
EDIT:
Ajax.Actionlink:
#Ajax.ActionLink(item.ItemNumber, "EditItem", new { id = item.QuoteLineID, enquiryId = item.EnquiryID }, new AjaxOptions()
{
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "EditItem"
})
This loads the form onto the view.
Ajax.BeginForm:
#using (Ajax.BeginForm("EditItem", new { controller = "QuoteLines" }, new AjaxOptions()
{
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "Summary"
}, new { #class = "manual-search cf" }))
{
...Other Model inputs
#Html.DropDownListFor(model => model.SourceID, ((IEnumerable<btn_intranet.Areas.DayBook.Models.DayBookSource>)ViewBag.PossibleSource).Select(option => new SelectListItem
{
Text = (option == null ? "None" : option.SourceName),
Value = option.SourceID.ToString(),
Selected = (Model != null) && (option.SourceID == Model.SourceID)
}))
<input type="submit" class="update-items" value="Update Line" />
}
EditItem Action GET request:
public virtual ActionResult EditItem(int id)
{
try
{
DayBookQuoteLines q = context.QuoteLines.Single(x => x.QuoteLineID == id);
ViewBag.PossibleSource = context.Source.ToList();
if (Request.IsAjaxRequest())
{
return PartialView("_EditItem", q);
}
else
{
return RedirectToAction("SalesDetails", new { controller = "Enquiries", id = q.EnquiryID });
}
}
catch (Exception ex)
{
return PartialView("_Error", ex.Message);
}
}
EditItem Action POST request:
[HttpPost]
public virtual ActionResult EditItem(DayBookQuoteLines q)
{
try
{
ViewBag.PossibleSource = context.Source.ToList();
if (ModelState.IsValid)
{
context.Entry(q).State = EntityState.Modified;
context.SaveChanges();
return PartialView("_GetSummary", context.Vehicles.Where(x => x.EnquiryID == q.EnquiryID).ToList());
}
return PartialView("_EditItem", q);
}
catch (Exception ex)
{
return PartialView("_Error", ex.Message);
}
}
I've fixed it, I renamed my GET request for EditItem to EditItemGet and then in my #Ajax.ActionLink I did:
#Ajax.ActionLink(item.ItemNumber, "EditItemGet", new { id = item.QuoteLineID, enquiryId = item.EnquiryID }, new AjaxOptions()
{
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "EditItem",
HttpMethod = "POST"
})
It was a Cache issue. Thats why it only failed in IE which likes Cache. I read before that making the request a POST request prevents Caching.

MVC3 Custom HTMLHelper, partial view or other solution to apply DRY principle

I've got an MVC3 Read Only view that contains a table displaying properties for an Item.
For many of the properties of the Item, we track the changes a Vendor has made to the item. So, for example, a vendor may update a property named 'Color' from a value of 'Blue' to 'Red'. In this View a table lists each property tracked in a table row, with a column showing the 'Old Value' and the 'New Value'. The next column either shows the current change's status (Awaiting Approval, Approved, or Rejected). However, for Admin users, the column will contain Links ('Approve', 'Reject', or 'Reset to Awaiting Approval').
My markup and Razor code for this is very repetitive and getting out of hand. I'd like to create an HTMLHelper for this, or possibly a partial view that I can use to move all the code into and then use it for each Item Property.
Here is an example of the code used for one Property. This code is repeated for another 10 or so properties.
I'm using some jquery and ajax for the actions. For example, when an change is rejected, the user must enter a reason for rejecting the change.
<tr id="rowId-color">
<td>#Html.LabelFor(model => model.Color)</td>
<td>#Html.DisplayFor(model => model.Color)</td>
#if (Model.ChangeLog != null && Model.ChangeLog.Item("Color") != null) {
var change = Model.ChangeLog.Item("Color");
var changeStatus = (ItemEnumerations.ItemChangeStatuses)change.ItemChangeStatusID;
<td>#change.OldValueDisplay</td>
<td id="tdstatusId-#change.ItemChangeID">
#if (changeStatus == ItemEnumerations.ItemChangeStatuses.AwaitingApproval && User.IsInRole("TVAPMgr")) {
#Ajax.ActionLink("Approve", "Approve", new { itemChangeID = change.ItemChangeID }, new AjaxOptions { HttpMethod = "POST", Confirm = "Approve this change?", OnSuccess = "actionCompleted" })
#Html.Raw("|")
<a href="#dialog" name="reject" data-id="#change.ItemChangeID" >Reject</a>
}
else if ((changeStatus == ItemEnumerations.ItemChangeStatuses.Rejected || changeStatus == ItemEnumerations.ItemChangeStatuses.Approved) && User.IsInRole("TVAPMgr")) {
#Ajax.ActionLink("Reset to Awaiting Approval", "Reset", new { itemChangeID = change.ItemChangeID }, new AjaxOptions { HttpMethod = "POST", Confirm = "Reset this change to Awaiting Approval?", OnSuccess = "actionCompleted" })
}
else {
#changeStatus.ToDisplayString()
}
</td>
<td id="tdreasonId-#change.ItemChangeID">#Html.DisplayFor(m => m.ChangeLog.Item(change.ItemChangeID).RejectedReason)</td>
}
else {
<td colspan="3">No Change</td>
}
</tr>
This really sounds more like a DisplayTemplate for the ItemChangeModel type, that way you can just do:
<tr id="rowId-color">
<td>#Html.LabelFor(model => model.Color)</td>
<td>#Html.DisplayFor(model => model.Color)</td>
#Html.DisplayFor(m => m.ChangeLog.Item("Color"))
</tr>
For each ChangeLog cell and the display template then is like a mini-view with a typed model of ItemChangeModel. So your view file would like like this:
#model ItemChangeModel
#if(Model != null) {
<td>#Html.DisplayFor(m => m.OldValueDisplay)</td>
<td id="tdstatusId-#Model.ItemChangeID">
#switch((ItemEnumerations.ItemChangeStatuses) Model.ItemChangeStatusID) {
case ItemEnumerations.ItemChangeStatuses.AwaitingApproval:
if(User.IsInRole("TVAPMgr")) {
#Ajax.ActionLink("Approve", "Approve", new { itemChangeID = change.ItemChangeID }, new AjaxOptions { HttpMethod = "POST", Confirm = "Approve this change?", OnSuccess = "actionCompleted" })
#Html.Raw("|")
<a href="#dialog" name="reject" data-id="#change.ItemChangeID" >Reject</a>
}
break;
case ItemEnumerations.ItemChangeStatuses.Rejected:
case ItemEnumerations.ItemChangeStatuses.Approved:
if(User.IsInRole("TVAPMgr")) {
#Ajax.ActionLink("Reset to Awaiting Approval", "Reset", new { itemChangeID = change.ItemChangeID }, new AjaxOptions { HttpMethod = "POST", Confirm = "Reset this change to Awaiting Approval?", OnSuccess = "actionCompleted" })
} else {
#changeStatus.ToDisplayString()
}
#break;
}
</td>
<td id="tdreasonId-#change.ItemChangeID">#Html.DisplayFor(m => m.RejectedReason) </td>
} else {
<td colspan="3">No Change</td>
}
(Hard to code in editor box, this could use some cleanup, but I think you will get the idea)
You add this display template (with the file name ItemChangeModel.cshtml) to the Views\Shared\DisplayTemplates folder and it will get used whenever a DisplayFor call is made on that type.
Its been noted in comments that you can't use a method in DisplayFor, but you can change that to an indexed property:
public class ChangeLog
{
public ItemChangeModel this[string key] { get { return Item("Color"); } }
}
Then use:
#Html.DisplayFor(m => m.ChangeLog["Color"])
You haven't shown nor explained how your domain and view models look like but I suspect that what you are using here is not an appropriate view model for this specific requirement of the view. A better view model would have been one that has a list of properties to approve which would be shown in the table.
Anyway, one possible approach is to write a custom HTML helper so that your view looks like this:
<tr id="rowId-color">
#Html.DisplayFor(x => x.Color)
#Html.ChangeLogFor(x => x.Color)
</tr>
...
and the helper might be something along the line of:
public static class HtmlExtensions
{
public static IHtmlString ChangeLogFor<TProperty>(
this HtmlHelper<MyViewModel> html,
Expression<Func<MyViewModel, TProperty>> ex
)
{
var model = html.ViewData.Model;
var itemName = ((MemberExpression)ex.Body).Member.Name;
var change = model.ChangeLog.Item(itemName);
if (change == null)
{
return new HtmlString("<td colspan=\"3\">No Change</td>");
}
var isUserTVAPMgr = html.ViewContext.HttpContext.User.IsInRole("TVAPMgr");
var changeStatus = (ItemChangeStatuses)change.ItemChangeStatusID;
var sb = new StringBuilder();
sb.AppendFormat("<td>{0}</td>", html.Encode(change.OldValueDisplay));
sb.AppendFormat("<td id=\"tdstatusId-{0}\">", change.ItemChangeID);
var ajax = new AjaxHelper<MyViewModel>(html.ViewContext, html.ViewDataContainer);
if (changeStatus == ItemChangeStatuses.AwaitingApproval && isUserTVAPMgr)
{
sb.Append(
ajax.ActionLink(
"Approve",
"Approve",
new {
itemChangeID = change.ItemChangeID
},
new AjaxOptions {
HttpMethod = "POST",
Confirm = "Approve this change?",
OnSuccess = "actionCompleted"
}).ToHtmlString()
);
sb.Append("|");
sb.AppendFormat("Reject", change.ItemChangeID);
}
else if ((changeStatus == ItemChangeStatuses.Rejected || changeStatus == ItemChangeStatuses.Approved) && isUserTVAPMgr)
{
sb.Append(
ajax.ActionLink(
"Reset to Awaiting Approval",
"Reset",
new {
itemChangeID = change.ItemChangeID
},
new AjaxOptions {
HttpMethod = "POST",
Confirm = "Reset this change to Awaiting Approval?",
OnSuccess = "actionCompleted"
}
).ToHtmlString()
);
}
else
{
sb.Append(changeStatus.ToDisplayString());
}
sb.AppendLine("</td>");
sb.AppendFormat(
"<td id=\"tdreasonId-{0}\">{1}</td>",
change.ItemChangeID,
html.Encode(model.ChangeLog.Item(change.ItemChangeID).RejectedReason)
);
return new HtmlString(sb.ToString());
}
}
A better approach would be to re-adapt your view model to the requirements of this view and simply use display templates.

Resources