I am looking for best practices on how to create a reusable "control" for use on several MVC 3 views. I could make an Html helper extension method (programmatically or with the declarative helpers in razor) or I could create a partial view.
The trick, in my case is that I need to do more than just dump some HTML in precisely the spot that the view calls the helper/partial. In addition to putting some HTML markup in that spot, I also need to add some javascript code to make it work. Normally, I would put this code elsewhere in the page (e.g. the bottom). This is of course strictly not required. Also, note, the javascript is not control instance specific. In other words, it is possible to write javascript that finds all instances of the control HTMl markup on a page and "activates" it with appropriate events, etc.
I have thought of several possibilities.
- Have the helper/partial dump out the HTML and a
<script>
tag right in the place it is called. This seems like a bad idea since it means the control could only be used once per page.
- Have two helpers. One to output the HTML markup, and a second that spits out the javascript. The second would be called only ever once and would be called from the bottom of the page so that the script ends up in the right place. If I invented a second control that was like this, I would have 4 helpers (2 for HTML, 2 for Javascript).
- Invent some kind of ScriptManager that controls can register javascript with. The scriptmanager would have some kind of helper that would be added to the bottom of pages and would dump out the javascript for all controls that had registered some snippet of script. The call to the scriptmanager helper could be in the _layout.cshtml so that it automatically happens on any page that needs it. It would do nothing when a page didn't need it. This doesn't seem very MVC-ish, I guess.
- Just have a helper that spits out the HTML and then add the javascript to the site.js that is included on every page. The javascript would be smart enough to not do anything if it could not find any relevant markup on the page (markup would have a wrapper element with a particular class). There would be overhead of this search for markup on all pages though, including pages that don't have any of these controls.
- Same as #4, but put the javascript in its own .js file and include it only on pages that use the control. This is more efficient for pages that don't use the control, but it is an additional .js file and associated HTTP request for pages that do.
I am leaning towards #4, but is this a solved problem? Is there a better, 6th option?
My company is employing MVCContrib portable areas to package the code into a DLL for reusable "components"
Each of these components can be called via an extension method. For example:
Html.Components().ShowPoll();
Within each of these components, there is a way to register multiple css/js files that are embedded resources. Our main site will extract the resource files and minify + combines them before serving it.
The component will also register any on page event that will be called during the Document.OnReady event of JQuery. This allows each of the components to be mini-sites, standalone functionality with its own routes, models, and views.
Across the entire site, the same zip up JS for all the components are served. One because the file will be cached, and two - removes the complexity of determining what components are on the page and the resources it needs.
Let me know if you want more information regarding this setup.
I managed this issue with a layout page that had a section called jsCode.
@RenderSection("jsCode",false)
Then when I need it, I create in the content page:
@section jsCode
{
<script type="text/JavaScript" src="myJsCode.js"></script>
}
you could also create a helper that spits out the scripts you need for a particular page and encapsulate it that way, like this:
@helper MyJSFunction(string FunctionName){
switch(FunctionName)
{
case "firstFN":
<text>
<script type="text/JavaScript">
function SaySomethingSilly()
{
var sillySaying="We are the knights who say 'ni'!";
alert(sillySaying);
}
</script>
</text>
break;
case "secondFN":
<text>
<script type="text/JavaScript">
function SaySomethingElse()
{ var sillySaying="We are looking for a shrubbery...";
alert(sillySaying);
}
</script>
</text>
break;
}
}
Then my content page (assuming I want both functions) looks like this:
@{
Page.Title="Using helpers to create scripts";}
@section jsCode
{
@JSHelpers.MyJSFunction("firstFN")
@JSHelpers.MyJSFunction("secondFN")
}
<p>It works!</p>
which produced the output:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Using helpers to create scripts</title>
<script type="text/JavaScript">
function SaySomethingSilly()
{
var sillySaying="We are the knights who say 'ni'!";
alert(sillySaying);
}
</script>
<script type="text/JavaScript">
function SaySomethingElse()
{ var sillySaying="We are looking for a shrubbery...";
alert(sillySaying);
}
</script>
</head>
<body>
<p>It works!</p>
</body>
</html>
Now if you wanted to get super-efficient, (though other words come to mind) you could pass the function a JSON array or a dictionary or even a string array with the names of the scripts you wanted to use, or the JavaScript to import, or whatever, and just use a single call to the helper. I think separate calls is easier to maintain, regardless of how you load the return values of the helper function, because you can see cleanly at a glance which scripts you are using on a given page, and to eliminate or add one is a simple one-line change rather than changing one element of an array.
As always, your mileage may vary, but I ran the sample based on this code in WebMatrix Beta 2 without any issues.
Lisa Z. Morgan
http://www.lairhaven.com