Introduction
Nowadays, there are a lot of Web frameworks (in PHP, Java, .NET, etc.) allowing to generate HTML layout with the help of certain template engines. Indeed, sometimes, these frameworks are quite powerful and amazing. The only problem is that these template engines introduce their own external DSLs which are not native for .NET developers. It makes the usage of such template engines a little bit difficult, not to say more.
The Sharp DOM project allows you to develop HTML layout as C# code using all the power of the modern object-oriented C# 4.0 language. You can use features of C# 4.0 like inheritance, polymorphism, etc. In other words, designing of HTML layout can be transformed into the development of strongly typed HTML code. :-)
Usage of Sharp DOM
- Simply download DLL files from the Downloads page - http://sharpdom.codeplex.com/releases
- Reference SharpDom.dll file and SharpDom.Mvc.dll file in your ASP.NET MVC project.
- Create a class inherited from the
SharpDomView<>
class (where the generic parameter is the view model).
- Override the
CreateTagTree()
method of this newly created class and write the HTML layout inside this method in the form of C# code.
- In the controller, create the view model and populate it with proper data.
- Write some plumbing code to embed the Sharp DOM view engine into the MVC platform:
public class MvcApplication : System.Web.HttpApplication
{
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
RegisterViewEngines(ViewEngines.Engines);
}
public static void RegisterViewEngines(ViewEngineCollection viewEngines)
{
viewEngines.Clear();
viewEngines.Add(new SharpDomViewEngine());
}
}
Example of Transformation
Let's look at the source HTML layout we are going to transform from old plain HTML into strongly typed HTML:
<html>
<head>
<title>Title of the page</title>
<meta http-equiv="Content-Type"
content="text/html;charset=utf-8">
<link href="css/style.css"
rel="stylesheet" type="text/css">
<script
href="http://www.codeproject.com/JavaScripts/jquery-1.4.2.min.js"
type="text/javascript">
</head>
<body>
<div>
<h1>Test Form to Test</h1>
<form id="Form1" type="post">
<label>Parameter</label> =
<input type="text">Enter value</input>
<input type="submit" text="Submit !" />
</form>
<div>
<p>Textual description of the footer</p>
<a href="http://google.com">
<span>You can find us here</span>
</a>
<div>
Another nested container
</div>
</div>
</div>
</body>
</html>
This could be rewritten using C# 4.0 language as:
html[
head[
title[ "Title of the page" ],
meta.attr(http-equiv: "contenttype", content: "html", charset: "utf-8"),
link.attr(href: "css/style.css", rel: "stylesheet", type: "css"),
script.attr(href:"/JavaScripts/jquery-1.4.2.min.js", type: "javascript")
],
body[
div[
h1[ "Test Form to Test" ],
form.attr(id: "Form1", type: "post")[
label[ "Parameter" ], "=", input.attr(type:"text", value: "Enter value"), br,
input.attr(type: "submit", value: "Submit !")
],
div[
p[ "Textual description of the footer" ],
a.attr(href: "http://google.com" )[
span[ "You can find us here"]
]
],
div[ "Another nested container" ]
]
]
]
As you can see, both the listings are very correlating to each other - each line in the HTML layout unambiguously correlates to the only line in C# code and vice verse. In other words, the HTML-to-C# mismatch impedance is quite small. On the other hand, a developer having some skills in the C# language could easily write strongly typed HTML code. And all the benefits of using the C# compiler are available now: compile-time checks, tight control over generated HTML by using embedded C# code, etc.
Advanced Topics
ViewModel: You can embed the values coming from your view model (that you pass from the controller to the view) into your HTML code.
public class ViewModel
{
public string Title { get; set; }
}
public override ContainerTag CreateTagTree(ContainerTag container)
{
return container
[
html[
head[
title[ Model.Title ] ],
body[
div[
span[ "Hello World !!!" ]
]
]
]
];
}
As you can see, all you have to do is to override the CreateTagTree()
method and fill it with HTML layout using C# language. The variable container
is a container into which you are to add your custom HTML layout. Finally, the container
variable is to be returned. During the construction of the HTML layout, you are free to use the view model supplied by the controller.
View Logic: Besides HTML tags expressed in the form of C#, you can embed some logic into HTML code using the so-called "operators".
Operator is some C# construct allowing you to manipulate the nested HTML layout. Now there are only two operators supported - Foreach
and If
.
ForEach
duplicates nested HTML tags using data items supplied in the form of IEnumerable
collections - so each duplicate of the nested HTML piece can be customized using the proper supplied item.
If
just "hides" nested HTML tags if the parameter is "False
". HTML tags are not cut away, just the Visibility
property is set to "False
". So HTML tags are not rendered but they are still present in the tree of tags.
public class ViewModel
{
public class UlItem
{
public string Text { get; set; }
public int Value { get; set; }
}
public string Title { get; set; }
public UlItem[] UlItems { get; set; }
}
public override ContainerTag CreateTagTree(ContainerTag container)
{
return container
[
html[
head[
title[ Model.Title ]
],
body[
div[
ul[
ForEach(Model.UlItems, (item, c) => c[
li.attr(id: item.Value.ToString())[ item.Text ]
])
],
If(Model.UlItems.Length % 2 != 0)[
span[ "Amount of items is odd ..." ]
]
]
]
]
];
}
User Controls: You can divide the HTML layout into small pieces using a-la WebForms' UserControls.
User Control is a packed form of HTML island which can be reused in other places many times. It correlates to ASP.NET WebForms' Custom Control rather well.
Here is the code for a User Control:
public class ControlModel
{
public string Text { get; set; }
public string Value { get; set; }
}
public class ControlBuilder : UserControlBuilder<controlmodel>
{
public override ContainerTag CreateTagTree(ContainerTag container)
{
return container
[
div[ "Text is " + Model.Text ],
div[ "Value is " + Model.Value ]
];
}
}
Code for the page:
public class ViewModel
{
public string Title { get; set; }
public ControlModel ControlModel { get; set; }
}
public class PageBuilder : HtmlPageBuilder<viewmodel>
{
public virtual ControlBuilder ControlBuilder
{ get { return CreateBuilder<controlbuilder>(); } }
public override ContainerTag CreateTagTree(ContainerTag container)
{
return container
[
html[
head[
title[ Model.Title ]
],
body[
div[
ControlBuilder.CreateTags(Model.ControlModel)
]
]
]
];
}
}
- Initially, during design, the user control should derive from the
UserControlBuilder
class and override the CreateTagTree
method in order to supply the needed HTML layout. The user control has its own view model which can be used for its customization.
- When using, the page builder should have a property of the user control's class type. This property should create the user control, and then this property can be used in the HTML layout directly.
- User controls could have nested user controls in the same manner as pages could have nested user controls (see such an example in the following section).
Master Pages: You can use the master-page approach similar to the same approach existing in WebForms. It is also possible to dynamically (at run-time) choose what master page to use with the current content page.
Our master pages are quite similar to ASP.NET WebForms' Master Pages - you design a master page builder which is completely separated from the content page builder. Then the master page builder's output is combined with the content page builder's output, and finally the combined resulting HTML is shipped to the browser. For it, the master page builder is to contain some placeholders. The placeholders are to be substituted in the content page builders. The chain of {master page, contain page} could be extended, and the master page could have a super master page, and so on. Another useful feature around the master page approach is the possibility to dynamically (at run-time) choose what master page should be used with the current content page.
Master page code:
public class MasterModel
{
public string Title { get; set; }
}
public class MasterBuilder : HtmlPageBuilder<mastermodel>
{
public virtual PlaceHolderDelegate FirstPlaceHolder { get; set; }
public virtual PlaceHolderDelegate SecondPlaceHolder { get; set; }
public override ContainerTag CreateTagTree(ContainerTag container)
{
return container
[
html[
head[
title[Model.Title]
],
body[
div[
PlaceHolder(FirstPlaceHolder),
span[ "some intermediary text" ],
PlaceHolder(SecondPlaceHolder, c => c[
span[ "stub for second placeholder" ]
])
]
]
]
];
}
}
Content page code:
public class ContentModel
{
public string Text { get; set; }
public string Value { get; set; }
public MasterModel MasterModel;
}
public class ContentBuilder : HtmlPageBuilder<contentmodel>
{
public virtual ContainerTag FirstPlaceHolderContent(ContainerTag container)
{
return container
[
span[ Model.Text ],
span[ "somtehing other" ],
span[ Model.Value ]
];
}
public override ContainerTag CreateTagTree(ContainerTag container)
{
return
CreateBuilder<masterbuilder>(container)
.Attach<masterbuilder>(b => b.FirstPlaceHolder = FirstPlaceHolderContent)
.CreateTags(Model.MasterModel);
}
}
- In the master page builder, you declare some placeholder delegates. Then, in the master page's HTML layout, you can use these delegates with or without predefined content for such placeholders.
- In the content page builder, you specify the functions (with signature the same as the placeholder delegates) which are to define the HTML layouts.
- In the content page builder, in the
CreateTagsTree()
method, you have to execute three steps:
- Create the needed master page (so, you can choose the master page dynamically),
- Attach your functions by creating content for the master page's placeholder delegates,
- Ask the master page to create combined content.
Templated User Controls: You can use templated user controls. In WebForms, the nearest analogy is Repeater
's TemplateItem
tag in layout.
This feature is not likely to be widely used, so please see the full example of this feature on the Documentation on the CodePlex site - http://sharpdom.codeplex.com/documentation.
Using ASP.NET MVC's HTML and URL Helpers
Although Sharp DOM view engine rather differs from the default view engine of ASP.NET MVC (= WebForms), we believe, that some useful things from the WebForms view engine could be great and helpful additions to your Sharp DOM projects. So we implemented some initial support for the Html
and Url
helper classes defined in the WebForms view engine. You can continue to use methods like ActionLink()
, ValidationMessageFor()
, etc., directly inside your HTML layout expressed in C# form. Sounds great, doesn't it? Following is an example about how the usage of MVC's helpers can be seamlessly transformed into Sharp DOM:
Example of usage of MVC helper inside WebForms view engine:
<div class="editor-label">
<%= Html.LabelFor(m => m.UserName) %>
</div>
It can be easily transformed into a Sharp DOM project like:
div.attr(class: "editor-label")[
Html.LabelFor(m => m.UserName).ToLiteral()
]
The only thing to notice is that you have to use the ToLiteral
extension method in order to convert a MvcHtmlString
object to Sharp DOM's Tag
object.
Unfortunately, full 100% support of all such helpers is not possible because they are very aligned to WebForm's internals. So our current support is limited to the helper methods returning a MvcHtmString
instance. Although the biggest majority of methods of such MVC helpers return a MvcHtmlString
object, some methods don't. Probably the most important exception is BeginForm()
/EndForm()
methods. In such cases, you will have to use similar (but not identical) things of the Sharp DOM project:
Example of usage of BeginForm()
/EndForm()
inside WebForms view engine:
<% using (Html.BeginForm()) { %>
<div>
Blah-blah-blah-blah
</div>
<% } %>
This can be transformed with the help of Sharp DOM elements into:
form.attr(action:UrlHelper.Action("LogOn"), method:"POST")[
div[
"Blah-blah-blah-blah"
]
]
As you can see, we use the UrlHelper.Action()
helper without any change to get the correct action URL.
Extensibility: There is also a possibility to traverse the generated tree of HTML tags and alter it with the help of LINQ. Not only traverse, but also alter.
Maintainability: In the case when your HTML code expressed in C# is supported by another developer, she can use all features of C# to extend/override/modify the tree of generated HTML tags before the tags are shipped to the browser. Open/close approach is in action.
See an example of using each concrete feature in the Documentation on CodePlex site - http://sharpdom.codeplex.com/documentation.
What C# 4.0 artifacts are used?
C# Artifacts
The mostly used thing is indexer - each time you see square brackets, you see an indexer of a certain C# object, and this C# object has the same name as the name of the correlated HTML tag. So if you see code like:
h1[ "Test Form to Test" ]
you know, that there is C# object named "h1
" and this object correlates to the H1
tag of the HTML specification. Next, you can notice that there are optional named parameters, in sentences like:
form.attr(id: "Form1", type: "post")
Also, params
parameters are used (all parameters are of the same base type - Tag
):
body[ h1[...], form[...], div[...], div[...] ]
There is also an overloaded implicit conversion operator from string
type to Tag
class:
label[ "Parameter" ], "=", input.attr(type: "text", value: "Enter value")
Advantages
There are some advantages of switching from plain HTML code to C# code:
- Compile-time checks.
- Freedom for you as a developer to use any presentation-level helper classes/constants, etc., directly inside the view.
- Possibility to maintain already written HTML layout (expressed in a C# manner) using features of C# like inheritance (all methods of Sharp DOM are
public
and virtual
).
- Presence of all generated HTML tags in the form of a tree - so an amount of possibilities are open now:
- possibility to use LINQ to Objects to find certain elements in the tree,
- possibility to change/alter/extend found items in your sub-class - and even in the inherited classes.
Rather good things for maintainability.
Maintainability
Extendibility and maintainability of the HTML layout is one of the main reasons why the Sharp DOM project was developed. During our programming lives, we have come across many situations when we had HTML layout as an ASPX file, but:
- we weren't able to alter the ASPX layout directly - because there was a possibility that in the near future the layout would be reverted back to the source state,
- we weren't able to extend the code-behind code - because it was obfuscated or sealed or not properly architected, not allowing desired changes.
That's why we spent a lot of time thinking over the architecture of the Sharp DOM project, trying to find the optimal form of code from the point of view of maintainability. That's why we did the following things inside the code:
- Practically each and every method/property is
public
and virtual
. It allows to use class inheritance for future maintenance (in case when subscribing to events is not enough).
- Template Method Design Pattern is widely used (and sequence steps are virtual methods).
- Each tag has a unique internal ID which can be reused for maintenance benefits.
- Each tag has some events -
OnPreRender
and OnPostRender
. Each builder (master page builders, content page builders, user control builders) has the same events plus OnPre/PostCreateTags
and some other. Also, all these events are propagated up through the tag tree. So it is possible to subscribe only to the root tag to listen to all events.
- The whole tree of generated HTML tags is split into nested areas - areas where different builders have worked. So it is possible to easily recognize what code generates what HTML. It could simplify the life of developers working in support departments and who have to improve the somehow already designed HTML code in order to fulfill changed business requirements.
Authors
Please leave your comments and other feedback on the Discussion page - http://sharpdom.codeplex.com/Thread/List.aspx.
We quite hope the project is helpful for you!