I am building a custom HTML.LabelFor helper that looks like this :
public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> self, Expression<Func<TModel, TValue>> expression, Boolean showToolTip)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, self.ViewData);
...
}
To be able to get the proper name for the property I am using the following code :
metadata.DisplayName
And on the property of the ModelView class I got :
[DisplayName("Titel")]
The problem is that I also need a description. There is an Attribute called Display and that has Name and Description but I do not see how to extract this with the metadata variable in the above code?
Disclaimer: The following works only with ASP.NET MVC 3 (see the update at the bottom if you are using previous versions)
Assuming the following model:
public class MyViewModel
{
[Display(Description = "some description", Name = "some name")]
public string SomeProperty { get; set; }
}
And the following view:
<%= Html.LabelFor(x => x.SomeProperty, true) %>
Inside your custom helper you could fetch this information from the metadata:
public static MvcHtmlString LabelFor<TModel, TValue>(
this HtmlHelper<TModel> self,
Expression<Func<TModel, TValue>> expression,
bool showToolTip
)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, self.ViewData);
var description = metadata.Description; // will equal "some description"
var name = metadata.DisplayName; // will equal "some name"
// TODO: do something with the name and the description
...
}
Remark: Having [DisplayName("foo")] and [Display(Name = "bar")] on the same model property is redundant and the name used in the [Display] attribute has precedence in metadata.DisplayName.
UPDATE:
My previous answer won't work with ASP.NET MVC 2.0. There are a couples of properties that it is not possible to fill by default with DataAnnotations in .NET 3.5, and Description is one of them. To achieve this in ASP.NET MVC 2.0 you could use a custom model metadata provider:
public class DisplayMetaDataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(
IEnumerable<Attribute> attributes,
Type containerType,
Func<object> modelAccessor,
Type modelType,
string propertyName
)
{
var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
var displayAttribute = attributes.OfType<DisplayAttribute>().FirstOrDefault();
if (displayAttribute != null)
{
metadata.Description = displayAttribute.Description;
metadata.DisplayName = displayAttribute.Name;
}
return metadata;
}
}
which you would register in Application_Start:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
ModelMetadataProviders.Current = new DisplayMetaDataProvider();
}
and then the helper should work as expected:
public static MvcHtmlString LabelFor<TModel, TValue>(
this HtmlHelper<TModel> self,
Expression<Func<TModel, TValue>> expression,
bool showToolTip
)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, self.ViewData);
var description = metadata.Description; // will equal "some description"
var name = metadata.DisplayName; // will equal "some name"
// TODO: do something with the name and the description
...
}
Related
I have some hardcoded value which I want to change to read the value from web.config.
[Display(Name = "SomeValue")]
I tried with
[Display(Name = ConfigurationManager.AppSettings["SomeValue"].ToString())]
An attribute argument must be a constant expression, ...
To do this properly, you would create a new attribute to use instead of DisplayAttribute, and a new ModelMetadataProvider which correctly reads that attribute and the other display attributes.
To do it simply, you create a new HtmlHelper extension that reads the attribute, then access Web.config to find that AppSettings key, and returns the value.
Model
public class SomeClass
{
[Display(Name ="IdColText")]
public int Id { get; set; }
}
Extension
using System;
using System.Configuration;
using System.Linq.Expressions;
using System.Web.Mvc;
namespace ProjectName
{
public static class DisplayConfigNameExtension
{
public static MvcHtmlString DisplayConfigNameFor<TModel, TResult>(this HtmlHelper<TModel> html, Expression<Func<TModel, TResult>> expression)
{
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, new ViewDataDictionary<TModel>());
string configName = metadata.DisplayName;
return MvcHtmlString.Create(ConfigurationManager.AppSettings[configName]);
}
}
}
View
#model SomeClass
// The following is required for the HtmlHelper extension
#using ProjectName;
#Html.DisplayConfigNameFor(m => m.Id)
This will also work if using the DisplayNameAttribute instead of DisplayAttribute, and should also work for classes with MetadataTypeAttribute.
i have a .net core project where in one of my views i have to display the
DisplayName in English and in Arabic in the same view
only i get one language as the view have localized resource file
i created an extension method which take the model and return the DisplayName metadata
actually my method and the original #html helper return one language
here the method i hope some one can modify it so it return the original English metadata instead of the localized value
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using System;
using System.Linq.Expressions;
namespace iSee.IHtmlHelpers
{
public static class HtmlExtensions
{
public static IHtmlContent DisplayNameForEn<TModel, TValue>(
this IHtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression
)
{
var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, htmlHelper.ViewData, htmlHelper.MetadataProvider);
var metadata = modelExplorer.Metadata;
var DisplayName = metadata.DisplayName;
return new HtmlString(DisplayName);
}
}
}
You could do it with code like the example below.
public static IHtmlContent DisplayNameFor<TModel, TValue>(
this IHtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression,CultureInfo culture
)
{
if (culture == null)
{
culture = CultureInfo.CurrentCulture;
}
var displayAttribute = (expression.Body as MemberExpression)?.Member.GetCustomAttributes()
.FirstOrDefault(tt => tt is DisplayAttribute) as DisplayAttribute;
if (displayAttribute == null)
{
return new HtmlString("");
}
var resourceType = displayAttribute.ResourceType;
var name = displayAttribute.Name;
if(resourceType == null)
{
return new HtmlString(name);
}
var resourceManager = new global::System.Resources.ResourceManager(resourceType);
var displayName = resourceManager.GetString(name, culture);
return new HtmlString(displayName);
}
In general you want to take the DisplayAttribute.ResourceType and the DisplayAttribute.Name then use the Resource Manager along with the culture info of the language you want to translate it.
EDIT 2:
A possible Exception on .NET Core would be due to the changes on how the .resx files are used in .NET Core. Making the method above more suitable for .NET Framework applications and the IStringLocalizer<T> solution from the first EDIT more suitable for the ASP.NET Core
EDIT:
You can also use the IStringLocalizer<T> feature of ASP.NET Core find some reference here: Globalization and Localization. For example you could update the above method to use the IStringLocalizer
var culture = new CultureInfo("en-gb");
var localizer = factory.Create(typeof(Startup));
var specificLoc = localizer.WithCulture(culture);
Thanks to #Nick Polideropoulos the answer above was helpful and inspired me to find an answer for the question
here the answer in case some one else search for
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq.Expressions;
using System.Reflection;
namespace iSee.IHtmlHelpers
{
public static class HtmlExtensions
{
public static IHtmlContent DisplayNameForLatin<TModel, TValue>(
this IHtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression)
{
var memberExpression = expression.Body as MemberExpression;
if (memberExpression == null)
{
return new HtmlString("");
}
var displayAttribute = memberExpression.Member.GetCustomAttribute<DisplayAttribute>();
return new HtmlString(displayAttribute.Name);
}
}
}
So I've got a partial view that displays a model containing some info for a person at an organization. One of the properties is a list of titles which has a UIHint attribute on it to determine what display template to use for it.
Lets say that the model looks like this:
public class Info
{
[UIHint("Titles")]
[DataType("Titles")]
public virtual IEnumerable<string> Titles { get; set; }
}
Let's say that the template for Info looks like:
#model Info
#Html.DisplayFor(x=> x.Titles)
Now we have a very specific type of person-at-org instance that we want to display using the same template but we want to use a different display template for the Titles property so we create a subclass of a Info model:
public class SpecificInfo : Info
{
[UIHint("SpecificTitles")]
[DataType("SpecificTitles")]
public override IEnumerable<string> Titles { get; set; }
}
But it's still trying to use the "Titles" display template presumably because the expression passed into the DisplayFor helper thinks that it's accessing the property on the Info class.
Is there any way to get that helper to use the correct display template? I've been thinking that a possible solution would be to create my own DisplayFor extension method that figures out what the runtime type of the model is and uses reflection to find the property and check to see if we are specifying a template there but I can't shake the feeling that there might be an easier way to do it.
You are right setting #model Info in your view makes DisplayeFor use htmlHelper<Info>. It has only acces to Info attributes. You can spcify in view which template to use:
#Html.DisplayFor(x=> x.Titles, Model is SpecificInfo ? "SpecificInfo" : "Info")
but then there is no reason for UIHintAttribute.
As you write you can also write custom DisplayFor method and you can use htmlHelper.ViewData.Model to get actual model attribute(as you suggested in comment ;)):
public static MvcHtmlString CustomDisplayFor<TModel, TValue>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression)
{
MemberExpression memberExpression = (MemberExpression)expression.Body;
var propertyName = memberExpression.Member is PropertyInfo ? memberExpression.Member.Name : (string)null;
var prop = htmlHelper.ViewData.Model.GetType().GetProperty(propertyName);
var customAttributeData = prop.GetCustomAttributesData().FirstOrDefault(a => a.AttributeType.Name == "UIHintAttribute");
if (customAttributeData != null)
{
var templateName = customAttributeData.ConstructorArguments.First().Value as string;
return DisplayExtensions.DisplayFor<TModel, TValue>(htmlHelper, expression, templateName);
}
return DisplayExtensions.DisplayFor<TModel, TValue>(htmlHelper, expression);
}
View:
#Html.CustomDisplayFor(x => x.Titles)
I want to expose a globalized help text on to an MVC view.
Currently the code looks like this,
Custom attribute class
class HelpTextAttribute : Attribute
{
public string Text { get; set; }
}
View model property and custom annotation
[HelpText(Text = "This is the help text for member number")]
public string MemberNo { get; set; }
(The literal string must come from a resource class)
The question is how do i write an Html extension that could do the following
#Html.HelpTextFor(m => m.MemberNo)
You're gonna need to extend the HtmlHelper class with the following:
public static MvcHtmlString HelpTextFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expr)
{
var memberExpr = expr.Body as MemberExpression;
if (memberExpr != null)
{
var helpAttr = memberExpr.Member.GetCustomAttributes(false).OfType<HelpTextAttribute>().SingleOrDefault();
if (helpAttr != null)
return new MvcHtmlString(#"<span class=""help"">" + helpAttr.Text + "</span>");
}
return MvcHtmlString.Empty;
}
Then use it as requested:
#Html.HelpTextFor(m => m.MemberNo)
Also, be sure to mark your HelpTextAttribute with the public modifier.
Maybe you are doing things wrong because i think that DataAnnotations and MVC Helpers are different things.
i would do something like this:
a helper view on my App_Code with the code:
#helper HelpTextFor(string text) {
<span>#text</span>
}
and then use it as you wrote.
About localizing the string (I cannot comment because I do not have enough points yet)
Add the following attributes to your HelpTextAttribute
public string ResourceName { get; set; }
public Type ResourceType { get; set; }
and then adjust the HelpTextFor as follows:
var helpAttr = memberExpr.Member.GetCustomAttributes(false).OfType<HelpTextAttribute>().SingleOrDefault();
Assembly resourceAssembly = helpAttr.ResourceType.Assembly;
string[] manifests = resourceAssembly.GetManifestResourceNames();
// remove .resources
for (int i = 0; i < manifests.Length; i++)
{
manifests[i] = manifests[i].Replace(".resources", string.Empty);
}
string manifest = manifests.Where(m => m.EndsWith(helpAttr.ResourceType.FullName)).First();
ResourceManager manager = new ResourceManager(manifest, resourceAssembly);
if (helpAttr != null)
return new MvcHtmlString(#"<span class=""help"">" + manager.GetString(helpAttr.ResourceName) + "</span>");
Please see the following link on why to remove .resources
C# - Cannot getting a string from ResourceManager (from satellite assembly)
Best regards
Dominic Rooijackers
.NET software developer
I am trying to create a custom ModelMetadataProvider to provide unobtrusive attributes for the JQuery UI Autocomplete widget.
I have a custom attribute that looks like this:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class AutocompleteAttribute : Attribute, IMetadataAware
{
public void OnMetadataCreated(ModelMetadata metadata)
{
metadata.TemplateHint = "Autocomplete";
}
}
and an editor template that looks like this:
#{
var attributes = new RouteValueDictionary
{
{"class", "text-box single-line"},
{"autocomplete", "off"},
{"data-autocomplete-url", "UrlPlaceholder" },
};
}
#Html.TextBox("", ViewContext.ViewData.TemplateInfo.FormattedModelValue, attributes)
I have a viewModel with a property of type string that includes the AutocompleteAttribute like this:
public class MyViewModel
{
[Autocomplete]
public string MyProperty { get; set; }
}
When I use this viewModel in my view I check the generated html and I am getting an <input> tag which has an attribute like this: data-autocomplete-url="UrlPlaceholder".
What I want to do next is to be able to specify the URL in my view that uses my viewModel like this:
#model MyViewModel
#{ ViewBag.Title = "Create item"; }
#Html.AutoCompleteUrlFor(p => p.MyProperty, UrlHelper.GenerateUrl(null, "Autocomplete", "Home", null, Html.RouteCollection, Html.ViewContext.RequestContext, true))
// Other stuff here...
<div>
#Html.ActionLink("Back to List", "Index")
</div>
My AutoCompleteForUrl helper just saves the generated URL in a dictionary, using the property name as a key.
Next I have created a custom ModelMetadataProvider and registered it in global.asax using this line of code ModelMetadataProviders.Current = new CustomModelMetadataProvider();.
What I want to do is to insert the URL to be used by the JQuery UI Autocomplete widget into the metadata.AdditionalValues dictionary to be consumed by the Autocomplete editor template.
My custom ModelMetadataProvider looks like this:
public class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(IEnumerable<System.Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
if (metadata.TemplateHint == "Autocomplete")
{
string url;
if(htmlHelpers.AutocompleteUrls.TryGetValue(metadata.propertyName, out url)
{
metadata.AdditionalValues["AutocompleteUrl"] = url;
}
}
return metadata;
}
}
and my updated editor template looks like this:
#{
object url;
if (!ViewContext.ViewData.ModelMetadata.TryGetValue("AutocompleteUrl", out url))
{
url = "";
}
var attributes = new RouteValueDictionary
{
{"class", "text-box single-line"},
{"autocomplete", "off"},
{"data-autocomplete-url", (string)url },
};
}
#Html.TextBox("", ViewContext.ViewData.TemplateInfo.FormattedModelValue, attributes)
The problem is, the TemplateHint property never equals "Autocomplete" in my custom model metadata provider so my logic to generate the URL never gets called. I would have thought that at this point the TemplateHint property would be set as I have called the base implementation of CreateMetadata of the DataAnnotationsModelMetadataProvider.
Here's what I can confirm:
The CustomModelMetadataProvider is correctly registered as it contains other code which is getting called.
The correct editor template is getting picked up as the Html that is generated contains an attribute called "data-autocomplete-url".
If I put a breakpoint in the Autocomplete template, Visual Studio goes to the debugger.
So can anyone shed any light on this for me please? What am I misunderstanding about the ModelMetadataProvider system?
After looking through the ASP.NET MVC 3 source code I have discovered that the reason for this is because the CreateMetadata method is called prior to the OnMetadataCreated method of any IMetadataAware attributes that are applied to the model.
I have found an alternative solution that allows me to do what I wanted.
First of all I updated my AutocompleteAttribute:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class AutocompleteAttribute : Attribute, IMetadataAware
{
public const string Key = "autocomplete-url";
internal static IDictionary<string, string> Urls { get; private set; }
static AutocompleteAttribute()
{
Urls = new Dictionary<string, string>();
}
public void OnMetadataCreated(ModelMetadata metadata)
{
metadata.TemplateHint = "Autocomplete";
string url;
if (Urls.TryGetValue(metadata.PropertyName, out url))
{
metadata.AdditionalValues[Key] = url;
Urls.Remove(metadata.PropertyName);
}
}
}
and my Html helper method for setting the url in my views looks like this:
public static IHtmlString AutocompleteUrlFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string url)
{
if (string.IsNullOrEmpty(url))
throw new ArgumentException("url");
var property = ModelMetadata.FromLambdaExpression(expression, html.ViewData).PropertyName;
AutocompleteAttribute.Urls[property] = url;
return MvcHtmlString.Empty;
}
And then all I have to do in my editor template is this:
#{
object url;
ViewData.ModelMetadata.AdditionalValues.TryGetValue(AutocompleteAttribute.Key, out url);
var attributes = new RouteValueDictionary
{
{"class", "text-box single-line"},
{"autocomplete", "off"},
{ "data-autocomplete-url", url },
};
}
#Html.TextBox("", ViewContext.ViewData.TemplateInfo.FormattedModelValue, attributes)