ASP.NET MVC: Dynamically adding an existing View as a Partial View to a parent

Let’s say you have a pre-existing Model/View/Controller. For my purposes I will call it FirstController, FirstModel and the view, Details.

In this post I will explain how to reuse this form and model in another View/Controller which will become it’s parent. I will also show how to dynamically add this child view to the parent via Ajax. This way we can conditionally add the view based on the user’s input.

Here is our pre-existing MVC:

namespace ChildModel.Controllers
{
    public class FirstController : Controller
    {
        public ActionResult Details()
        {
            var model = new FirstModel { Name = "Gilles",
                                         Number = 42 };
            return View(model);
        }

        [HttpPost]
        public ActionResult Details(FirstModel model)
        {
            if (!ModelState.IsValid)
            {
                return View();
            }

            return RedirectToAction("Index", "Home");
        }
    }
}

namespace ChildModel.Models
{
    public class FirstModel
    {
        public string Name { get; set; }
        public int Number { get; set; }
    }
}

With this simple view:

@model ChildModel.Models.FirstModel

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>First - Details</title>
</head>
<body>
<div>
<h1>First - Details</h1>
@using (Html.BeginForm("Details", "First"))
        {
            @Html.LabelFor(model => model.Name, "Name")
            @Html.TextBoxFor(model => model.Name)

            @Html.LabelFor(model => model.Number, "Number")
            @Html.TextBoxFor(model => model.Number)

            <input type="submit" value="Submit" />
        }</div>
</body>
</html>

first1

Here is the existing Model/View/Controller which will eventually become the parent:

namespace ChildModel.Controllers
{
    public class ParentController : Controller
    {
        public ActionResult Index()
        {
            var model = new ParentModel { IsChecked = true };
            return View(model);
        }

        [HttpPost]
        public ActionResult Submit(ParentModel model)
        {
            if (!ModelState.IsValid)
            {
                return View();
            }

            return RedirectToAction("Index", "Home");
        }
    }
}

namespace ChildModel.Models
{
    public class ParentModel
    {
        public bool IsChecked { get; set; }
    }
}
@model ChildModel.Models.ParentModel

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Parent</title>
</head>
<body>
<div>
<h1>Parent</h1>
@using (Html.BeginForm("Submit", "Parent"))
        {
            @Html.LabelFor(model => model.IsChecked, "Is Checked ?")
            @Html.CheckBoxFor(model => model.IsChecked)

            <input type="submit" value="Submit" />
        }</div>
</body>
</html>

parent1

Let’s start by including First into Parent. We will do the dynamic Ajax part afterwards.

Since we don’t want to include the form in our parent page, because our parent already has a form and submit button, we will take out the fields we want to reuse and put them in their own partial view.

_FirstFields.cshtml:

@model ChildModel.Models.FirstModel

@Html.LabelFor(model => model.Name, "Name")
@Html.TextBoxFor(model => model.Name)

@Html.LabelFor(model => model.Number, "Number")
@Html.TextBoxFor(model => model.Number)

And call this Partial View in First/Details.cshtml:

@using (Html.BeginForm("Details", "First"))
{
    Html.RenderPartial("_FirstFields");

    <input type="submit" value="Submit" />
}

In our ParentModel we will need to add a new property of type FirstModel:

public class ParentModel
{
    public bool IsChecked { get; set; }
    public FirstModel ChildModel { get; set; }
}

In our Parent/Index.cshtml view we can include the partial view. Notice the use of the ViewDataDictionary in RenderPartial. If we simply asked for the view, the model binder wouldn’t be able to get the data back on a Post. The HtmlFieldPrefix in our ViewDataDictionary is set to the name of our child model property.

This will have for effect of changing the Ids and names of the fields on the partial view allowing the model binder to correctly bind the fields.

@using (Html.BeginForm("Submit", "Parent"))
{
    @Html.LabelFor(model => model.IsChecked, "Is Checked ?")
    @Html.CheckBoxFor(model => model.IsChecked)

    Html.RenderPartial("../First/_FirstFields",
                       Model.ChildModel,
        new ViewDataDictionary(Html.ViewData)
        {
            TemplateInfo = new TemplateInfo
            {
                HtmlFieldPrefix = "ChildModel"
            }
        });

    <input type="submit" value="Submit" />
}

Now we have completed the first part by including a child model/view in our parent. Let’s add the Ajax part.

We will remove our RenderPartial and replace it with a div element which will serve to host the dynamic html code. We will also add an Ajax action link to call a new method on our ParentController.

@using (Html.BeginForm("Submit", "Parent"))
{
    @Html.LabelFor(model => model.IsChecked, "Is Checked ?")
    @Html.CheckBoxFor(model => model.IsChecked)

    @Ajax.ActionLink("Add Child Form", "GetChildElement",
                     "Parent",
                    new AjaxOptions
                    {
                        AllowCache = false,
                        InsertionMode =
                                InsertionMode.Replace,
                        HttpMethod = "Get",
                        UpdateTargetId = "childContainer",
                    })
<div id="childContainer"></div>
<input type="submit" value="Submit" />
}

In our ParentController we add the Ajax action method GetChildElement this time setting the prefix manually:

public PartialViewResult GetChildElement()
{
    ViewData.TemplateInfo.HtmlFieldPrefix = "ChildModel";
    return PartialView("../First/_FirstFields");
}

Don’t forget to add Ajax capabilities to your project like detailed here.

And everything should be working.

parent2

JavaScript tip 3: Namespaces

To avoid collisions, declarations overwriting each other and such, we can use JavaScript to create namespaces.

While not supported directly by the language the pattern is pretty common.

First let’s imagine we are including two separate JS files which both include a function named customRounding.

// first included file
var customRounding = function(number) {
  return number.toFixed(2);
}

// second included file
var customRounding = function(num) {
  return num.toFixed(3);
}

In our index.html we are calling this function:

console.log(customRounding(1.23456));

The problem is that the second file will have overwritten the declaration in the first file. To prevent such a collision, it is advisable to use a namespace likewise:

// first included file
var NS1 = {
  customRounding: function(number) {
    return number.toFixed(2);
  }
};

// second included file
var NS2 = {
  customRounding: function(num) {
    return num.toFixed(3);
  }  
};

// in our index.html file
console.log(NS1.customRounding(1.23456));

We can also use this pattern to make some variables and functions private by converting our namespace to an IIFE and then declaring the private members as variables and returning our public members:

var NS1 = (function() {
  // private stuff
  var aPrivateVar = 42;
  var aPrivateFunction = function(number) {
    alert('Hi!');
  };

  // public stuff
  return  {
    customRounding: function(number) {
      return number.toFixed(2);
    }  
  };  
})();

 

 

JavaScript tip 2: Function factories using closures

Here’s how we can use closures to build “function factories”:

var addElement = function(destination) {
  return function(element) {
    document.getElementById(destination)
            .appendChild(element);
  }
};

var addElementToMain = addElement("main");
var addElementToAlternate = addElement("alternate");

var p = document.createElement("p");
p.appendChild(document.createTextNode("Vanilla JS"));

addElementToMain(p);

You can also check out and run this example as a CodePen here.

In this example, addElement is a function which will return a new function with the destination parameter closed. This way we can create functions to add child elements to various parent elements.

The addElementToMain and addElementToAlternate are two functions created this way.

JavaScript tip 1: Function Expressions

If you are creating your JavaScript functions using Function Declarations, a good tip is to switch to Function Expressions.

First here is an example of a Function Declaration:

function declared() {
  console.log("First declared function.");
}

Sadly because of variable hoisting, which means that declarations are hoisted to the top of their scope in JavaScript, we can run into some problems likewise:

function declared() {
  console.log("First declared function.");
}

declared();

function declared() {
  console.log("Overwriting declared function.");
}

In this case since both Function Declarations are hoisted to the top we end up with “Overwriting declared function.” being logged out to the console.

Here’s what the code looks like after hoisting:

function declared() {
  console.log("First declared function.");
}

function declared() {
  console.log("Overwriting declared function.");
}

declared();

Function Expressions provide another way to create a function and avoid this kind of problem. Here’s the previous example using Function Expressions:

var expression = function() {
  console.log("First Function Expression.");
};

expression();

var expression = function() {
  console.log("Not gonna happen!");
};

Which this time is returning “First Function Expression.”. While both expression variable declarations are hoisted up, their definitions aren’t.

Here’s this last example after hoisting:

var expression = undefined;
var expression = undefined;

expression = function() {
  console.log("First Function Expression.");
};

expression();

expression = function() {
  console.log("Not gonna happen!");
};

It’s thus safer to use Function Expressions over Function Declarations.