This website is no longer actively supported
Written by John 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
- HTML helpers: http://www.asp.net/mvc/tutorials/creating-custom-html-helpers-cs
- ViewBag: http://weblogs.asp.net/hajan/archive/2010/12/11/viewbag-dynamic-in-asp-net-mvc-3-rc-2.aspx
- Telerik MVC : Extending the Telerik HTML Helper
Hey,
I see you're creating custom HtmlHelpers with Generics. I am trying to create a Telerik style HtmlHelper.
Basically, I would like to declare a model and have the generic types be inferred rather than have to implicitly declare them in the view. I have actually created a Stackoverflow question about this very topic. If you have any advice I would surely appreciate it. The helper actually does work, it's just not the exact pattern I was going for. Here is a link to the stackoverflow question.
http://stackoverflow.com/questions/6524180/mvc3-htmlhelpers-using-generics-and-linq
I hope you have a moment to share some insight. Thanks for your time.
Gabe
Hi Gabe,
I came up with a solution and wrote up an article showing how I created a TableBuilder class that gets passed into a RenderTable HtmlHelper extension method to render an IEnumerable list of model objects. The TableBuilder class can be configured using a variable list of lambda expressions to identify the model properties to be rendered in the table.
Here is the article: ASP.NET MVC 3 : Creating a Custom HTML Helper to Render List of Models as a Table
Regards,
John
John DeVight
Telerik MVP
Hi John,
Any idea how to take this one step further so you could mark your model with an attribute [ReadOnly(true)] ala Data Annotations?
Regards,
Brian
hi.
i just wanna make a control just like lookup for my mvc3 project.but i don't know how to make it or whether it's possible.
i would be grateful if you could help me.
regards.
Hi John
I want to make a custom textbox which accepts only number how do i acheive this using htmlhelper class extending textboxfor
Regards
Rohit