Internet Explorer displaying cache data over Ajax Pull with 304 Result - c#

I have a tool that works perfectly fine in Chrome and FF. But with any versions of IE the browser is displaying cache info over doing an Ajax pull to retrieve the data.
Here's my setup:
I have criterias that I loop through:
#foreach (var item in Model)
{
<div class="sizeTDCriteria">
#Html.DisplayFor(modelItem => item.Text)
</div>
<div class="sizeTDCriteriaAction">
#Ajax.ImageActionLink(Url.Content("~/Content/images/icons/edit.png"), "Edit Criteria", "AddOrEditCriteria", "Management", new { competencySectionId = ViewBag.competencySectionId, criteriaId = item.Id }, new AjaxOptions { UpdateTargetId = "AddOrEditCriteriaFormContainer" }, new { #class = "iconPosition" })
#Ajax.ImageActionLink(Url.Content("~/Content/images/icons/delete.png"), "Delete Criteria", "RemoveCriteria", "Management", new { criteriaId = item.Id }, new AjaxOptions { UpdateTargetId = "CompetenciesManagementWrapper" }, new { #class = "iconPosition" })
</div>
}
The ImageActionLink is just a helper that creates a ActionLink with an image inside, I've tried doing this with a normal ActionLink and the issue occurs as well so you can ignore that. I've also tried to change the whole ImageActionLink by a plain <img> with a jQuery trigger with no difference.
What happens is that when a user clicks on the Edit link it will do an ajax call to the "AddOrEditCriteria", that ActionResult, finds the criteria, and displays the PartialView form back into the div "#AddOrEditCriteriaFormContainer".
So far so good, this works fine in all browsers.
But when I click a second time on that edit, instead of doing the ajax call, IE simply displays the PartialView from what it had in the cache, when all other browsers correctly pull the data again (which is required, as that view allows to edit the criterias, it could have been edited by someone else in the mean time).
The weird part is that IE is making the call but somehow it never reaches the server, it just uses the cache by using a Result 304. You can see from this Network capture:
URL Method Result Type Received Taken Initiator Wait?? Start?? Request?? Response?? Cache read?? Gap??
/PerformanceMVC/Management/AddOrEditCriteria?competencySectionId=178&criteriaId=369&X-Requested-With=XMLHttpRequest GET 304 text/html 182 B < 1 ms JS Library XMLHttpRequest
/PerformanceMVC/Management/AddOrEditCriteria?competencySectionId=178&criteriaId=369&X-Requested-With=XMLHttpRequest GET 304 text/html 182 B < 1 ms JS Library XMLHttpRequest
/PerformanceMVC/Management/AddOrEditCriteria?competencySectionId=178&criteriaId=369&X-Requested-With=XMLHttpRequest GET 200 text/html 1.53 KB 1.24 s JS Library XMLHttpRequest
The last one is the first to happen, the first two were done after and are getting a 304 return.
I found a way to fix it by adding a "breakcache" parameter to the ajax call with a random number, but that just doesn't seem like a good solution.
This issue has been screwing our users because they see data that should is not updated all because of IE.

IE will cache AJAX GETs unless you tell it not to.
You tell it not to via the Cache-Control header.
So here's what we do:
[CacheControl(HttpCacheability.NoCache), HttpGet]
public ActionResult MyAction() { return Json("Hi!", JsonRequestBehavior.AllowGet); }
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public sealed class CacheControlAttribute : ActionFilterAttribute
{
public CacheControlAttribute(HttpCacheability cacheability)
{
this._cacheability = cacheability;
}
public HttpCacheability Cacheability { get { return this._cacheability; } }
private HttpCacheability _cacheability;
public override void OnActionExecuted(ActionExecutedContext filterContext)
{
HttpCachePolicyBase cache = filterContext.HttpContext.Response.Cache;
cache.SetCacheability(_cacheability);
}
}

Related

Why Fetch Post request with JSON parameter in the body to ASP.NET Core 3.1 WEB API or MVC Controller does not get anything?

Trying over a week so far with no success, to find out why I can't get the response from my controller.
Note I am not fluent in JS, only C#, but I already have a good understanding of what the fetch does.
Now learning ASP.NET Core, but still somewhere under junior level in my overall C# and .Net experience maybe.
It is regarding the process of redirecting to Stripe checkout, but fetch and the method as mentioned, can't agree around 200.
Originally the controller was a normal MVC controller but among many things tried, I made it also an API controller, hoping something which I apparently don't understand, might fix the issue with its method.
Turned over the web upside down searching for similar issues, and I have no other options except reaching to you guys, many experienced devs here.
The method functions 100%, I mean the logic inside. It returns a session object.
The API version returns the session; the MVC version returns this.Json(session);
I know that both versions work because I was debugging them by entering inside after a button click
Its all happening on localhost, therefore I provided credentials: 'include' in the headers... if I am correct.
Trying anything with Postman with authorization of course, but no luck so far.
Firefox Debugging currently throws no errors when going through the fetch.
The Questions:
- Does the fetch function properly according to my code?
- Is the controller prepared to get the payment id from the JSON body of the request?
- When I hardcode the paymentId inside the controller so that I don't expect any parameter, why this doesn't work as well (the fetch has session undefined)?
Here is the fetch in my Pay.cshtml view
<script src="https://js.stripe.com/v3/"></script>
<div id="checkout">
<form id="payment-form">
<div class="row d-flex justify-content-center">
<div class="col-6 py-4 text-center">
<button id="checkout-button"
value="Pay"
class="btn btn-sm btn-labeled btn-primary">
<span class="btn-label">
<ion-icon name="card-outline"></ion-icon>
</span>Pay $ #Model.Amount
</button>
</div>
</div>
</form>
</div>
<script>
var stripe = Stripe('#HomyTestPublishableKey');
var payButton = document.getElementById('checkout-button');
payButton.addEventListener('click', function(event) {
event.preventDefault();
stripe.redirectToCheckout({
sessionId: sessionId
});
});
var sessionId;
var paymentId = '#Model.Id';
fetch("/api/payment/createsession/", {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({id: paymentId })
}).then(function(r) {
return r.json();
}).then(function(response) {
sessionId = response.id;
})
.catch(error => console.error('Error:', error));
</script>
The method in the API Controller looks like this:
// Create PaymentSession
[HttpPost]
[Route("createsession")]
public async Task<Session> CreateSession([FromBody]string id)
{
var userId = this.userManager.GetUserId(this.User);
var payment = await this.paymentService.GetPaymentDetailsAsync(id, userId);
var successStringUrl = "https://localhost:44319/Checkout/success?session_id={CHECKOUT_SESSION_ID}";
var cancelStringUrl = "https://localhost:44319/Checkout/cancel";
var options = new SessionCreateOptions
{
PaymentMethodTypes = new List<string>
{
"card",
},
LineItems = new List<SessionLineItemOptions>
{
new SessionLineItemOptions
{
Quantity = 1,
Amount = (long)payment.Amount * 100,
Currency = CurrencyUSD,
Description = $"Payment Id: {payment.Id} for rental at {payment.RentalAddress}",
Name = $"Rent Payment for {DateTime.UtcNow.Month} | {DateTime.UtcNow.Year} for rental at {payment.RentalAddress}",
},
},
PaymentIntentData = new SessionPaymentIntentDataOptions
{
ApplicationFeeAmount = (long)((payment.Amount * 0.01m) * 100),
CaptureMethod = "manual",
TransferData = new SessionPaymentIntentTransferDataOptions
{
Destination = payment.ToStripeAccountId,
},
},
SuccessUrl = successStringUrl,
CancelUrl = cancelStringUrl,
};
var service = new SessionService();
Session session = service.Create(options);
return session; // the MVC version returns Task<IActionResult> of this.Json(session);
}
The options method of Stripe is not important, its only importnat why the fetch can't get anything. and what am I doing fundamentally wrong.
Here is a Stripe engineer CJ Avilla, demonstrating this with PHP and JS but you could get an idea.
After minute mark 20:30 you want to see him creating the fetch and later on, how checkout flow cicks in.
https://www.youtube.com/watch?v=VQ5jccnZ2Ow&t=1160s
Will appreciate any suggestions. Throw them to me.
Anyone with serious experience and a little time to dig in, I can provide access to the private github repo (its my first web project and I have to defend it at the end of the ASP.NET Core course at SoftUni).
Thank you!
P.S.
Edit 1:
Added binding model for the Id param, as I was wrongly expecting string but passing json object. However this still does not prove or allow the method to be accessed by the fetch...
Screen-shot added from my browser debugging.
Edit 2:
This code return Status 400 in console
fetch("https://localhost:44319/api/payment/createsession", {
method: 'POST',
mode: 'same-origin',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({ id: paymentId })
})
.then(r => r.json().then(data => ({status: r.status, body: data})))
.then(obj => console.log(obj));
why the fetch can't get anything. and what am I doing fundamentally wrong
Your CreateSession action accepts a string-type parameter id, on your JavaScript client side, you can pass data through request body like below.
body: JSON.stringify(paymentId)
Besides, if you'd like to make your action can handle body: JSON.stringify({id: paymentId }) well from client, you can also modify your action to accept a custom model parameter, like below.
[HttpPost]
[Route("createsession")]
public async Task<Session> CreateSession([FromBody]MyTestModel myTestModel)
{
var id = myTestModel.id;
//your code logic here
//...
MyTestModel class
public class MyTestModel
{
public string id { get; set; }
}
let serverResponse = await fetch('https://localhost:{port}/api/payment/createsession/',
{
method: 'POST',
mode: 'cors',
headers: {
'Accept': 'application/json'
},
body: JSON.stringify({ id: paymentId })
}
);
serverResponse.json().then(function (data) {
// server response value come to "data" variable
debugger;
});

Temp data is not getting empty(null) when hitting back(button) through browser

I am sending a success message in response from API to controller and setting its value in TempData["Message"], now redirecting to some other action method.
like this:
[HttpPost]
public ActionResult AddFeedbackCategory(IFeedbackCategoryModel modelData)
{
var message = JsonConvert.DeserializeObject<ApiResponse>(response);
TempData["Message"] = message.Message;
return RedirectToAction("CategoryList");
}
[HttpGet]
public ActionResult CategoryList()
{
var categoryData = JsonConvert.DeserializeObject<List<CategoryModel>>(data);
return View(categoryData);
}
and in the view of CategoryList I have written code something like this:
#if (TempData["Message"] != null)
{
<div id="snackbar" class="snack_success">#TempData["Message"].ToString()
</div>
TempData["Message"] = null;
}
It help to show a success message(retains for 2 second) whenever I add some record in my system like this.
Now the problem is when I hit back button and then again return to category list this success message appear again. Why its value not becoming null? is there any specific reason or I am doing something wrong?
Let's say you navigate from Page A to Page B. Then you click Back on Page B (to go back to Page A). If you do not explicitly set caching headers on Page A, the browser may decide to display the old version of Page A (that it rendered before going to Page B).
To solve this, one option is to disable browser caching. For example, use this against the GET endpoint's action method:
[OutputCache(NoStore = true, Duration = 0, VaryByParam = "*", Location = OutputCacheLocation.None)]
This will ensure that, if you press the Back button, the browser will be forced to hit the server again to re-render Page A (rather than just showing the old version of Page A).

ASP.NET MVC - How to call void controller method without leaving the view?

Question background:
I am implementing some basic 'shopping cart' logic to an MVC app. Currently when I click a link - denoted as 'Add To Cart' on the screen shot below this calls to an 'AddToCart' method in the 'ProductController' as shown:
Product.cshtml code:
#Html.ActionLink("Add To Cart", "AddToCart")
'AddToCart' method in the ProductController:
public void AddToCart()
{
//Logic to add item to the cart.
}
The issue:
Not an issue as such but currently when I click the 'Add To Cart' button on the ActionLink on the ProductDetail.cshtml view the page calls the 'AddToCart' method on the ProductController and gives a blank view on the page - as shown below. I want the view to stay on 'ProductDetail.cshtml' and just call the 'AddToCart' method, how do I do this?
Basically #Html.ActionLink() or <a></a> tag uses get request to locate the page. Hence whenever you clicked it, you request to your AddToCart action method in ProductController and if that action method returns null or void so a blank or empty page is shown as you experienced (because or #Html.ActionLink() get request by Default).
So if you want to add your value to cart then call AddToCart method using ajax i.e:
HTML:
#Html.ActionLink("Add To Cart", "AddToCart", null, new { id="myLink"})
Jquery or Javascript:
$("#myLink").click(function(e){
e.preventDefault();
$.ajax({
url:$(this).attr("href"), // comma here instead of semicolon
success: function(){
alert("Value Added"); // or any other indication if you want to show
}
});
});
'AddToCart' method in the ProductController:
public void AddToCart()
{
//Logic to add item to the cart.
}
Now this time when the call goes to AddToCart method it goes by using ajax hence the whole page will not redirect or change, but its an asynchronous call which execute the AddToCart action method in your ProductController and the current page will remains same. Hence the product will also added to cart and page will not change to blank.
Hope this helps.
The answer of Syed Muhammad Zeeshan is what you are looking for, however you may return an EmptyResult.
public ActionResult AddToCart()
{
//Logic to add item to the cart.
return new EmptyResult();
}
According to this it has no impact on your code ASP.Net MVC Controller Actions that return void
But maybe sometime you want to return data and then you could do something like this:
if (a)
{
return JSon(data);
}
else
{
return new EmptyResult();
}
As many people mentioned here you will need to use AJAX if your using asp.net MVC to hit a controller POST function without having to leave your view.
A good use case for this is if you want to upload a file without refreshing the page and save that on the server.
All of the
return new EmptyResult();
Wont work, they will still redirect you.
Here is how you do it, in your view have the follow form as an example:
<form enctype="multipart/form-data" id="my-form">
<p>
The CSV you want to upload:
</p>
<input type="file" class="file-upload" name="FileUpload" />
</div>
<div>
<button type="submit" class="btn btn-default" name="Submit" value="Upload">Upload</button>
</div>
</form>
Then in the JavaScript side you need to add this to your view with within Script tags.
$("#my-form").on('submit', function (event) {
event.preventDefault();
// create form data
var formData = new FormData();
//grab the file that was provided by the user
var file = $('.file-upload')[0].files[0];
// Loop through each of the selected files.
formData.append('file', file);
if (file) {
// Perform the ajax post
$.ajax({
url: '/YourController/UploadCsv',
data: formData,
processData: false,
contentType: false,
type: 'POST',
success: function (data) {
alert(data);
}
});
}
});
Your controller might look something like this to process this type of file:
[HttpPost]
public void UploadCsv()
{
var listOfObjects = new List<ObjectModel>();
var FileUpload = Request.Files[0]; //Uploaded file
//check we have a file
if (FileUpload.ContentLength > 0)
{
//Workout our file path
string fileName = Path.GetFileName(FileUpload.FileName);
string path = Path.Combine(Server.MapPath("~/App_Data/"), fileName);
//Try and upload
try
{
//save the file
FileUpload.SaveAs(path);
var sr = new StreamReader(FileUpload.InputStream);
string csvData = sr.ReadToEnd();
foreach (string r in csvData.Split('\n').Skip(1))
{
var row = r;
if (!string.IsNullOrEmpty(row))
{
//do something with your data
var dataArray = row.Split(',');
}
}
}
catch (Exception ex)
{
//Catch errors
//log an error
}
}
else
{
//log an error
}
}
There are many ways to accomplish what you want, but some of them require a lot more advanced knowledge of things like JavaScript than you seem aware of.
When you write ASP.NET MVC applications, you are required to have more intimate knowledge of how browsers interact with the web server. This happens over a protocol called HTTP. It's a simple protocol on the surface, but it has many subtle details that you need to understand to successfully write ASP.NET MVC apps. You also need to know more about Html, CSS, and JavaScript.
In your case, you are creating an anchor tag (<a href="..."/>), which when click upon, instructs the browser to navigate to the url in the href. That is why you get a different page.
If you don't want that, there are a number of ways change your application. The first would be, instead of using an ActionLink, you instead simply have a form and post values back to your current controller. And call your "add to cart" code from your post action method.
Another way would be have your AddToCart method look at the referrer header (again, part of that more subtle knowledge of http) and redirect back to that page after it has processed its work.
Yet another way would be to use Ajax, as suggested by Syed, in which data is sent to your controller asynchronously by the browser. This requires that you learn more about JavaScript.
Another option is to use an embedded iframe and have your "add to cart" be it's own page within that iframe. I wouldn't necessarily suggest that approach, but it's a possibility.
Controller should return ActionResult. In this case a redirect to the caller page.
using System.Web.Mvc;
using System.Web.Mvc.Html;
public ActionResult Index()
{
HtmlHelper helper = new HtmlHelper(new ViewContext(ControllerContext, new WebFormView(ControllerContext, "Index"), new ViewDataDictionary(), new TempDataDictionary(), new System.IO.StringWriter()), new ViewPage());
helper.RenderAction("Index2");
return View();
}
public void Index2(/*your arg*/)
{
//your code
}
I was struggling with this and couldn't get it working with ajax.
Eventually got a working solution by making my controller method return type ActionResult rather than void and returning a RedirectToAction() and inputting the action relating to the view I wanted to remain on when calling the controller method.
public ActionResult Method()
{
// logic
return RedirectToAction("ActionName");
}

MVC #Html.ActionLink no-op from controller

I have an #Html.ActionLink inside of a partial view that when clicked I'd like to have either send the user to another view or stay on the current view without changing anything. Is this possible?
Our controller looks like:
public ActionResult Edit(int id)
{
if (ShouldAllowEdit(id))
{
return this.View("Edit", ...edit stuff...)
}
return ????????
}
We tried return new EmptyResult(); but that just dumps the user to a blank page.
This is a little different approach to the issue, but it should do what you want.
Instead of giving the user a link to navigate to, do an ajax call on link/button click, and do the id check. Return either the url to navigate to in a JsonResult, or nothing if the id is invalid.
On return of the ajax call, navigate to the url if appropriate.
(swap out the hard coded ids and the == 0 with your ShouldAllowEdit function in the example of course)
In the View:
<div class="btn btn-danger" id="myButton">Button</div>
#section scripts{
<script>
$("#myButton").click(function () {
$.ajax("#Url.Action("Edit", new { id = 0 })", { type : "POST" })
.success(function (data) {
if (data.url !== "") {
window.location.href = data.url;
}
});
});
</script>
}
In the controller:
[HttpPost]
public JsonResult Edit(int id)
{
if (id == 0)
{
return Json(new {url = ""});
}
else
{
return Json(new { url = Url.Action("EditPage", new { id = id }) });
}
}
An answer is to redirect to the view action - and maybe give some feed back why they failed.
public ActionResult Edit(int id)
{
if (ShouldAllowEdit(id))
{
return this.View("Edit", ...edit stuff...)
}
ModelState.AddModelError("id", "Not allowed to edit this item");
return RedirectToAction(Edit(id));
}
If the user clicks a link they will be taken away. They might be sent back right to the same page, but the page will unload, be requested from the server again, and then re-rendered in the browser. If you don't want that to happen, you don't give the user the link in the first place. In other words, conditionally render the link or not based on the user's roles or whatever.
#if (userCanEdit)
{
#Html.ActionLink(...)
}
Where userCanEdit is whatever logic you need to make that determination.
If the user fails whatever check you determine, then they don't get the link. Simple.
However, since there's malicious people in the world, you can't just leave it entirely there. There's potential for the user to figure out the link to edit something and go there manually. So, to prevent that you check for the edit permission in your action (like you've already got in your code sample), but if the user is not allowed, then you just return a forbidden status code:
return new HttpStatusCodeResult(HttpStatusCode.Forbidden);
Or
return new HttpStatusCodeResult(403);
They both do the same thing.
UPDATE
Based on your comment above, it appears that the user is normally allowed to edit but can't in a particular instance because another user is editing. A 403 Forbidden is not appropriate in that case, so really all you've got is a simple redirect back to the page they were on, perhaps with a message explaining why they're back there.
TempData["EditErrorMessage"] = "Sorry another user is editing that right now.";
return RedirectToAction("Index");

ActionLink with HttpMethod=DELETE does not work

I try to implement DELETE method for my mvc-application.
There is my link placed in View:
#Ajax.ActionLink("Delete", "Delete", "Users", new { userId = user.Id }, new AjaxOptions { HttpMethod = "DELETE" })
There is my controller method:
[HttpDelete]
public ActionResult Delete(string userId)
{
//...
return View("Index");
}
When I click on Delete-link, I get a 404 error.
In Fiddler I see, that my request perfomed by GET method!
If I execute DELETE request with the same Headers from Fiddler, I get the expected result - my request is coming right into the Delete method.
How can I use #Ajax.ActionLink correctly?
P.S.: I want to use only built-in methods.
Are you sure all the Unobtrusive libraries are loaded? An #Ajax.ActionLink generates a standard anchor tag. If the JavaScript libraries aren't loaded to handle the click event, you'll get the GET request you see in Fiddler.
Check to see if the jquery.unobtrusive-ajax.js script is included in a bundle that is referenced from your layout page or that you're explicitly loading it on specific pages in a scripts region.
Try this:
#Ajax.ActionLink("Delete", "Delete", "Users",
new { userId = user.Id },
new AjaxOptions { HttpMethod = "POST" })
I am not sure why you used 'Delete' for the HTTPMethod. Post will send the Id for data you want removed to the server and call the 'Delete' ActionResult specified here #Ajax.ActionLink("Delete", "Delete", "Users",.

Categories

Resources