I have a very simple 1 page form using ASP.NET MVC 3.
It has 3 inputs. If the user enters the correct inputs and the database retrieves a pdf, that pdf is displayed in the next page. I have used the PRG pattern, in so far as it is possible in this scenario:
public ActionResult Index()
{
SomeModel model = new SomeModel
{
... properties
};
return View(model);
}
[HttpPost]
public ActionResult Index(SomeModel model)
{
byte[] blob = someService.GoGetDetails(model);
TempData.Add("blobkey", blob);
return RedirectToAction("DisplayPdf");
}
public ActionResult DisplayPdf()
{
return File(TempData["blobkey"] as byte[], "application/pdf");
}
The problem use case is as follows.
User enters incorrect details and clicks Submit button. Red error text displays.
User then enters correct details and clicks Submit button. Pdf displays.
User THEN hits back button. Form displays with previous red error text visible.
A tester has made up a requirement that the red text should not be there when the back button is clicked.
Seems easy. Until you try. Does anyone have the magic bullet for this.
One thing I tried as a compromise did not work. That is, I tried to kill caching on the browser with this code inside the DisplayPdf acttion method:
this.ControllerContext.HttpContext.Response.Expires = 60;
this.ControllerContext.HttpContext.Response.ExpiresAbsolute = DateTime.Now.AddSeconds(-1);
this.ControllerContext.HttpContext.Response.AddHeader("pragma", "no-cache");
this.ControllerContext.HttpContext.Response.CacheControl = "no-cache";
It has no effect. Perhaps that works in the webforms world. Not here with the FileResult.
When back button is hit, there is no request to the server. It is all client-side. So, I do have access to javascript. But javascript has no access to the http headers, so there's no way of differentiating from where the page was loaded.
I'm keen to see if this has been solved before. My Googling came up empty.
Thanks
OK. I'm now satisfied. If :
Amazon.com
JsFiddle.net
Outlook.com
Dropbox.com
all exhibit the same behavour, then my work is not falling short of best practice.
Intercepting the navigation buttons of browsers seems very hacky to me. This is one use case where we have to make sure our code does no damage when users do dumb stuff. But resolve elegantly? Probably not a big driver when control is taken out of your hands by the browser.
Related
I have a form that is at the bottom of the page.
When model validation failes on server side return Page() gets me back to the top of the page and that is very inconvenient because the user has to scroll down to the form to see the validation errors.
My questions are:
1. Can I redirect to contact form div ID?
2. The best scenario would be if model validation can be done asynchronously so there would be only partial page refresh. Is that possible? (that was done easily in Web Forms with UpdatePanel).
I appreciate any information you might share.
Jacob
First of all try to validate on the client first to prevent unnecessary postbacks. You can use attributes to set validation:
using System.ComponentModel.DataAnnotations;
public class ForgotPasswordViewModel()
{
[Required]
[EmailAddress]
public string Email { get; set; }
}
In the view check the Modelstate:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if (ModelState.IsValid)
{
Of course client side isn't enough, so in case the client side validation is skipped the server makes sure that the model validates.
One way to scroll to a position is to use an anchor. The anchor is a client side way to bookmark parts in the document. Normally you would see something like this:
<a href"home#contact">Contact</a>
By clicking the link you would scroll to the contact bookmark in the home document. You can do the same for the post:
<div id="contact">
<form method="post" action="home#contact">
#Html.AntiForgeryToken()
</form>
</div>
In case of an error the page will automatically scroll to the form, since on the client the #contact hash was set. The assumption is that in case of success you redirect to another view. This is afaik the only way to scroll without the use of javascript. And since javascript validation didn't work...
Please note that with .Net Core AntiForgery is added automatically if you use the tag helper. But since I didn't use it I have to add this myself.
There are other options that involve javascript or some server side redirection, but I think this is an easy solution.
I do not know if partial page refresh is the best solution. I think it depends. There are plenty of examples available on how you can post asynchronous. Check this link: https://www.w3schools.com/jquery/ajax_ajax.asp
Maybe remote validation?
"Remote validation is a great feature to use when you need to validate data on the client against data on the server. For example, your app may need to verify whether an email or user name is already in use..."
https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation
I have found solution that works fine for me:
#if (Model.validationError)
{
<text>
document.getElementById('contact').scrollIntoView();
</text>
}
I have two forms in one View. One of them does the job of uploading pictures to the database.
I need the whole page -and the data the user has typed into the "other form"- to remain untouched, meanwhile the user could also upload pictures. But it doesn't work.
The problem is that when the user submits the photos, the whole page changes. And worse than that, the new page only contains the "photo uploading" form !! Because upon Return View(); of the "upload pictures" Action method, it's View is rendered. :/
Look at the action method:
public ActionResult Upload(IEnumerable<HttpPostedFileBase> files)
{
//Upload the pictures .
return View();
}
Is it possible to achieve the result I need only by changing the return type of the Action method, (maybe ?) in order not to render anything and say just show a message to the user like "Files have been uploaded!" ??
Or should I use Ajax instead ?
Thanks for the advice, in advance ;)
You can't upload files using AJAX but you could use some third-party components that will upload files using hidden iframe which mimics like AJAX. You can google jquery ajax upload plugin.
For the other form you can use Ajax.BeginForm/PartialView to avoid complete refresh.
If the views are strongly typed against a model then you can always pass the model back to the user to "refill" the constant form (probably worst option). Another option (probably best) would be ajax of course.
I am in the process of porting a site I wrote from ASP.NET webforms to MVC3 and need some guidance as outlined below. I'm new to MVC3.
In my existing ASP.NET web forms project I have a simple page where the user enters a username, they then click a button which causes a postback, on postback there is some basic code that checks if the entered username exists in a user repository - if it does, a textbox containing the users e-mail is shown and the username textbox is made invisible. This happens with ajax and so when the username is entered, the textbox containing the e-mail along with an "Update" button is shown without a full page refresh.
I created a model such as:
public class ChangeEmailModel
{
[Required]
public string Username { get; set; }
[Required]
public string Email { get; set; }
}
Problem is that when the user first enters the page, they should only see a textbox prompting them to enter a username. Once the username is entered and an update button clicked, only then their e-mail is shown (retrieved from the database). Once the e-mail is shown, they can edit the e-mail and click update, which then will need to post to a controller action that saves the updated e-mail. I'm not yet fully used to thinking in the MVC way, so I'm not sure if I've started on the wrong foot with the model above...
Can someone give me some guidance on how this can be accomplished in MVC3 so I can give it a try?
I will start off by suggesting that you start using JQuery for your javascript/ajax functions. ASP.Net MVC3 supports JQuery nicely. I will ignore validation of the email for now as it will be much easier to get you started without it. A high level overview will be:
Add the JQuery script to your page
Add the JQuery vsdoc script to your page so you have some intellisense
Create a partial view to show the email and submit button
Create a controller action that performs the email lookup you mentioned
Create a div to accept the newly returned Email Update form
Use JQuery to override the submit on your username lookup to perform an ajax update instead (and populate the Email Update form div)
1. Add the JQuery script to your page
This should be pretty easy - just drag it from your scripts folder. I think mvc3 comes with jquery-1.5.1.js. Use the min (minified) version when you release to production.
2. Add the JQuery vsdoc script to your page so you have some intellisense
Not quite as easy here - you will want to use an if statement that always evaluates to false so the script is not actually included in your content. Having it on the page though, will cause VS to use it for intellisense. Put this near the top of your view:
#if (false) { <script src="../../Scripts/jquery-1.5.1-vsdoc.js" type="text/javascript"></script> }
Hopefully you are using Razor. If not, start using it. It seemed a little foreign to me at first, but it requires much less markup.
3. Create a partial view to show the email and submit button
You could use the ViewBag to pass the Email address and UserName (for now as we are ignoring validation), but go ahead and make it strongly typed to your Model from above. Your view may look something like this:
#model ChangeEmailModel
#{using (Html.BeginForm("UpdateEmail", "Home", FormMethod.Post, new { id = "UpdateEmailForm" }))
{
<input type="hidden" name="userName" value="#Model.UserName" />
#Html.EditorFor(m => m.Email)
<button id="submitEmailUpdate" type="submit">Submit</button>
}
}
Note that we have given Ids to the form and the submit button. JQuery will find the form and button based on these ids. (if we need to, which we will if we want to "ajaxify" the action of updating the email. I did not go into that detail here, but it will be the same process to get that working as it is for the original username lookup)
4. Create a controller action that performs the email lookup you mentioned
I won't go into controllers much here (as you are asking about ajax type updates) but it might look like:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult LookupEmail(string userName)
{
//connect to db and lookup email based on passed in username
//create a new instance of your model
var changeEmailModel = new ChangeEmailModel(.....)
//return a partial view
return PartialView("EmailUpdateForm", changeEmailModel);
}
Make sure to return a PartialView here rather than a View.
5. Create a div to accept the newly returned Email Update form
Make sure this div is not contained in your Username lookup form (as you want to hide it). We will be working with two separate forms. This div could be hidden if you prefer (but will start out empty anyway) I am calling it emailFormDiv
6. Use JQuery to override the submit on your username lookup to perform an ajax update instead
JQuery will allow you to attach functions to... well a lot of things, but we will be using it to override the submit button on your username lookup form. Assume that your original username lookup form with an id of "formUserNameLookup" that has a submit button with an id of "submitUserNameLookup". You would then create a script tag that looks something like this:
<script type="text/javascript" language="javascript">
$(document).ready(function () { //The document.ready function will fire when the html document is... ready
$('#submitUserNameLookup').click(function (ev) { //fires when the submit button is clicked
ev.preventDefault(); //prevent the normal action of the button click
$.post($('#formUserNameLookup').attr('action'), //get the url from the form's action attribute. Could be hard coded for simplicity
$('#formUserNameLookup').serialize(), //serialize the data in the form
function (response, status) {
$('#emailFormDiv').html(response); //replace the html of your div with the response
$('#formUserNameLookup').hide(); //hide the original form
}, 'html'); //states that we are expecting html back from the post
});
});
</script>
The code above is attaching a function to be run when the submit button is clicked. It won't run, of course, until the button is actually clicked. Using JQuery/Javascript to attach functions to html elements, rather than embedding them directly inside the element is definitely preferred, and is referred to as unobtrusive javascript. If you continue with ajaxifying more of your page, you will want to look into JQuery's live and/or delegate functions. Note that there are plenty of things that can be changed once you start looking toward performance and/or best practices. The above should get you going though. I hope I haven't made too many assumptions on your current level of familiarity with ASP.Net MVC (like controllers and posting to controllers) but by all means, ask if you need further help.
I have a page (Controller Action that renders a View) that a user could navigate to from 3 different pages. Basically, a user gets there, does a few selections and clicks on Save button. At this point, I need to redirect the user to where he came from.
I'm wondering, what's the best practice to do that?
I know, for example, I could look in Request, figure out where he came from, then redirect back to there... But that doesn't look like the ideal approach to me.
I'm wondering, what's the best practice to do that?
Pass a returnUrl parameter when invoking this action. Store it in a hidden field if necessary. Make sure that the controller action that performs the validation and needs to redirect gets this parameter as action argument somehow and when the time comes return Redirect(returnUrl);
(Posted on behalf of the question author).
This is what I ended up doing.
Controller:
public ActionResult Index()
{
ViewBag.Referrer = Request.UrlReferrer.LocalPath;
//.....
return View();
}
View (Razor syntax):
Back
I've read lots of different posts on this topic and I will continue to do so and try various things. There's so much variation in the information though that, with my relative inexperience with MVC, etc, I feel like I'm going round in circles a bit.
Here's the situation. I am displaying a (Razor) view in an MVC3 app that contains records in a jqGrid. Double-clicking a record opens that record in a jQuery UI dialog. The original view contains just the that represents the dialog. The invoked action returns a partial view that becomes the content for the dialogue div.
function editItem(gridID, url) {
var rowID = getSelectedRowID(gridID);
if (rowID == null) {
alert('No row selected.\r\nPlease select a row and try again.');
return;
}
else {
var grid = getGrid(gridID);
$("#editDialogue").load(url + "/" + grid.SelectedRowID, function (html) {
$("#editDialogue")[0].value = html;
$("#editTabs").tabs();
$("#editDialogue").dialog("open");
});
}
}
That part works, although I have a feeling it could stand a little bit of cleanup. My real issue is later submitting the form contained in that dialog. It's supposed to post back to an action with the same name, which it does. That action is supposed to validate and save or return the same partial view with validation errors. At the moment I'm trying to just return the view without doing anything else. The view is returned alright but the entire page is replaced, instead of just the contents of the dialog.
I've tried various things, including using Ajax.BeginForm and specifying the id of the dialog div as the UpdateTargetId of the AjaxOptions. Nothing has worked so far. Note that the form being submitted is inside the div so it will be replaced too. Not sure if this is part of the problem.
I'm packing up for the day now so I thought I'd post and see what happens overnight before attacking the problem again in the morning.
jmcilhinney,
You were right to set the AjaxOptions.UpdateTargetId, but you also need to set AjaxOptions.InsertionMode to InsertionMode.Replace.
I had the same issue occurring as well. The fix was to make sure that the jquery.unobtrusive-ajax.is file is on the page. That fixed my issue. Just double check to be sure. I was confident I had all the js files, but found out I was missing that one.