Knockout with ASP.Net MVC2 - HTML Extension Helpers for input controls
- by Renso
Goal:
Defining Knockout-style input controls can be tedious and also may be something that you may find obtrusive, mixing your HTML with data bind syntax as well as binding your aspx, ascx files to Knockout. The goal is to make specifying Knockout specific HTML tags easy, seamless really, as well as being able to remove references to Knockout easily.
Environment considerations:
ASP.Net MVC2 or later
Knockoutjs.js
How to:
public static class HtmlExtensions
{
public static string DataBoundCheckBox(this HtmlHelper helper, string name, bool isChecked, object htmlAttributes)
{
var builder = new TagBuilder("input");
var dic = new RouteValueDictionary(htmlAttributes) { { "data-bind", String.Format("checked: {0}", name) } };
builder.MergeAttributes(dic);
builder.MergeAttribute("type", @"checkbox");
builder.MergeAttribute("name", name);
builder.MergeAttribute("value", @"true");
if (isChecked)
{
builder.MergeAttribute("checked", @"checked");
}
return builder.ToString(TagRenderMode.SelfClosing);
}
public static MvcHtmlString DataBoundSelectList(this HtmlHelper helper, string name, IEnumerable<SelectListItem> selectList, String optionLabel)
{
var attrProperties = new StringBuilder();
attrProperties.Append(String.Format("optionsText: '{0}'", name));
if (!String.IsNullOrEmpty(optionLabel)) attrProperties.Append(String.Format(", optionsCaption: '{0}'", optionLabel));
attrProperties.Append(String.Format(", value: {0}", name));
var dic = new RouteValueDictionary { { "data-bind", attrProperties.ToString() } };
return helper.DropDownList(name, selectList, optionLabel, dic);
}
public static MvcHtmlString DataBoundSelectList(this HtmlHelper helper, string name, IEnumerable<SelectListItem> selectList, String optionLabel, object htmlAttributes)
{
var attrProperties = new StringBuilder();
attrProperties.Append(String.Format("optionsText: '{0}'", name));
if (!String.IsNullOrEmpty(optionLabel)) attrProperties.Append(String.Format(", optionsCaption: '{0}'", optionLabel));
attrProperties.Append(String.Format(", value: {0}", name));
var dic = new RouteValueDictionary(htmlAttributes) {{"data-bind", attrProperties}};
return helper.DropDownList(name, selectList, optionLabel, dic);
}
public static String DataBoundSelectList(this HtmlHelper helper, String options, String optionsText, String value)
{
return String.Format("<select data-bind=\"options: {0},optionsText: '{1}',value: {2}\"></select>", options, optionsText, value);
}
public static MvcHtmlString DataBoundTextBox(this HtmlHelper helper, string name, object value, object htmlAttributes)
{
var dic = new RouteValueDictionary(htmlAttributes);
dic.Add("data-bind", String.Format("value: {0}", name));
return helper.TextBox(name, value, dic);
}
public static MvcHtmlString DataBoundTextBox(this HtmlHelper helper, string name, string observable, object value, object htmlAttributes)
{
var dic = new RouteValueDictionary(htmlAttributes);
dic.Add("data-bind", String.Format("value: {0}", observable));
return helper.TextBox(name, value, dic);
}
public static MvcHtmlString DataBoundTextArea(this HtmlHelper helper, string name, string value, int rows, int columns, object htmlAttributes)
{
var dic = new RouteValueDictionary(htmlAttributes);
dic.Add("data-bind", String.Format("value: {0}", name));
return helper.TextArea(name, value, rows, columns, dic);
}
public static MvcHtmlString DataBoundTextArea(this HtmlHelper helper, string name, string observable, string value, int rows, int columns, object htmlAttributes)
{
var dic = new RouteValueDictionary(htmlAttributes);
dic.Add("data-bind", String.Format("value: {0}", observable));
return helper.TextArea(name, value, rows, columns, dic);
}
public static string BuildUrlFromExpression<T>(this HtmlHelper helper, Expression<Action<T>> action)
{
var values = CreateRouteValuesFromExpression(action);
var virtualPath = helper.RouteCollection.GetVirtualPath(helper.ViewContext.RequestContext, values);
if (virtualPath != null)
{
return virtualPath.VirtualPath;
}
return null;
}
public static string ActionLink<T>(this HtmlHelper helper, Expression<Action<T>> action, string linkText)
{
return helper.ActionLink(action, linkText, null);
}
public static string ActionLink<T>(this HtmlHelper helper, Expression<Action<T>> action, string linkText, object htmlAttributes)
{
var values = CreateRouteValuesFromExpression(action);
var controllerName = (string)values["controller"];
var actionName = (string)values["action"];
values.Remove("controller");
values.Remove("action");
return helper.ActionLink(linkText, actionName, controllerName, values, new RouteValueDictionary(htmlAttributes)).ToHtmlString();
}
public static MvcForm Form<T>(this HtmlHelper helper, Expression<Action<T>> action)
{
return helper.Form(action, FormMethod.Post);
}
public static MvcForm Form<T>(this HtmlHelper helper, Expression<Action<T>> action, FormMethod method)
{
var values = CreateRouteValuesFromExpression(action);
string controllerName = (string)values["controller"];
string actionName = (string)values["action"];
values.Remove("controller");
values.Remove("action");
return helper.BeginForm(actionName, controllerName, values, method);
}
public static MvcForm Form<T>(this HtmlHelper helper, Expression<Action<T>> action, FormMethod method, object htmlAttributes)
{
var values = CreateRouteValuesFromExpression(action);
string controllerName = (string)values["controller"];
string actionName = (string)values["action"];
values.Remove("controller");
values.Remove("action");
return helper.BeginForm(actionName, controllerName, values, method, new RouteValueDictionary(htmlAttributes));
}
public static string VertCheckBox(this HtmlHelper helper, string name, bool isChecked)
{
return helper.CustomCheckBox(name, isChecked, null);
}
public static string CustomCheckBox(this HtmlHelper helper, string name, bool isChecked, object htmlAttributes)
{
TagBuilder builder = new TagBuilder("input");
builder.MergeAttributes(new RouteValueDictionary(htmlAttributes));
builder.MergeAttribute("type", "checkbox");
builder.MergeAttribute("name", name);
builder.MergeAttribute("value", "true");
if (isChecked)
{
builder.MergeAttribute("checked", "checked");
}
return builder.ToString(TagRenderMode.SelfClosing);
}
public static string Script(this HtmlHelper helper, string script, object scriptAttributes)
{
var pathForCRMScripts = ScriptsController.GetPathForCRMScripts();
if (ScriptOptimizerConfig.EnableMinimizedFileLoad)
{
string newPathForCRM = pathForCRMScripts + "Min/";
ScriptsController.ServerPathMapper = new ServerPathMapper();
string fullPath = ScriptsController.ServerMapPath(newPathForCRM);
if (!File.Exists(fullPath + script))
return null;
if (!Directory.Exists(fullPath))
return null;
pathForCRMScripts = newPathForCRM;
}
var builder = new TagBuilder("script");
builder.MergeAttributes(new RouteValueDictionary(scriptAttributes));
builder.MergeAttribute("type", @"text/javascript");
builder.MergeAttribute("src", String.Format("{0}{1}", pathForCRMScripts.Replace("~", String.Empty), script));
return builder.ToString(TagRenderMode.SelfClosing);
}
private static RouteValueDictionary CreateRouteValuesFromExpression<T>(Expression<Action<T>> action)
{
if (action == null)
throw new InvalidOperationException("Action must be provided");
var body = action.Body as MethodCallExpression;
if (body == null)
{
throw new InvalidOperationException("Expression must be a method call");
}
if (body.Object != action.Parameters[0])
{
throw new InvalidOperationException("Method call must target lambda argument");
}
// This will build up a RouteValueDictionary containing the controller name, action name, and any
// parameters passed as part of the "action" parameter.
string name = body.Method.Name;
string controllerName = typeof(T).Name;
if (controllerName.EndsWith("Controller", StringComparison.OrdinalIgnoreCase))
{
controllerName = controllerName.Remove(controllerName.Length - 10, 10);
}
var values = BuildParameterValuesFromExpression(body) ?? new RouteValueDictionary();
values.Add("controller", controllerName);
values.Add("action", name);
return values;
}
private static RouteValueDictionary BuildParameterValuesFromExpression(MethodCallExpression call)
{
// Build up a RouteValueDictionary containing parameter names as keys and parameter values
// as values based on the MethodCallExpression passed in.
var values = new RouteValueDictionary();
ParameterInfo[] parameters = call.Method.GetParameters();
// If the passed in method has no parameters, just return an empty dictionary.
if (parameters.Length == 0)
{
return values;
}
for (int i = 0; i < parameters.Length; i++)
{
object parameterValue;
Expression expression = call.Arguments[i];
// If the current parameter is a constant, just use its value as the parameter value.
var constant = expression as ConstantExpression;
if (constant != null)
{
parameterValue = constant.Value;
}
else
{
// Otherwise, compile and execute the expression and use that as the parameter value.
var function = Expression.Lambda<Func<object>>(Expression.Convert(expression, typeof(object)),
new ParameterExpression[0]);
try
{
parameterValue = function.Compile()();
}
catch
{
parameterValue = null;
}
}
values.Add(parameters[i].Name, parameterValue);
}
return values;
}
}
Some observations:
The first two DataBoundSelectList overloaded methods are specifically built to load the data right into the drop down box as part of the HTML response stream rather than let Knockout's engine populate the options client-side. The third overloaded method does it client-side via the viewmodel. The first two overloads can be done when you have no requirement to add complex JSON objects to your lists. Furthermore, why render and parse the JSON object when you can have it all built and rendered server-side like any other list control.