ASP.NET Core MVC submit form in partial view / ViewComponent - asp.net-core-mvc

I have a menu and a content area which displays content based on the selected item in the menu. Since the user is allowed to change the structure of the main menu, I decided that everything will be on page /home/index and will have a guid assigned to the content which needs to be shown. I started with the idea to introduce partial views, but realized that ASP.NET Core doesn't have RenderAction anymore and was replaced by ViewComponents.
So I used ViewComponents and everything works fine, except that I've stumbled on a situation where I need to have a component as a submit form.
In an example: One menu item is the menu is a component that shows a list of users. Another menu item is a component that creates a new user. On create user component I'll have a form that needs to be filled and on successful submit, I want to redirect the user to the component that shows a list of users. In the case of unsuccessful submit, error, wrong input I would of course not want to redirect the user to the list of users.
Since ViewComponents' job is to display view, how should I approach this issue? I'm looking for pointers in the right direction.
I have little experience in this field so any help would be appreciated.
UPDATE
In Index.cshtml:
<tbody>
#foreach (string item in Model.Components)
{
<tr>
<td>
<div class="col-md-3">
#await Component.InvokeAsync(#item)
</div>
</td>
</tr>
}
</tbody>
This is inside the content area. Components are string names of components I'd like to show in the content area (currently listed one after the other).
My ViewComponent which will get called when I click on the menu item to display the form:
public class TestFormViewComponent : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync()
{
return View("_TestForm", new TestModelPost());
}
}
My _TestForm.cshtml component:
#model TestModelPost
<form asp-controller="Home" asp-action="TestPost" method="post">
<label asp-for="Name"></label>
<input asp-for="Name" /><br />
<label asp-for="Surname"></label>
<input asp-for="Surname" /><br />
<button type="submit">Go</button>
</form>
And the action TestPost called:
[HttpPost]
public IActionResult TestPost(TestModelPost model)
{
// Save model data, etc
// !ModelState.IsValid -> go back to form
// Success -> go to specific id
return RedirectToAction("Index", new { id = 1 });
}
How should I approach this? Or rather, am I even on the right track? I'm not sure how I would go "back" to that view I created in my _TestForm component in case the input was incorrect.

View components, just like child actions before them in ASP.NET MVC do not support POSTs. You need a full fledged action to handle the form post, which will need to return a full view. Essentially, you just need to think about it more abstractly.
A view component is just ultimately a means of dumping some HTML into the eventual response. When the full response is returned to the client, there's no concept of what was a view component, a partial view, etc. It's just an HTML document. Part of that HTML document is your form. That form will have an action, which ultimately should be a route that's handled by one of your controller actions. A traditional form post cause the entire browser view to change, so the response from this action should either be a full view or a redirect to an action that returns a full view. The redirect is the more appropriate path, following the PRG pattern. It is then the responsibility of that view that is eventually returned to determine how the content is constructed, using view components, partials, etc. as appropriate.
Long and short, this really has nothing to do with your view component at all. All it is doing is just dropping the form code on the page.
For forms like this that are part of the layout, it's usually best to include a hidden input that contains a "return URL" in the form. The action that handles the form, then can redirect back to this URL after doing what it needs to do, in order to give the semblance that the user has stayed in the same place.

Kindly refer to the following link:
https://andrewlock.net/an-introduction-to-viewcomponents-a-login-status-view-component/
it might be of help
public class LoginStatusViewComponent : ViewComponent
{
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
public LoginStatusViewComponent(SignInManager<ApplicationUser> signInManager, UserManager<ApplicationUser> userManager)
{
_signInManager = signInManager;
_userManager = userManager;
}
public async Task<IViewComponentResult> InvokeAsync()
{
if (_signInManager.IsSignedIn(HttpContext.User))
{
var user = await _userManager.GetUserAsync(HttpContext.User);
return View("LoggedIn", user);
}
else
{
return View();
}
}
}
Our InvokeAsync method is pretty self explanatory. We are checking if the current user is signed in using the SignInManager<>, and if they are we fetch the associated ApplicationUser from the UserManager<>. Finally we call the helper View method, passing in a template to render and the model user. If the user is not signed in, we call the helper View without a template argument.
As per your code segment you can try to modify it as follows:
public class TestFormViewComponent : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync()
{
if (_signInManager.IsSignedIn(HttpContext.User))
{
//user list display
return View("_TestForm", new TestModelPost());
}
else
{
return View();
}
}
}

Related

How to get model from Partial View in MVC

I have a main Index view from which I call view called Create, into which I pass type of the widget I want to create as a string.
Index view:
<i class="fa fa-image"></i> Create Image Widget -
<i class="fa fa-file-text"></i> Create Text Widget
Create Action:
public ActionResult Create(string wType)
{
ViewBag.wType = wType;
return View();
}
the type is then passed into view via ViewBag.wType and this is evaluated in the Create View
Create view:
#using (Html.BeginForm())
{
<section class="row">
#{
if (ViewBag.wType == "image")
{
Html.RenderPartial("~/Views/WidgetEditor/_CreateImageWidget.cshtml");
}
else if (ViewBag.wType == "text")
{
Html.RenderPartial("~/Views/WidgetEditor/_CreateTextWidget.cshtml");
}
}
</section>
}
and depending on this, appropriate partial view is loaded.
Partial views have different models so when the form is submitted, I do not know how which model is passed back. The one from _CreateImageWidget or _CreateTextWidget.
If the HttpPost controller look like this
[HttpPost]
public ActionResult Create(DisplayWidgetImageViewModel imageModel, DisplayWidgetTextViewModel textModel)
{
return new ViewResult();
}
I will get populated imageModel if _CreateImageWidget partial is chosen and textMode if _CreateTextWidget partial is chosen.
This is acceptable it the number of widgets types does not change, but this is not the case.
Is there a way to get somehow specific model from a partial view and know/find out which one it is or am I doing this completely wrong way?
You can create multiple forms in single page. You can also use different action methods per partial:
#using (Html.BeginForm("Action", "Controller")) {
Html.RenderPartial("~/Views/WidgetEditor/_CreateImageWidget.cshtml")
}
You all this without having to use Ajax.
I have used this answer to solve my problem: determine-the-model-of-a-partial-view-from-the-controller-within-mvc
there are also several other link with more resources.

Wizard in ASP.NET MVC3 and Multiple HttpPost

I am using the Wizard control described in http://afana.me/post/create-wizard-in-aspnet-mvc-3.aspx
It works great, but I need to have Multiple HttpPost within the same Controller. In my scenario, I need to add to a collection before moving to next step. In the partial view for that step. I have following set up:
#using (Html.BeginForm("AddJobExperience", "Candidate"))
{
<input type="submit" value="Add Experience" />
}
When I press the Add Experience input, it is routed to the
[HttpPost, ActionName("Index")]
public ActionResult Index(CandidateViewModel viewModel)
{
}
instead of
[HttpPost, ActionName("AddJobExperience")]
public ActionResult AddJobExperience(CandidateViewModel col)
{
}
what am I doing wrong?
It sounds like you need to break up your CandidateViewModel into separate ViewModels and your big Wizard View into separate Views so that there is one per action for each step of the wizard.
Step one they add the job experience, so have a view, viewmodel and an action for that, Step two they do whatever else and you have a separate view, viewmodel and action for that as well. etc, etc
Breaking up your CandidateViewModel into separate ViewModels will mean that you can just focus on the data required for that step, and can add the validation, then when they click submit, it posts the data to the next step.
Then, when you want to improve the UI behaviour, add some AJAX, and maybe use something like JQuery UI Tabs to make it behave more like a wizard in a desktop app.
It sounds like you still have nested forms. Don't do this, it is not valid HTML.
You have 2 options here, depending on what you are trying to achieve. If you want to post your job experiences separately one at a time, then put them in their own #using(Html.BeginForms, but don't nest them in an outer form. When a user clicks the Add Experience button, do your work on that one experience and then return a new view to the user.
If you want to post all of the job experiences at the same time, then wrap all of them in a single #using(Html.BeginForm and do not put #using(Html.BeginForm in your partial views. See another question I answered here for more info on how to post a collection of items in a single HTTP POST.
The 2nd method is what it sounds like you are trying to achieve, and for this, you should probably use AJAX to add multiple job experiences to your collection without doing a full postback. You can then do 1 HTTP POST to submit all job experiences in the collection to your wizard controller. It's not very difficult to implement a feature like this:
Given I can see the Add Experience button
When I click the Add Experience button
Then I should see a new experience partial view with input fields
And I should enter data in these fields
When I click the Add Experience button a second time
Then I should see another new experience partial view with input fields
And I should enter data in these fields
When I click the Add Experience button a third time
Then I should see a third new experience partial view with input fields
And I should enter data in these fields
When I click the Next button in the wizard
Then my controller will receive data for all 3 experiences I submitted in a single form
you need to use ActionMethodSelectorAttribute or ActionNameSelectorAttribute which allow to add new attribute on action to call different action on respective of button click
In View:
#using (Html.BeginForm())
{
<input type="submit" value="Add Experience" name="AddExperience" />
<input type="submit" value="Add Experience" name="AddJobExperience" />
}
add new class FormValueRequiredAttribute in application which extend ActionMethodSelectorAttribute class to check on which button is clicked
//controller/FormValueRequiredAttribute.cs
public class FormValueRequiredAttribute : ActionMethodSelectorAttribute
{
public string ButtonName { get; set; }
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
var req = controllerContext.RequestContext.HttpContext.Request;
return !string.IsNullOrEmpty(req.Form[this.ButtonName]);
}
}
then you should add this attribute on action to call corresponding action
In Controller
[HttpPost]
[FormValueRequired(ButtonName = "AddExperience")]
public ActionResult Index(CandidateViewModel viewModel)
{
return View();
}
[HttpPost]
[ActionName("Index")]
[FormValueRequired(ButtonName = "AddJobExperience")]
public ActionResult AddJobExperience_Index(CandidateViewModel viewModel)
{
return View();
}
Note if your Html.BeginForm method in Index.cshtml then you don't need specify ActionName attribute on Index Action, now AddJobExperience_Index act same as Index Action.

server side validation in the main view cause also validation in partial view, how to stop that?

I have layout with login partial view (username, password and submit button) and models ( some controls and submit button)with validation(server side and client side) displayed in normal views (#RenderBody() in layout).
My problem is when do server side validation in any of my views it also validate the login partial view because it execute the httppost function of the login. how can I stop that??
login view controller
[HttpGet]
public ActionResult LogOn()
{
return PartialView();
}
//
// POST: /Account/LogOn
[HttpPut]
public ActionResult LogOn(LogOnModel model)
{
if (ModelState.IsValid)
{
if (MembershipService.ValidateUser(model.UserName, model.Password))
{
FormsService.SignIn(model.UserName, model.RememberMe);
ViewBag.UserName = model.UserName;
}
else
{
ModelState.AddModelError("", Resources.Account.Account.LoginFailureText);
}
}
return PartialView(model);
}
and model controller
public ActionResult MyModel()
{
ViewBag.DisplayThxMsg = false;
return View();
}
[HttpPost]
public ActionResult MyModel(Models.FeedbacksModel feedback)
{
if (ModelState.IsValid)
{
//do something
}
else{
//do another thing
}
return View(feedback);
}
I find your question very difficult to understand. Im guessing your problem is you have a login partial control displayed as part of site layout and is shown on all pages. So while submitting any page, the username password validation kicks in, and you want to prevent that.
Understand that all validation # server - side happens while model binding, As properties are bound to the posted fields, the attributes on the fields are looked at and honored / catered to. So to prevent server side validation simply put the login partial view in it's own form so it is not sent while submitting other forms on the page.
In short have 2 forms - one form for login and one for feedback. Don't put all input fields in the same form.
If you still have validation errors after that, then it is because of other reasons like, type conversion problems. The default model binder will add some errors for basic type conversion issues (for example, passing a non-number for something which is an "int").The sample DataAnnotations model binder will fill model state with validation errors taken from the DataAnnotations attributes on your model.
EDIT
If you look at line number 125
#using (Html.BeginForm()){Html.RenderAction("LogOn", "Account");}
You have the above code which will render the login form.
It will do so inside the other form at line 45
<form id="form1" runat="server" method="post">
This has no end tag therefore it will encompass the whole document till </html>
You should change the structure from
<form id="form1" runat="server" method="post">
#using (Html.BeginForm()){Html.RenderAction("LogOn", "Account");}
</form
to
<form id="form1" runat="server" method="post">
</form>
#using (Html.BeginForm()){Html.RenderAction("LogOn", "Account");}
This line #using (Html.BeginForm()){Html.RenderAction("LogOn", "Account");} will render this form <form id="LoginView1" action="LogOn"> and all child elements of it.
LATEST EDIT
In your layout page use this :
#Html.Partial("~/Views/Shared/LogOnPartial.cshtml", new LogOnModel())
instead of this :
#Html.Action("LogOnPartial", "Account")
The reason why it all works is, the LogOnPartial method marked with [HttpPost] is called because the request was in a POST context. What you want is, You just need the view without the action executing even when POSTing. The above code does that. It renders the view without calling the action method. MVC3 is sort of a stupid servent : It only knows that it should call the Action method marked with [HttpPost] when the request is in a post context. It doesn't know that the request is in a post context for another action (index) and not this one (logonpartial). So now you can remove this method
public ActionResult LogOnPartial()
{
return PartialView();
}
which will no longer be used.
Note that you need to change the account controller's LogOnPartial Method to return
return RedirectToAction("Index","Home"); instead of return PartialView(model); on successful login. And on FAILURE you cannot render a partialview as you have coded. You must return an entirely new View. It must neither be index nor LogonPartails - just return return View("Login_Error_View"); which has its own layout. Otherwise it will be difficult to control the workflow.

Best method to pass value in MVC

I am still new to MVC, so sorry if this is an obvious question:
I have a page where the user can choose one of several items. When they select one, they are taken to another form to fill in their details.
What is the best way to transfer that value to the form page?
I don't want the ID of the item in the second (form) pages URL.
so it's /choose-your-item/ to /redemption/ where the user sees what was selected, and fills the form in. The item selected is displayed, and shown in a hidden form.
I guess one option is to store in a session before the redirect, but was wondering if there was another option.
I am using MVC3
Darin Dimitrov's answer would be best if you don't need to do any additional processing before displaying the /redemption/ page. If you do need to some additional processing, you're going to have to use the TempDataDictionary to pass data between actions. Values stored in the TempDataDictionary lasts for one request which allows for data to be passed between actions, as opposed to the values stored in the ViewDataDictionary which only can be passed from an action to a view. Here's an example below:
public ActionResult ChooseYourItem()
{
return View();
}
[HttpPost]
public ActionResult ChooseYourItem(string chosenItem)
{
TempData["ChosenItem"] = chosenItem;
// Do some other stuff if you need to
return RedirectToAction("Redemption");
}
public ActionResult Redemption()
{
var chosenItem = TempData["ChosenItem"];
return View(chosenItem);
}
If you don't want the selected value in the url you could use form POST. So instead of redirecting to the new page, you could POST to it:
#using (Html.BeginForm("SomeAction", "SomeController"))
{
#Html.DropDownListFor(...)
<input type="submit" value="Go to details form" />
}
To help others, this is how I resolved my issue of needing multiple buttons posting back, and wanting to pass a different Id each time.
I have a single form on the page, that posts back to my controller:
The Form:
#using (Html.BeginForm("ChooseYourItem", "Item", FormMethod.Post))
{
And the code
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult ChooseYourItem(string itemId)
{
TempData["ITEMID"] = itemId
return RedirectToAction("Create", "Redemption");
}
Then, inside the form, I create buttons whose name == "itemId", but has a different value each time.
For example
<strong>Item 1</strong>
<button value="123" name="itemid" id="btn1">Select</button>
<strong>Item 2</strong>
<button value="456" name="itemid" id="btn2">Select</button>

Calling another Controller post method

I am using ASP.NET MVC 3 for my website.
I have created a partial view that has "Previous, Next and Save" buttons. I am calling this partial view on my master page.
My requirement is that on what ever View I am I must be able to call different Save methods in different controllers by passing respective Model data to controller actions.
Example
I have 4 step data input, I have a different controller for each step.
If i am on step 1 and i click Save the form Values should go to Step1Controller's action method,
If I am on step 2 then post should call Step2Controller
Something like this this:
public ActionResult Save(GenericModel model)
{
//use reflection to find out model type
//call appropriate controller action with model
return RedirectToAction("Create", new { Controller = "Conference", Action = "Create" });
}
This save method will be called for Save button on the Master page. How can I achieve this?
Are the forms on separate actions on the controllers?
If so, just set the form action on each page to point to the relevant controller. So form 1 is
<form method="post" action="/step1controller/action">
Form 2 is:
<form method="post" action="/step2controller/action">
Does that solve your problem?
I would create a private method in the controller and call in every part of the first steps.
private bool Save(GenericModel model)
{
......
}
[HttpPost]
public bool SaveStep1(GenericModel model)
{
this.Save(model);
}
[HttpPost]
public bool SaveStep2(GenericModel model)
{
this.Save(model);
}

Resources