Introduction
In ASP.NET MVC, we are able to use strongly typed view pages that give much flexibility in design as well as data presentation and data retrieval on form submission. But in traditional ASP.NET applications, we do not use strongly typed models, even when we are using domain model objects and generic lists; those are not associated with our .aspx pages. Our page is totally unaware about which model object it is using, so we have to do an extra task to present data in input controls or in read only format and retrieve the data back to its original format while submitting the form.
I am trying to reduce this overhead by making an extension class and a set of input controls. I am not sure whether it will help someone or not. Anyway, I am sharing my thoughts with you.
This is my first posting in this site. If you find any glaring mistakes in the code, please let me know and I will try to fix those.
ViewPage Base Class
In contrast to the traditional way, the code-behind class inherits from the ViewPage<T>
class instead of the Page
class in System.Web.UI
. ViewPage<T>
is an extension of the Page
class and it accepts a type parameter of the domain model object which you want to associate with the page. The ViewPage
class mainly contains three properties:
Model
- Get or set the domain model instance which you want to operate on.
Html
- Returns an HTML helper class which helps you to render input controls.
ModelState
- Helps to validate the user input with the DataAnnotation rules applied.
Here is the implementation of the ViewPage
class:
public abstract class ViewPage<T> : Page
{
ModelStateValidator<T> modelState;
protected virtual T Model
{
get;
set;
}
protected virtual HtmlHelper<T> Html
{
get
{
return new HtmlHelper<T>(Model, ModelState);
}
}
protected virtual ModelStateValidator<T> ModelState
{
get
{
if (modelState == null)
{
modelState = new ModelStateValidator<T>(Model);
}
return modelState;
}
}
......................
}
In the ViewPage
class, the OnInit
method of the Page
class is overridden to recollect the data from the Request
key collection if the request is a postback.
protected override void OnInit(EventArgs e)
{
T model = Activator.CreateInstance<T>();
if (this.IsPostBack)
{
string modelName = typeof(T).Name;
HttpRequest request = HttpContext.Current.Request;
foreach (PropertyInfo property in typeof(T).GetProperties())
{
if (request[modelName + "_" + property.Name] != null)
{
property.SetValue(model, Convert.ChangeType(
request[modelName + "_" + property.Name],
property.PropertyType), null);
}
}
}
Model = model;
base.OnInit(e);
}
HtmlHelper Class
The HtmlHelper
class accepts a type parameter of your domain model object, letting you to render the appropriate HTML input controls as well as the input validation controls for your model object. The HtmlHelper
class constructor takes two parameters. One is your domain model instance and the other is the ModelStateValidator
instance; I will explain this soon.
Here is the implementation of the HtmlHelper
class:
public class HtmlHelper<T>
{
T model;
ModelStateValidator<T> modelState;
public HtmlHelper(T model, ModelStateValidator<T> modelState)
{
this.model = model;
this.modelState = modelState;
}
public string TextBoxFor(Expression<Func<T, object>>
modelProperty, Object htmlAttributes)
{
..............
}
private string GetPropertyName(Expression<Func<T, object>> expression)
{
.............
}
}
TextBoxFor Method
HtmlHelper
helps to render popular input controls; here I am illustrating how an input type text will be rendered with the TextBoxFor
method of the HtmlHelper
class. If you are familiar with MVC HTML helper methods, it will be easy to understand the TextBoxFor
method. The TextBoxFor
method takes the model class property by an expression parameter. The htmlAttributes
parameter can be any attribute supported by the input control.
public string TextBoxFor(Expression<Func<T, object>> modelProperty, Object htmlAttributes)
{
string property = GetPropertyName(modelProperty);
string modelName = typeof(T).Name;
Object value = typeof(T).GetProperty(property).GetValue(model, null);
StringBuilder control = new StringBuilder(String.Format("<input type={0}text{0} ", '"'));
control.Append(String.Format("name={0}" + modelName + "_" + property + "{0} ", '"'));
foreach (PropertyInfo _property in htmlAttributes.GetType().GetProperties())
{
control.Append(String.Format(_property.Name + "={0}" +
_property.GetValue(htmlAttributes, null) + "{0} ", '"'));
}
control.Append(String.Format("value={0}" + Convert.ToString(value) + "{0} />", '"'));
return control.ToString();
}
GetPropertyName Method
The GetPropertyName
method retrieves the property name from the expression:
private string GetPropertyName(Expression<Func<T, object>> expression)
{
MemberExpression memberExp = null;
if (expression.Body.NodeType == ExpressionType.Convert)
{
memberExp = ((UnaryExpression)expression.Body).Operand as MemberExpression;
}
else if (expression.Body.NodeType == ExpressionType.MemberAccess)
{
memberExp = expression.Body as MemberExpression;
}
return (memberExp != null) ? memberExp.Member.Name : "";
}
ModelStateValidtor class
The ModelStateValidator
class validates the user input with the DataAnnotation rules applied on each property in the domain model object. ModelStateValidator
is inherited from a Generic Dictionary. The constructor argument will be your domain model instance to validate.
Here is the implementation of the ModelStateValidator
class:
public class ModelStateValidator<T> : Dictionary<String, String>
{
T model;
RequiredAttribute required;
public ModelStateValidator(T model)
{
this.model = model;
}
public virtual bool IsValid
{
..........
}
}
IsValid Property
The IsValid
property of the ModelStateValidator
checks whether the model instance is valid as per validation rules applied or not. Here I am trying to illustrate how it validates the RequiredAttribute
of a model property.
public virtual bool IsValid
{
get
{
bool isValid = true;
foreach (PropertyInfo property in typeof(T).GetProperties())
{
if (property.GetCustomAttributes(
typeof(RequiredAttribute), false).Length > 0)
{
required = (RequiredAttribute)property.GetCustomAttributes(
typeof(RequiredAttribute), false)[0];
if (!required.IsValid(property.GetValue(model, null)))
{
isValid = false;
this.Add(property.Name, String.IsNullOrEmpty(required.ErrorMessage) ?
property.Name + " is required." : required.ErrorMessage);
}
}
return isValid;
}
}
Consuming Web Page Extensions
To consume the web page extension feature, first we need to create a domain model type. Here I am creating a model type Category
having three properties:
public class Category
{
public int Id { get; set; }
[Required()]
public string Name { get; set; }
public int ParentId { get; set; }
}
We next inherit our code-behind class from the ViewPage<T>
base class instead of the System.Web.UI.Page
class:
public partial class _Default : ViewPage<Category>
Next, we add the input controls to the page using the HTML helper class.
<%= Html.TextBoxFor(x => x.Id)%> <br />
<%= Html.TextBoxFor(x => x.Name)%> <br />
<%= Html.TextBoxFor(x => x.ParentId)%> <br />
<asp:Button ID="btnSubmit" runat="server" Text="Submit"
onclick="btnSubmit_Click" />
In the button click event, you can simply access the model object like this:
Source
The included source contains a few more input controls and their usage.