FormMethod.Post in MVC4 has unexpected behavior - forms

I have following View:
#model DocuLive.ViewModels.InstallationRequestViewModel
#{
ViewBag.Title = "AttachDB";
Layout = "~/Views/Shared/_AdminPage.cshtml";
}
<h2>AttachDB</h2>
#using (Html.BeginForm("AttachDB","AppStart", FormMethod.Post)) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<p>Database DocuLive already exists on the server where you attempted installation. Do wish to attach existing DB to this installation of DocuLive? Details below will be used for this attempt.</p>
<fieldset>
<p>
<input type="submit" name="command" value="Attach" />
<input type="submit" name="command" value="Start over" />
</p>
<legend>DB server credentials</legend>
<div class="display-label">
#Html.DisplayNameFor(model => model.Server)
</div>
<div class="display-field">
#Html.DisplayFor(model => model.Server)
</div>
<div class="display-label">
#Html.DisplayNameFor(model => model.UserName)
</div>
<div class="display-field">
#Html.DisplayFor(model => model.UserName)
</div>
<div class="display-label">
#Html.DisplayNameFor(model => model.Password)
</div>
<div class="display-field">
#Html.DisplayFor(model => model.Password)
</div>
</fieldset>
}
I have follownt two methods in controller:
public ActionResult AttachDB(InstallationRequestViewModel requestVM)
{
if (requestVM != null)
return View("AttachDB", requestVM);
else
{
TempData["Fail"] = "DocuLive database exist, but inicalization request has null value and cannot be used to attach DB";
return RedirectToAction("Index");
}
}
[HttpPost]
private async Task<ActionResult> AttachDB(InstallationRequestViewModel requestVM, string command)
{
try
{
switch (command)
{
case "Attach":
// do something complex and return RedirectToAction
case "Start over":
return RedirectToAction("Index");
default:
return RedirectToAction("Index");
}
}
catch (Exception ex)
{
TempData["Fail"] = ex.Message;
return RedirectToAction("Index");
}
}
For some reason when i submit the form with either button, it hits the first method with no regard to fact that I explicitly specified FormMethod.Post for the form to make sure that submitting the form will take user to the second method that actually contains some business logic.
It is very strange because I am using similar approach all over the app and so far I had no issues with this.
Can anyone advise, why submitting the form with either button is considered Get instead of POST?
Thanks in advance...

Found it. I made the second method private by accident. Pretty stupid mistake...

Related

How to submit a form multiple times (asp.net core razor)

I am trying to create a form that can be submitted multiple times with different information, while retaining a common value in one field.
I have a list view from a SQL table in ASP.NET Core Razor that is a list of construction projects. For each row in the list I have a link that goes to a "create" template page where users can create a bid entry for the project which is stored in a different table. The Project Number is assigned to a route value (asp-route-Number = "the project number from the previous list")and populates a hidden field in the "create new bid" form.
Using the default code for the razor page, everything works great. You click submit and are taken back to the list of projects.
What I want to do is have another option on the "create new bid" form that will allow you to save and enter another bid for the same project. I created another button and handler to do this but I am stuck on actually implementing it. If I use return Page() the form posts and the page is returned with route data intact, but the text fields still contain the previous data and the drop-down list is empty. If I use return RedirectToPage(CreateNewBid, Route data) the form posts but the route data does not seem to be passed along and creates a null value error.
This is the link from the Projects list (inside the foreach table), which takes you to the "Create Bid" form and works fine.
<a asp-page="CreateBid" asp-route-Number="#item.ProjectNumber" asp-route-opwid="#item.Id">New Bid</a>
The Create Bid form has the following to submit and create another entry
int num = int.Parse(Request.Query["Number"]);
int idnum = int.Parse(Request.Query["opwid"]);
<input type="submit" value="Save and enter another"
asp-page-handler="Another" asp-route-opwid="#idnum"
asp-route-Number="#num" class="btn btn-primary"/>
And the handler:
public async Task<IActionResult> OnPostAnotherAsync(int Number, int opwid)
{
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
return Page();
//return RedirectToPage("./CreateBid", (Number == num, opwid == idnum));
}
I have also tried several things in the route parameters (as opposed to using the variables) in the "Redirect to Page" and nothing seems to work.
Is there an easier way, or am I just missing something?
This is the cshtml file:
#page
#model Authorization_AD.Pages.GenSvc.BidEntry.CreateBidModel
#{
ViewData["Title"] = "CreateBid";
}
#{ int num = int.Parse(Request.Query["Number"]);
int idnum = int.Parse(Request.Query["opwid"]);
}
<h1>Create Bid</h1>
<h4>OPW number #num</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<input asp-for="OpwBids.OpwProject" value="#idnum" hidden class="form-control" />
</div>
<div class="form-group">
<label asp-for="OpwBids.OpeningDate" class="control-label"></label>
<input asp-for="OpwBids.OpeningDate" class="form-control" />
<span asp-validation-for="OpwBids.OpeningDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="OpwBids.Contractor" class="control-label"></label>
<select asp-for="OpwBids.Contractor" class="form-control" asp-items="ViewBag.Contractor">
<option disabled selected>--- SELECT ---</option>
</select>
</div>
<div class="form-group">
<label asp-for="OpwBids.BidAmount" class="control-label"></label>
<input asp-for="OpwBids.BidAmount" class="form-control" />
<span asp-validation-for="OpwBids.BidAmount" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save and enter another"
asp-page-handler="Another" asp-route-opwid="#idnum"
asp-route-Number="#num" class="btn btn-primary"/>
<input type="submit" value="Save and return to list" asp-page-handler="Done" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
This is the C# file:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Authorization_AD.Models;
namespace Authorization_AD.Pages.GenSvc.BidEntry
{
public class CreateBidModel : PageModel
{
private readonly Authorization_AD.Models.OPWContext _context;
public CreateBidModel(Authorization_AD.Models.OPWContext context)
{
_context = context;
}
public IActionResult OnGet()
{
ViewData["Contractor"] = new SelectList(_context.Contractors, "Id", "ContractorName");
ViewData["OpwProject"] = new SelectList(_context.MainProjectsListing, "Id", "ProjectNumber");
return Page();
}
[BindProperty]
public OpwBids OpwBids { get; set; }
public async Task<IActionResult> OnPostDoneAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
public async Task<IActionResult> OnPostAnotherAsync(int Number, int opwid)
{
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
return Page();
//return RedirectToPage("./CreateBid", (Number == OpwBids.OpwProjectNavigation.ProjectNumber, opwid == OpwBids.OpwProject));
}
}
}
You can add a property to your page that will be used to bind the value of the clicked button.
public class CreateBidModel : PageModel {
//...
//Add this property to your page.
[BindProperty]
public string Button {get;set;}
public void OnGet(int number,string opwid){
//Set the number and opwid to the target properties
}
public Task<IActionResult> OnPostAsync(){
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
if(Button == "finish"){
return RedirectToPage("./Index");
}
else {
return RedirectToPage("./CreateBid", (Number == OpwBids.OpwProjectNavigation.ProjectNumber, opwid == OpwBids.OpwProject));
}
}
}
To the view you need to add two buttons that have the same name and that value will be mapped to the Button property.
<form method="post">
... Other content goes here
<button name="#Html.NameFor(m => m.Button)" value="another">Create another</button>
<button name="#Html.NameFor(m => m.Button)" value="finish">Finish</button>
</form>
The value of the clicked button will be parsed to the Button property of the Pagemodel. Based on the value you can decide how to further handle the response of the request (Finish / Create another one in your case).
Thanks for everyone's help. I got it to do what I want by adding the following to the "OnPostAnotherAsync" task:
public async Task<IActionResult> OnPostAnotherAsync(int Number, int opwid)
{
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
ViewData["Contractor"] = new SelectList(_context.Contractors, "Id", "ContractorName");
ModelState.SetModelValue("OpwBids.BidAmount", new ValueProviderResult(string.Empty, CultureInfo.InvariantCulture));
ModelState.SetModelValue("OpwBids.Contractor", new ValueProviderResult(string.Empty, CultureInfo.InvariantCulture));
return Page();
}
After the "Save Changes" I needed to re-load the view data for the "Contractor" drop down list. Then it was just a matter of clearing the form fields before returning the page.

How to implement validation rules for elements from a partial view

We are developing a .net core 3.1 MVC application (actual with MVVMC).
We have implemented custom validation attributes. And want them to be checked on server and on client side.
The view is a standard view, however the user has the possibility to add multiple partial views to the standard view (via a button).
In the partial view we were not able to use the 'asp-for' tag helper within the input fields, because one can have several times the same item added, and we need to be able to create a list in the ViewModel out of those additional partial views selected. That's why we tried to build the tags by ourselfes.
Once a partial view has been requested via ajax we are calling the validator again. Server validation works, client validation only works for those inputs, which are on the main view, not for the inputs on the partial views. However, after refreshing or when the server sends the user back after a validation error, client validation starts working (because site is completely refreshed and jquery validation takes input fields of partial views into account).
Please find the code below:
Implementation of validation attribute
public class AllowedExtensionsAttribute: ValidationAttribute, IClientModelValidator
{
private readonly List<string> _extensions = new List<string>();
public AllowedExtensionsAttribute(string extensions)
{
_extensions.Add(extensions);
}
protected override ValidationResult IsValid(object value, ValidationContext context)
{
if (value is IFormFile file)
{
var extension = Path.GetExtension(file.FileName);
if (!_extensions.Contains(extension.ToLower()))
{
return new ValidationResult(ErrorMessage);
}
}
return ValidationResult.Success;
}
public void AddValidation(ClientModelValidationContext context)
{
context.Attributes.Add("data-val", "true");
context.Attributes.Add("data-val-extension", ErrorMessage);
}
}
MainViewModel
[AllowedExtensions(".pdf", ErrorMessage = "This data type is not allowed!")]
public IFormFile PdfDocumentOne { get; set; }
PartialViewModel
[AllowedExtensions(".pdf", ErrorMessage = "This data type is not allowed!")]
public IFormFile PdfDocumentTwo { get; set; }
Main View
<form method="post" asp-controller="Home" asp-action="MainView" enctype="multipart/form-data">
<div class="form-group top-buffer">
<div class="row">
<div class="col-2">
<label asp-for="PdfDocumentOne" class="control-label"></label>
</div>
<div class="col-3">
<input asp-for="PdfDocumentOne" class="form-control-file" accept="application/pdf" />
<span asp-validation-for="PdfDocumentOne" class="text-danger"></span>
</div>
</div>
</div>
<div id="container">
<div id="containerFull" class="form-group">
<div class="row">
<div class="col-2">
<label class="control-label">...</label>
</div>
<div class="col-10">
<div id="containerPartialView">
</div>
</div>
</div>
<div class="row">
<div class="col-2">
</div>
<div class="col-3">
<button id="AddPartialView" type="button" class="form-control">...</button>
</div>
</div>
</div>
</div>
...
<div class="form-group top-buffer">
<div class="row">
<div class="col-2">
<input type="submit" value="Submit" class="form-control" id="checkBtn" />
</div>
</div>
</div>
</form>
Partial View
<input id="Lists[#Model.ViewId].ViewId" name="Lists[#Model.ViewId].ViewId" class="partialViewModel" type="hidden" value="#Model.ViewId" />
<div id="Lists[#Model.ViewId].MainContainer" class="partialView">
<div class="form-group">
<div>
<div class="col-2">
<label asp-for="PdfDocumentTwo" class="control-label"></label>
</div>
<div class="col-3">
<input name="Lists[#Model.ViewId].PdfDocumentTwo" id="Lists[#Model.ViewId].PdfDocumentTwo " type="file" class="form-control-file" accept="application/pdf"
data-val="true" data-val-extension="This data type is not allowed!"/>
<span class="text-danger field-validation-valid" data-valmsg-for="Lists[#Model.ViewId].PdfDocumentTwo" data-valmsg-replace="true"></span>
</div>
</div>
</div>
...
</div>
Javascript
function AddPartialView() {
var i = $(".partialView").length;
$.ajax({
url: '/Home/AddPartialView?index=' + i,
success: function (data) {
$('#containerPartialView').append(data);
$().rules('remove','extension');
jQuery.validator.unobtrusive.adapters.addBool("extension");
},
error: function (a, b, c) {
console.log(a, b, c);
}
});
}
$('#AddPartialView').click(function () {
AddPartialView();
});
jQuery.validator.addMethod("extension",
function (value, element, param) {
var extension = value.split('.').pop().toLowerCase();
if ($.inArray(extension, ['pdf']) == -1) {
return false;
}
return true;
});
jQuery.validator.unobtrusive.adapters.addBool("extension");
HomeController
[HttpPost]
public IActionResult MainView(MainViewModel vm)
{
if (vm == null)
{
return RedirectToAction("Index");
}
DatabaseHelper.GetMainViewModel(vm);
if (!ModelState.IsValid)
{
#ViewData["StatusMessageNegative"] = "The entered data is not valid. Please scroll down to correct your data.";
return View(vm);
}
return RedirectToAction("UploadDocument", new { Id = vm.Id});
}
[HttpGet]
public ActionResult AddPartialView(int index)
{
PartialViewModel pvm = DatabaseHelper.GetPartialViewModel(index);
return PartialView("Partial", pvm);
}
After searching in the internet we found the following argument (data-ajax="true"). However, we don't know where to place it or how to use it correctly.
Have you tried re-apply validation after loading your partial view?
$.ajax({
url: '/Home/AddPartialView?index=' + i,
success: function (data) {
$('#containerPartialView').append(data);
$().rules('remove','extension');
jQuery.validator.unobtrusive.adapters.addBool("extension");
},
error: function (a, b, c) {
console.log(a, b, c);
}
}).then(function () {
$("form").each(function () { $.data($(this)[0], 'validator', false); });
$.validator.unobtrusive.parse("form");
});

Play! framework template engine form, how to show custom error messages

I'm using play! framework with scala to deploy a web application.
I would like to open a popup message when an User fail to login.
This is my scala template code:
#form(routes.Application.login) {
<div class="emailaddress">E-mail address:</div>
<div class="inputbox">
#helper.input(userForm("email"), '_id -> "email", '_label->"" ) { (id, name, value, args) =>
<input placeholder="Insert email" type="text" class="email-input" id="#id" name="#name" value="" #toHtmlArgs(args)>
}
</div>
<div class="password">Password:</div>
<div class="inputbox1">
#helper.input(userForm("password"), '_id -> "password", '_label->"") { (id, name, value, args) =>
<input placeholder="Insert password" class="password-input" type="password" id="#id" name="#name" value="" #toHtmlArgs(args)>
}
</div>
<div >
<button id="submit-button" class="myButton" >Submit</button>
</div>
#if(userForm.hasGlobalErrors) {
<div class="alert alert-error">
<a class="close" data-dismiss="alert">×</a>
function to javascriptPopUp(#userForm.globalError.get.message);
</div>
}
}
and my controller code is :
def login = Action { implicit request =>
userForm.bindFromRequest.fold(
errors=> BadRequest(views.html.user.userLogin(UserDB.findAll, errors)),
user => {
val use = UserDB.findUserByLogin(user.email, user.password)
if(use!=None){
Redirect(routes.DataManagementController.projects).withSession(Security.username -> user.email).flashing("success" -> "You've been logged in")
}else{
Ok(views.html.user.userLogin(UserDB.findAll, userForm))
}
}
)
}
But even with error it never gets to my javascript function.
Can anybody explain to me why I never get in?
(if you need more code, ask and I will post it, I'm just saving space on this page because Login works, I can't just show message errors).

mvc4 ef5 return points to folder not controller

I have 3 sequential date fields: Start, End, Certify. When the user indicates he wants to update a date in a given row, I actionlink to the controller for that table, Task, where I have added code to determine which field is null and then direct to a view customized to that field. My concept was that the return would go to the scaffold generated edit where the data would be saved. So 3 views with a common return.
I'm getting a 404 error. Since I know the name is present, it must be unavailable.
The scaffold generated post code:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(Task task)
{
if (ModelState.IsValid)
{
db.Entry(task).State = EntityState.Modified;
db.SaveChanges();
}
And my selection code:
public ActionResult EditBegin(int? id)
{
Task ViewModel = db.Tasks.Find(id);
{
if (ViewModel.SStart == null)
{
ViewModel.TaskID = id.Value;
ViewModel.SStart = DateTime.Now;
return View("EditStart", ViewModel);
}
else if (ViewModel.SEnd == null)
{
ViewModel.TaskID = id.Value;
ViewModel.SEnd = DateTime.Now;
return View("EditEnd", ViewModel);
}
else if (ViewModel.SCert == null)
{
ViewModel.TaskID = id.Value;
ViewModel.SCert = DateTime.Now;
return View("EditCert", ViewModel);
}
return View("EditCert", ViewModel); //solves not all paths have return error
}
}
And in the EditEnd view the EditorFor and Actionlink.
#Html.EditorFor(model => model.SEnd) // preloaded with NOW
#Html.ActionLink("Save End Date", "Edit", "Task" ) //is the TaskID passed back as part of this?
So in the EditEnd view, press the "Save end date" button and I get the 404 error. I've tested the Task edit function to confirm "the resource is available" and it works fine.
I've discovered the path is to a folder .../task/edit not the controller.
How do I get it to reference the controller. Removing the quotes doesn't help.
Thanks
My entire view is:
#model MVCBSV.Models.Task
#{
ViewBag.Title = "Scanedit";
}
<h2>Add Start Date</h2>
#using (Html.BeginForm()) {
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
Html.BeginForm("Edit", "Task", FormMethod.Post);
<fieldset>
<legend>Task</legend>
#Html.HiddenFor(model => model.TaskID)
<div class="editor-label">
Step Name
</div>
<div class="editor-field">
#Html.DisplayFor(model => model.StepName);
</div>
<div class="editor-label">
#Html.LabelFor(model => model.SStart)
</div>
<div class="editor-field">
#Html.EditorFor( model => model.SStart)
#Html.ValidationMessageFor(model => model)
</div>
#* <p>
<input type="submit" value="Save" />
</p>*#
</fieldset>
}
<div>
#Html.ActionLink("Save Start Date", "Edit", "Task" )
</div>
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
You edit method is decorated as an HTTP Post method. An anchor tag will provide a link which your browser uses to make an HTTP GET request. You can change from attribute to HttpGet and this will work.
However, it's better practice to actually POST a form to your save method.

Upload in MVC4, got 2 parameters in my action but file is empty

I'm trying to upload a file to a directory. The following code worked for me
CONTROLLER
[HttpPost]
public ActionResult Index(HttpPostedFileBase uploadFile)
{
//string path = #"C:\Users\thomas\Desktop";
if (uploadFile != null)
{
string filePath = Path.Combine(Server.MapPath("/files"), Path.GetFileName(uploadFile.FileName));
uploadFile.SaveAs(filePath);
}
return RedirectToAction("Index");
}
HTML PAGE
#{
ViewBag.Title = "Index";
}
<h2>Index</h2>
<form action="/Post/Index" method="post" enctype="multipart/form-data">
<label for="uploadFile">Upload file: </label>
<input name="uploadFile" id="uploadFile" type="file" />
<input value="uploadFile" type="submit" />
</form>
Now i'm trying to implement this in a function where i create a message which is created by a model that is containing a message and an item class. When i submit the form the model is passed to my savecontroller but the file is null in my parameter controller.
HTML PAGE
Create new message
#model GeoCitytroopers.Models.MessageItemModel
#{
ViewBag.Title = "Create";
}
#using (Html.BeginForm()) {
#Html.ValidationSummary(true)
<fieldset>
<legend>Event</legend>
<div class="editor-label">
#Html.LabelFor(model => model.Message.MessagePicture)
</div>
<div>
<label for="uploadFile">Upload file: </label>
<input name="uploadFile" id="uploadFile" type="file" />
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Message.MessagePicture)
#Html.ValidationMessageFor(model => model.Message.MessagePicture)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Item.ItemTitle)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Item.ItemTitle)
#Html.ValidationMessageFor(model => model.Item.ItemTitle)
</div>
<div class="editor-label">
#Html.LabelFor(model => model.Item.ItemDescription)
</div>
<div class="editor-field">
#Html.EditorFor(model => model.Item.ItemDescription)
#Html.ValidationMessageFor(model => model.Item.ItemDescription)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
CONTROLLER
[HttpPost]
public ActionResult Create(HttpPostedFileBase uploadFile, MessageItemModel ViewModel)
{
if (ModelState.IsValid)
{
Utility ut = new Utility();
Item viewItem = ViewModel.Item;
Message viewMessage = ViewModel.Message;
if (uploadFile != null)
{
string filePath = Path.Combine(Server.MapPath("/files"), Path.GetFileName(uploadFile.FileName));
uploadFile.SaveAs(filePath);
}
//ADD USER TO ITEM
viewItem = ut.AddUserToItem(viewItem);
//ADD ITEM
viewItem.ItemCreateddate = DateTime.Now;
//ADD DISTRICT TO ITEM
viewItem.DistrictID = ut.GetUserDistrict();
db.Items.Add(viewItem);
//ADD LINK
viewMessage.Item = viewItem;
db.Messages.Add(viewMessage);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(ViewModel);
}
How can i pass the uploading file to my controller?
Thanks in advance!
You forgot set the correct enctype to the form. You cannot upload files without that:
#using (Html.BeginForm(null, null, FormMethod.Post, new { enctype = "multipart/form-data" })) {
...
}
Now the upload will work and your uploadFile parameter will not be null.
My initial guess is that the you created using Html helper doesn't have the necessary encrypt on it.
try using
using(#Html.BeginForm("action-name","controller-name",
FormMethod.Post, new { enctype="multipart/form-data"}){
}
with appropriate values for action-name and controller-name