Overview
In this article, I will show you how to build a component model for ASP.NET MVC3 with Builder and Factory design patterns. You could use this model to write your strongly typed components to binding data, handling hierarchical objects and write jQuery UI controls / Html5 controls for ASP.NET MVC3.
Background
As we know, in classical ASP.NET, we could write controls by using ASP.NET control model for our web applications. Today in MVC3, we’re usually writing helpers instead of controls. I think helper methods are a good choice for some simple use scenarios but not for all. For rendering single tag, image or scripts, helper methods do well. I can help think how to develop more complex controls with help methods? How to write template container controls? How to write a data bindable treeview and menu? How to write and use those complex controls easily? In fact, I love the helper method and I hate them at the same time. Because helper methods are the easy way to output my tags on view, but that is not the OOP way ! Helpers are easy to use but hard to extend and maintain. How could you read, remember and maintain a method with over 10 overload versions? That is hell! So we need another way out! The way that could be easy to learn, more readable, OOP, extensible and familiar for our control developing.
Before You Start
There are some concepts you need to know:
Objectives
We need a component model for ASP.NET MVC.
- Supports Razor and Web Form syntax
- Offers interfaces or abstract classes to implement
- Supports control template
- Supports composite components
- Supports data binding
- Easy to integrate with client ability (JavaScripts jQuery)
Usage Design
The biggest difference between MVC3 and classic ASP.NET is MVC3 uses Views and helpers to render HTML tags directly but not any component model like classic ASP.NET. So before designing the component model, we need to know how many ways we could write controls in MVC3, then we also need to know the common behaviors, properties, usages of the controls. Now let’s look back in MVC3 and do some testing.
Usage 1: Extension Method
We call “Html.XXX()
” and “Ajax.XXX()
” MVC Helpers or Extensions, actually they are static
extension methods that are a good way to inject new methods to an existing class. When you are using Razor, it provides a new syntax to write the helper method by using @helper
. I put build-in helpers which we are often using in the following list (notes: each method has more then 5 overloaded versions):
Html.Display()
Html.DisplayFor()
Html.Label()
Html.LabelFor()
Html.LablelForModel()
Html.ActionLink()
Html.RoutLink()
Html.Editor()
Html.EditorFor()
Html.EditorForModel()
Html.CheckBox()
Html.CheckBoxFor()
Html.Hidden()
Html.Password()
Html.PasswordFor()
Html.RadioButton()
Html.RadioButtonFor()
Html.TextBox()
Html.TextBoxFor()
Html.ListBox()
Html.ListBoxFor()
Html.DropDownList()
Html.DropDownListFor()
Html.TextArea()
Html.TextAreaFor()
There are almost over 100 methods with overload versions of this list! Please note that these helpers only use to render <input> <a> <select> <textArea>
tags maybe call forms. Why does helper have so many overloads ?! Just for “DIY“ and “Clean” I think. Although the helpers would be more simple to use but as we see they will bring a large number of overloads. They become more unreadable, hard to remember and hard to maintain.
Conclusion: The helper methods should be used for simply controls without many options. That is not the OOP way.
Usage 2: Template
In classic ASP.NET, we would usually use Template in our container control that is awesome! But how could we do that in MVC? I couldn’t find any solution in the ASP.NET official website but only write helper methods or view files. How to write a strongly typed control that enables template feature? For example how to write a Panel control in MVC3 ? Even for form tag, the MVC offers two strange methods ”BeginForm
”, “EndForm
” to implement it. Why not provide a method like this:
Razor
@{
Html.Form()
.Body(@<text>
<p>Here is form body</p>
</text>)
.End();
}
Web Form
<%
Html.Form()
.Body(()=>{%>
<p>Here is form body</p>
<%})
.End();
%>
That is what I want. As you see, Web Form and Razor have implantation of Template. In Web Form syntax, we should use Action delegate to embed other HTML tags. For Razor ViewEngine only allows using inline block text @<text></text>
, in the example above, I use “Builder” design pattern act as a control role of M-V-C pattern.
OK, I have found the way to implement Template for controls. That is very useful for every container control.
Usage 3: Composite Controls
At the age of the classic ASP.NET (in my time, I am an old man you know ^^), I remember I had often implemented the CompositeControl
base class of my complex controls. Obviously, when our control becomes more complex, we need to provide more options for them to control their behaviors, only using helper methods is not enough. We use a component model to implement the composite controls such as TreeView
, Grid
, ComobBox
, ListBox
, Menu
, SiteMap
, Toolbar
and so on. These controls are common controls we would use anywhere and anytime.
So let’s imagine how to use these “Controls” in our MVC project:
@{
Ajax.DJME().ComboBox()
.Width(200)
.Items(items=>{
items.Add("text1","value1");
items.Add("text1","value1");
items.Add().Template(@<text><a href="#">
I am template item.</a></text>);
})
.Render();
}
@{
Ajax.DJME().TreeView()
.Nodes(nodes=>{
nodes.Add("node1","http://domain.com");
nodes.Add("node2","http://domain.com");
nodes.Add("node3","http://domain.com")
.Nodes(children=>{
children.Add("subNode1","http://sub.domain.com");
});
})
}
Composite usage is very similar to the Template usage. It also uses Action delegate to implement it.
Usage 4: Data Binding
In MVC way, we will generate the Model instance in Controller side and pass it in ViewData.Model
to View side. So our component only runs in View side, we should not be concerned how to generate model, it just a parameter for controls. And data binding is just the automatic version of the Composites usage. The data binding usage may be like this:
@{
Ajax.DJME().ListBox()
.Bind(Model)
.Render();
}
@{
Ajax.DJME().Grid(Model)
.AutoGenerateColumns()
.Sortable()
.Pagable()
.Render();
}
ComponentModel:Using Builder and Factory Method Design Patterns to Implement M-V-C Pattern
We previously discussed a number of usages in how the MVC may be written, where we will be able to explore in depth how to build an abstract level object model and implementation.
First, before discussing the object model design, you need to remember that ASP.NET MVC is only a framework or a set of development tools, in fact M-V-C is a very classic design pattern. So our component model will be implementing the MVC pattern. Now let’s take a look in the class diagram of the abstraction level objects.
Model
We should not be concerned about how to generate the model. For components Model, just use as an external data source.
View
The ViewComponent
base class acts as View, it uses to hold properties values and render the HTML content. The sub classes override the RenderXXX()
methods to write the HTML contents to View response. The ViewComponent
is similar to classic ASP.NET Control base class. The following code is written in C# for the ViewComponent
.
public abstract class ViewComponent
{
private ViewContext _viewContext;
private string _id;
public ViewContext ViewContext
{
get { return _viewContext; }
}
public ViewComponent(ViewContext viewContext)
{
this._viewContext = viewContext;
}
public virtual string Name { get; set; }
public virtual string ClientID
{
get
{
if ((string.IsNullOrEmpty(_id)) && (!string.IsNullOrEmpty(Name)))
{
var _tag = new TagBuilder(TagName);
_tag.GenerateId(Name);
if (_tag.Attributes.ContainsKey("id"))
_id = _tag.Attributes["id"];
else
_id = Name;
}
return _id;
}
private set { _id = value; }
}
public virtual string TagName
{
get
{
return "div";
}
}
public virtual void Render(HtmlTextWriter writer)
{
RenderBeginContent(writer);
RenderConent(writer);
RenderEndConent(writer);
}
public virtual void RenderBeginContent(HtmlTextWriter writer)
{
TagBuilder _tag = new TagBuilder(TagName);
if (!string.IsNullOrEmpty(Name))
{
_tag.GenerateId(Name);
if (!_tag.Attributes.ContainsKey("id"))
_tag.MergeAttribute("id", Name);
Id = _tag.Attributes["id"];
if (TagName.Equals("input", StringComparison.OrdinalIgnoreCase) ||
TagName.Equals("textarea", StringComparison.OrdinalIgnoreCase))
_tag.MergeAttribute("name", Name);
}
writer.Write(_tag.ToString(TagRenderMode.StartTag));
}
public virtual void RenderConent(HtmlTextWriter writer) { }
public virtual void RenderEndTag(HtmlTextWriter writer)
{
writer.WriteEndTag(TagName);
}
public string GetHtml()
{
var result = new StringBuilder();
using (var writer = new HtmlTextWriter(new StringWriter(result)))
{
Render(writer);
}
return result.ToString();
}
}
Control
Then ViewComponentBuilder
act as Control role of MVC pattern and it implements the Builder design pattern. It controls the ViewComponent
build progress.
public class ViewComponentBuilder<TComponent,TBuilder>
where TComponent : ViewComponent
where TBuilder : ViewComponentBuilder<TComponent, TBuilder>
{
public ViewComponentBuilder(TComponent component)
{
this.Component = component;
}
private TComponent component;
public TComponent Component
{
get { return component; }
private set { component = value; }
}
public ViewContext ViewContext
{
get
{
return component.ViewContext;
}
}
public TBuilder GenerateId()
{
if (string.IsNullOrEmpty(Component.Name))
{
string prefix = Component.GetType().Name;
string key = "DJME_IDSEQ_" + prefix;
int seq = 1;
if (ViewContext.HttpContext.Items.Contains(key))
{
seq = (int)ViewContext.HttpContext.Items[key] + 1;
ViewContext.HttpContext.Items[key] = seq;
}
else
ViewContext.HttpContext.Items.Add(key, seq);
Component.Name = prefix + seq.ToString();
}
return this as TBuilder;
}
public virtual TBuilder Name(string name)
{
Component.Name = name;
return this as TBuilder;
}
public virtual void Render()
{
RenderComponent();
}
protected void RenderComponent()
{
using (var writer = new HtmlTextWriter(ViewContext.Writer))
{
Component.Render(writer);
}
}
public virtual MvcHtmlString GetHtml()
{
return MvcHtmlString.Create(Component.GetHtml());
}
}
In client side, the developer will not use the ViewComponent
directly but ViewComponentBuilder
. In order to use the builder and write the code inline, each control method of builder should return itself. We need to call Render()
or GetHtml()
method to get the build result after finishing the build progress.
The following code is the usage of the ViewComponentBuilder
:
@{
Ajax.YourControl()
.Name("Your control name")
.Render();
}
Why the ViewComponentBuilder
has Render()
and GetHtml()
result getting methods? Because of considerations of the WebForm and Razor's ViewEngines output mechanisms. The following list is the explanation of these methods:
Render
- Invoke by ViewEngine
and write the ViewComponent
output result to response. It will be lazy called and return nothing. GetHtml
– It will return the ViewComponent
output result as MvcHtmlString
when it will be invoked.
Factory Method and Helper Method
Finally we need a class to construct the ViewComponent
and ViewCompoentBuilder
and put them to work together. And we also need to return the builder and inject the construct method into HtmlHelper
or AjaxHelper
. Now we will be using Factory method design pattern to implement it.
#1. Create an extension method and inject to HtmlHelper
that makes Html.Demo()
could return a ComponentFactory
instance.
public static class Extensions
{
public static SimpleFactory Demo(this HtmlHelper helper)
{
return new SimpleFactory (helper.ViewContext);
}
}
#2. Create a component factory that contains the ViewComponentBuilder
construct methods.
public class SimpleFactory
{
private ViewContext _viewContext;
public ViewContext ViewContext
{
get { return _viewContext; }
}
public SimpleFactory (ViewContext viewContext)
{
this._viewContext = viewContext;
}
public TextViewComponentBuilder Text()
{
return new TextViewComponentBuilder
(new TextViewComponent(this.ViewContext)).GenerateId();
}
}
#3. We could use the factory like this:
@Html.Demo().Text().GetHtml()
The factory does not inherit from any base classes. It only supports construction of ViewComponentBuilder
. You can define various factories to group different kinds of components methods. The factory method and static
extension method together can bring us various challenges. We can use abstract
factory to create a serial of component models having the same behavior. You can even use DI in static
extension method to create/construct component factory dynamically. Imagine you write a component set of HTML Input, and you can provide a component set of Html5 input or jQuery input later. Just modify abstract
factory in static
methods.You can inject the implement of abstract
factory via DI. How to dynamically construct component is a vivid topic, while I just offer here. I would write some articles to share with you later on this topic.
Tutorial 1: Implement HTML5 Input Controls
Now let’s use this component mode to create a Html5 inputs control set.
The following figure is the class diagram of the Html5 input classes.
Step 1: Implement ViewComponent
Create Html5InputViewComponent
derived from ViewComponent.Override
the Render
method and render the input HTML tag:
public class Html5InputViewComponent:ViewComponent
{
public Html5InputViewComponent(ViewContext viewContext) : base(viewContext) { }
private Html5InputTypes inputType = Html5InputTypes.Text;
public Html5InputTypes InputType
{
get { return inputType; }
set { inputType = value; }
}
public object Value { get; set; }
public override void Render(System.Web.UI.HtmlTextWriter writer)
{
TagBuilder input = new TagBuilder("input");
input.GenerateId(this.Name);
if (InputType.Equals(Html5InputTypes.DatetimeLocal))
input.MergeAttribute("type", "datetime-local");
else
input.MergeAttribute("type", inputType.ToString().ToLower());
if (!input.Attributes.ContainsKey("name"))
input.Attributes.Add("name", this.Name);
if (Value != null)
input.MergeAttribute("value", Value.ToString());
writer.Write(input.ToString(TagRenderMode.SelfClosing));
}
}
Step 2: Implement ViewComponentBuilder
Create a Html5InputViewComponentBuilder
to control the Html5InputViewComponent
. In this case, we only add a "Value
" method to set the Html5InputViewComponent.Value
property.
public class Html5InputViewComponentBuilder :
ViewComponentBuilder<Html5InputViewComponent, Html5InputViewComponentBuilder>
{
public Html5InputViewComponentBuilder(Html5InputViewComponent component) :
base(component) { }
public Html5InputViewComponentBuilder InputType(Html5InputTypes type)
{
Component.InputType = type;
return this;
}
public Html5InputViewComponentBuilder Value(object value)
{
Component.Value=value;
return this;
}
}
Step 3: Create a Factory
The Html5ViewComopnentBuilderFactory
class contains a set of construct methods. This factory provide some simplify wrapper method that pre build the Html5InputViewComponent
.
public class Html5ViewComponentBuilderFactory
{
private ViewContext _viewContext;
public ViewContext ViewContext
{
get { return _viewContext; }
}
public Html5ViewComponentBuilderFactory(ViewContext viewContext)
{
this._viewContext = viewContext;
}
public Html5InputViewComponentBuilder Input(Html5InputTypes type)
{
return new Html5InputViewComponentBuilder
(new Html5InputViewComponent(this.ViewContext))
.GenerateId()
.InputType(type);
}
public MvcHtmlString Number(int value=0)
{
return new Html5InputViewComponentBuilder
(new Html5InputViewComponent(this.ViewContext))
.GenerateId()
.InputType(Html5InputTypes.Number)
.Value(value)
.GetHtml();
}
public MvcHtmlString Range(int value = 0)
{
return new Html5InputViewComponentBuilder
(new Html5InputViewComponent(this.ViewContext))
.Value(value)
.GenerateId()
.InputType(Html5InputTypes.Range)
.GetHtml();
}
public Html5InputViewComponentBuilder Date()
{
return new Html5InputViewComponentBuilder
(new Html5InputViewComponent(this.ViewContext))
.GenerateId()
.InputType(Html5InputTypes.Date);
}
}
Step 4: Write an extension method to inject the Factory into HtmlHelper
public static class Extenstions
{
public static Html5ViewComponentBuilderFactory Html5(this HtmlHelper helper)
{
return new Html5.Html5ViewComponentBuilderFactory(helper.ViewContext);
}
}
Step 5: Write the client code in View
<fieldset>
<legend>Html5 Input components demo</legend>
<p>
Number:
</p>
<p>
@Html.Html5().Number(20)
</p>
<p>
Range:
</p>
<p>
@Html.Html5().Range(5)
</p>
<p>
Date:
</p>
<p>
@Html.Html5().Date()
</p>
</fieldset>
Templating
In order to add the template ability to component, I defined an IHtmlTemplate
interface.
public interface IHtmlTemplate
{
Action Content { get; set; }
Func<object, object> InlineContent { get; set; }
bool IsEmpty { get; }
void WriteTo(HtmlTextWriter writer);
}
The IHtmlTemplate
provides two template properties:
Content
– This template property is an Action delegate that allows developers using it in View like this: Content(()=>{ @*content body*@ }
. It can support WebForm and Razor syntax. InlineContent
– This template property only works for Razor ViewEngine. Razor recognizes Func<object,object>
deletage and renders it as inline text block @<text>
.
Implement IHtmlTemplate
interface:
public class HtmlTemplate : IHtmlTemplate
{
public Action Content { get; set; }
public Func<object, object> InlineContent { get; set; }
public void WriteTo(System.Web.UI.HtmlTextWriter writer)
{
if (Content != null)
{
Content.Invoke();
}
else
{
if (InlineContent != null)
writer.Write(InlineContent(null).ToString());
}
}
public bool IsEmpty
{
get
{
return ((Content == null) && (InlineContent == null));
}
}
}
In the following tutorial, I will show you how to use the IHtmlTemplate
and HtmlTemplate
in components.
Tutorial 2: Container Component
This tutorial is how to create a Panel
component with body template.
Step 1. Implement PanelViewComponent
Add a PanelViewComponent
derived from ViewComponent
. Add a Content
property and set the type as HtmlTemplate
(It also could be set to IHtmlTemplate
).
public class PanelViewComponent:ViewComponent
{
public PanelViewComponent(ViewContext context) : base(context)
{
Content = new HtmlTemplate();
}
public HtmlTemplate Content { get; set; }
public string Title { get; set; }
public override void RenderConent(System.Web.UI.HtmlTextWriter writer)
{
writer.WriteFullBeginTag("div");
writer.Write(Title);
writer.WriteEndTag("div");
if (!Content.IsEmpty)
Content.WriteTo(writer);
}
}
Step 2. Implement PanelViewComponentBuilder
public class PanelViewComponentBuilder:ViewComponentBuilder
<PanelViewComponent,PanelViewComponentBuilder>
{
public PanelViewComponentBuilder(PanelViewComponent component) :
base(component) { }
public PanelViewComponentBuilder Title(string title)
{
Component.Title = title;
return this;
}
public PanelViewComponentBuilder Body(Action body)
{
if (body != null)
Component.Content.Content = body;
return this;
}
public PanelViewComponentBuilder Body(Func<object, object> body)
{
if (body != null)
Component.Content.InlineContent = body;
return this;
}
}
We expose the Action
and Func
delegate in Body
methods to developers.
Step 3: Add the factory method
public static class Extenstions
{
public static DNA.Mvc.ComponentModel.Html5.Html5ViewComponentBuilderFactory Html5
(this HtmlHelper helper)
{
return new Html5.Html5ViewComponentBuilderFactory(helper.ViewContext);
}
public static PanelViewComponentBuilder Panel(this HtmlHelper helper)
{
return new PanelViewComponentBuilder
(new PanelViewComponent(helper.ViewContext)).GenerateId();
}
}
Step 4: Using the Panel in View
@{
Html.Panel()
.Title("Panel title")
.Body(@<text>
<p> Hi here is panel body contents. </p>
</text>)
.Render();
}
Composites Component
As is stated above, it becomes a complex component when our component combines different sub-components. Basically, ViewComponent
, ViewComponentBuilder
and IHtmlTemplate
can implement most of the components. And what we mostly think of complex components is:
- How to construct children components programmatically
- How to construct children components for data binding
Simply, composite components are made of component container and children components. In many cases, the composite component is made up of many different types of children components. In order to make the usage of children components with container in the same way, we should pass a constructor method of child component builder in container Builder
method context. It reduces the coupling and is easy to make changes.
Tutorial 3: LinkList
In this tutorial, I will show you how to create a simple list component. It is made up of a list container (LinkList
) and numbers of link list item components. I will show how to add the link list item within the linklist builder method.
Step1: Create container component and children component
public class LinkListComponent:ViewComponent
{
public LinkListComponent(ViewContext context) : base(context)
{ Links = new List<LinkComopnent>(); }
public virtual ICollection<LinkComopnent> Links { get; set; }
public override string TagName
{
get
{
return "ul";
}
}
public override void RenderConent(System.Web.UI.HtmlTextWriter writer)
{
foreach (var link in Links)
{
link.Render(writer);
}
}
}
public class LinkListComponentBuilder:ViewComponentBuilder
<LinkListComponent,LinkListComponentBuilder>
{
public LinkListComponentBuilder(LinkListComponent component) : base(component) { }
public LinkListComponentBuilder Items(Action<LinkComponentBuilderFactory> items)
{
var factory=new LinkComponentBuilderFactory(this.Component);
if (items != null)
items.Invoke(factory);
return this;
}
}
public class LinkComopnent:ViewComponent
{
public LinkComopnent(ViewContext context) : base(context) { }
public string Title { get; set; }
public string Url { get; set; }
public override string TagName
{
get
{
return "li";
}
}
public override void RenderConent(System.Web.UI.HtmlTextWriter writer)
{
var a = new TagBuilder("a");
a.MergeAttribute("href", Url);
a.InnerHtml = Title;
writer.Write(a.ToString());
}
}
public class LinkComponentBuilder:ViewComponentBuilder
<LinkComopnent,LinkComponentBuilder>
{
public LinkComponentBuilder(LinkComopnent component) : base(component) { }
public LinkComponentBuilder Title(string title)
{
this.Component.Title = title;
return this;
}
public LinkComponentBuilder Url(string url)
{
Component.Url = url;
return this;
}
}
Step2: Add children component builder factory and container component builder
public class LinkComponentBuilderFactory
{
public LinkListComponent ItemContainer { get; set; }
public LinkComponentBuilderFactory(LinkListComponent container)
{
this.ItemContainer = container;
}
public LinkComponentBuilder Add()
{
var item=new LinkComopnent(ItemContainer.ViewContext);
ItemContainer.Links.Add(item);
return new LinkComponentBuilder(item);
}
}
- Create the children component and add to container component.
- Create children component builder and bind to children component.
- Return the children component builder to client view.
public class LinkListComponentBuilder:ViewComponentBuilder
<LinkListComponent,LinkListComponentBuilder>
{
public LinkListComponentBuilder(LinkListComponent component) : base(component) { }
public LinkListComponentBuilder Items(Action<LinkComponentBuilderFactory> items)
{
var factory=new LinkComponentBuilderFactory(this.Component);
if (items != null)
items.Invoke(factory);
return this;
}
}
We pass the LinkComponentBuilderFactory
as a parameter in Action
delegate to client view.
Step 3: Using in View
@{
Html.LinkList()
.Items(items =>
{
items.Add().Title("Item1").Url("#");
items.Add().Title("Item1").Url("#");
items.Add().Title("Item1").Url("#");
})
.Render();
}
Now we provide the way to add the children component in View manually. When we want the component could create the child itself, that is another usage - DataBinding
.
We only make some changes that make the LinkListComponentBuilder
support binding to data Model.
public class LinkListComponentBuilder : ViewComponentBuilder<LinkListComponent,
LinkListComponentBuilder>
{
public LinkListComponentBuilder(LinkListComponent component) : base(component) { }
public LinkListComponentBuilder Items(Action<LinkComponentBuilderFactory> items)
{
var factory = new LinkComponentBuilderFactory(this.Component);
if (items != null)
items.Invoke(factory);
return this;
}
public LinkListComponentBuilder Bind<T, TKey, TValue>(IEnumerable<T> model,
Expression<Func<T, TKey>> keySelector,
Expression<Func<T, TValue>> valueSelector)
where T : class
{
if (model != null)
{
this.Items(items=>{
foreach (var t in model)
{
items.Add()
.Title(keySelector.Compile().Invoke(t) as string)
.Url(valueSelector.Compile().Invoke(t) as string);
}
});
}
return this;
}
}
@{
var listitems = new List<Dna.Demo.Web.Collation>();
listitems.Add(new DNA.Demo.Web.Collation() { Key="BindingItem1",Value="#" });
listitems.Add(new DNA.Demo.Web.Collation() { Key = "BindingItem2", Value = "#" });
listitems.Add(new DNA.Demo.Web.Collation() { Key = "BindingItem3", Value = "#" });
Html.LinkList()
.Bind(listitems, m => m.Key, m => m.Value)
.Render();
}
Addition
I used this component model and jQuery client ability to create an open source project DJME - The jQuery MVC Extensions. Including all jQuery UI widgets and many common components such as TreeView
, Menu
,Toolbar
, SiteMap
, ContextMenu
, DropDownMenu
, Textbox
, ComobBox
, ListBox
, RichTextBox
and as so on. If you are interested in jQuery MVC extensions, you can go to http://djme.codeplex.com and download it. At the same time, you can also go to http://www.dotnetage.com to try the online demo.