I was thinking it would be very useful to have an extended version of
the EditorFor HTML helper which automatically writes out value data bindings for Knockout JS.
This would be for where the client side view model and the server side view model are the same - I've already autogenerated the client side view model by using ko mapping and getting the viewmodel via AJAX.
Has anyone else attempted something like this, or are there any projects which include something similar to what I'm thinking here?
The advantage of this would be that when refactoring there would be no danger of the data bound values being missed.
We have done something along these lines, its far from perfect, and we have much more in our custom extensions, but I extracted the essence.
using System.Web.Mvc.Html;
namespace System.Web.Mvc
{
public static class EditorForExtensions
{
public static MvcHtmlString TextBoxForViewModel<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
{
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var htmlAttributes = HtmlAttributesForKnockout(metadata);
return htmlHelper.TextBoxFor(expression, htmlAttributes);
}
private static Dictionary<string, object> HtmlAttributesForKnockout(ModelMetadata metadata)
{
var htmlAttributes = new Dictionary<string, object>();
var knockoutParameter = String.Format("value: {0}", metadata.PropertyName);
htmlAttributes.Add("data-bind", knockoutParameter);
return htmlAttributes;
}
}
}
This can then be used like:
#Html.TextBoxForViewModel(m => m.Name)
Wanted to add the htmlAttributes overload to Chad's answer above for anyone looking to drop it in and have it work. All the other helpers can be built from these examples pretty easily. (Thanks Chad, your extension helped ease my transition into using knockout!)
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Web.Mvc.Html;
namespace System.Web.Mvc {
public static class KnockoutExtensions {
public static MvcHtmlString KnockoutTextBoxFor<TModel, TProperty>( this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression ) {
var metadata = ModelMetadata.FromLambdaExpression( expression, htmlHelper.ViewData );
var htmlAttributes = HtmlAttributesForKnockout( metadata );
return htmlHelper.TextBoxFor( expression, htmlAttributes );
}
public static MvcHtmlString KnockoutTextBoxFor<TModel, TProperty>( this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object attributes ) {
// convert passed anonymous object (attributes) into IDictionary<string,object> to pass into attribute parser
var attrs = HtmlHelper.AnonymousObjectToHtmlAttributes( attributes ) as IDictionary<string, object>;
var metadata = ModelMetadata.FromLambdaExpression( expression, htmlHelper.ViewData );
var htmlAttributes = HtmlAttributesForKnockout( metadata, attrs );
return htmlHelper.TextBoxFor( expression, htmlAttributes );
}
private static Dictionary<string, object> HtmlAttributesForKnockout( ModelMetadata metadata, IEnumerable<KeyValuePair<string, object>> attributes = null ) {
var htmlAttributes = new Dictionary<string, object>();
var knockoutParameter = String.Format( "value: {0}", metadata.PropertyName );
htmlAttributes.Add( "data-bind", knockoutParameter );
if ( attributes != null ) foreach ( var attr in attributes ) htmlAttributes.Add( attr.Key, attr.Value );
return htmlAttributes;
}
}
}
Related
I created a custom HTMLHelper extension method for Razor
public static MvcHtmlString BootstrapButtonGroupForEnum<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression)
{
// render stuff
}
And use it like this:
#Html.BootstrapButtonGroupForEnum(model => model.PriceType)
It works fine for adding records, but how to I pass the value of the model's method to it? So I can make a value preselected. Do I need to pass the value via an additional parameter or is there a more elegant way of doing this. I had a look in Watch and did not see the value there, did I miss something?
Thanks!
EDIT: The full implementation of the html helper:
public static MvcHtmlString BootstrapButtonGroupForEnum<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression)
{
var metaData = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
string[] listOfNames = Enum.GetNames(metaData.ModelType);
var sb = new StringBuilder();
if (listOfNames != null)
{
sb = sb.Append("<div class=\"btn-group btn-group\" role=\"group\" >");
foreach (var value in listOfNames)
{
sb.Append("<button type=\"button\" class=\"btn btn-primary\" onclick=\"");
// onclick event hadler
sb.AppendFormat("$('#{0}').val('{1}');", metaData.PropertyName, value); // get the value to the hidden
sb.Append("$(this).addClass('active').siblings().removeClass('active');"); // retain selection
sb.Append("\">");
sb.Append(value.ToString().UnPascalCase());
sb.Append("</button>");
}
sb = sb.Append("</div>");
sb = sb.AppendFormat("<input type=\"hidden\" id=\"{0}\" name=\"{0}\" />", metaData.PropertyName);
}
return MvcHtmlString.Create(sb.ToString());
}
I suppose I could try using #HiddenFor(...) instead of rendering it directly as html?
I needed to assign the value of the hidden to metaData.Model and take it from there.
sb = sb.AppendFormat("<input type=\"hidden\" id=\"{0}\" name=\"{0}\" value=\"" + metaData.Model + "\" />", metaData.PropertyName);
CORRECTION: After following Stephens advice and looking at MVC source code I concluded that the hidden should be rendered like this:
var hidden = InputExtensions.HiddenFor(htmlHelper, expression, null);
sb.Append(hidden );
I'd like to add some classes to my labels, so I added Label.cshtml Under Shared/EditorTemplates folder. It seems Razor is ignoring it. I've tried DisplayTemplatesfolder too, but it didn't work neither.
Can I have editor template for labels? If no, what's the best way to customize them?
Label.cshtml:
#model object
#Html.LabelFor(m => m, new { #class = "col-xs-4 control-label" })
UPDATE:
This is my code that I took from this post and with some changes from this useful link. It still doesn't work, I don't know what's going on. Can anyone help?
public static class Extensions
{
public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, object htmlAttributes)
{
return LabelFor(html, expression, new RouteValueDictionary(htmlAttributes));
}
public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression)
{
return LabelFor(html, expression, null);
}
public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, IDictionary<string, object> htmlAttributes){
ModelMetadata metaData = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
string FieldName = ExpressionHelper.GetExpressionText(expression);
string LabelText = metaData.DisplayName ?? metaData.PropertyName ?? FieldName.Split('.').Last();
if (string.IsNullOrEmpty(LabelText))
return MvcHtmlString.Empty;
TagBuilder tag = new TagBuilder("label");
tag.MergeAttributes(htmlAttributes);
tag.SetInnerText(LabelText);
tag.Attributes.Add("for", html.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldId(FieldName));
tag.Attributes.Add("class", "control-label col-xs-2");
return MvcHtmlString.Create(tag.ToString());
}
}
EditorTemplate's are based on the type of a property (not on the html element they generate). You would need to create your own html helper
public static MvcHtmlString BootstrapLabelFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression)
{
var attributes = new { #class = "col-xs-4 control-label" };
return helper.LabelFor(expression, attributes);
}
and use it in the view as
#Html.BootstrapLabelFor(m => m.yourProperty)
However this seems an inflexible way to use a helper just to save adding the html attributes in the standard LabelFor() helper. Perhaps it might be more useful to combine the associated textbox and validation message in the one helper as per this answer (Note this answer also shows how to add the namespace to the web.config file
EditorTemplate and DisplayTemplate are based on property type. You'd do this by creating your own HTML helper.
public static class LabelExtensions
{
public static string Label(this HtmlHelper helper, string target, string text)
{
return String.Format("<label for='{0}'>{1}</label>", target, text);
}
}
I'm providing an extra overload to RadioButtonFor and want to add a Key Value pair to the HTML Attributes that are passed in.
As an example I am passing in something like:
new { id = "someID" }
When i the use the HtmlHelper.AnonymousObjectToHtmlAttributes method as seems to be the suggestions I'm finding), its resulting in a dictionary with 4 items with Keys of "Comparer", "Count", "Keys", "Values". I then try to use Reflection to iterate over the values in both "Keys" and "Values", but cannot get that to work either.
Essentially all I want to do is to be able to cast the htmlAttributes to an IDictionary , add a item and then pass it on to a regular RadioButtonFor method.
Edit:
Heres what Im actually trying to do. Provide an overload called isDisabled to be able to set the disabled state of the radio button as this cant be easily done directly using HTML attributes because disabled = false stillr esults in disabled being rendered to tag and disables the radio.
public static MvcHtmlString RadioButtonFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object value, bool isDisabled, object htmlAttributes)
{
var linkAttributes = System.Web.Mvc.HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
Dictionary<string, object> htmlAttributesDictionary = new Dictionary<string, object>();
foreach (var a in linkAttributes)
{
if (a.Key.ToLower() != "disabled")
{
htmlAttributesDictionary.Add(a.Key, a.Value);
}
}
if (isDisabled)
{
htmlAttributesDictionary.Add("disabled", "disabled");
}
return InputExtensions.RadioButtonFor<TModel, TProperty>(htmlHelper, expression, value, htmlAttributesDictionary);
}
Looks like you might be applying the AnonymousObjectToHtmlAttributes either twice or to the wrong item.
Without more of your code, it's hard to tell
var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(new { id = "someID" });
attributes.Count = 1
attributes.Keys.First() = id
compared with
var attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(HtmlHelper.AnonymousObjectToHtmlAttributes(new { id = "someID" }));
attributes.Count = 3
attributes.Keys.Join = Count,Keys,Values
When writing your overload, make sure your parameter is: object htmlAttributes for the new { } part with an overload with the IDictionary, eg:
Public static MvcHtmlString MyRadioButtonFor(..., object htmlAttributes)
{
return MyRadioButtonFor(...., HtmlHelper.AnonymousObjectToHtmlAttrbites(htmlAttributes);
}
public static MvcHtmlString MyRadioButtonFor(..., IDictionary<string, object> htmlAttributes)
{
htmlAttributes.Add("item", item);
return RadioButtonFor(..., htmlAttributes);
}
(just to be clear, never use My... - it's just for illustration)
Its unclear why you would not just use and existing overload that accepts object htmlAttributes to add the disabled="disabled" attribute, however the following should work
public static MvcHtmlString RadioButtonFor<TModel, TProperty>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression, object value, bool isDisabled, object htmlAttributes)
{
IDictionary<string, object> attributes = HtmlHelper.AnonymousObjectToHtmlAttributes(htmlAttributes);
if (isDisabled && !attributes.ContainsKey("disabled"))
{
attributes.Add("disabled", "disabled");
}
return InputExtensions.RadioButtonFor<TModel, TProperty>(htmlHelper, expression, value, attributes);
}
I would like to be able to do things like:
<input name=#Model.SomeProperty (as the actual property name and not its value) value=#Model.SomeProperty type="text/>
so that model binding still works, but I don't need to use the Razor helpers.
You can add a Helper Method that return your DisplayName:
public static MvcHtmlString GetPropertyName<TModel, TProperty>( this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TProperty>> expression )
{
var metaData = ModelMetadata.FromLambdaExpression<TModel, TProperty>(expression, htmlHelper.ViewData);
string value = metaData.PropertyName ?? expressionHelper.GetExpressionText(expression);
return MvcHtmlString.Create(value);
}
And then you just use: #Html.GetPropertyName(m => m.SomeProperty)
How do I create an ASP.Net MVC Helper for an Html.Label which takes in attributes?
Currently when I define an Html.TextBox I am able to pass in a list of attributes. Sort of like below:
new {disabled="disabled", #class="pcTextBoxWithoutPaddingDisabled"})%>
However, it does not look as though the Html.Label has this feature. As a result, I have to define my labels using the label tag. Sort of like below:
<label class="pcLabelBlackWithoutPadding">
I would like to be consistent I how my Html element get created.
So, how do I create an Html.Label that will take in a list of attributes?
Thanks for your help.
This is updated version for MVC3:
public static MvcHtmlString Label(this HtmlHelper helper, String htmlFieldName, String labelText, Object htmlAttributes)
{
ModelMetadata metadata = ModelMetadata.FromStringExpression(htmlFieldName, helper.ViewData);
String innerText = labelText ?? (metadata.DisplayName ?? (metadata.PropertyName ?? htmlFieldName.Split('.').Last()));
if (String.IsNullOrEmpty(innerText))
{
return MvcHtmlString.Empty;
}
TagBuilder tagBuilder = new TagBuilder("label");
tagBuilder.Attributes.Add("for", TagBuilder.CreateSanitizedId(helper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(htmlFieldName)));
tagBuilder.MergeAttributes(new RouteValueDictionary(htmlAttributes));
tagBuilder.SetInnerText(innerText);
return new MvcHtmlString(tagBuilder.ToString(TagRenderMode.Normal));
}
I have modified Alexandr code a bit with lambda expression, in case anyone needed the lambda expression.
usage:
#Html.LabelFor(model => model.Property , new { #class = "bigFont" })
code:
public static MvcHtmlString LabelFor<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression, Object htmlAttributes)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, htmlHelper.ViewData);
var innerText = metadata.DisplayName ?? metadata.PropertyName;
if (String.IsNullOrEmpty(innerText))
{
return MvcHtmlString.Empty;
}
var tagBuilder = new TagBuilder("label");
tagBuilder.Attributes.Add("for", TagBuilder.CreateSanitizedId(htmlHelper.ViewContext.ViewData.TemplateInfo.GetFullHtmlFieldName(metadata.PropertyName)));
tagBuilder.MergeAttributes(new RouteValueDictionary(htmlAttributes));
tagBuilder.SetInnerText(innerText);
return new MvcHtmlString(tagBuilder.ToString(TagRenderMode.Normal));
}
I'd suggest creating your own HtmlHelper extension method and using a TagBuilder to create the label.
public static HtmlHelperExtensions
{
public static Label( this HtmlHelper helper, string labelText, object properties )
{
var builder = new TagBuilder("label");
builder.MergeAttributes( new RouteValueDictionary( properties ) );
builder.SetInnerText( labelText );
return builder.ToString( TagRenderMode.Normal );
}
}
See the MVC source code for ideas on how to create a strongly-typed label helper. Note that you'll need to add the namespace containing your extensions either to the page or the web.config to be able to use it.