Dynamically Adding JavaScript To Your ASP.NET Master Page From A Child Page

A common challenge when working with ASP.NET master pages is how to dynamically add JavaScript that is relevant to a specific child page.

In other words, maybe your site has 10 child pages that share one master page / template. One of them is, let’s say, a contact us / directions page, and on that page, you want to display a map from one of the many mapping APIs out on the Web; for simplicity’s sake, let’s use the Google Maps API.

Proper implementation of the Google Maps API requires you to call, in the head section of your page’s HTML, the API library. So, you’re left with four options:

  1. Import the library in the master page’s code for all child pages, and thus incur that overhead for every page using that master page / template;
  2. Create a second version of the master page, containing the code, and apply that master to the child page that needs it;
  3. Don’t use a master page for the page that needs the API; have that erstwhile “child” page contain all the HTML it needs; or
  4. Figure out a way to add the needed code to the head of the master page for the child that needs it.

Option 1 seems reasonable, but it’s messy; although simply bringing in the Google Maps API for every page isn’t resource-intensive in this strong-computer, fast-bandwidth world, it’s sloppy at best and a potential security risk at worst (although the Google Maps API is pretty much safe to import, even if you’re not going to use it).

Option 2 leaves you with what is fundamentally two versions of the same thing, which is pretty much the dictionary definition of “inelegant.” Option 3 isn’t any better.

Option 4 is the best approach, and thankfully, there are a couple of ways to do it in ASP.NET. I’m going to describe the way to do it by dynamically adding HTML elements to the master page’s head, but first, a quick digression on using a PlaceHolder control.

Using The PlaceHolder Control

In the earliest iterations of ASP.NET, you couldn’t add a PlaceHolder, Panel or similar control outside of the body section of an ASP.NET page. In ASP.NET 2.0, it was permissible but discouraged; in ASP.NET 3.5, the default page templates included an automatic PlaceHolder control in the head section.

Dynamically adding controls to a PlaceHolder control is really pretty straightforward. Basically, you find the relevant PlaceHolder control, create whatever tag / control you want to add to it, and then just add the thing. So here’s the markup for your master page’s head section:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<head runat="server">
    <title>Untitled Page</title>
    <asp:PlaceHolder ID="PlaceHolder1" runat="server" />
</head>

You then just add the following code to the child page’s Page_Load event handler:

Sub Page_Load(ByVal Sender As Object, ByVal E As EventArgs) Handles Me.Load
	'this subroutine assumes you have a label control ID'ed as lblError on your child page

	'create local PlaceHolder object
	Dim objPH As New PlaceHolder()
	'reference local PlaceHolder object as the head's PlaceHolder
	objPH = Master.FindControl("PlaceHolder1")

	'if we were able to reference the head's PlaceHolder ...
	If Not objPH Is Nothing Then
		'create a string for the inner HTML of the script
		Dim sbScript As New StringBuilder()
		sbScript.Append("function myFunction() {" & vbCrLf)
		sbScript.Append(vbTab & "alert('Hello World!');")
		sbScript.Append("}")
		sbScript.Append(vbCrLf)
		sbScript.Append("window.onload = myFunction;")

		'create script control
		Dim objScript As New HtmlGenericControl("script")
		'add javascript type
		objScript.Attributes.Add("type", "text/javascript")
		'set innerHTML to be our StringBuilder string
		objScript.innerHTML = sbScript.ToString()

		'add script to PlaceHolder control
		objPH.Controls.Add(objScript)
	Else
		lblError.Text = "Could not find PlaceHolder1 on master page."
	End If
End Sub

This is a perfectly acceptable way of adding JavaScript to an ASP.NET 2.0 or later master page from a child page. But I don’t like it for a few reasons, not the least of which being that it seems to me wasteful to have a server control hanging around on your master page, if you’re only going to use it once in a blue moon.

Sure, I recognize that ASP.NET pages are compiled and often cached, which will limit performance issues; but again, having that PlaceHolder hanging around seems inelegant to me. If I was constantly tinkering with the head of my master page, adding stuff on a page-by-page basis for nearly every page in the site, I would probably stick with this approach. But I’m interested in a special case, so my preferred method is to directly inject scripts, without using a PlaceHolder at all.

Directly Adding Scripts To A Master Page’s head Without A PlaceHolder Control

We actually don’t even need the PlaceHolder control at all in order to add JavaScript to a master page’s head section. We can just go ahead and dynamically build some HtmlGenericControls to contain our JavaScript, then inject them.

So, here’s our simplified master page head section:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<head runat="server">
    <title>Untitled Page</title>
</head>

Note that the head is set to runat the server. That’s because, if it is not running at the server, we can’t access the master page’s Page.Header property, which we need to be able to access in order to directly add our script tags to the page (using the ControlCollection‘s Add method). (Also, if the head wasn’t set to run at the server, you wouldn’t be able to change the page title with the child page’s title property.

A brief aside on HtmlGenericControl and ASP.NET’s lack of support for client-side scripting: It speaks volumes about Microsoft’s “a day late and a dollar short” Web technologies — or, more specifically, their emphatic (I’d say stubborn) focus on server-side, proprietary code authoring — that it took an add-on to ASP.NET 2.0 to provide AJAX support. It’s even more damning that even as late as ASP.NET 3.5, they haven’t created a direct script control.

And yes, I understand that I could always go and write one myself, if I wanted. The point is, I shouldn’t have to drag this behemoth into Web 2.0; they ought to wake up, smell the coffee and help me use their technologies to produce the results I can get with other solutions.

When I add JavaScript to a page dynamically, I prefer to place my script in a separate file, then call it as the src attribute of the script tag I am adding.

I prefer that approach because it prevents me from having to build a string in my ASP.NET code behind file for the body of the script tag I will inject. I also like the level of abstraction of separating code, HTML and stylesheets into distinct files. And, of course, if I am going to use the same script on more than one page, it makes sense to have one copy of the code that gets reused on each page.

Step 1: Create The JavaScript File

We need to start with a simple, local JavaScript file, which will add our map to our page:

function makeMap() {
	if (GBrowserIsCompatible()) {
		// get the div which will contain the map
		var mapDiv = document.getElementById("map");
		// set div padding to 0; resize for map
		mapDiv.style.padding = "0";
		mapDiv.style.width = "780px";
		mapDiv.style.height = "400px";

		// place map on page
		var map = new GMap2(mapDiv);

		// create point for ball of twine
		var point = new GLatLng(39.50933, -98.433734);

		// center map at point of interest
		map.setCenter(point, 16);

		//use default controls
		map.setUIToDefault();

		// create marker for ball of twine
		var marker = new GMarker(point);
		// add marker to map
		map.addOverlay(marker);

		// create infoWindow HTML string
		var infoString = "<p><strong>The World's Largest Ball Of Twine</strong> is right here, in <a href=\"http://www.roadsideamerica.com/story/8543\">Cawker City, KS!</a></p>";

		// add info window for marker
		map.openInfoWindowHtml(point, infoString);

		// add event listener to toggle info window on marker click
		GEvent.addListener(marker, "click", function() {
	        var info = map.getInfoWindow();
	        if (info.isHidden()) {
                info.show();
                map.openInfoWindowHtml(point, infoString);
	        }
	        else {
	            info.hide();
	        }
		});
	}
}

window.onload = makeMap;

Note that the JavaScript file invokes the windows.onload event to actually draw the map. It’s bad policy to add onload events to your pages’ body tags. Actually, even that is kind of lazy; we should actually write an event handler that can handle our load requests, or use a library such as jQuery, which includes methods to wire up script onload requests.

Another aside on auto JavaScript wireups in ASP.NET 3.5: I realize that in ASP.NET 3.5, there are a number of ways to wire up JavaScript functions on page load, including simply naming your JavaScript onload function “pageLoad().” However, I have not been able to get those methods to work in a master-child page relationship such as I describe here. That may well be because I’m not doing it right; but since I can’t get those methods to work, but I can get what I am doing here to work, this is how I am proceeding.

Step 2: Create The Child Page’s Code Behind To Insert Our Scripts

With the local JavaScript file on hand, we can now put together the code behind on our child page, to import into our master page the needed files. Again, this code is applied to the child page, not the master page.

The first thing we do is add the call to the remotely hosted Google Maps API, which simply means creating a script tag, adding type and src attributes, then adding the control to the page:

Sub ImportGMapLibrary()
	'create Google Maps API library script tag
	Dim objLibrary As New HtmlGenericControl("script")

	'add attributes
	objLibrary.Attributes.Add("type", "text/javascript")
	objLibrary.Attributes.Add("src", "http://maps.google.com/maps?file=api&v=2&sensor=false&key=ABQIAAAAynoIQZ5YX-BdZ9UvBsREmBRZz1l8SlBkcLc8c82DcC4MDIosshQLjjGtypnNhjFkfxXwjfObtAgcyw")

	'add to master page
	Master.Page.Header.Controls.Add(objLibrary)
End Sub

A couple of notes about the src attribute above:

  • The API key above will only work on dougv.net. You need an API key of your own if you want to run this demo. It’s free from Google.
  • Note that we don’t change the ampersands into HTML entities when we add them as a control attribute. ASP.NET will automatically convert them into entities for us when it renders the control. If you forget, and use &amp; instead of just & in your src URL, you’ll get an error from Google telling you that your API key is bad.

With the API library called, we can now add a reference to our local JavaScript file:

Sub ImportGMapScript()
	'create Google Maps API library script tag
	Dim objScript As New HtmlGenericControl("script")

	'add attributes
	objScript.Attributes.Add("type", "text/javascript")
	objScript.Attributes.Add("src", "/demos/add_js_to_master_page/map.js")

	'add to master page
	Master.Page.Header.Controls.Add(objScript)
End Sub

And finally, we add both those subroutines to the Page_Load event handler for our child page:

Sub Page_Load(ByVal Sender As Object, ByVal E As EventArgs) Handles Me.Load
	ImportGMapLibrary()
	ImportGMapScript()
End Sub

And viola: Your master page now has the proper script references on it, and your JavaScript map should show up.

You can see a working demo of this here: http://www.dougv.net/demos/add_js_to_master_page/

I distribute code under the GNU GPL. See Copyright & Attribution for details.

All links contained in this post, on delicious.com: http://delicious.com/dougvdotcom/dynamically-adding-javascript-to-your-asp-net-master-page-from-a-child-page

2 thoughts on “Dynamically Adding JavaScript To Your ASP.NET Master Page From A Child Page

Leave a Reply