I would like to have a model with a dynamic label in the razor view that gets set at runtime but is based on a string from a resource file using string formatting.
Lets say I have a simple model with a single property
public class Simple
{
[Display(ResourceType = (typeof(Global)), Name = "UI_Property1")]
[Required(ErrorMessageResourceType = (typeof(Global)), ErrorMessageResourceName = "ERROR_Required")]
[StringLength(40, ErrorMessageResourceType = (typeof(Global)), ErrorMessageResourceName = "ERROR_MaxLength")]
public string Property1{ get; set; }
}
And the resource file has the following strings
UI_Property1 {0}
ERROR_Required Field {0} is required.
ERROR_MaxLength Maximum length of {0} is {1}
and I would like to do something like this in the razor view
#Html.LabelFor(m => m.Property1, "xyz", new { #class = "control-label col-sm-4" })
and the resulting view would show the field label as 'xyz' and the value 'xyz' would also be shown in the validation messages returned from the server model validation.
I have been looking at various ways of doing this with no luck. I have investigated overriding the DisplayAttribute but this is a sealed class.
I also looked at overriding the DisplayName attribute but this does not get picked up properly with the required validation messages. Plus I wasn't sure how to inject the dynamic text in to the attribute which I assume will need to be done in the attribute constructor.
I have also looked at writing a custom DataAnnotationsModelMetadataProvider but cannot see a way of using this to achieve what I want. This may be down to my lack of coding skills.
The 'xyz' string will come from a setting in the web.config file and does not need to be injected at the LabelFor command but can be injected somewhere else if it would make more sense.
If anyone can give me clue as to how I might achieve this that would be great.
I found this post
Is it valid to replace DataAnnotationsModelMetadataProvider and manipulate the returned ModelMetadata
which led me to a solution as follows:
I added a custom section to my web config
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="labelTranslations" type="AttributeTesting.Config.LabelTranslatorSection" />
... other sections here
</configSections>
<labelTranslations>
<labels>
<add label=":Customer:" translateTo="Customer Name" />
<add label=":Portfolio:" translateTo="Portfolio Name" />
<add label=":Site:" translateTo="Site Name" />
</labels>
</labelTranslations>
The class for handling the custom section loads the labels that are to be translated
public class LabelElement : ConfigurationElement
{
private const string LABEL = "label";
private const string TRANSLATE_TO = "translateTo";
[ConfigurationProperty(LABEL, IsKey = true, IsRequired = true)]
public string Label
{
get { return (string)this[LABEL]; }
set { this[LABEL] = value; }
}
[ConfigurationProperty(TRANSLATE_TO, IsRequired = true)]
public string TranslateTo
{
get { return (string)this[TRANSLATE_TO]; }
set { this[TRANSLATE_TO] = value; }
}
}
[ConfigurationCollection(typeof(LabelElement))]
public class LabelElementCollection : ConfigurationElementCollection
{
protected override ConfigurationElement CreateNewElement()
{
return new LabelElement();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((LabelElement)element).Label;
}
public LabelElement this[string key]
{
get
{
return this.OfType<LabelElement>().FirstOrDefault(item => item.Label == key);
}
}
}
public class LabelTranslatorSection : ConfigurationSection
{
private const string LABELS = "labels";
[ConfigurationProperty(LABELS, IsDefaultCollection = true)]
public LabelElementCollection Labels
{
get { return (LabelElementCollection)this[LABELS]; }
set { this[LABELS] = value; }
}
}
The translator then uses the custom section to translate a given label to the translated version if it exists otherwise it returns the label
public static class Translator
{
private readonly static LabelTranslatorSection config =
ConfigurationManager.GetSection("labelTranslations") as LabelTranslatorSection;
public static string Translate(string label)
{
return config.Labels[label] != null ? config.Labels[label].TranslateTo : label;
}
}
I then wrote a custom Metadata provider which modifies the displayname based on the translated version
public class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(
IEnumerable<Attribute> attributes,
Type containerType,
Func<object> modelAccessor,
Type modelType,
string propertyName)
{
// Call the base method and obtain a metadata object.
var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
if (containerType != null)
{
// Obtain informations to query the translator.
//var objectName = containerType.FullName;
var displayName = metadata.GetDisplayName();
// Update the metadata from the translator
metadata.DisplayName = Translator.Translate(displayName);
}
return metadata;
}
}
after that it all just worked and the labels and the validation messages all used the translated versions. I used the standard LabelFor helpers without any modifications.
The resource file looks like this
ERROR_MaxLength {0} can be no more than {1} characters long
ERROR_Required {0} is a required field
UI_CustomerName :Customer:
UI_PortfolioName :Portfolio:
UI_SiteName :Site:
Related
Is it possible in ASP.Net Core to automatically convert camel case property names in view models to insert spaces into the corresponding labels when using tag helpers?
If my view model looks like this...
[Display(Name = "First Name")]
public string FirstName { get; set; }
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Display(Name = "Referral Date")]
public DateTime ReferralDate { get; set; }
It seems like a lot of extra configuration applying data annotations such as
[Display(Name = "First Name")]
to simply insert a space between words. It would make sense that Tag Helpers would insert the space by default to avoid this manual configuration and potential typos.
If not could a custom tag helper assist in this situation and if so how would it work?
If you only care about label, you can easily override LabelTagHelper.
[HtmlTargetElement("label", Attributes = "title-case-for")]
public class TitleCaseTagHelper : LabelTagHelper
{
public TitleCaseTagHelper(IHtmlGenerator generator) : base(generator)
{
}
[HtmlAttributeName("title-case-for")]
public new ModelExpression For { get; set; }
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (context == null)
throw new ArgumentNullException("context");
if (output == null)
throw new ArgumentNullException("output");
string name = For.ModelExplorer.Metadata.DisplayName ?? For.ModelExplorer.Metadata.PropertyName;
name = name.Humanize(LetterCasing.Title);
TagBuilder tagBuilder = this.Generator.GenerateLabel(
this.ViewContext,
this.For.ModelExplorer,
this.For.Name,
name,
(object) null);
if (tagBuilder == null)
return;
output.MergeAttributes(tagBuilder);
if (output.IsContentModified)
return;
TagHelperContent childContentAsync = await output.GetChildContentAsync();
if (childContentAsync.IsEmptyOrWhiteSpace)
output.Content.SetHtmlContent((IHtmlContent) tagBuilder.InnerHtml);
else
output.Content.SetHtmlContent((IHtmlContent) childContentAsync);
}
}
Usage
<label title-case-for="RememberMe" class="col-md-2 control-label"></label>
Please ensure to place using statement and #addTagHelper inside _ViewImports.cshtml.
#using YourNameSpace.Helpers
#addTagHelper *, YourNameSpace
Note
I use Humanizer English Only NuGet Package - Humanizer.Core. It is more robust than writing my own method. If you doesn't like overhead, you can just use Regular Expression.
You can achieve this by building and registering a custom display metadata provider. There are libraries that will perform more elaborate "humanization" to the property names, but you can achieve something pretty useful with some trusty regular expressions.
public class HumanizerMetadataProvider : IDisplayMetadataProvider
{
public void CreateDisplayMetadata(DisplayMetadataProviderContext context)
{
var propertyAttributes = context.Attributes;
var modelMetadata = context.DisplayMetadata;
var propertyName = context.Key.Name;
if (IsTransformRequired(propertyName, modelMetadata, propertyAttributes))
{
modelMetadata.DisplayName = () => SplitCamelCase(propertyName);
}
}
private static string SplitCamelCase(string str)
{
return Regex.Replace(
Regex.Replace(
str,
#"(\P{Ll})(\P{Ll}\p{Ll})",
"$1 $2"
),
#"(\p{Ll})(\P{Ll})",
"$1 $2"
);
}
private static bool IsTransformRequired(string propertyName, DisplayMetadata modelMetadata, IReadOnlyList<object> propertyAttributes)
{
if (!string.IsNullOrEmpty(modelMetadata.SimpleDisplayProperty))
return false;
if (propertyAttributes.OfType<DisplayNameAttribute>().Any())
return false;
if (propertyAttributes.OfType<DisplayAttribute>().Any())
return false;
if (string.IsNullOrEmpty(propertyName))
return false;
return true;
}
}
The IsTransformRequired method ensures that you can still override the provider with a decorated property when you need to.
Register the provider on startup through the AddMvcOptions method on IMvcBuilder:
builder.AddMvcOptions(m => m.ModelMetadataDetailsProviders.Add(new HumanizerMetadataProvider()));
how about using a custom attribute and overriding DisplayNameAttribute
public class DisplayWithSpace : DisplayNameAttribute
{
public DisplayWithSpace([System.Runtime.CompilerServices.CallerMemberName] string memberName ="")
{
Regex r = new Regex(#"(?!^)(?=[A-Z])");
DisplayNameValue = r.Replace(memberName, " ");
}
}
and your property will be
[DisplayWithSpace]
public string FatherName { get; set; }
ASP.NET Core introduced custom tag helpers which can be used in views like this:
<country-select value="CountryCode" />
However, I don't understand how can I get model property name in my classes:
public class CountrySelectTagHelper : TagHelper
{
[HtmlAttributeName("value")]
public string Value { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
...
// Should return property name, which is "CountryCode" in the above example
var propertyName = ???();
base.Process(context, output);
}
}
In the previous version I was able to do this by using ModelMetadata:
var metadata = ModelMetadata.FromLambdaExpression(expression, html.ViewData);
var property = metadata.PropertyName; // return "CountryCode"
How can I do the same in the new ASP.NET tag helpers?
In order to get property name, you should use ModelExpression in your class instead:
public class CountrySelectTagHelper : TagHelper
{
public ModelExpression For { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var propertyName = For.Metadata.PropertyName;
var value = For.Model as string;
...
base.Process(context, output);
}
}
You can pass a string via the tag helper attribute.
<country-select value="#Model.CountryCode" />
The Value property will be populated by Razor with the value of Model.CountryCode by prepending #. So you get the value directly without the need to pass the name of a model property and accessing that afterwards.
I am not sure whether you got what you wanted. If you are looking to pass the complete model from view to the custom tag helper, this is how i do it.
You can pass in your current model from the view using any custom attributes. See the example below.
Assuming your model is Country
public class Country
{
public string Name { get; set; }
public string Code { get; set; }
}
Now declare a property in your custom tag helper of the same type.
public Country CountryModel { get; set; }
Your controller would look something like this
public IActionResult Index()
{
var country= new Country
{
Name = "United States",
Code = "USA"
};
return View("Generic", country);
}
In this setup, to access your model inside the taghelper, just pass it in like any other custom attribute/property
Your view should now look like something like this
<country-select country-model="#Model"></country-select>
You can receive it in your tag helper like any other class property
public override void Process(TagHelperContext context, TagHelperOutput output)
{
...
// Should return property name, which is "CountryCode" in the above example
var propertyName = CountryModel.Name;
base.Process(context, output);
}
Happy coding!
I have a model say
public class Contact
{
[Display(Name = "Phone Number", Description = "This is Phone number to contact")]
[Visibility(ShowForDisplay=false)]
public string Phone { get; set; }
[Display(Name = "Mail To Support", Description = "This is Mail for support")]
public string Email { get; set; }
}
Now in Mvc html I m doing at several places like
#Html.DisplayTextFor(x=>x.Phone)
Now I want a attribute based something like this which can manage at model level for turning of this display into the view . Like for eg the #html.DisplayTextFor(x=>x.Phone) should be there but when I do [Visibility(ShowForDisplay=false)] then all the visibility for the values or texts should not be rendered on the html .
How can be done through attribute like custom attribute [Visibility(ShowForDisplay=false)] ?
All Html Helper methods working with Model MetaData, and you can't change ModelMetada class so you should make your own Html helper, and ofcourse you need a custom attribute. Check this code:
First create a custom attribute:
public class VisibilityAttribute : ValidationAttribute
{
private bool _isVisible;
public VisibilityAttribute(bool visible = true)
{
_isVisible = visible;
}
public bool ShowForDisplay
{
get
{
return _isVisible;
}
set
{
_isVisible = value;
}
}
}
Then create a Html helper:
public static class MyHtmlExtensions
{
public static MvcHtmlString DisplayTextForCustom<TModel, TResult>(this HtmlHelper<TModel> html,
Expression<Func<TModel, TResult>> expression)
{
ExpressionType type = expression.Body.NodeType;
if (type == ExpressionType.MemberAccess)
{
MemberExpression memberExpression = (MemberExpression) expression.Body;
PropertyInfo pi = memberExpression.Member as PropertyInfo;
var attributes = pi.GetCustomAttributes();
foreach (var attribute in attributes)
{
if (attribute is VisibilityAttribute)
{
VisibilityAttribute vi = attribute as VisibilityAttribute;
if (vi.ShowForDisplay)
{
var metadata = ModelMetadata.FromLambdaExpression<TModel, TResult>(expression, html.ViewData);
return MvcHtmlString.Create(metadata.SimpleDisplayText);
}
}
}
}
return MvcHtmlString.Create("");
}
}
Then call it from your View like this:
#Html.DisplayTextForCustom(x=>x.Phone)
PS: To write this code I looked at Html.DisplayTextFor source code and I try to write a code as simple as possible.
Can you create custom data annotations for the model that can be read inside the T4 template for the View like property.Scaffold is read? I would like to add data annotation parameters like Scaffold based on which I would build the view.
Thank you
I wrote a blog post on the solution I came up with for MVC5. I'm posting it here for anyone who comes along:
https://johniekarr.wordpress.com/2015/05/16/mvc-5-t4-templates-and-view-model-property-attributes/
Edit: In your entities, decorate property with custom Attribute
namespace CustomViewTemplate.Models
{
[Table("Person")]
public class Person
{
[Key]
public int PersonId { get; set;}
[MaxLength(5)]
public string Salutation { get; set; }
[MaxLength(50)]
public string FirstName { get; set; }
[MaxLength(50)]
public string LastName { get; set; }
[MaxLength(50)]
public string Title { get; set; }
[DataType(DataType.EmailAddress)]
[MaxLength(254)]
public string EmailAddress { get; set; }
[DataType(DataType.MultilineText)]
public string Biography { get; set; }
}
}
With this Custom Attribute
namespace CustomViewTemplate
{
[AttributeUsage(AttributeTargets.Property)]
public class RichTextAttribute : Attribute
{
public RichTextAttribute() { }
}
}
Then create a T4Helper that we'll reference in our template
using System;
namespace CustomViewTemplate
{
public static class T4Helpers
{
public static bool IsRichText(string viewDataTypeName, string propertyName)
{
bool isRichText = false;
Attribute richText = null;
Type typeModel = Type.GetType(viewDataTypeName);
if (typeModel != null)
{
richText = (RichTextAttribute)Attribute.GetCustomAttribute(typeModel.GetProperty(propertyName), typeof(RichTextAttribute));
return richText != null;
}
return isRichText;
}
}
}
So, this is how you do it.
Follow this tutorial on how to create a custom attribute http://origin1tech.wordpress.com/2011/07/20/mvc-data-annotations-and-custom-attributes/
To read this attribute values in the T4 scaffolding templates, first add the template files as described here http://www.hanselman.com/blog/ModifyingTheDefaultCodeGenerationscaffoldingTemplatesInASPNETMVC.aspx
Then, for example, open List.tt from the AddView folder. This template creates the Index view.
Go to the end of the template file and find the definition for class ModelProperty. Add your property value to it ( public string MyAttributeValue { get; set; }
Now go a bit down in the List.tt and find bool Scaffold(PropertyInfo property) method. You will need to add your own attribute property reader. This method, for the above mentioned tutorial, would be:
string OptionalAttributesValueReader(PropertyInfo property){
foreach (object attribute in property.GetCustomAttributes(true)) {
var attr = attribute as OptionalAttributes ;
if (attr != null) {
return attr.style;
}
}
return String.Empty;
}
Then find the method List GetEligibleProperties(Type type) at the bottom of the file. Add your reader to it like this:
...
IsForeignKey = IsForeignKey(prop),
IsReadOnly = prop.GetSetMethod() == null,
Scaffold = Scaffold(prop),
MyAttributeValue = OptionalAttributesValueReader(prop)
When you want to use and read this attribute you can do it like the Scaffold property is used in the List.tt
List<ModelProperty> properties = GetModelProperties(mvcHost.ViewDataType);
foreach (ModelProperty property in properties) {
if (property.MyAttributeValue != String.Empty) {
//read the value
<#= property.MyAttributeValue #>
}
}
Since these classes are defined in my project, I had to add my project dll and namespace to the top of the List.tt:
<## assembly name="C:\myProjectPath\bin\myMVCproject.dll" #>
<## import namespace="myMVCproject.CustomAttributes" #>
If your model changes and you need to find these new changes in the scaffolding, you need to rebuild your project.
Hope anyone looking for the solution will find this useful. Ask if there is anything unclear.
This is how I did it in MVC 5. I did this a long time ago and I may be forgetting stuff, I'm just copy/pasting what I see in my modified templates.
I needed a way to set the order of properties in (for example) the create/edit views or in the list view table. So I created a custom attribute OrderAttribute with an integer property Order.
To access this attribute in the T4 templates I modified the file ModelMetadataFunctions.cs.include.t4. At the top I added one method that retrieves the Order value set in the attribute from a PropertyMetadata object, and another method to simply order a list of PropertyMetadata items by that order:
List<PropertyMetadata> GetOrderedProperties(List<PropertyMetadata> properties, Type modelType) {
return properties.OrderBy<PropertyMetadata, int>(p => GetPropertyOrder(modelType, p)).ToList();
}
int GetPropertyOrder(Type type, PropertyMetadata property) {
var info = type.GetProperty(property.PropertyName);
if (info != null)
{
var attr = info.GetCustomAttribute<OrderAttribute>();
if (attr != null)
{
return attr.Order;
}
}
return int.MaxValue;
}
Finally, in the List template for example, I have added a part where I call the GetOrderedProperties method:
var typeName = Assembly.CreateQualifiedName("AcCtc, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", ViewDataTypeName);
var modelType = Type.GetType(typeName);
var properties = ModelMetadata.Properties.Where(p => p.Scaffold && !p.IsPrimaryKey && !p.IsForeignKey && !(p.IsAssociation && GetRelatedModelMetadata(p) == null)).ToList();
properties = GetOrderedProperties(properties, modelType);
foreach (var property in properties)
{
//...
}
Unfortunately I needed the name of the project to be able to create a Type object which I needed to get the attributes from. Not ideal, perhaps you can get it some other way but I couldn't manage it without this string including all the version stuff.
I've written an If-IsRequired custom attribute to validate that a property contains a value depending on the values of some other properties in the model. Since I want to make this attribute apply to as many situations as possible, I want to allow the option for the developer leveraging the attribute to supply an infinite number of matched parameters. And lastly, I want to be able to enforce that all the parameters are matched correctly.
This is what I've written thus far. While I'm currently using arrays of strings, I'd be perfectly happy to use some sort of collection, which been unable to work. In addition, I now have a need to support the current attribute definition and create a new overload that includes the comparison operator. This will allow me to make less than, greater than, and not equal comparisons in addition to the original definition which just assumes all comparisons are done with equals.
/// <summary>
/// A custom attribute that checks the value of other properties passed to it in order to
/// determine if the property this attribute is bound to should be required.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)]
public class IsPropertyRequiredAttribute : ValidationAttribute
{
private const string DefaultErrorMessage = "{0} is required.";
public string[] _selectionContextNames { get; private set; }
public string[] _expectedValues { get; private set; }
/// <summary>
/// Creates a new instance of the IsPropertyRequriedAttribute.
/// </summary>
/// <param name="SelectionContextNames">The name of the other property in the view model to check the value of.</param>
/// <param name="ExpectedValues">The expected value of the other property in the view model in order to determine if the current property the attribute is bound to should be required.</param>
public IsPropertyRequiredAttribute(string[] SelectionContextNames, string ExpectedValues)
: base(DefaultErrorMessage)
{
_selectionContextNames = SelectionContextNames;
_expectedValues = ExpectedValues;
}
public override bool IsValid(object value)
{
if (_selectionContextNames == null || _expectedValues == null)
{
if (_selectionContextNames != null || _expectedValues != null)
{
string paramName;
if (_selectionContextNames == null)
{
paramName = "ExpectedValues";
}
else
{
paramName = "SelectionContextNames";
}
throw new ArgumentException("Key/Value pairs need to match for IsPropertyRequired.", paramName);
}
}
else if (_selectionContextNames.Length != _expectedValues.Length)
{
string paramName;
if (_selectionContextNames.Length < _expectedValues.Length)
{
paramName = "ExpectedValues";
}
else
{
paramName = "SelectionContextNames";
}
throw new ArgumentException("Parameter element counts need to match for IsPropertyRequired.", paramName);
}
bool paramsValid = true;
if (_selectionContextName!= null)
{
for (int i = 0; i < _selectionContextName.Length; i++)
{
string paramValue = HttpContext.Current.Request[_selectionContextName[i]];
if (_expectedValue[i] != paramValue)
{
paramsValid = false;
}
}
if (paramsValid == true)
{
return (value != null);
}
else
{
return true;
}
}
else
{
return true;
}
}
public override string FormatErrorMessage(string name)
{
return String.Format(DefaultErrorMessage, name);
}
}
While using the attribute to decorate the property will depend on how the attribute is defined, this is what I have currently implemented (which could also probably be improved):
[IsPropertyRequired(new string[] {"prop1", "prop2", "prop3", "prop4"}, new string[] {"1", "2", "3", "4"})]
public string SomeText { get; set; }
Also, I want to prevent, as much as I can, the following decoration from happening:
[IsPropertyRequired(new string[] {"prop1", "prop2", "prop3", "prop4", "prop5withoutvalue"}, new string[] {"1", "2", "3", "4"})]
public string SomeOtherText { get; set; }
And with the new overload including comparison operators as a parameter, we could now have:
[IsPropertyRequired(new string[] {"prop1", "prop2", "prop3", "prop4"}, new string[] {"==", ">", "!=", "<="}, new string[] {"1", "2", "3", "4"})]
public string SomeComparisonText { get; set; }
Attributes in .NET are very limited in the allowed types you can specify, as mentioned on MSDN. If you want more complex data to be specified, I would recommend writing the attribute to specify an alternate location for the richer data structure.
For example, imagine an attribute with this syntax:
[ValidationRules(typeof(MyValidationRuleInfo, "MyRuleSet")]
public int SomeProperty { get; set; }
...
public static class MyValidationRuleInfo {
public static Dictionary<string, ValidationRule> MyRuleSet {
get {
return new { ... rules go here ... }
}
}
And the attribute would look up the property on the target class and get all the rules there. It's still up to you to implement all the logic of all the rules, but you get to avoid attribute soup, and you also avoid unwieldy data structures.
In fact, the xUnit.NET unit testing library does something similar with its Theory and PropertyData attributes, as shown here.