ASP.NET MVC 3 : Creating Custom HTML Helpers

Written by John DeVightJohn DeVight on 13 Jun 2011 17:49

Download RAZOR Source Code
Download ASPX Source Code

Overview

I recently needed to be able to render a page as editable or read-only. A common approach to this is to create 2 separate pages, one that is for editing and one that is read-only. Another way is to put if statements through out the page to determine whether the control is editable or read-only. I decided that the best solution was to create custom HTML helpers that would render the HTML according to whether it should be editable or read-only. To do this:

  • I created HTML helpers that took a bool parameter called editable. Depending on the value of the editable parameter, I rendered the control appropriately.
  • I put logic in the controller code to determine if the view should be editable or not.
  • In the view I called the HTML helpers and passed in t he editable flag to render the control

Creating an HTML Helper to Extend TextBoxFor()

MVC 3 has an HTML helper called TextBoxFor(). I wanted to extend the TextBoxFor() method to take an additional parameter of editable. If the TextBox should be read-only, I added additional HtmlAttributes to set the input text field to be read-only.

Here is the code:

namespace System.Web.Mvc.Html;
{
    public static class MvcHtmlExtensions
    {
        public static MvcHtmlString TextBoxFor<TModel, TValue>(this HtmlHelper<TModel> htmlHelper,
            Expression<Func<TModel, TValue>> expression, bool editable)
        {
            MvcHtmlString html = default(MvcHtmlString);
 
            if (editable)
            {
                html = Html.InputExtensions.TextBoxFor(htmlHelper, expression);
            }
            else
            {
                html = Html.InputExtensions.TextBoxFor(htmlHelper, expression,
                    new { @class = "readOnly", @readonly = "read-only" });
            }
            return html;
        }
    }
}

Controller Code to Determine if the View is Editable

The application that I am developing contains a lot of business code to determine if a page was editable or not. To keep this example simple, pass a parameter called editable of type bool to the WikiDetails method. If it is, then the document is editable; if it is not, then the document is read-only; and I set the Wiki.Editable property to true or false.

Here is the code:

namespace MvcRazorApp.Controllers
{
    public class HomeController : Controller
    {
        //
        // GET: /Home/
 
        public ActionResult Index()
        {
            return View();
        }
 
        public ActionResult WikiDetails(bool editable)
        {
            Wiki wiki = new Wiki
            {
                Name = "ASP.NET Wiki",
                Url = "http://aspnet.wikidot.com",
                Editable = editable
            };
 
            return View(wiki);
        }
    }
}

Calling the HTML Helper from the View

In the view I can use the new HTML helper to render input text fields as editable or read-only.

Here is the code:

<div style="display:inline-block; float:left; width:50px;">Name:</div>
@Html.TextBoxFor(model => model.Name, Model.Editable)

Creating an HTML Helper to Extend DropDownListFor()

Implementing the HTML helper to extend DropDownListFor was a bit more complicated. I wanted to render the select field if editable, or a read-only input text field if not editable. To be able to display a read-only text field, it was necessary to evaluate the LINQ expression and take the result of the LINQ expression to lookup the appropriate text value to display in the read-only input text field.

Here is the code:

namespace System.Web.Mvc.Html
{
    public static class MvcHtmlExtensions
    {
        public static MvcHtmlString DropDownListFor<TModel, TValue>(this HtmlHelper<TModel> htmlHelper,
            Expression<Func<TModel, TValue>> expression, IEnumerable<SelectListItem> selectList,
            object htmlAttributes, bool editable)
        {
            Func<TModel, TValue> deleg = expression.Compile();
            var result = deleg(htmlHelper.ViewData.Model);
 
            if (editable)
            {
                return Html.SelectExtensions.DropDownListFor(htmlHelper, expression, selectList, htmlAttributes);
            }
            else
            {
                string name = ExpressionHelper.GetExpressionText(expression);
 
                string selectedText = SelectInternal(htmlHelper, name, selectList);
 
                RouteValueDictionary routeValues = new RouteValueDictionary(htmlAttributes);
                routeValues.Add("class", "readOnly");
                routeValues.Add("readonly", "read-only");
 
                return Html.InputExtensions.TextBox(htmlHelper, name, selectedText, routeValues);
            }
        }
 
        private static string SelectInternal(HtmlHelper htmlHelper, string name, IEnumerable selectList)
        {
            ModelState state;
 
            string selectedText = string.Empty;
 
            string fullHtmlFieldName = htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(name);
 
            object obj2 = null;
            if (htmlHelper.ViewData.ModelState.TryGetValue(fullHtmlFieldName, out state) && (state.Value != null))
            {
                obj2 = state.Value.ConvertTo(typeof(string), null);
            }
            if (obj2 == null)
            {
                obj2 = htmlHelper.ViewData.Eval(fullHtmlFieldName);
            }
 
            if (obj2 != null)
            {
                IEnumerable source = ((IEnumerable)new object[] { obj2 });
                HashSet<string> set = new HashSet<string>(source.Cast<object>().Select<object, string>(delegate(object value)
                {
                    return Convert.ToString(value, CultureInfo.CurrentCulture);
                }), StringComparer.OrdinalIgnoreCase);
                foreach (SelectListItem item in selectList)
                {
                    if ((item.Value != null) ? set.Contains(item.Value) : set.Contains(item.Text))
                    {
                        selectedText = item.Text;
                        break;
                    }
                }
            }
 
            return selectedText;
        }
    }
}

Creating an HTML Helper to Extend TextBoxFor that has HtmlAttributes

In the example above "Creating an HTML Helper to Extend TextBoxFor", the extension method didn't allow for additional HtmlAttributes to be passed in. I assumed that there were no additional HtmlAttributes and set the HtmlAttributes to make the input text field read-only. However, to allow for HtmlAttributes to be passed in and then adding the aditional HtmlAttributes to make the input text field read-only, the RouteValueDictionary is used.

Here is the code:

namespace System.Web.Mvc.Html
{
    public static class MvcHtmlExtensions
    {
        public static MvcHtmlString TextBoxFor<TModel, TValue>(this HtmlHelper<TModel> htmlHelper,
           Expression<Func<TModel, TValue>> expression, object htmlAttributes, bool editable)
        {
            MvcHtmlString html = default(MvcHtmlString);
 
            if (editable)
            {
                html = Html.InputExtensions.TextBoxFor(htmlHelper, expression, htmlAttributes);
            }
            else
            {
                RouteValueDictionary routeValues = new RouteValueDictionary(htmlAttributes);
                routeValues.Add("class", "readOnly");
                routeValues.Add("readonly", "read-only");
 
                html = Html.InputExtensions.TextBoxFor(htmlHelper, expression, routeValues);
            }
            return html;
        }
    }
}

Calling the HTML Helper from the View

In the view I can use the new HTML helper to render input text fields with additional htmlAttributes as editable or read-only.

Here is the code:

<div style="display:inline-block; float:left; width:50px;">Url:</div>
@Html.TextBoxFor(model => model.Url, new { style = "width:400px" }, Model.Editable)

Additional Details About the Sample Application

Index View

In the Index View I added two Html.ActionLink methods. One to display the WikiDetails View as editable and one to display the WikiDetails View as read-only.

Here is the code:

@Html.ActionLink("Editable Wiki Details Page", "WikiDetails", new { editable = true })
<br />
@Html.ActionLink("Read-only Wiki Details Page", "WikiDetails", new { editable = false })

Site Stylesheet

In the Site.css stylesheet I added a style called readOnly to display the background of the html element as buttonface to give it a readOnly look.

Here is the code:

.readOnly
{
    background: buttonface;
}

Models

I created a BaseModel class that has a property of Editable. I then created a Wiki class that derives from BaseModel with two additional properties of Name and Url.

Here is the code for the BaseModel class:

public class ModelBase
{
    public bool Editable { get; set; }
}

Here is the code for the Wiki class:

public class Wiki : ModelBase
{
    public string Name { get; set; }
    public string Url { get; set; }
}

Need to Extend the MVC HTML Helper?

I'll be adding more to this wiki page as I extend the HTML Helpers. However, if you have a need to extend the HTML Helper, leave me a comment and I'll see what I can do.

References

Comments

Add a New Comment

Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License