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
Related
I'm developing ASP.NET MVC appliation. I've found Fluent Validation great validation tool and it works, but with my current architecture it has one drawback. The validator does not care about Metadata. I'm using Metadata on seperate class for clarity.
Model
[MetadataType(typeof(DocumentEditMetadata))]
[Validator(typeof(DocumentValidator))]
public class DocumentEditModel
{
public string DocumentNumber { get; set; }
(etc...)
}
Metadata Model
public class DocumentEditMetadata
{
[Required]
[StringLength(50)]
[Display(ResourceType = typeof(Label), Name = "DocumentNumber")]
public string DocumentNumber { get; set; }
(etc...)
}
Can anyone point a solution? I need data annotations for localization of labels (hence the DisplayAttribute).
Think you need to write your own Display name resolver for fluent validation (guess this should be placed in your global.asax).
Caution
This solution is only trying to resolve the display name.
Your other "validation" attributes (Required, StringLength) should no more be used, as you will manage that with FluentValidation.
ValidatorOptions.DisplayNameResolver = (type, memberInfo, expression) =>
{
//this will get in this case, "DocumentNumber", the property name.
//If we don't find anything in metadata / resource, that what will be displayed in the error message.
var displayName = memberInfo.Name;
//we try to find a corresponding Metadata type
var metadataType = type.GetCustomAttribute<MetadataTypeAttribute>();
if (metadataType != null)
{
var metadata = metadataType.MetadataClassType;
//we try to find a corresponding property in the metadata type
var correspondingProperty = metadata.GetProperty(memberInfo.Name);
if (correspondingProperty != null)
{
//we try to find a display attribute for the property in the metadata type
var displayAttribute = correspondingProperty.GetCustomAttribute<DisplayAttribute>();
if (displayAttribute != null)
{
//finally we got it, try to resolve the name !
displayName = displayAttribute.GetName();
}
}
}
return displayName ;
};
Personal point of view
By the way, if you just use Metadata classes for clarity, don't use them !
It may be a solution if you have no choice (when entity classes are generated from an edmx and you really want to manage the display names this way), but I would really avoid them if it's not necessary.
public class CreateHireViewModel
{
[Display(Name = nameof(CreateHireViewModel.Title), ResourceType = typeof(Resource.HireResource.Hire))]
public string Title { get; set; }
}
public class CreateHireViewModelValidator : AbstractValidator<CreateHireViewModel>
{
public CreateHireViewModelValidator(IStringLocalizer<Resource.HireResource.Hire> l)
{
RuleFor(x => x.Title).NotEmpty().WithName(l[nameof(CreateHireViewModel.Title)]);
RuleFor(x => x.Title).Length(3, 50).WithName(l[nameof(CreateHireViewModel.Title)]);
}
}
Currently I am trying to set dynamic the DisplayName of a property but I cannot find a way how get information of current property inside an attribute.
This is what I want to achieve:
Desired Outcome
My Attribute
public class DisplayNameFromPropertyAttribute : DisplayNameAttribute
{
public DisplayNameFromPropertyAttribute(string propertyName)
: base(GetDisplayName(propertyName))
{
}
private string GetDisplayBame(string propertyName)
{
// Get the value from the given property
}
}
My Model
I am trying to read the value from MyDisplayName into my custom DisplayNameFromProperty attribute
public class MyAwesomeModel
{
public string MyDisplayName { get; set; }
[Required]
[DisplayNameFromProperty("MyDisplayName")]
public string MyValue { get; set; }
}
My Page
#Html.LabelFor(model => model.MyValue)
#Html.TextBoxFor(model => model.MyValue)
#Html.ValidationMessageFor(model => model.MyValue)
Question
Currently I cannot find any reference on internet doing the same. Can someone help me out?
If this is not possible: What are the alternatives to achieve same result?
The ComponentModel.DataAnnotations validation attributes should use my custom display name
The #Html.LabelFor(model => model.MyValue) should use my custom display name
You can create a cusom HTML extension method that lets you do #Html.DictionaryLabelFor(x=>x.Property) and pull it from a dictionary
public static IHtmlString DictionaryLabelFor<TModel, TValue>(
this HtmlHelper<TModel> html,
Expression<Func<TModel, TValue>> expression, string text = null, string prefix = null)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
var displayName = metadata.DisplayName;
if (string.IsNullOrWhiteSpace(text))
{
// Your code to get the label via reflection
// of the object
string labelText = "";
return html.Label(prefix + metadata.PropertyName, labelText);
}
else
{
return html.Label(prefix + metadata.PropertyName, text);
}
}
Overriding the properties on this works, only thing missing is the custom html attributes which wasn't needed when I wrote it
The validation error message is slightly different, you should always write custom errors for these fields so you can rely on them in a resx, you look at the modelstate for the (prefix + key) to get the errors, then get the translated value for each case.
You're best avoiding overwriting the standard HTML call as you'll be making excess calls where not needed elsewhere.
When you have that working and understand it the error messages section is pretty trivial to write in yourself, depends how you want to do the formatting on errors. I'm not writing it in as it's basically doing everything for you and if you don't understand how it works you'll be SOL when it comes to writing further extensions
I'm using ASP.NET MVC3 with Razor and C#. I am making a form builder of sorts, so I have a model that has a collection of the following object:
public class MyFormField
{
public string Name { get; set; }
public string Value { get; set; }
public MyFormType Type { get; set; }
}
MyFormType is just an enum that tells me if the form field is a checkbox, or textbox, or file upload, or whatever. My editor template looks something like this (see the comment):
~/Views/EditorTemplates/MyFormField.cshtml
#model MyFormField
#{
switch (Model.Type)
{
case MyFormType.Textbox:
#Html.TextBoxFor(m => m.Value)
case MyFormType.Checkbox:
#Html.CheckBoxFor(m => m.Value) // This does not work!
}
}
I tried casting/converting the m.Value to a bool in the lambda expression for CheckBoxFor(), but that threw an error. I would just manually construct a checkbox input, but CheckBoxFor() seems to do two things that I can't seem to replicate:
Creates a hidden input that somehow gets populated by the checkbox. This appears to be what the model binder picks up.
Generates the name form the object so that the model binder gets the value into the right property.
Does anyone know a way around using CheckBoxFor() on a string, or a way to replicate its functionality manually, so that I can make this work?
You could also add a property on your viewmodel:
public class MyFormField
{
public string Name { get; set; }
public string Value { get; set; }
public bool CheckBoxValue
{
get { return Boolean.Parse(Value); }
}
public MyFormType Type { get; set; }
}
Your view would be something like this:
#model MyFormField
#{
switch (Model.Type)
{
case MyFormType.Textbox:
#Html.TextBoxFor(m => m.Value)
case MyFormType.Checkbox:
#Html.CheckBoxFor(m => m.CheckBoxValue) // This does work!
}
}
Use Boolean.TryParse if you want to avoid exceptions.
One way is to create your own htmlhelper extension method.
public static MvcHtmlString CheckBoxStringFor<TModel>(this HtmlHelper<TModel> html, Expression<Func<TModel, string>> expression)
{
// get the name of the property
string[] propertyNameParts = expression.Body.ToString().Split('.');
string propertyName = propertyNameParts.Last();
// get the value of the property
Func<TModel, string> compiled = expression.Compile();
string booleanStr = compiled(html.ViewData.Model);
// convert it to a boolean
bool isChecked = false;
Boolean.TryParse(booleanStr, out isChecked);
TagBuilder checkbox = new TagBuilder("input");
checkbox.MergeAttribute("id", propertyName);
checkbox.MergeAttribute("name", propertyName);
checkbox.MergeAttribute("type", "checkbox");
checkbox.MergeAttribute("value", "true");
if (isChecked)
checkbox.MergeAttribute("checked", "checked");
TagBuilder hidden = new TagBuilder("input");
hidden.MergeAttribute("name", propertyName);
hidden.MergeAttribute("type", "hidden");
hidden.MergeAttribute("value", "false");
return MvcHtmlString.Create(checkbox.ToString(TagRenderMode.SelfClosing) + hidden.ToString(TagRenderMode.SelfClosing));
}
The usage is the same as CheckBoxFor helper (e.Value is a string)
#Html.CheckBoxStringFor(e => e.Value)
Use the Checkbox, this simple way works fine
#Html.CheckBox("IsActive", Model.MyString == "Y" ? true : false)
I had this problem as well but was unable to modify the view model. Tried mdm20s solution but as i suspected it does not work on collection properties (it does not add the indexes to the names and ids like the native html helpers). To overcome this you can use the Html.CheckBox instead. It adds the proper indexes and you can pass the value of the checkbox yourself.
If you really want to use an expression you can always write a wrapper similar to mdm20s but replace everything after the TryParse with
return Html.CheckBox("propertyName", isChecked). Obviously you will need to add using System.Web.Mvc.Html as well.
When I use DisplayAttribute in ASP.NET MVC 3 models it quickly becomes a pain writing them because we have to either hardcode the string or reference the string from a some static class that contains const strings (which is what I have now, see below). But even that is too much for me.
I would like to come up with an attribute that would be called something like [SimpleDisplay] and it would implicitly construct the string for resources by looking at
class name,
property name that the attribute is attached to.
Is this possible?
Something like this
public class Product {
[SimpleDisplay] // it will take Product and Name and do something like this Product_Name
public string Name { get; set; }
}
This is what I want to get rid of, if possible:
[Display(ResourceType = typeof(Resources.Localize), Name = ResourceStrings.product_prettyid)]
public virtual int PrettyId
{
get;
set;
}
[Display(ResourceType = typeof(Resources.Localize), Name = ResourceStrings.product_name)]
public virtual string Title
{
get;
set;
}
Now I know that it is not possible to inherit the DisplayAttribute cause it's sealed. What other options I have? Does it even make sense?
I would try creating just a standard attribute and custom DataAnnotationsModelMetadataProvider. You can override CreateMetadata method, which gets IEnumerable<Attribute>. You should than search for your attribute
attributes.OfType<SimpleDisplayAttribute>().FirstOrDefault();
and populate model metadata in any way you want.
If i have a correct understanding what you mean, you may just create a simple custom attribute like this one:
public class LocalizedDisplayNameAttribute : DisplayNameAttribute {
public LocalizedDisplayNameAttribute(string expression) : base(expression) { }
public override string DisplayName {
get {
try {
string[] vals = base.DisplayName.Split(',');
if(vals != null && vals.Length == 2)
return (string)HttpContext.GetGlobalResourceObject(vals[0].Trim(), vals[1].Trim());
} catch {}
return "{res:" + base.DisplayName + "}";
}
}
}
You may then use it as an attribute on your properies. MVC HTML extensions will pickup your custom attribute.
[LocalizedDisplayName("LBL, lbl_name1")]
public string[] Name1 { get; set; }
I have created a custom ResourceProvider to pull localization information from a database. I now want to use DataAnnotation to add validation to the model.
DataAnnotation has ErrorMessageResourceType and ErrorMessageResourceName properties but ErrorMessageResourceType only accepts System.Type (i.e. a compiled resource file)
Is there any way to get DataAnnotation to use the custom ResourceProvider?
I realize this is an old question, but wanted to add a bit. I found myself in the same situation and there doesn't appear to be any documentation/blogumentation on this topic. Nevertheless, I figured out a way to use a custom resource provider, with one caveat. The caveat is that I'm in an MVC application so I still have HttpContext.GetLocalResourceObject() available. This is the method that asp.net uses to localize items. The absence of the resource object doesn't stop you from writing our own solution, even if its a direct query of the DB tables. Nevertheless, I thought it was worth pointing out.
While I'm not terribly happy with the following solution, it seems to work. For each validation attribute I want to use I inherit from said attribute and overload the IsValid(). The decoration looks like this:
[RequiredLocalized(ErrorMessageResourceType= typeof(ClassBeginValidated), ErrorMessageResourceName="Errors.GenderRequired")]
public string FirstName { get; set; }
The new attribute looks like this:
public sealed class RequiredLocalized : RequiredAttribute {
public override bool IsValid(object value) {
if ( ! (ErrorMessageResourceType == null || String.IsNullOrWhiteSpace(ErrorMessageResourceName) ) ) {
this.ErrorMessage = MVC_HtmlHelpers.Localize(this.ErrorMessageResourceType, this.ErrorMessageResourceName);
this.ErrorMessageResourceType = null;
this.ErrorMessageResourceName = null;
}
return base.IsValid(value);
}
}
Notes
You need to decorate your code with the derived attribute, not the standard one
I'm using ErrorMessageResourceType to pass the type of the class being validated. By that I mean if I'm in a customer class and validating the FirstName property I would pass typeof(customer). I'm doing this because in my database backend I'm using the full class name (namespace + classname) as a key (the same way a page URL is used in asp.net).
MVC_HtmlHelpers.Localize is just a simple wrapper for my custom resource provider
The (semi-stolen) helper code looks like this ....
public static string Localize (System.Type theType, string resourceKey) {
return Localize (theType, resourceKey, null);
}
public static string Localize (System.Type theType, string resourceKey, params object[] args) {
string resource = (HttpContext.GetLocalResourceObject(theType.FullName, resourceKey) ?? string.Empty).ToString();
return mergeTokens(resource, args);
}
private static string mergeTokens(string resource, object[] args) {
if (resource != null && args != null && args.Length > 0) {
return string.Format(resource, args);
} else {
return resource;
}
}
I have used fluent validation to achieve this. It saves me lots of time. This is what my Globalized validator looks like. It does mean that you don't use data anotations, but sometimes data anotations get a bit big and messy.
Here is an example:
(Errors.Required, Labels.Email and Errors.AlreadyRegistered are in my blobal resources folder.)
public class CreateEmployerValidator : AbstractValidator<CreateEmployerModel> {
public RegisterUserValidator() {
RuleFor(m => m.Email)
.NotEmpty()
.WithMessage(String.Format(Errors.Required, new object[] { Labels.Email }))
.EmailAddress()
.WithMessage(String.Format(Errors.Invalid, new object[] { Labels.Email }))
.Must(this.BeUniqueEmail)
.WithMessage(String.Format(Errors.AlreadyRegistered, new object[] { Labels.Email }));
}
public bool BeUniqueEmail(this IValidator validator, string email ) {
//Database request to check if email already there?
...
}
}
Like I said, it is a move away form data annotations, only because I already have too many annotations on my methods already!
I'll add my findings since I had to fight with this. Maybe it will help someone.
When you derive from RequiredAttribute, it seems to break client side validation. So to fix this I implemented IClientValidatable and implemented the GetClientValidationRules method. Resources.GetResources is static helper method I have that wraps around HttpContext.GetGlobalResourceObject.
The custom required attribute:
public class LocalizedRequiredAttribute : RequiredAttribute, IClientValidatable
{
public LocalizedRequiredAttribute(string resourceName)
{
this.ErrorMessage = Resources.GetResource(resourceName);
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
yield return new ModelClientValidationRule
{
ErrorMessage = this.ErrorMessage,
ValidationType= "required"
};
}
}
Usage:
[LocalizedRequired("SomeResourceName")]
public string SomeProperty { get; set; }
And my Resources helper if anyone is interested:
public class Resources
{
public static string GetResource(string resourceName)
{
string text = resourceName;
if (System.Web.HttpContext.Current != null)
{
var context = new HttpContextWrapper(System.Web.HttpContext.Current);
var globalResourceObject = context.GetGlobalResourceObject(null, resourceName);
if (globalResourceObject != null)
text = globalResourceObject.ToString();
}
return text;
}
}