MVC3 RemoteAttribute and muliple submit buttons - asp.net-mvc-3

I have discovered what appears to be a bug using MVC 3 with the RemoteAttibute and the ActionNameSelectorAttribute.
I have implemented a solution to support multiple submit buttons on the same view similar to this post: http://blog.ashmind.com/2010/03/15/multiple-submit-buttons-with-asp-net-mvc-final-solution/
The solution works however, when I introduce the RemoteAttribute in my model, the controllerContext.RequestContext.HttpContext.Request no longer contains any of my submit buttons which causes the the "multi-submit-button" solution to fail.
Has anyone else experienced this scenario?

I know this is not a direct answer to your question, but I would propose an alternative solution to the multiple submit-buttons using clientside JQuery and markup instead:
Javascript
<script type="text/javascript">
$(document).ready(function () {
$("input[type=submit][data-action]").click(function (e) {
var $this = $(this);
var form = $this.parents("form");
var action = $this.attr('data-action');
var controller = $this.attr('data-controller');
form.attr('action', "/" + controller + "/" + action);
form.submit();
e.preventDefault();
});
});
</script>
Html
#using (Html.BeginForm())
{
<input type="text" name="name" id="name" />
<input type="submit" value="Save draft" data-action="SaveDraft" data-controller="Home" />
<input type="submit" value="Publish" data-action="Publish" data-controller="Home" />
}
It might not be as elegant as a code-solution, but it offers somewhat less hassle in that the only thing that actually changes is the action-attribute of the form when a submitbutton is clicked.
Basically what it does is that whenever a submit-button with the attribute data-action set is clicked, it replaces its parent forms action-attribute with a combination of the attributes data-controller and data-action on the clicked button, and then fires the submit-event of the form.
Of course, this particular example is poorly generic and it will always create /Controller/Action url, but this could easily be extended with some more logic in the click-action.
Just a tip :)

i'm not sure that its a bug in mvc 3 as it's not something that you were expecting. the RemoteAttribute causes javascript to intercept and validate the form with an ajax post. to do that, the form post is probably canceled, and when the validation is complete, the form's submit event is probably called directly, rather than using the actual button clicked. i can see where that would be problematic in your scenario, but it makes sense. my suggestion, either don't use the RemoteAttributeand validate things yourself, or don't have multiple form actions.

The problem manifests itself when the RemoteAttribute is used on a model in a view where mutliple submit buttons are used. Regardless of what "multi-button" solution you use, the POST no longer contains any submit inputs.
I managed to solve the problem with a few tweeks to the ActionMethodSelectorAttribute and the addition of a hidden view field and some javascript to help wire up the pieces.
ViewModel
public class NomineeViewModel
{
[Remote("UserAlreadyRegistered", "Nominee", AdditionalFields="Version", ErrorMessage="This Username is already registered with the agency.")]
public string UserName { get; set; }
public int Version {get; set;}
public string SubmitButtonName{ get; set; }
}
ActionMethodSelectorAttribute
public class OnlyIfPostedFromButtonAttribute : ActionMethodSelectorAttribute
{
public String SubmitButton { get; set; }
public String ViewModelSubmitButton { get; set; }
public override Boolean IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
var buttonName = controllerContext.HttpContext.Request[SubmitButton];
if (buttonName == null)
{
//This is neccessary to support the RemoteAttribute that appears to intercepted the form post
//and removes the submit button from the Request (normally detected in the code above)
var viewModelSubmitButton = controllerContext.HttpContext.Request[ViewModelSubmitButton];
if ((viewModelSubmitButton == null) || (viewModelSubmitButton != SubmitButton))
return false;
}
// Modify the requested action to the name of the method the attribute is attached to
controllerContext.RouteData.Values["action"] = methodInfo.Name;
return true;
}
}
View
<script type="text/javascript" language="javascript">
$(function () {
$("input[type=submit][data-action]").click(function (e) {
var action = $(this).attr('data-action');
$("#SubmitButtonName").val(action);
});
});
</script>
<% using (Html.BeginForm())
{%>
<p>
<%= Html.LabelFor(m => m.UserName)%>
<%= Html.DisplayFor(m => m.UserName)%>
</p>
<input type="submit" name="editNominee" value="Edit" data-action="editNominee" />
<input type="submit" name="sendActivationEmail" value="SendActivationEmail" data-action="sendActivationEmail" />
<%=Html.HiddenFor(m=>m.SubmitButtonName) %>
<% } %>
Controller
[AcceptVerbs(HttpVerbs.Post)]
[ActionName("Details")]
[OnlyIfPostedFromButton(SubmitButton = "editNominee", ViewModelSubmitButton = "SubmitButtonName")]
public ActionResult DetailsEditNominee(NomineeViewModel nom)
{
return RedirectToAction("Edit", "Nominee", new { id = nom.UserName });
}
[AcceptVerbs(HttpVerbs.Post)]
[ActionName("Details")]
[OnlyIfPostedFromButton(SubmitButton = "sendActivationEmail", ViewModelSubmitButton = "SubmitButtonName")]
public ActionResult DetailsSendActivationEmail(NomineeViewModel nom)
{
return RedirectToAction("SendActivationEmail", "Nominee", new { id = nom.UserName });
}
[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
public ActionResult UserAlreadyRegistered(string UserName, int Version)
{
//Only validate this property for new records (i.e. Version != zero)
return Version != 0 ? Json(true, JsonRequestBehavior.AllowGet)
: Json(! nomineeService.UserNameAlreadyRegistered(CurrentLogonDetails.TaxAgentId, UserName), JsonRequestBehavior.AllowGet);
}

I encountered the same issue.
I also attached an on submit event to prepare the form before submit. Interestingly, when I insert a break point in the on submit function, and then continue, the problem has disappeared.
I ended up with an Ajax form by removing the Remote attribute and validate the field using the ModelState.

Related

How do you deal with Razor Pages PageRemote validation on 'loaded' data (e.g edit ViewModel Page)?

I am probably missing the obvious, but nevertheless a little stuck with PageRemote validation. Like a lot of us, I am following Mike’s helpful tutorial on the subject: https://www.mikesdotnetting.com/article/343/improved-remote-validation-in-razor-pages
I won’t copy his code here, as it is easy to follow, and works exactly as demonstrated. Great for a ‘Create User’ page!
My problem is though, when applying it to an ‘Edit User’ page, then I have hit a couple snags. In the OnGet() I load the ‘User’ from a QueryString, and populate the form, including the remote validated field. If I touch no fields, and straight away hit the submit button, it doesn’t trigger the submit’s OnPostSubmit() handler, but the PageRemote’s validation OnPost() instead (as presumably the field is dirty, even if the user didn’t do it).
So how do I make sure the submit button fires as expected, in this scenario? According to my break point, it never fires the OnPostSubmit() handler, in this scenario.
Following this scenario, that PageRemote’s OnPost returns ‘true’ (as nothing changed, and everything is still valid), but something else seems to be going on, as a SelectList that is normally loaded OnGet() is now empty, and means the form is now not complete. If before I click the submit button, I enter any of the form’s fields, and force the PageRemote to normally fire, my SelectList is fine. The loss of loaded SelectList values being lost, is only when the PageRemote fires when immediately clicking submit without touching any fields. Why does it behave differently? Surely I am not suppose to be reloading data in this PageRemote validation OnPost() handler, especially in the normal scenario’s, I don’t have to…
I hope this makes sense, and I hope I have not upset anyone by not putting any code up. I am happy to edit my questions with some code, but it is 99% as in Mike’s article. The only difference I have, is populating the ViewModel and SelectList OnGet().
EDIT for code:
#page
#model Redbook.Pages.Test.EditAccountModel
#{
ViewData["Title"] = "EditAccount";
}
<h1>EditAccount</h1>
<form method="post" id="frmUserDetails">
<div class="form-group">
<label class="pt-1">Email</label>
<input id="txtEmail" type="email" inputmode="email" class="form-control" asp-for="Email">
<span class="text-danger" asp-validation-for="Email"></span>
</div>
<div class="form-group">
<label class="pt-1">User Select Option</label>
<select class="form-control" asp-for="UserSelectListOption" asp-items="Model.UserSelectListOptions"></select>
<span class="text-danger" asp-validation-for="UserSelectListOption"></span>
</div>
<button id="btnContinue" type="submit" asp-page-handler="Continue" class="btn btn-outline-info">
Save
</button>
</form>
#section Scripts
{
<script src="~/lib/jquery/dist/jquery.min.js"></script>
#await Html.PartialAsync("_ValidationScriptsPartial")
<script src="~/lib/jquery-ajax-unobtrusive/dist/jquery.unobtrusive-ajax.min.js"></script>
}
CodeBehind
public class EditAccountModel : PageModel
{
[Required(ErrorMessage = "Email Address Required")]
[EmailAddress(ErrorMessage = "Invalid Email Address")]
[PageRemote(
ErrorMessage = "Email/User already in use.",
AdditionalFields = "__RequestVerificationToken",
HttpMethod = "post",
PageHandler = "CheckEmail"
)]
[BindProperty]
public string Email { get; set; }
[Required(ErrorMessage = "User Select List Option Required")]
[BindProperty]
public int UserSelectListOption { get; set; }
public SelectList UserSelectListOptions { get; set; }
public async Task<IActionResult> OnGet()
{
//Normally would pass param (querystring) to load 'user' to edit, but this is just a test!
await Task.CompletedTask;
Email = "joe.bloggs#test.com";
UserSelectListOption = 2;
//Our user form needs a drop down option.
LoadSelectList();
return Page();
}
public async Task<IActionResult> OnPostContinueAsync()
{
if (!ModelState.IsValid)
{
LoadSelectList();
return Page();
}
//Normally we would do something here (e.g get UserID), but again, this is just a test!
await Task.CompletedTask;
//We dont hit this when we first hit submit, unless we pass focus to the 'Email' control first.
//Instead 'OnPostCheckEmail' is triggered only
//Not only that, but when that happens, 'UserSelectListOptions' is empty, so we fail the required validation for that control.
//If we do touch the 'Email' control before submission, validation triggers normally,
//It doesn't affect 'UserSelectListOptions'
//Then we do hit this point successfully.
return Page();
}
public JsonResult OnPostCheckEmail()
{
var existingEmails = new[] { "jane#test.com", "claire#test.com", "dave#test.com" };
var valid = !existingEmails.Contains(Email);
return new JsonResult(valid);
}
public void LoadSelectList()
{
List<SelectListOption> selectListOptions = new List<SelectListOption>();
selectListOptions.Add(new SelectListOption(1, "Option1"));
selectListOptions.Add(new SelectListOption(2, "Option2"));
selectListOptions.Add(new SelectListOption(3, "Option3"));
UserSelectListOptions = new SelectList(selectListOptions, "OptionID", "OptionName");
}
public class SelectListOption
{
public SelectListOption(int optionID, string optionName)
{
this.OptionID = optionID;
this.OptionName = optionName;
}
public int OptionID { get; set; }
public string OptionName { get; set; }
}
}
The "[PageRemote ...]" example didn't work with my bound viewModel; so I created some script to onblur put the email text into my asp-for viewMode.Email input; that way onsubmit, my viewModel.Email has the email value already
enter code here
<input class="viewEmail-input" asp-for="viewModel.Email" />
...
<input asp-for="Email" placeholder="Email" class="email-input" />
$(document).ready(function () {
$('.email-label').on("blur", function () {
var value = $('.email-input').val();
$('.viewEmail-input').val(value);
});
});
I also put my PageRemote into a seperate common cshtml file so more than one razor page can call the same code RemoteValidation_cshtml_cs
Then modified the PageRemote as follows:
enter code here
[PageRemote(
ErrorMessage = "Email Address already exists",
AdditionalFields = "__RequestVerificationToken",
HttpMethod = "post",
PageHandler = "CheckEmail",
PageName = "RemoteValidation"
)]
[Required, EmailAddress]
[RegularExpression(#"^[a-zA-Z0-9_.+-]+#[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$", ErrorMessage = "Invalid Email format")]
[BindProperty]
public string Email { get; set; }
Finally, I notice that there was a database call everytime a character was typed in or deleted from the email input textbox; to prevent this I created a quick routine to verify the passed in email was actually an email address before checking the database; for now, it still checks after typing "c" "o" and "m", but that's only three calls instead of dozens.
enter code here
public async Task<JsonResult> OnPostCheckEmail(string email)
{
// In order to not call the database, check if email is valid email before calling database
// Have to return true... so error is not shown to user; other validation will catch it on submit.
if (!IsValidEmailFormat(email)) return new JsonResult(true);
var user = await _userManager.FindByEmailAsync(email);
if (user == null)
{
return new JsonResult(true);
}
return new JsonResult($"Email { email } is already in use");
}
private bool IsValidEmailFormat(string email)
{
String AllowedChars = #"^[a-zA-Z0-9_.+-]+#[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$";
if (Regex.IsMatch(email, AllowedChars))
{
return true;
}
return false;
}
}

Handling multiple submit button in form

i was looking for good trick to handle multiple submit button in form and then i got some advice from this url and i followed but fail.
How do you handle multiple submit buttons in ASP.NET MVC Framework?
posted by #Andrey Shchekin.
he just said create a class like below one so i did in same controller
public class HttpParamActionAttribute : ActionNameSelectorAttribute {
public override bool IsValidName(ControllerContext controllerContext, string actionName, MethodInfo methodInfo) {
if (actionName.Equals(methodInfo.Name, StringComparison.InvariantCultureIgnoreCase))
return true;
if (!actionName.Equals("Action", StringComparison.InvariantCultureIgnoreCase))
return false;
var request = controllerContext.RequestContext.HttpContext.Request;
return request[methodInfo.Name] != null;
}
}
then multiple submit button in the view look like & also controller code look like below
<% using (Html.BeginForm("Action", "Post")) { %>
<!— …form fields… -->
<input type="submit" name="saveDraft" value="Save Draft" />
<input type="submit" name="publish" value="Publish" />
<% } %>
and controller with two methods
public class PostController : Controller {
[HttpParamAction]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult SaveDraft(…) {
//…
}
[HttpParamAction]
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Publish(…) {
//…
}
}
but when i test his code it never work. so any can tell me where i am making the mistake or code itself is wrong for handling the situation. thanks
View:
<input type="submit" name="mySubmit" value="Save Draft" />
<input type="submit" name="mySubmit" value="Publish" />
Controller Action:
[HttpPost]
public ActionResult ActionName(ModelType model, string mySubmit)
{
if(mySubmit == "Save Draft")
{
//save draft code here
} else if(mySubmit == "Publish")
{
//publish code here
}
}
I had to deal with the similar scenario when I had the requirement that Users can finalize or save progress of the hospital infant record - essentially both actions are submit but one validates the record for insertion into the main DB table and another one saves it into a temp table without any validation. I handled it like this:
I have 2 buttons both are type submit with different IDs (btnSave and btnFinalize). When btnSave is clicked I intercept that event with some JQuery code:
$("#btnSave").click(function () {
$("#SaveForm").validate().settings.rules = null;
$('#SaveForm').attr('action', '#(Url.Content("~/Home/EditCase?finalize=false"))');
});
As you can see I modify the action attribute of the form to point to a different URL with a querystring attribute of finalize = false. I also remove any validation present on the model. If the other button is clicked I do nothing - executes the default behavior.
And in my controller I have a single action that handles both submit actions:
public ActionResult EditCase(EditInfantModel model, bool finalize = true)
{
// Logic for handling submit in here...
}
I think you can apply the similar technique for your problem. I'm not sure if it's the answer you're looking for but I thought it was worth mentioning...

How to use ajax link instead of submit button for form?

I have Ajax Form in my view:
#using (Ajax.BeginForm("SearchHuman", "Search", new AjaxOptions(){
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "result" }))
{
<div class="editor-field">
#DescriptionStrings.Lastname:
#Html.TextBox("LastName")
</div>
<div class="editor-field">
#DescriptionStrings.Firstname:
#Html.TextBox("Name")
</div>
//submit button
<input type="submit" value='Start Searching' />
//submit link
#Ajax.ActionLink("search", "OtherSearch", new{lastName ="",...}, new AjaxOptions()
{
InsertionMode = InsertionMode.Replace,
UpdateTargetId = "tab"
})
}
I want to have submit button and the link for 2 different searches (in different databases) using only one form. But how to pass route values from the textboxes of the form into Ajax.ActionLink?
Thanks in advance!
But how to pass route values from the textboxes of the form into Ajax.ActionLink?
You can't. You should use a submit button if you want to send the values to the server. You could have 2 submit buttons in the same form which both submit to the same controller action. Then inside this action you can test which button was clicked and based on its value perform one or the other search.
Example:
<button type="submit" name="btn" value="search1">Start Searching</button>
<button type="submit" name="btn" value="search2">Some other search</button>
and then inside your controller action:
[HttpPost]
public ActionResult SomeAction(string btn, MyViewModel model)
{
if (btn == "search1")
{
// the first search button was clicked
}
else if (btn == "search2")
{
// the second search button was clicked
}
...
}
The solution we opted for was to implement a custom ActionMethodSelectorAttribute which allowed us to differentiate which button was pressed based on its name property. We then decorated many methods with the ActionName decorator giving them all the same action name (the one specified in the BeginFrom helper), and then we used our custom ActionMethodSelector decorator to differentiate which method is to be called based on the name of the button clicked. The net result is that each submit button leads to a separate method being called.
Some code to illustrate:
In controller:
[ActionName("RequestSubmit")]
[MyctionSelector(name = "Btn_First")]
public ActionResult FirstMethod(MyModel modelToAdd)
{
//Do whatever FirstMethod is supposed to do here
}
[ActionName("RequestSubmit")]
[MyctionSelector(name = "Btn_Second")]
public ActionResult SecondMethod(MyModel modelToAdd)
{
//Do whatever SecondMethod is supposed to do here
}
In view:
#using (Ajax.BeginForm("RequestSubmit",.....
<input type="submit" id="Btn_First" name="Btn_First" value="First"/>
<input type="submit" id="Btn_Second" name="Btn_Second" value="Second"/>
As for the custom attribute:
public string name { get; set; }
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
var btnName = controllerContext.Controller.ValueProvider.GetValue(name);
return btnName != null;
}

MVC3: button to send both form (model) values and an extra parameter

In an MVC3 project, i use an Html.BeginForm to post some (model-)values. Along with those i want to send an extra parameter that is not part of the form (the model) but in the ViewBag. Now, when i use a Button (code in answer here: MVC3 razor Error in creating HtmlButtonExtension), all the form values are posted but the extra parameter remains null. When i use an ActionLink, the parameter is posted but the form values are not :) Any know how i can combine the two? Thanks!
#Html.Button("Generate!", new { id = ViewBag.ProjectID })
#Html.ActionLink("Generate!", "Post", new { id = #ViewBag.ProjectID })
My advice would be to declare a new Object in your App.Domain.Model something like this
namespace App.Domain.Model
{
public class CustomEntity
{
public Project projectEntity { get; set; }
public int variableUsed { get; set; }
}
}
In your view you can acces them easily by using CustomEntity.projectEntity and CustomEntity.variableUsed.
Hope it helps
You can do something like below.
View code
#using (Html.BeginForm("ActionName", "ControllerName", FormMethod.Post, new { #id = "frmId", #name = "frmId" }))
{
#*You have to define input as a type button not as a sumit. you also need to define hidden variable for the extra value.*#
<input type="hidden" name="hndExtraParameter" id="hndExtraParameter" />
<input value="Submit" type="button" id="btnSubmit" onclick="UpdateHiddenValue()" />
}
<script type="text/javascript">
function ValidateUser() {
$("#hndExtraParameter").val('Assignvaluehere');
$("#frmId").submit();
}
</script>
Controller Code
[HttpPost]
public ActionResult ActionName(Model model, string hndExtraParameter)
{
//Do your operation here.
}

Using two button in a form calling different actions

I have a form on my page:
#using(Html.BeginForm("DoReservation","Reservation"))
{
...some inputs
<button id="recalculate">Recalculate price</button>
<button id="submit">Submit</button>
}
When I click the "Recalculate price" button I want the following action to be invoked:
public ActionResult Recalculate(FormCollection form)
{
var price = RecalculatePrice(form);
... do some price recalculation based on the inputs
return PartialView("PriceRecalculation",price);
}
When I click the "Submit" button I want the "DoReservation" action to be invoked (I want the form to be submitted).
How can I achieve something like that?
What I can suggest is , adding a new property to your view model and call it ActionType.
public string ActionType { get; set; }
and then change your cshtml file like below
#using (Html.BeginForm())
{
<div id="mytargetid">
...some inputs*#
</div>
<button type="submit" name="actionType" value="Recalculate" >Recalculate price</button>
<button type="submit" name="actionType" value="DoReservation" >Submit</button>
}
in post action method based on ActionType value you can decide what to do !
I noticed that in your comments you mentioned you need to return partial and replace if with returning partial , no problem , you can use
#using (Ajax.BeginForm("DoProcess", new AjaxOptions { UpdateTargetId = "mytargetid", InsertionMode = InsertionMode.Replace }))
and in controller change your action to return partial view or java script code to redirect page
public ActionResult DoProcess(FormModel model)
{
if (model.ActionType == "Recalculate")
{
return PartialView("Test");
}
else if (model.ActionType == "DoReservation")
{
return JavaScript(string.Format("document.location.href='{0}';",Url.Action("OtherAction")));
}
return null;
}

Resources