Html tags inside DisplayAttribute - html

I would like the display name to contain some html text.
For example something like:
[Display(Name = "First Name <b class=\"red\">boldtext</b>)]
public string FirstName { get; set; }
But on the browser's screen I see:
First Name <b class="red">boldtext</b>
Instead of:
First Name boldtext
Is there anything I can do to make it work with the Display attribute?
Basically I would like to display red * after all the required attributes, or is there some another way that I could do this better if this way is not able to work?

Display property belong to Model, so it should contain plain text data, no any format.
If you want to add format to name, you can do like this
Write an extension method to get value from Display Name
public static MvcHtmlString DisplayNameFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string templateName)
{
Expression expressionBody = expression.Body;
if (expressionBody is MemberExpression)
{
MemberExpression memberExpression = (MemberExpression)expressionBody;
string propertyName = memberExpression.Member.Name;
return html.DisplayFor(expression, templateName, new { Message = html.ViewData.ModelMetadata.Properties.Single(p => p.PropertyName == propertyName).Name});
}
}
View:
#Html.DisplayNameFor(model => model.FirstName, "_NameTemplate")
Template: _NameTemplate.cshtml
<span>#ViewData["Message"]</span>
Hope it's useful for you

You can create your own DisplayFor template to render this object with whatever template you like.

Related

How to make the asp-for input tag helper generate camelCase names?

If I have a view model like this:
public class MyModel{
public DateTime? StartDate {get;set;}
}
And on a view an input tag is used with an asp-for tag helper like so:
<input asp-for="StartDate" />
The default html that is generated by this is
<input type="datetime" id="StartDate" name="StartDate" value="" />
But what I want it to generate is html that looks like this:
<input type="datetime" id="startDate" name="startDate" value="" />
How can I make the asp-for input tag helper generate camel case names like above without having to make my model properties camelCase?
After studying the code that #Bebben posted and the link provided with it, I continued to dig more into the Asp.Net Core source code. And I found that the designers of the Asp.Net Core provided some extensibility points that could be leveraged to achieve lower camelCase id and name values.
To do it, we need to implement our own IHtmlGenerator which we can do by creating a custom class that inherits from DefaultHtmlGenerator. Then on that class we need to override the GenerateTextBox method to fix the casing. Or alternatively we can override the GenerateInput method to fix the casing of name and id attribute values for all input fields (not just input text fields) which is what I chose to do. As a bonus I also override the GenerateLabel method so the label's for attribute also specifies a value using the custom casing.
Here's the class:
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.Routing;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.Text.Encodings.Web;
namespace App.Web {
public class CustomHtmlGenerator : DefaultHtmlGenerator {
public CustomHtmlGenerator(
IAntiforgery antiforgery,
IOptions<MvcViewOptions> optionsAccessor,
IModelMetadataProvider metadataProvider,
IUrlHelperFactory urlHelperFactory,
HtmlEncoder htmlEncoder,
ClientValidatorCache clientValidatorCache) : base
(antiforgery, optionsAccessor, metadataProvider, urlHelperFactory,
htmlEncoder, clientValidatorCache) {
//Nothing to do
}
public CustomHtmlGenerator(
IAntiforgery antiforgery,
IOptions<MvcViewOptions> optionsAccessor,
IModelMetadataProvider metadataProvider,
IUrlHelperFactory urlHelperFactory,
HtmlEncoder htmlEncoder,
ClientValidatorCache clientValidatorCache,
ValidationHtmlAttributeProvider validationAttributeProvider) : base
(antiforgery, optionsAccessor, metadataProvider, urlHelperFactory, htmlEncoder,
clientValidatorCache, validationAttributeProvider) {
//Nothing to do
}
protected override TagBuilder GenerateInput(
ViewContext viewContext,
InputType inputType,
ModelExplorer modelExplorer,
string expression,
object value,
bool useViewData,
bool isChecked,
bool setId,
bool isExplicitValue,
string format,
IDictionary<string, object> htmlAttributes) {
expression = GetLowerCamelCase(expression);
return base.GenerateInput(viewContext, inputType, modelExplorer, expression, value, useViewData,
isChecked, setId, isExplicitValue, format, htmlAttributes);
}
public override TagBuilder GenerateLabel(
ViewContext viewContext,
ModelExplorer modelExplorer,
string expression,
string labelText,
object htmlAttributes) {
expression = GetLowerCamelCase(expression);
return base.GenerateLabel(viewContext, modelExplorer, expression, labelText, htmlAttributes);
}
private string GetLowerCamelCase(string text) {
if (!string.IsNullOrEmpty(text)) {
if (char.IsUpper(text[0])) {
return char.ToLower(text[0]) + text.Substring(1);
}
}
return text;
}
}
}
Now that we have our CustomHtmlGenerator class we need to register it in the IoC container in place of the DefaultHtmlGenerator. We can do that in the ConfigureServices method of the Startup.cs via the following two lines:
//Replace DefaultHtmlGenerator with CustomHtmlGenerator
services.Remove<IHtmlGenerator, DefaultHtmlGenerator>();
services.AddTransient<IHtmlGenerator, CustomHtmlGenerator>();
Pretty cool. And not only have we solved the id and name casing issue on the input fields but by implementing our own custom IHtmlGenerator, and getting it registered, we have opened the door on all kinds of html customization that can be done.
I'm starting to really appreciate the power of a system built around an IoC, and default classes with virtual methods. The level of customization available with little effort under such an approach is really pretty amazing.
Update
#Gup3rSuR4c pointed out that my services.Remove call must be an extension method that's not included in the framework. I checked, and yep that true. So, here is the code for that extension method:
public static class IServiceCollectionExtensions {
public static void Remove<TServiceType, TImplementationType>(this IServiceCollection services) {
var serviceDescriptor = services.First(s => s.ServiceType == typeof(TServiceType) &&
s.ImplementationType == typeof(TImplementationType));
services.Remove(serviceDescriptor);
}
}
The simplest way to do this is to just write
<input asp-for="StartDate" name="startDate" />
Or do you want to have it generated completely automatically in camel case, for the whole application?
To do that, it seems like you have to implement your own InputTagHelpers in Microsoft.AspNetCore.Mvc.TagHelpers.
Here is the method where the name is generated:
private TagBuilder GenerateTextBox(ModelExplorer modelExplorer, string inputTypeHint, string inputType)
{
var format = Format;
if (string.IsNullOrEmpty(format))
{
format = GetFormat(modelExplorer, inputTypeHint, inputType);
}
var htmlAttributes = new Dictionary<string, object>
{
{ "type", inputType }
};
if (string.Equals(inputType, "file") && string.Equals(inputTypeHint, TemplateRenderer.IEnumerableOfIFormFileName))
{
htmlAttributes["multiple"] = "multiple";
}
return Generator.GenerateTextBox(
ViewContext,
modelExplorer,
For.Name,
value: modelExplorer.Model,
format: format,
htmlAttributes: htmlAttributes);
}
(The above code is from https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.TagHelpers/InputTagHelper.cs, Apache License, Version 2.0, Copyright .NET Foundation)
The line is "For.Name". The name is sent into some other methods, and the one that in the end gives the final name is in a static class (Microsoft.AspNetCore.Mvc.ViewFeatures.Internal.NameAndIdProvider), so nothing we can really plug into easily.

#Html.LabelFor with an <a> tag inside it

This should be a fairly simple question, all I want to do is use #Html.LabelFor within a razor view. One of my labels is different, it has an <a> tag in it. the problem is when I use LabelFor, it encodes the html as & lt;. I've tried a lot of different approaches to making this happen but none of them are working. Here's the code.
#Html.LabelFor(model => model.Question, new { #for = "Question" })
what should get outputted:
<label for="question">support#testdomain.com</label>
what does get outputted:
<label for="question"><a href=\"mailto:support#testdomain.com">support#testdomain.com</a></label>
(my < have been replaced with & lt ; without the spaces, thus the code shows on the page instead of rendering as a link)
how can I make it output what it should?
note, model.Question is set to support#testdomain.com
Why do you want your label to contain the full HTML? I think it would be a better approach to store email address, and in your view you could do:
#Model.Email
To get the expected result.
EDIT
IMO by encoding things in label is just going to make your work more complicated as labels are not meant to hold formatted HTML or feature or anything like that, merely a caption to display. Example usage:
Model code:
[Display(Name = "Email address")]
public string EmailAddress { get; set; }
In view:
#Html.LabelFor(model => model.EmailAddress):
#Html.DisplayFor(model => model.EmailAddress)
<!-- OR (no label element just the label text)-->
#Html.DisplayNameFor(model => model.EmailAddress):
#Html.DisplayFor(model => model.EmailAddress)
Then can be combined in any way:
[Display(Name = "Send Email")]
public string EmailAddress { get; set; }
And
#Html.LabelFor(model => model.EmailAddress): #Model.Email
In runtime will be:
Send Email: abc#server.com
You're fighting the Razor system since it encodes by default. That's the beauty of using Razor. Many of the tag classes including Labelwill ultimately render themselves by calling TagBuilderExtensions.ToMvcHtmlString
internal static class TagBuilderExtensions
{
internal static MvcHtmlString ToMvcHtmlString(this TagBuilder tagBuilder, TagRenderMode renderMode)
{
return new MvcHtmlString(tagBuilder.ToString(renderMode));
}
}
MvcHtmlString represents an HTML-encoded string that should not be encoded again. So you can't do what you're trying to do via LabelFor. Razor is in fact specifically ensuring that you can't.
But since your malto: is not really a label anyway you can just:
#Html.Raw(Server.HtmlDecode(Model.MyField)

StringLength in ViewModel not setting maxlength on textbox

Here the property in my ViewModel:
[Display(Name = "Ext.")]
[MaxLength(6, ErrorMessage = "Must be a maximum of 6 characters")]
[StringLength(6)]
public string Extension { get; set; }
And in my View:
#Html.EditorFor(model => model.Extension)
And it renders:
<input class="text-box single-line" data-val="true" data-val-length="The field Ext. must be a string with a maximum length of 6." data-val-length-max="6" id="Extension" name="Extension" type="text" value="" />
Should this be setting the maxlength attribute on my textbox? If not, how can I do that with DataAttributes?
I'd like the attribute that I set in the ViewModel to control this if
possible.
ASP.NET MVC provides an extensible system for doing exactly this. Here is what you need to do:
Implement a custom ModelMetadataProvider.
Look for the StringLengthAttribute or the MaxLengthAttribute, extract the information and add it to the ModelMetadata.
Provide a custom Editor template that makes use of the information.
Step 1: Implement a custom ModelMetadataProvider.
Create a class that derives from ModelMetadataProvider. Typically you would derive from the DataAnnotationsModelMetadataProvider as this provides some default functionality which means you only have to override a single method called CreateMetadata.
Step 2: Extract the information:
To get the information, you need to look for the attribute, extract the maximum length information and add it to the AdditionalValues dictionary of the ModelMetadata. The implementation would look something like this (this is the entire implementation):
public class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(
IEnumerable<Attribute> attributes,
Type containerType,
Func<object> modelAccessor,
Type modelType,
string propertyName)
{
// Call the base class implementation to create the default metadata...
var metadata = base.CreateMetadata(
attributes,
containerType,
modelAccessor,
modelType,
propertyName);
// Extract the stringLengthAttribute (you can do the same for the
// MaxLengthAttribute if you want).
var attr = attributes
.OfType<StringLengthAttribute>()
.First();
// This could be getting called on a property that doesn't have the
// attribute so make sure you check for null!
if (attr != null)
{
metadata.AdditionalValues["maxLength"] = attr.MaximumLength;
}
return metadata;
}
}
In order for ASP.NET MVC to use this you need to register it in the Application_Start method in Global.asax.
ModelMetadataProviders.Current = new CustomModelMetadataProvider();
Step 3: Create a custom editor template.
You now need to create a view that uses the information. Create a new view called String in the Views\Shared\ folder.
String.cshtml
#{
object maxLength;
if (!ViewData.ModelMetadata.AdditionalValues
.TryGetValue("maxLength", out maxLength))
{
maxLength = 0;
}
var attributes = new RouteValueDictionary
{
{"class", "text-box single-line"},
{ "maxlength", (int)maxLength },
};
}
#Html.TextBox("", ViewContext.ViewData.TemplateInfo.FormattedModelValue, attributes)
When you run your application you will get the following HTML output by calling #Html.EditorFor.
<input class="text-box single-line" id="Extension" maxlength="6" name="Extension" type="text" value="" />
If you want to know more about the model metadata provider system, Brad Wilson has a series of blog posts that detail how it works (these were written prior to the Razor view engine so some of the view Syntax is a bit funky but otherwise the information is sound).
Essentially based on Brad's answer, wrapped in an extension on the Html helper using lambda syntax so you don't pollute your Razor views with reflection stuff:
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Web.Mvc;
public static class HtmlHelper
{
public static int? MaxLength<TModel, TProperty>(
this HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, TProperty>> expression)
{
MemberExpression memberExpression = (MemberExpression)expression.Body;
PropertyInfo property = typeof(TModel)
.GetProperty(memberExpression.Member.Name);
StringLengthAttribute attribute = (StringLengthAttribute)property
.GetCustomAttributes(typeof(StringLengthAttribute), true)
.FirstOrDefault();
if (attribute != null)
{
return attribute.MaximumLength;
}
return null;
}
}
Use it like such:
#Html.TextBoxFor(x => x.Name, new { maxlength = Html.MaxLength(x => x.Name) })
where x refers to your model.
If the StringLengthAttribute is not declared for the property, null will be returned and the maxlength attribute will be empty on the textbox element.
Remember to include using in your razor page so you can access the method.
#using HtmlHelper
You also need to use none null-able result for the method to overcome compile error.
I ran into something similar, here was my quick and dirty solution:
at the top of your .cshtml file add the line:
#{
var max = ((System.ComponentModel.DataAnnotations.StringLengthAttribute)(typeof(MyType))
.GetProperty("MyProp")
.GetCustomAttributes(typeof(System.ComponentModel.DataAnnotations.StringLengthAttribute), true)[0]).MaximumLength;
}
below that in your html replace the EditorFor with:
#Html.TextBoxFor(model => model.Extension, htmlAttributes: new {maxlength=max })
I eventually decided I'd rather just do it in script:
<script>
$(function ()
{
var max = $("#myinput").attr("data-val-length-max");
$("#myinput").attr("maxlength", max);
});
</script>
but if you don't want to to add script the first example should work.

How to have a conditional checked, disabled, ... with the html helper?

For a view, I've to generate some checkbox.
I've one collection of items:
public class ItemSelection
{
public int Id { get; set; }
public String Name { get; set; }
public Boolean IsSelected { get; set; }
public Boolean IsActive { get; set; }
}
and in the view, I'm iterating on this
#foreach(ItemSelection item in Model.Items){
Html.CheckBoxFor(m=>item.IsSelected)//HERE I WOULD LIKE TO HAVE DISABLED properties if I've a IsActive=falsel
Html.HiddenFor(m=>item.Id)
}
Now I see that I can do a "if" in which I create a different HtmlAttribute array, depending of this property, but is there a way to create only one array
new {disabled=item.IsActive?"ONE_SPECIAL_VALUE_HERE":"disabled"}
I tried to put false, or some other things, nothing worked.
You can't avoid the if:
The problem is with the special nature of the disabled attribute because there is no "special" value which would make your sample work, because:
"disabled" is only possible value for this attribute. If the input
should be enabled, simply omit the attribute entirely.
So you need to omit the attribute to enable the control but all HTML helpers will serialize all the properties of the anonymous objects passed in as the htmlattributes. And there is no way to conditionally add properties to anonymous types.
However if you have multiple common attributes for the enable/disable case, and you don't want to create two anonymoues types, you can put the attributes in a dictionary with the optional disabled attribute and use the dictionary as the htmlAttributes:
var checkboxHtmlAttributes = new Dictionary<string, object>
{{"attibute1", "value1"},
{"attribute2", "value2"}};
if (!item.IsActive)
{
checkboxHtmlAttributes.Add("disabled", "disabled");
}
#Html.CheckBoxFor(m=>item.IsSelected, checkboxHtmlAttributes)

IEnumerable in my ViewModel is not displayed with EditorForModel

ViewModel
[Validator(typeof(ProdutoCategoriaValidator))]
public class ProdutoCategoriaViewModel
{
[HiddenInput(DisplayValue = false)]
public Guid ID { get; set; }
public IEnumerable<SelectListItem> Tipos { get; set; } // <<<<------- Is not showing in my view
[AdditionalMetadata("data-bind", "event: { change: function(data) { Link(data.Nome()); }}")]
public string Nome { get; set; }
[DataType(DataType.Url)]
[AdditionalMetadata("Prefixo", "Produtos/{tipo-de-produto}#")]
public string Link { get; set; }
public int? Ordem { get; set; }
public ProdutoCategoriaViewModel()
{
ID = Guid.NewGuid();
}
}
Solution
View (_Formulario.cshtml)
#model ProdutoCategoriaViewModel
#using (Html.BeginForm(null, null, FormMethod.Post, new { id="form-produtocategoria", data_bind = "submit: salvar" }))
{
#Html.AntiForgeryToken()
<legend>#Html.MvcSiteMap().SiteMapTitle()</legend>
<fieldset>
#Html.ValidationSummary(false, "Verifique os erros abaixo:")
#Html.EditorForModel()
</fieldset>
<div class="buttons">
#Html.ActionLink("Cancelar", "Index")
<input type="submit" value="SALVAR" />
</div>
}
SelectListItem.cshtml
#model IEnumerable<SelectListItem>
#Html.DropDownListFor(m => m, Model)
<p>Test</p>
Result
Full image: http://i.imgur.com/I7HxA.png
Notes
I've tried to put the attribute "UIHint" but still nothing is displayed!
Questions
What am I doing wrong?
By default when you use Html.EditorForModel don't expect this to recurse down to complex properties such as your Tipos property which is of type IEnumerable<SelectListItem>. Brad Wilson explained this in his blog post (more specifically read the Shallow Dive vs. Deep Dive section towards the end of the post). You will need to write a custom editor template for the Object type if you want this to happen.
Another possibility is to specify the template name:
#Html.EditorFor(x => x.Tipos, "SelectListItem")
Also bear in mind that your editor template for the SelectListItem is wrong because you are binding the DropDownListFor to the model as first argument. Don't forget that the first argument of this helper must be a scalar property that will be used to hold the selected value. You need a string or integer property on your view model for this. The second argument represents the collection.
Another important aspect about editor templates is that when you have a property of type IEnumerable<T> and an editor template called T.cshtml this editor template must be strongly typed to the T class and not IEnumerable<T> as you did with your SelectListItem.cshtml template. This doesn't apply if you use UIHint or specify the template name as second argument to the EditorFor helper. n this case the template will be typed to the collection.
So to recap, you could either implement a custom object editor template as Brad Wilson suggested that will recurse down to complex properties or you could modify your _Formulario.cshtml view to specify EditorFor each individual elements.
A #foreach loop renders something that looks right, but the resulting markup will have the same id for each row's controls. It also will not post the enumerable collection back with the model instance.
There are two ways to make this work such that you have a unique id for each item in the collection, and so that the collection is hydrated on postbacks:
1. Use the default editor template rather than a named one
// editor name parameter must be omitted; default editor template's model type
// should be a single instance of the child object, not an IEnumerable. This
// convention looks wrong, but it fully works:
#Html.EditorFor(parentObject => parentObject.Items)
2. Use a #for loop, not a #foreach:
#for (int i = 0; i < parentObject.Items.Count ; i++) {
// the model binder uses the indexer to disambiguate the collection items' controls:
#Html.EditorFor(c => Model.Items[i], "MyEditorTemplate")
}
This will not work, however:
// this will error out; the model type will not match the view's at runtime:
#Html.EditorFor(parentObject => parentObject.Items, "MyEditorTemplate")
Nor will this:
#foreach(var item in parentObject.Items) {
// this will render, but won't post the collection items back with the model instance:
#Html.EditorFor(c => item, "MyEditorTemplate")
}
For a detailed answer why this is, look at this question: MVC can't override EditorTemplate name when used in EditorFor for child object.