Given a view model like this:
public class ParentViewModel
{
public object ChildViewModel { get; set; }
}
If I use Html.LabelFor
like this:
@Html.LabelFor(model => model.ChildViewModel)
I would get an output like this:
<label for="Model_ChildViewModel">ChildViewModel</label>
What I actually want though is for the generated label to use the DisplayName
attribute applied to the object E.G.
[DisplayName("My Custom Label")]
public class ChildViewModel
{
}
with an output of:
<label for="Model_ChildViewModel">My Custom Label</label>
I understand that the Html.LabelFor
method takes an expression that expects a property and it will look for the DisplayName
attribute on that property rather than the object itself.
I have created an Html helper method to achieve what I want that looks like this:
public static IHtmlString CreateLabel<TModel>(this HtmlHelper html, Func<TModel, object> func)
where TModel : class
{
TagBuilder tb = new TagBuilder("label");
var model = html.ViewData.Model as TModel;
if (model != null)
{
object obj = func(model);
if (obj != null)
{
var attribute = obj.GetType().GetCustomAttributes(
typeof(DisplayNameAttribute), true)
.FirstOrDefault() as DisplayNameAttribute;
if (attribute != null)
{
tb.InnerHtml = attribute.DisplayName;
return MvcHtmlString.Create(tb.ToString());
}
else
{
tb.InnerHtml = obj.ToString();
return MvcHtmlString.Create(tb.ToString());
}
}
}
tb.InnerHtml = html.ViewData.Model.ToString();
return MvcHtmlString.Create(tb.ToString());
}
Instead of taking an expression, my helper takes a Func<TModel, object>
which returns the object that I want to check for the DisplayName
attribute.
The first problem I had was when I tried to call this method in razor like this:
@Html.CreateLabel(model => model.ChildObject)
I get the following error:
The type arguments for method 'CreateLabel<TModel>(this HtmlHelper,
Func<TModel, object>) cannot be inferred from usage. Try specifying
the arguments explicitly.'
So I call the method like this instead:
@{ Html.CreateLabel<ChildViewModel>(model => model.ChildObject); }
but nothing gets rendered. If I use the debugger to step through my helper method, the label tag is being generated but nothing is shown when my page is rendered.
So my questions are:
- How do I fix this to generate the label to my view?
- What do I have to do so that the generic parameter can be inferred?
- Is there any way to write the Html helper to do the same thing but using an expression? I have no experience using expressions so don't know where to start.
Update
I thought I'd post the final code as I made a few minor changes. First of all I took a look at the helpers in the MVC source code and decided to split the method into three separate methods in line with the provided examples. I also removed all the TagBuilder
stuff as all I really needed was to generate the text that was to be injected between the <legend></legend>
tags. The final code is below. Once again, thanks to Sylon for helping me out with this.
public static IHtmlString LegendTextFor<TModel, TObject>(this HtmlHelper<TModel> html, Expression<Func<TModel, TObject>> expression)
{
return LegendTextHelper(html,
ModelMetadata.FromLambdaExpression(expression, html.ViewData),
ExpressionHelper.GetExpressionText(expression),
expression.Compile().Invoke(html.ViewData.Model));
}
private static IHtmlString LegendTextHelper<TModel, TObject>(this HtmlHelper<TModel> html, ModelMetadata metadata, string htmlFieldName, TObject value)
{
string resolvedLabelText = metadata.DisplayName ?? value.GetDisplayName() ?? metadata.PropertyName ?? htmlFieldName.Split('.').Last();
if (String.IsNullOrEmpty(resolvedLabelText))
return MvcHtmlString.Empty;
return MvcHtmlString.Create(resolvedLabelText);
}
private static string GetDisplayName<T>(this T obj)
{
if (obj != null)
{
var attribute = obj.GetType()
.GetCustomAttributes(typeof(DisplayNameAttribute), false)
.Cast<DisplayNameAttribute>()
.FirstOrDefault();
return attribute != null ? attribute.DisplayName : null;
}
else
{
return null;
}
}