Partial view posting submodel in aspnet core? - c#

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.

Related

ASP.NET MVC Core/6: Multiple submit buttons

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.

One controller, multplie views, multiple forms

The goal is to have one page with a wizard. Each step of the wizard is a partial view containing a form. I have only one controller (Insurance) with an action for each view. The actions receive the posted data and return the viewmodel for the next step, or the viewmodel of the current step containing the error details.
The page (Index.cshtml) has partial views, rendered as
#Html.Partial("~/Views/Shared/_RegistrationCode.cshtml")
and the partial view itself contains a form, rendered as
#using (Html.BeginForm("RegistrationCodeDetails", "Insurance", FormMethod.Post)) {
and a
<input type="submit" name="nextButton" value="Verder" class="btn btn-success" />
within the form to submit it.
The code works as intended up to the point where the first action returns the viewmodel for the next step (partial view _Product) using
return PartialView("_Product", productViewModel);. The ActionResult is not sent to the partial view, but rendered as a full view, so the result is a partial being rendered as the only thing on the screen.
I've fiddled with #using (Ajax.BeginForm("RegistrationCodeDetails", "Insurance", new AjaxOptions { UpdateTargetId = "articleProductOutput", HttpMethod = "Post" })) { but the data is not rendered in the second wizard step partial.
Edit:
We've decided to take a different approach: One page, one controller and basically one viewmodel. The initial data is rendered right away, data depending on other steps in the wizard is retrieved using JSON and partial views.
Unless you mark your partial view as [ChildActionOnly], it won't load in the same page!
you Partial view should look like
[ChildActionOnly]
public ActionResult _ParialView1()
{
//TODO: Add the required code here
}
//and your PartialView should be included in the main view as :
#{Html.Action("_PartialView1","Controller1");}
Thanks and hope this helps!

Displaying blog categories in partial view on layout page

Currently I have a partial view where I manually display all my blog categories with links. I would like to make it dynamic by pulling from the database. I am not sure how to accomplish this within a partial view. I would even be willing to do it within the actual _layout page if it's easier that way.
Here is what I have right now.
_Categories.cshtml
<h2>Categories</h2>
<hr/>
<p>
ASP.Net MVC<br/>
Ruby on Rails<br/>
</p>
I would like to create these links dynamically as opposed to hard coding.
_Layout.cshtml
#Html.Partial("_Categories")
The main problem is there is no controller for the layout of a partial which is why I can't figure out how to go about it.
Thanks in advance for any help.
Create a controller action named ListCategories in BlogController (or in a new CategoryController). Add all the categories to the ViewBag in the action by querying them from your back-end database
public ActionResult ListCategories()
{
ViewBag.Categories = db.Categories;
}
And use a #foreach loop in the view for the action ListCategories.cshtml:
<h2>Categories</h2>
<hr/>
<p>
#foreach(Category c in ViewBag.Categories)
{
#c.Name<br/>
}
</p>
Finally, change your _Layout.cshtml to point to this action:
#Html.Action("ListCategories")
// or #Html.Action("ListCategories", "CategoryController")

How can a view fill a model?

I have an ASP.NET MVC view and related model.
How can I fill its related model from within the view?
You don't. MVC does, and it does this automatically when the form is posted back to the controller (assuming you're using model binding, and not a FormsCollection)
You POST to a controller action from your view and the model binder will populate it:
View:
#using(Html.BeginForm())
{
#Html.EditorFor(m => m.SomeProperty)
<input type="submit" value="Submit" />
}
Controller:
[HttpPost]
public ActionResult SomeAction(SomeModel model)
{
// your model has been populated by what was in the form at this point
}

How can I invoke an Action and output it's result, as if it were a "user control"?

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;
}

Categories

Resources