Is it possible to invoke signalr from ActionResut which returns a default view - asp.net-mvc

I have a POST method in mvc which saves a customer and it redirects to Index View by default.
Now I want to include a notification if the customer is created. I have tried the basic SignalR implementation but I am not sure how I can invoke SignalR from an ActionMethod so that it will be reflected in client.
My confusion is redirecting to Index and invoking the client using SignalR can be written in the same Acton method, or is it required to use other method from the client after the customer is saved.
Here is the code:
[HttpPost]
public ActionResult SaveCustomer(Customer customer)
{
//Save Customer
db.Customer.Add(customer);
db.SaveChanges();
// Notify Client something similar this
//$.connection.hub.start().done(function () {
//chat.server.send(customer.Name);
//});
ChatHub chat = new ChatHub();
chat.Clients.Send(customer.Name)
RedirectToAction("Index");
}
In my Layout page I have messagenotificationPartialView
$(function () {
// Reference the auto-generated proxy for the hub.
var chat = $.connection.chatHub;
// Create a function that the hub can call back to display messages.
chat.client.send = function (customerName) {
//add to a span
};
I am not sure if I am in the right direction. Is this the right way or are there any other options to implement this?

Related

EntityFramework SaveChanges() throws concurrent transaction error

In my MVC application, I have a page that loads a record from my POLICIES table and then uses it in my View. My View then has the data from this record displayed on the page, however in order to edit the record data the user needs to click the "Edit Policy" button, which launches a jQuery UI dialog with the same record in EDIT mode. I realize I could just allow them to edit it from the main View, however that is not what my client wants.
The trouble I'm having is, when I'm in my jQuery UI Dialog, I get the error below when I try to save the record.
FirebirdSql.Data.FirebirdClient.FbException: lock conflict on no wait
transaction
The Controller method for my dialog executes the following code. The PolicyModel is simply a class which serves as the ViewModel for the dialog, and the Policy property is an object representing the Policy table.
public ActionResult Policy(int policyNo) {
PolicyModel policyModel = new PolicyModel();
policyModel.Policy = dbContext.POLICIES.FirstOrDefault(db => db.POLICY_NO == policyNo);
return View(policyModel);
}
In the "Policy" View, I do a standard form using:
#using (Html.BeingForm("SavePolicy", "MyController", FormMethod.Post)) {
//hidden element for policyNo created with #Html.HiddenFor
//form elements here created using the #Html.TextBoxFor..etc.
}
The dialog button to save simply creates new FormData with var formData = new FormData($('#myformid').get(0)); I then pass that to my save controller method.
The Save method is set up like the following
public ActionResult SavePolicy(PolicyModel policyModel) {
var policy = dbContext.POLICIES.FirstOrDefault(db => db.POLICY_NO == policyModel.POLICY_NO);
if (TryUpdateModel(policy,"Policy", updateFields.ToArray())) {
dbContext.Entry(policy).State = EntityState.Modified;
dbContext.SaveChanges();
}
return Json( new { result = 1, JsonRequestBehavior.AllowGet } );
}
If I change the POLICY_NO manually though to ANY other policy number than the one currently active in the dialog like this...
var policy = dbContext.POLICIES.FirstOrDefault(db => db.POLICY_NO == 12345);
The save will update correctly.
It's like the dialog View is holding onto the resource or something. any ideas?
UPDATE
For reference, the dbContext is scoped to the Controller class in which my SavePolicy method lives as seen below...
public class MainController : Controller {
private DBModel dbContext = new DBModel();
// other methods
public ActionResult SavePolicy(PolicyModel policyModel) {
// method code as see above
}
}
ASP.NET MVC Controllers usually have this:
protected override void Dispose(bool disposing)
{
if (disposing)
{
db.Dispose();
}
base.Dispose(disposing);
}
So, if you are declaring your context outside of your action, you should verify if this method is implemented.
Turns out that, at the first execution (a select), your context keeps track of the record at Firebird and it is never disposed. The second execution will try to select the same entry again, which is still tracked by another context that was not disposed properly.
Using the scoped context inside each action is another way to solve, but it is a bit more cumbersome in my standpoint.

Receiving a real incoming call in Twilio's ClientQuickstart

For the time being I use a test account in Twilio, but I hope that this has no relevance regarding my question.
As my firs experimental step towards Twilio, I'm testing the client-quickstart-csharp-1.4 package on Visual Studio 2017 on Windows.
Outgoing calls work fine to my verified phone, but I have problems with incoming calls. When I make a call from a real phone to my Twilio phonenumber, then my code in VoiceController.cs doesn't run (doesn't hit any breakpoint) and I hear a voice message about that I should reconfigure something my application (but I don't understand, what). In contrast of this, when I make a call from my TwiMLApp config page, pressing the red Call button (see picture),
then my code stops at the breakpoints, and says the text I wrote in the argumet of response.Say().
My questions:
Why does the call work differently from a real phone then from my
TwiMLApp config page?
How can I achieve my code run (i.e. say the text I wrote in the code) also when I make a call from a real phone?
How Can I achieve a real, live voice dialogue between the caller phone and my computer's speaker and microphone at incoming calls (similarly to
the outgoing calls)?
Remark 1.
Both outgoing an incoming calls work fine in Agile CRM using the Twilio widget for voice calls. But for the time of my experiments I've removed this widget (and also the "Agile CRM Twilio Saga" TwiML App from Twilio), to avoid the interferences between the different applications.
Remark 2.
Perhaps I should configure something with this screen (the screenshot found here), but I don't find this page on my twilio portal.
Instead of this, I have a page like this:
But I don't know what to change here to make my program work.
It seems that this application is designed
to manage outgoing calls (to a real phone, or to an other client of this
application) and
accept calls from the web (from an another client,
or from the TwiML App setting page, seen on the first screenshot on the o.p.), but not from a real phone.
Every (outgoing or incoming) call falls into the Index() method of the VoiceController class. This method tries to find out whether a call is incoming or outgoing.
In the case of on outgoing call, the To property of the request parameter of this method is a phonenumber, while at an incoming call from the web is a string (a username), or null (when the call comes from the TwiML App setting page). This justifies the if-else structure in the original code (extended just my remarks starting with (mma))
public ActionResult Index(VoiceRequest request)
{
var callerId = ConfigurationManager.AppSettings["TwilioCallerId"];
var response = new TwilioResponse();
if (!string.IsNullOrEmpty(request.To))
{
// wrap the phone number or client name in the appropriate TwiML verb
// by checking if the number given has only digits and format symbols
if (Regex.IsMatch(request.To, "^[\\d\\+\\-\\(\\) ]+$")) //(mma) supposed to be an outgoing call
{
response.Dial(new Number(request.To), new { callerId });
}
else //(mma) a call from one client to antorher
{
response.Dial(new Client(request.To), new { callerId });
}
}
else //(mma) incoming call from the TwiML App setting page
{
response.Say("Thanks for calling!");
}
return TwiML(response);
Question 3. can be separated into the following two parts:
If at an incoming call we want to establish a real connection with a pre-specified client (say calledUser) instead of reading out the "Thanks for calling!" message, we should replace response.Say("Thanks for calling!"); by response.Dial(cl, new { request.From }); where cl = new Client(calledUser); We can put the value of calledUser into our Local.config, so we can read it from there: var calledUser = ConfigurationManager.AppSettings["calledUser"];
If we want to accept a call from a real phone, then we should recognize this situation. This is exactly when request.To == callerId( = our Twilio phononumber) , so we must split the first condition according this. The new branch will call the pre-specified user.
Putting these together, our new code in VoiceController.cs will look like this:
public ActionResult Index(VoiceRequest request)
{
var callerId = ConfigurationManager.AppSettings["TwilioCallerId"];
var calledUser = ConfigurationManager.AppSettings["calledUser"];
var response = new TwilioResponse();
if (!string.IsNullOrEmpty(request.To))
{
// wrap the phone number or client name in the appropriate TwiML verb
// by checking if the number given has only digits and format symbols
if (Regex.IsMatch(request.To, "^[\\d\\+\\-\\(\\) ]+$"))
{
if (request.To != callerId) //(mma) supposed to be an outgoing call
{
response.Dial(new Number(request.To), new { callerId });
}
else //(mma) supposed to be an incoming call from a real phone
{
var cl = new Client(calledUser);
response.Dial(cl, new { request.From });
}
}
else //(mma) a call from one client to antorher
{
response.Dial(new Client(request.To), new { request.From });
}
}
else //(mma) incoming call from the TwiML App setting page
{
var cl = new Client(calledUser);
response.Dial(cl, new { request.From });
}
return TwiML(response);
}
Of course, if we want to accept a call, then we should start a client with the pre-defined username (calledUser). In order to do this, we can introduce a new Url parameter User, put its value into TempData["User"] by the HomeController and change the var identity = Internet.UserName().AlphanumericOnly(); line in the TokenController.cs to var identity = TempData["User"] == null ? Internet.UserName().AlphanumericOnly() : TempData["User"].ToString();
So, our new HomeController and TokenController look like this:
public class HomeController : Controller
{
public ActionResult Index(string user)
{
TempData["User"] = user;
return View();
}
}
and this:
public class TokenController : Controller
{
// GET: /token
public ActionResult Index()
{
// Load Twilio configuration from Web.config
var accountSid = ConfigurationManager.AppSettings["TwilioAccountSid"];
var authToken = ConfigurationManager.AppSettings["TwilioAuthToken"];
var appSid = ConfigurationManager.AppSettings["TwilioTwimlAppSid"];
// Create a random identity for the client
var identity = TempData["User"] == null ? Internet.UserName().AlphanumericOnly() : TempData["User"].ToString();
// Create an Access Token generator
var capability = new TwilioCapability(accountSid, authToken);
capability.AllowClientOutgoing(appSid);
capability.AllowClientIncoming(identity);
var token = capability.GenerateToken();
return Json(new
{
identity,
token
}, JsonRequestBehavior.AllowGet);
}
}
And, of course, our Local.config file should contain such a line:
<add key="calledUser" value="TheNameOfThePreDefinedUser" />

Updating Twitter status using LinqToTwitter

here's what set out to do:
Make an MVC app on which the user clicks a button and is taken to the Twitter login page
After giving the credentials the user is redirected to a second page
On the secong page there is a text box and a 'Tweet' button
Entering a message and clicking on 'Tweet' will update the status
I got till the 2nd point by following the samples from LinqToTwitter codeplex page.
The code from OAuth controller works fine and it does redirect back to MVC app's second page.
But I am missing something which is not posting the status.
This is the code in the button click from which I pass the user entered status:
public ActionResult Status(string status)
{
var auth = new MvcAuthorizer
{
CredentialStore = new SessionStateCredentialStore()
};
auth.CompleteAuthorizeAsync(Request.Url);
var twitterContext = new TwitterContext(auth);
TweetAsync(twitterContext, status);
return View(); //return some view to the user
}
void TweetAsync(TwitterContext twitterCtx, string statusToUpdate)
{
var tweet = twitterCtx.TweetAsync(statusToUpdate);
if (tweet != null)
{
// Inform the user about success
}
}
Both the above methods are also in OAuth controller.
Can someone please help me with this?
Thanks in advance.
Change your method to use async and return a Task:
public async Task Status(string status)
{
//...
var tweet = await twitterContext.TweetAsync(twitterContext, status);
// ...
}
and then await TweetAsync, assigning the response to a Status entity named tweet. If you want a separate method for calling TweetAsync, make that async also. With async, you must make every method in the call chain async.

Back button doesn't cause postback to a controller action in MVC

When I click the back button in IE10 or Chrome on Win7, it does not hit my break point in my MVC controller. The Network tab in IE developer's tools shows it had a 304 not modified and Fiddler doesn't capture the request.
I was expecting the post back, so I could do work in my controller.
In my case, the bug is:
Sign in
make sure you are on the default page
click the browser back button on the top left you'll now be back to
the login screen
sign in with your same credentials again when you
do that - I get "The provided anti-forgery token was meant for user "", but the current user is "username".
I've tried putting this in my controller, without success:
this.HttpContext.Response.CacheControl = "private";
this.HttpContext.Response.Cache.SetMaxAge(TimeSpan.FromSeconds(0));
public ActionResult Index()
{
// Get: /Home/Index
if (this.User.Identity.IsAuthenticated)
{
// send the user to the GlobalAssetDashboard
return this.RedirectToAction(
"GlobalAssetDashboard",
"Dashboard",
new
{
area = "DashboardArea"
});
}
return this.View("Login");
}
public ActionResult Login()
{
// GET: /Home/Login
if (this.User.Identity.IsAuthenticated)
{
// send the user to the GlobalAssetList
return this.RedirectToAction(
"GlobalAssetDashboard",
"Dashboard",
new
{
area = "DashboardArea"
});
}
return this.View("Login", new LoginModel());
}
Is there a way to force the postback or detect this and cause a refresh in JavaScript? Or maybe I have my controller methods implemented incorrectly?
Typically caching rules like this aren't conditional upon the logic they perform, the URL as a whole is either cached or it isn't. In which case something as simple as this should suffice.
[OutputCache(NoStore=true, Duration=0)]
public ActionResult Login()
{
}
http://msdn.microsoft.com/en-us/library/dd492556(v=vs.108).aspx

How do I prevent multiple form submission in .NET MVC without using Javascript?

I want to prevent users submitting forms multiple times in .NET MVC. I've tried several methods using Javascript but have had difficulties getting it to work in all browsers. So, how can I prevent this in my controller? It there some way that multiple submissions can be detected?
Updated answer for ASP.NET Core MVC (.NET Core & .NET 5.0)
Update note: Remember ASP.NET Core is still called "Core" in .NET 5.0.
I'm going to stick to the least-impact use case like before, where you're only adorning those controller actions that you specifically want to prevent duplicate requests on. If you want to have this filter run on every request, or want to use async, there are other options. See this article for more details.
The new form tag helper now automatically includes the AntiForgeryToken so you no longer need to manually add that to your view.
Create a new ActionFilterAttribute like this example. You can do many additional things with this, for example including a time delay check to make sure that even if the user presents two different tokens, they aren't submitting multiple times per minute.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class PreventDuplicateRequestAttribute : ActionFilterAttribute {
public override void OnActionExecuting(ActionExecutingContext context) {
if (context.HttpContext.Request.HasFormContentType && context.HttpContext.Request.Form.ContainsKey("__RequestVerificationToken")) {
var currentToken = context.HttpContext.Request.Form["__RequestVerificationToken"].ToString();
var lastToken = context.HttpContext.Session.GetString("LastProcessedToken");
if (lastToken == currentToken) {
context.ModelState.AddModelError(string.Empty, "Looks like you accidentally submitted the same form twice.");
}
else {
context.HttpContext.Session.SetString("LastProcessedToken", currentToken);
}
}
}
}
By request, I also wrote an asynchronous version which can be found here.
Here's a contrived usage example of the custom PreventDuplicateRequest attribute.
[HttpPost]
[ValidateAntiForgeryToken]
[PreventDuplicateRequest]
public IActionResult Create(InputModel input) {
if (ModelState.IsValid) {
// ... do something with input
return RedirectToAction(nameof(SomeAction));
}
// ... repopulate bad input model data into a fresh viewmodel
return View(viewModel);
}
A note on testing: simply hitting back in a browser does not use the same AntiForgeryToken. On faster computers where you can't physically double click the button twice, you'll need to use a tool like Fiddler to replay your request with the same token multiple times.
A note on setup: Core MVC does not have sessions enabled by default. You'll need to add the Microsoft.AspNet.Session package to your project, and configure your Startup.cs properly. Please read this article for more details.
Short version of Session setup is:
In Startup.ConfigureServices() you need to add:
services.AddDistributedMemoryCache();
services.AddSession();
In Startup.Configure() you need to add (before app.UseMvc() !!):
app.UseSession();
Original answer for ASP.NET MVC (.NET Framework 4.x)
First, make sure you're using the AntiForgeryToken on your form.
Then you can make a custom ActionFilter:
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class PreventDuplicateRequestAttribute : ActionFilterAttribute {
public override void OnActionExecuting(ActionExecutingContext filterContext) {
if (HttpContext.Current.Request["__RequestVerificationToken"] == null)
return;
var currentToken = HttpContext.Current.Request["__RequestVerificationToken"].ToString();
if (HttpContext.Current.Session["LastProcessedToken"] == null) {
HttpContext.Current.Session["LastProcessedToken"] = currentToken;
return;
}
lock (HttpContext.Current.Session["LastProcessedToken"]) {
var lastToken = HttpContext.Current.Session["LastProcessedToken"].ToString();
if (lastToken == currentToken) {
filterContext.Controller.ViewData.ModelState.AddModelError("", "Looks like you accidentally tried to double post.");
return;
}
HttpContext.Current.Session["LastProcessedToken"] = currentToken;
}
}
}
And on your controller action you just...
[HttpPost]
[ValidateAntiForgeryToken]
[PreventDuplicateRequest]
public ActionResult CreatePost(InputModel input) {
...
}
You'll notice this doesn't prevent the request altogether. Instead it returns an error in the modelstate, so when your action checks if ModelState.IsValid then it will see that it is not, and will return with your normal error handling.
I've tried several methods using Javascript but have had difficulties getting it to work in all browsers
Have you tried using jquery?
$('#myform').submit(function() {
$(this).find(':submit').attr('disabled', 'disabled');
});
This should take care of the browser differences.
Just to complete the answer of #Darin, if you want to handle the client validation (if the form has required fields), you can check if there's input validation error before disabling the submit button :
$('#myform').submit(function () {
if ($(this).find('.input-validation-error').length == 0) {
$(this).find(':submit').attr('disabled', 'disabled');
}
});
What if we use $(this).valid()?
$('form').submit(function () {
if ($(this).valid()) {
$(this).find(':submit').attr('disabled', 'disabled');
}
});
Strategy
The truth is that you need several lines of attack for this problem:
The Post/Redirect/Get (PRG) pattern is not enough by itself. Still, it should always be used to provide the user with good experiences when using back, refresh, etc.
Using JavaScript to prevent the user from clicking the submit button multiple times is a must because it provides a much less jarring user experience compared to server-side solutions.
Blocking duplicate posts solely on the client side doesn't protect against bad actors and does not help with transient connection problems. (What if your first request made it to the server but the response did not make it back to the client, causing your browser to automatically resend the request?)
I'm not going to cover PRG, but here are my answers for the other two topics. They build upon the other answers here. FYI I'm using .NET Core 3.1.
Client-Side
Assuming you are using jQuery validation, I believe this is the cleanest/most efficient way to prevent your form submit button from being double-clicked. Note that submitHandler is only called after validation has passed, so there is no need to re-validate.
$submitButton = $('#submitButton');
$('#mainForm').data('validator').settings.submitHandler = function (form) {
form.submit();
$submitButton.prop('disabled', true);
};
An alternative to disabling the submit button is to show an overlay in front of the form during submission to 1) block any further interaction with the form and 2) communicate that the page is "doing something." See this article for more detail.
Server-Side
I started off with Jim Yarbro's great answer above, but then I noticed Mark Butler's answer pointing out how Jim's method fails if someone submits forms via multiple browser tabs (because each tab has a different token and posts from different tabs can be interlaced). I confirmed that such a problem really does exist and then decided to upgrade from tracking just the last token to tracking the last x tokens.
To facilitate that, I made a couple of helper classes: one for storing the last x tokens and one for making it easy to store/retrieve objects to/from session storage. The main code now checks that the current token is not found in the token history. Other than that, the code is pretty much the same. I just made some little tweaks to suit my tastes. I included both the regular and asynchronous versions. The full code is below, but these are the critical lines:
var history = session.Get<RotatingHistory<string>>(HistoryKey) ?? new RotatingHistory<string>(HistoryCapacity);
if (history.Contains(token))
{
context.ModelState.AddModelError("", DuplicateSubmissionErrorMessage);
}
else
{
history.Add(token);
}
Sadly, the fatal flaw of this approach is that the feedback from the first post (before any duplicates) gets lost. A better (but much more complex) solution would be to store the result of each unique request by GUID, and then handle duplicate requests by not only skipping doing the work again but also returning the same result from the first request, giving the user a seamless experience. This thorough article detailing Air BnB's methods of avoiding duplicate payments will give you an idea of the concepts.
PreventDuplicateFormSubmissionAttribute.cs
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
// This class provides an attribute for controller actions that flags duplicate form submissions
// by adding a model error if the request's verification token has already been seen on a prior
// form submission.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
public class PreventDuplicateFormSubmissionAttribute: ActionFilterAttribute
{
const string TokenKey = "__RequestVerificationToken";
const string HistoryKey = "RequestVerificationTokenHistory";
const int HistoryCapacity = 5;
const string DuplicateSubmissionErrorMessage =
"Your request was received more than once (either due to a temporary problem with the network or a " +
"double button press). Any submissions after the first one have been rejected, but the status of the " +
"first one is unclear. It may or may not have succeeded. Please check elsewhere to verify that your " +
"request had the intended effect. You may need to resubmit it.";
public override void OnActionExecuting(ActionExecutingContext context)
{
HttpRequest request = context.HttpContext.Request;
if (request.HasFormContentType && request.Form.ContainsKey(TokenKey))
{
string token = request.Form[TokenKey].ToString();
ISession session = context.HttpContext.Session;
var history = session.Get<RotatingHistory<string>>(HistoryKey) ?? new RotatingHistory<string>(HistoryCapacity);
if (history.Contains(token))
{
context.ModelState.AddModelError("", DuplicateSubmissionErrorMessage);
}
else
{
history.Add(token);
session.Put(HistoryKey, history);
}
}
}
public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
HttpRequest request = context.HttpContext.Request;
if (request.HasFormContentType && request.Form.ContainsKey(TokenKey))
{
string token = request.Form[TokenKey].ToString();
ISession session = context.HttpContext.Session;
await session.LoadAsync();
var history = session.Get<RotatingHistory<string>>(HistoryKey) ?? new RotatingHistory<string>(HistoryCapacity);
if (history.Contains(token))
{
context.ModelState.AddModelError("", DuplicateSubmissionErrorMessage);
}
else
{
history.Add(token);
session.Put(HistoryKey, history);
await session.CommitAsync();
}
await next();
}
}
}
RotatingHistory.cs
using System.Linq;
// This class stores the last x items in an array. Adding a new item overwrites the oldest item
// if there is no more empty space. For the purpose of being JSON-serializable, its data is
// stored via public properties and it has a parameterless constructor.
public class RotatingHistory<T>
{
public T[] Items { get; set; }
public int Index { get; set; }
public RotatingHistory() {}
public RotatingHistory(int capacity)
{
Items = new T[capacity];
}
public void Add(T item)
{
Items[Index] = item;
Index = ++Index % Items.Length;
}
public bool Contains(T item)
{
return Items.Contains(item);
}
}
SessonExtensions.cs
using System.Text.Json;
using Microsoft.AspNetCore.Http;
// This class is for storing (serializable) objects in session storage and retrieving them from it.
public static class SessonExtensions
{
public static void Put<T>(this ISession session, string key, T value) where T : class
{
session.SetString(key, JsonSerializer.Serialize(value));
}
public static T Get<T>(this ISession session, string key) where T : class
{
string s = session.GetString(key);
return s == null ? null : JsonSerializer.Deserialize<T>(s);
}
}
You could include a hidden (random or counter) value in the form post, a controller could track these values in an 'open' list or something similar; every time your controller hands out a form it embeds a value, which it tracks allowing one post use of it.
In its self, no, however depending on what the controller is actually doing, you should be able to work out a way.
Is a record being created in the database that you can check for to see if they've already submitted the form?
Just add this code at the end of your page. I am using "jquery-3.3.1.min.js" and "bootstrap 4.3.1"
<script type="text/javascript">
$('form').submit(function () {
if ($(this).valid()) {
$(this).find(':submit').attr('disabled', 'disabled');
}
});
</script>
Use the Post/Redirect/Get design pattern.
PS:
It looks to me that the answer by Jim Yarbro could have a fundamental flaw in that the __RequestVerificationToken stored in the HttpContext.Current.Session["LastProcessedToken"] will be replaced when a second form is submitted (from say another browser window). At this point, it is possible to re-submit the first form without it being recognized as a duplicate submission. For the proposed model to work, wouldn’t a history of __RequestVerificationToken be required? This doesn't seem feasible.
Dont reinvent the wheel :)
Use the Post/Redirect/Get design pattern.
Here you can find a question and an answer giving some suggestions on how to implement it in ASP.NET MVC.
You can also pass some sort of token in a hidden field and validate this in the controller.
Or you work with redirects after submitting values. But this get's difficult if you take heavily advantage of ajax.
This works on every browser
document.onkeydown = function () {
switch (event.keyCode) {
case 116: //F5 button
event.returnValue = false;
event.keyCode = 0;
return false;
case 82: //R button
if (event.ctrlKey) {
event.returnValue = false;
event.keyCode = 0;
return false;
}
}
}
You can do this by creating some sort of static entry flag that is user specific, or specific to whatever way you want to protect the resource. I use a ConcurrentDictionary to track entrance. The key is basically the name of the resource I'm protecting combined with the User ID. The trick is figuring out how to block the request when you know it's currently processing.
public async Task<ActionResult> SlowAction()
{
if(!CanEnterResource(nameof(SlowAction)) return new HttpStatusCodeResult(204);
try
{
// Do slow process
return new SlowProcessActionResult();
}
finally
{
ExitedResource(nameof(SlowAction));
}
}
Returning a 204 is a response to the double-click request that will do nothing on the browser side. When the slow process is done, the browser will receive the correct response for the original request and act accordingly.
Use this simple jquery input field and will work awesomely even if you have multiple submit buttons in a single form.
$('input[type=submit]').click(function () {
var clickedBtn = $(this)
setTimeout(function () {
clickedBtn.attr('disabled', 'disabled');
}, 1);
});

Resources