In MVC, there is this cool concept of a partial view. It sort of sounds like an old-school Control. And in reality, it's pretty close to that. Except, there's a huge problem if you actually want to use them as such. Let's say you have two forms on one page, and you want to submit each one separately. Why would you do that, you ask? Well, suppose you have a page for managing your two-step authentication profile. One the top of the page, you have a way to edit your primary phone number, and on the bottom page, you need to have a way to add a backup number. Technically, these are two separate scenarios, but there are reasons (into which I don't want to delve at the moment) for having them on the same page. So how can you solve this?
The Setup
To try this out, I created a simple MVC project, and added two Partial Views (PartialTop and PartialBottom). They basically contain a simple form (each) that posts to different actions depending on the scenario. The page looks (by default) like this:
@{
ViewBag.Title = "About";
}
@Html.Partial("PartialTop")
@Html.Partial("PartialBottom")
<p>Some content for the page.</p>
Nothing special, right? That's the plan. Now, let's explore the potential options.
The views look like this:
@model PartialForms.Models.BottomModel
<h4>This is our bottom view.</h4>
@using (Html.BeginForm("Bottom", "Home"))
{
@Html.ValidationMessageFor(x => x.SampleValue)
@Html.TextBoxFor(x => x.SampleValue)
<button type="submit">Submit</button>
}
<hr />
@model PartialForms.Models.TopModel
<h4>This is our top view</h4>
@using (Html.BeginForm("Top", "Home"))
{
@Html.ValidationMessageFor(x => x.SomeSpecialValue)
@Html.TextBoxFor(x => x.SomeSpecialValue)
<button type="submit">Store!</button>
}
<hr />
Option 0A - Submit & Redirect (bad)
The very basic option is to program our actions to simply save the value and return a redirect to the main action. Something like this:
[HttpPost]
public ActionResult Bottom(BottomModel model)
{
// save to database here
return RedirectToAction("About");
}
[HttpPost]
public ActionResult Top(BottomModel model)
{
// save to database here
return RedirectToAction("About");
}
We then load the values from the database upon the request hitting the server, and all is well and updated. Until there's a validation error. Then, things start to get complicated. When you do a redirect you basically loose your ModelState. Ah, you say, but I'll just return the main (top) view from that action, like so:
[HttpPost]
public ActionResult Bottom(BottomModel model)
{
if (!ModelState.IsValid)
return View("About", model);
// save to database here
return RedirectToAction("About");
}
This works great! If you only have one model. There is a potential solution, by using composition: you create a "AllModel" that contains BottomModel and TopModel and basically send it back. But if you're not careful (as demonstrated below), you get NullReference exceptions all over the place. So, basically, this approach won't really work.
Option 0B - The Single Form Approach (bad)
I'm willing to bet most inexperienced developers and/or developers who lack interest in UX, will opt for this approach. It basically implies creating the same AllModel, and simply submitting both values to the same target action (e.g. Post to About action).
public ActionResult About(AllModel model)
{
if (!ModelState.IsValid)
return View(model);
// save to database
return RedirectToAction("About");
}
Note: I didn't really have to redirect to action, but let's assume my db fetching logic is there, so that makes sense. But, the problem with this is, that I can leave the above textbox empty, and only enter a value for the bottom one - but my controller logic will never know the difference. And there is no way you can implement heuristics on that one without over complicating things (for forms that are more than just a simple text box, of course).
Option 1 - JavaScript (better)
Basically, the default way for most modern applications today, is to resort to using JavaScript. There are upsides and there are downsides to this approach. First though, let's take a look at how this might work. The basic change is to modify our controller actions to return a Partial View:
[HttpPost]
public ActionResult Bottom(BottomModel model)
{
if (ModelState.IsValid)
{
// save to database here
}
return PartialView("PartialBottom");
}
[HttpPost]
public ActionResult Top(BottomModel model)
{
if (ModelState.IsValid)
{
// save to database here
}
return PartialView("PartialTop");
}
If we try submitting a form now, we get some strange (but expected) results:
The idea now is, to intercept the submit of the form, and handle it with JavaScript - then replacing the form with the HTML returned. There are a lot of resources available online on how to do this. For example, this StackOverflow question.
<script type="text/javascript">
$(document).ready(function () {
$('form').each(function () {
setupForm($(this));
});
});
function setupForm(form) {
var f = $(form);
f.ajaxForm(function (res) {
console.log(f.parent('div'));
console.log(res);
f.parent('div').html(res);
console.log(res);
$(f.parent('div')).select('form').each(function () {
setupForm($(this));
});
});
}
</script>
Option 2 - Single Form with a Parameter Separator (CLEANER)
In some cases, using JavaScript is not a good option, but you still need to have those different forms. So, another option to try is using a parameter separator.
Let's get back to that AllModel approach from above, and introduce one more property to is. For example:
public string Button { get; set; }
From here, we change the main page to (1) have a model definition, and (2) wrap the partials in one single form:
@model PartialForms.Models.AllModel
@using (var f = Html.BeginForm())
{
@Html.Partial("PartialTop")
@Html.Partial("PartialBottom")
}
The last thing we have to do is change the partial views themselves. We have to change the model, first, and then add a little magic to the submit button:
@model PartialForms.Models.AllModel
<div>
<h4>This is our top view</h4>
@Html.ValidationMessageFor(x => x.Top.SomeSpecialValue)
@Html.TextBoxFor(x => x.Top.SomeSpecialValue)
<button type="submit" value="top" name="button">Store!</button>
<hr />
</div>
The key part are the name and value attributes on the button tag. These will help our action determine what to do based on what was pressed. The bottom view, by the way, has been modified the same way, but the value is obviously set to "bottom".
The final part of this solution is to wire up the action. We can use the action from the "main view", in our Demo, that's the About action. Here's what I changed:
[HttpPost]
public ActionResult About(AllModel model)
{
switch (model.Button)
{
case "top":
// do validation and storage logic for the top part
break;
case "bottom":
// do validation and storage logic for the bottom part
break;
}
// save to database
return View(model);
}
The key difference here, compared to our initial approach with the AllModel is that we don't need some complex logic to figure out what action to perform. We simply let the views tell us. From there we can actually perform validation that only relates to that particular data.
So there we go, I hope this gives you an idea into handling Partial Views and forms using ASP.NET MVC. If you have any questions, let me know.