I've been trying to Auto Increment my ID (BookId). All views are working(Edit,Delete,Details,Index), but the Create one is not working very well because it redirect to itself every time I try to add a new book.
In my Controller I have the following:
public ActionResult Create()
{
return View();
}
//
// POST: /Book/Create
[HttpPost]
public ActionResult Create(tbBooks tbbooks)
{
if (ModelState.IsValid)
{
db.tbBooks.Add(tbbooks);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(tbbooks);
}
And in the Create.cshtml:
#using (Html.BeginForm("Create","Book")){
#Html.ValidationSummary(true)
<fieldset>
<legend>tbBooks</legend>
<div class="editor-label">
#Html.HiddenFor(model => model.BookId)
</div>
<div class="editor-field">
#Html.HiddenFor(model => model.BookId)
</div>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
}
<div>
#Html.ActionLink("Back to List", "Index")
</div>
I've already created the database with the table and the column "BookId" is set with the Identity Specification = Yes.
enter image description here
Any help you can give me I'll appreciate,
Regards.
Well, a lot of things are going on here:
First, you'd better specify in the Create method if it is a [HttpGet] (in the first method).
Second, your view has:
#Html.HiddenFor(model => model.BookId)
This doesn't make any sense, once your are not passing any value from the [HttpGet] method to your view.
Third, it's not usual to place a label for a hidden field. You usually don't want to show hidden fields.
Also, for the structure in your database, you'd probably need filling out the fields name, price, etc in your view. If your ID is auto-incremented, you probably don't need to handle it yourself, database will do that for you.
Finally, when you submit your form, what's probably happenning is that the if command below is false (you can place a breakpoint and check):
if (ModelState.IsValid)
So the next command executed is:
return View(tbbooks);
Which makes you return to the View itself. I'm surprised you are not getting any errors. I suggest you take a look in some tutorials and actually use the code scaffolded by Visual Studio, which already contains some great indicators on how things work.
Your ModelState.IsValid property is returning false and hence it is executing the Return View(tbbooks) line. That means your form did not provide all the required values / some values were not acceptable as per the data annotation definition.
To know which valiations failed, In your razor view,
Change #Html.ValidationSummary(true) to #Html.ValidationSummary()
and submit the Add form again. You will see the validation errors
The boolean parameter you passed to ValidationSummary overload tells the system to exclude property Errors. that is why you could not see them.
public static MvcHtmlString ValidationSummary(
this HtmlHelper htmlHelper,
bool excludePropertyErrors
)
Related
I have a single View, which has tabs with various different sections. Each tab I have implemented as a partial view.
There is one ViewModel which has various sub classes to populate the view and partial views within the tabs.
FormCaptureViewModel
- FormDetailViewModel
- FormBrandingViewModel
- etc
[HttpGet]
public IActionResult FormCapture()
{
return View(new FormCaptureViewModel());
}
<div class="tab-pane" id="tab2">
#Html.Partial("_FormBrandingPartial", Model.FormBranding)
</div>
<div class="tab-pane" id="tab3">
#Html.Partial("_FormDesignerPartial", Model.FormDesigner)
</div>
<div class="tab-pane" id="tab4">
#Html.Partial("_FormAnalyticsPartial", Model.FormAnalytics)
</div>
Then I want each form to do a post back to FormCapture like so:
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult FormCapture(FormBrandingViewModel brandingModel)
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult FormCapture(FormDetailViewModel detailModel, SaveAction action)
{
return View();
}
Each partial view will have a form tag that looks like this:
<form role="form" method="post" asp-action="FormCapture" enctype="multipart/form-data">
However I get an AmbiguousActionException: Multiple actions matched. This makes sense because asp will not know which method to use. How do I go about fixing this?
I am trying to keep away from using ajax because using scripts in partial views is not great.
Any ideas how to implement something like this? Surely this is a normal use case for complicated views (with tabs, etc.)?
I cannot just submit the entire FormCaptureViewModel on each partial view as other tabs may have issues or be non-existent depending on the view's state.
My aim:
Utilize ModelState validations specific to the sub viewmodel.
The URL must not change on post back.
I need multiple submit buttons to perform different actions in the controller.
I saw an elegant solution here: How do you handle multiple submit buttons in ASP.NET MVC Framework?
With this solution, action methods can be decorated with a custom attribute. When the routes are processed a method of this custom attribute checks if the attribute's property matches the name of the clicked submit button.
But in MVC Core (RC2 nightly build) I have not found ActionNameSelectorAttribute (I also searched the Github repository). I found a similar solution which uses ActionMethodSelectorAttribute (http://www.dotnetcurry.com/aspnet-mvc/724/handle-multiple-submit-buttons-aspnet-mvc-action-methods).
ActionMethodSelectorAttribute is available but the method IsValidForRequest has a different signature. There is a parameter of type RouteContext. But I could not find the post data there. So I have nothing to compare with my custom attribute property.
Is there a similar elegant solution available in MVC Core like the ones in previous MVC versions?
You can use the HTML5 formaction attribute for this, instead of routing it server-side.
<form action="" method="post">
<input type="submit" value="Option 1" formaction="DoWorkOne" />
<input type="submit" value="Option 2" formaction="DoWorkTwo"/>
</form>
Then simply have controller actions like this:
[HttpPost]
public IActionResult DoWorkOne(TheModel model) { ... }
[HttpPost]
public IActionResult DoWorkTwo(TheModel model) { ... }
A good polyfill for older browsers can be found here.
Keep in mind that...
The first submit button will always be chosen when the user presses the carriage return.
If an error - ModelState or otherwise - occurs on the action that was posted too, it will need to send the user back to the correct view. (This is not an issue if you are posting through AJAX, though.)
ASP.NET Core 1.1.0 has the FormActionTagHelper that creates a formaction attribute.
<form>
<button asp-action="Login" asp-controller="Account">log in</button>
<button asp-action="Register" asp-controller="Account">sign up</button>
</form>
That renders like this:
<button formaction="/Account/Login">log in</button>
<button formaction="/Account/Register">sign up</button>
It also works with input tags that are type="image" or type="submit".
I have done this before and in the past I would have posted the form to different controller actions. The problem is, on a server side validation error you are either stuck with:
return View(vm) leaves the post action name in the url… yuck.
return Redirect(...) requires using TempData to save the ModelState. Also yuck.
Here is what I chose to do.
Use the name of the button to bind to a variable on POST.
The button value is an enum to distinguish the submit actions. Enum is type safe and works better in a switch statement. ;)
POST to the same action name as the GET. That way you don't get the POST action name in your URL on a server side validation error.
If there is a validation error, rebuild your view model and return View(viewModel), following the proper PGR pattern.
Using this technique, there is no need to use TempData!
In my use case, I have a User/Details page with an "Add Role" and "Remove Role" action.
Here are the buttons. They can be button instead of input tags... ;)
<button type="submit" class="btn btn-primary" name="SubmitAction" value="#UserDetailsSubmitAction.RemoveRole">Remove Role</button>
<button type="submit" class="btn btn-primary" name="SubmitAction" value="#UserDetailsSubmitAction.AddRole">Add Users to Role</button>
Here is the controller action. I refactored out the switch code blocks to their own functions to make them easier to read. I have to post to 2 different view models, so one will not be populated, but the model binder does not care!
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Details(
SelectedUserRoleViewModel removeRoleViewModel,
SelectedRoleViewModel addRoleViewModel,
UserDetailsSubmitAction submitAction)
{
switch (submitAction)
{
case UserDetailsSubmitAction.AddRole:
{
return await AddRole(addRoleViewModel);
}
case UserDetailsSubmitAction.RemoveRole:
{
return await RemoveRole(removeRoleViewModel);
}
default:
throw new ArgumentOutOfRangeException(nameof(submitAction), submitAction, null);
}
}
private async Task<IActionResult> RemoveRole(SelectedUserRoleViewModel removeRoleViewModel)
{
if (!ModelState.IsValid)
{
var viewModel = await _userService.GetDetailsViewModel(removeRoleViewModel.UserId);
return View(viewModel);
}
await _userRoleService.Remove(removeRoleViewModel.SelectedUserRoleId);
return Redirect(Request.Headers["Referer"].ToString());
}
private async Task<IActionResult> AddRole(SelectedRoleViewModel addRoleViewModel)
{
if (!ModelState.IsValid)
{
var viewModel = await _userService.GetDetailsViewModel(addRoleViewModel.UserId);
return View(viewModel);
}
await _userRoleService.Add(addRoleViewModel);
return Redirect(Request.Headers["Referer"].ToString());
}
As an alternative, you could post the form using AJAX.
An even better answer is to use jQuery Unobtrusive AJAX and forget about all the mess.
You can give your controller actions semantic names.
You don't have to redirect or use tempdata.
You don't have the post action name in the URL on server side validation errors.
On server side validation errors, you can return a form or simply the error message.
I'm getting values to my Html.Dropdownlist like below.
<div class="col-md-4">
<h2 style="color: #2f6207">dfdfDestination</h2>
#Html.DropDownList("accountID", new SelectList(ViewBag.Destination, "regCde", "Destination"))
</div>
#Html.ActionLink("SEARCH NOW", "MscHome")
it works fine.
now I want to get the user selected value and pass it to another controller when button clicked(action link)
public ActionResult MscHome(string accountID)
{
ViewBag.requested = Session["search"];
Thread.Sleep(10000);
return View();
}
but this shows null value.
One way to achieve that is to target the same controller first, get the value from the dropdown and then redirect to a second controller passing the value as parameter. Use RedirectToAction and either TempData or set the value as an argument in the call.
If you just return a view used by another controller, this could be okay in some cases. But if the view needs to be initialized by code located in a different controller then you'll open an empty view. That's what RedirectToAction is preventing.
I have a parent view model (Let's call it ParentViewModel) which has a list of children view models (Let's call them ChildViewModel). Each child view model can be edited independently and I have a separate form which I display in a loop. This works brilliantly but I cannot work out how to post just the child model and ignore the parent.
This is my form:
#model ParentViewModel
...
#foreach (var child in Model.Children)
{
#using (Html.BeginForm("_EditChild", "Admin", FormMethod.Post))
{
#Html.AntiForgeryToken()
<div class="form-group">
#Html.EditorFor(model => child.Content, new {htmlAttributes = new {#class = "form-control"}})
#Html.ValidationMessageFor(model => child.Content, "", new {#class = "text-danger"})
</div>
<div class="form-group">
<div class="col-md-12">
<input type="submit" value="Create" class="btn btn-default new-post" />
</div>
</div>
}
}
And this is the signature of my controller. It is expecting a type ChildViewModel which exists in ParentViewModel as a list.
[Authorize]
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult _EditPost([Bind(Include = "")] ChildViewModel childViewModel)
{
}
The form works and submits BUT ChildViewModel is null when it reaches the submit controller. This is certainly because binding between the Form Post and the Action is not happening.
I am afraid it is not possible to post the child model only , since the page can define one and only one model that is the parent model you have defined .
But you can solve your problem simply by posting the parent model and extracting the child model in the controller .
It is possible, just not intended by ASP.NET MVC. All you would have to do is remove the parent prefix from the name of your submitted inputs on the client side. Your input would probably look something like:
<input name="Children[0].SomeProperty" ../>
If your AdminController._EditChild action expects a ChildViewModel, then you'd just have to use javascript to rename the input to:
<input name="SomeProperty" ../>
and the model binder should build the ChildViewModel. Alternatively, you might also be able to solve it by creating a custom ValueProvider or ModelBinder that maps the inputs to a ChildViewModel even though it has the wrong prefix...although that would seem like an uglier hack to me than changing the input names. On that note, I would probably also update the IDs with javascript when updating the names just to keep them in sync, even though only the name is used for binding.
Note also, if you're not looping but simply want to submit a single child ViewModel of your model, you can just assign it to a variable:
#var childVM = Model.ChildProp;
#Html.HiddenFor(m => childVM.ID)
Notice m is disregarded in the property expression of HiddenFor. I think last time I did this the variable name had to match the action's parameter name so you would submit this to:
public ActionResult SomeAction(ChildViewModel childVM){ ... }
I'm currently trying to understand why this technique can't be combined with looping.
In my _Layout.cshtml file, I'd like to invoke something like this:
<div id="content">
<div id="left-wrapper" class="box">
#Html.Action("FreeThisWeek", "Products")
#RenderBody()
</div>
</div>
And this is my ProductsController file:
[ChildActionOnly]
public ActionResult FreeThisWeek()
{
//Some code that fetches the data and builds the model.
var model = BuildFreeProducts();
return View(model);
}
If I try to run this code, I get a StackOverflowException because the Action returns the View() which asks for the Layout, which runs the Action which returns the View(), and so on.
Understandable, but how do I accomplish this which correct code?
Where do I write the View that compounds this data model with the HTML I write?
try returning PartialView("yourview",model) . Make sure the view you are returning does not use this page as a layout. you can specify that by using #{Layout=null} at the top of the view you are returning.
You are returning the View inside your FreeThisWeek Action and inside the View you are using the _Layout again. So it become recursive.
Go to your FreeThisWeek View and set Layout as null
#{
Layout=null;
}