Twitter Bootstrap MVC Alternatives [closed] - html

Closed. This question does not meet Stack Overflow guidelines. It is not currently accepting answers.
Questions asking us to recommend or find a tool, library or favorite off-site resource are off-topic for Stack Overflow as they tend to attract opinionated answers and spam. Instead, describe the problem and what has been done so far to solve it.
Closed 9 years ago.
Improve this question
After reviewing https://www.twitterbootstrapmvc.com/ I found you had to pay for it.
Is there any free alternative or tuturiols around that allow you to make HTML helpers for bootstrap for thing like text boxes, check boxes, dropdowns etc?
I've tried creating simple HTML helpers but having issues even if i could override current HTML helpers like TextBoxFor but add classes and set how it should render this would be helpful.
An example of my first steps is within the Views folder i have created a folder named "EditorTemplates"
Within this folder i have added "string.cshtml" this is called for thing like TextBoxFor.
Within here i have:
#{
var placeholder = string.Empty;
if (ViewData.ModelMetadata.AdditionalValues.ContainsKey("placeholder"))
{
placeholder = ViewData.ModelMetadata.AdditionalValues["placeholder"] as string;
}
}
<div class="form-group">
#Html.Label(ViewData.ModelMetadata.PropertyName)
#Html.TextBox("", ViewData.TemplateInfo.FormattedModelValue, new { placeholder = placeholder, #class = "form-control"})
</div>
This creates a bootstrap looking input item, but doesnt have the required flexibility such as parameteres.
Thanks

Well, the easiest thing to do is do the helper yourself, it's really not hard and you have full control on them, especially as you can reuse default helper calls, for example for a simple Boostrap textbox group you could have:
public static class BootstrapHtmlHelper
{
public static MvcHtmlString TextboxGroupFor<TModel, TProperty>(
this HtmlHelper<TModel> html,
Expression<Func<TModel, TProperty>> expression,
string title,
string id,
string placeholder)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("<div class=\"form-group\">");
sb.AppendLine("<label for=\"{0}\">{1}</label>", id, title);
sb.AppendLine( htmlHelper.TextBoxFor( expression, new { #class = "form-control", #id = id, #placeholder = placeholder }) );
sb.AppendLine( htmlHelper.ValidationMessageFor( expression );
sb.AppendLine("</div>");
return new MvcHtmlString( sb.ToString() );
}
}
You can then add any parameters, check for the validation, add helper boxes, do any kind of formatting, etc.
Then you can use it in your views:
#Html.TextboxGroupFor(x => x.FirstName, "First Name", "first-name-id", "Enter first name...")
Update:
Example for a dropdown, simply reuse ListBoxFor and pass an IEnumerable<SelectListItem>:
public static class BootstrapHtmlHelper
{
public static MvcHtmlString DropdownGroupFor<TModel, TProperty>(
this HtmlHelper<TModel> html,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> list,
string selectedValue,
string title,
string id)
{
// Apply selected value to the list
var selectedList = list.Select(x => x.Value.Equals(selectedValue) ? x.Selected = true : x.Selected = false);
StringBuilder sb = new StringBuilder();
sb.AppendLine("<div class=\"form-group\">");
sb.AppendLine("<label for=\"{0}\">{1}</label>", id, title);
sb.AppendLine( htmlHelper.ListBoxFor( expression, selectedList, new { #class = "form-control", #id = id }) );
sb.AppendLine( htmlHelper.ValidationMessageFor( expression );
sb.AppendLine("</div>");
return new MvcHtmlString( sb.ToString() );
}
}
In the view:
#Html.DropdownGroupFor(
x => x.Country,
Model.Countries.Select(x => new SelectListItem
{
Text = x.Name,
Value = x.Id
}));

you can create your own helpers for controls and consume them. Below is sample for input button custom helper
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace CustomHelpers
{
public static class ButtonHelper
{
public static MvcHtmlString Button(this HtmlHelper helper, String displayText, String elementId, String cssClass, String onClick)
{
TagBuilder inputButton = new TagBuilder("input");
inputButton.AddCssClass(cssClass);
inputButton.MergeAttribute("value", displayText);
inputButton.MergeAttribute("id", elementId);
inputButton.MergeAttribute("type", "button");
inputButton.MergeAttribute("onclick","" + onClick + "");
return MvcHtmlString.Create(inputButton.ToString());
}
}
}
Add the namespace CustomHelpers in your views web.config file under tag and there you go you have your helper in the intellisense You just need to rebuild your solution for helper to be available in your views.

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.

Replacement for #helper in ASP.NET Core

So far, i don't think ViewComponent solves that neither does TagHelper. Is there any replacement to this? Something that takes parameters and returns a HtmlString?
I don't see anything harmful with:
#helper foo(string something) {
<div>Say #something</div>
}
var emailbody = classfilenameinAppCodefolder.foo("hello"); //store result in a variable for further processes
For now i believe its a temporary delete before RC. https://github.com/aspnet/Razor/issues/281 and https://github.com/aspnet/Mvc/issues/1130 Well! it better be. I hope someone is working on it. Without #helper, building large HtmlString or 'template' would be a serious pain.
Note: Partial View doesn't seem to do the trick. I think it only renders views not return view to variable.
Secondly, what happened to the App_Code folder?
According to the following Github issue, it looks like #helper is coming back and will be included in asp .net core 3.0.0 preview 4.
https://github.com/aspnet/AspNetCore/issues/5110
UPDATE
Starting in asp .net core 3, you can now define a local function within a Razor code block.
#{
void RenderName(string name)
{
<p>Name: <strong>#name</strong></p>
}
RenderName("Mahatma Gandhi");
RenderName("Martin Luther King, Jr.");
}
https://learn.microsoft.com/en-us/aspnet/core/mvc/views/razor?view=aspnetcore-3.1#razor-code-blocks
Alternatively you can use the #functions directive like this:
#{
RenderName("Mahatma Gandhi");
RenderName("Martin Luther King, Jr.");
}
#functions {
private void RenderName(string name)
{
<p>Name: <strong>#name</strong></p>
}
}
https://learn.microsoft.com/en-us/aspnet/core/mvc/views/razor?view=aspnetcore-3.1#functions
#{
Func<String, IHtmlContent> foo = #<div>Say #item</div>;
}
I'd like to expand on #Alexaku's answer and show how I've implemented a helper like function. It's only useful on one specific page but it allows you to execute a piece of razor code multiple times with input parameters. The syntax is not great but I've found it very useful in the absence of razor's #helper function. First declare some kind of Dto that will contain the input parameters into the function.
#functions {
private class Dto
{
public string Data { get;set; }
}
}
Then declare the razor function. Note that the displayItem value can be multi-line and also note that you access the Dto variable using the #item.
#{
Func<Dto, IHtmlContent> displayItem = #<span>#item.Data</span>;
}
Then when you want to use the razor template you can call it like the following from anywhere in the page.
<div>
#displayItem(new Dto {Data = "testingData1" });
</div>
<div>
#displayItem(new Dto {Data = "testingData2" });
</div>
For .NET Core 3, you can use local functions:
#{
void RenderName(string name)
{
<p>Name: <strong>#name</strong></p>
}
RenderName("Mahatma Gandhi");
RenderName("Martin Luther King, Jr.");
}
https://learn.microsoft.com/en-us/aspnet/core/mvc/views/razor?view=aspnetcore-3.1#razor-code-blocks
As #scott pointed out in his answer, local functions are finally available as of .NET Core 3. In prior versions one can resort to templated Razor delegates.
But none of the answers addresses the question "what happened to the App_Code folder?" The aforementioned features are local solutions, that is, helper functions defined in these ways cannot be shared between multiple views. But global helper functions could often be more convenient than the solutions MS provide out-of-the-box for view-related code re-use. (Tag helpers, partial views, view components all have their cons.) This was thoroughly discussed in this and this GitHub issue. According to these discourses, unfortunately, there's not much understanding from MS's side, so not much hope is left that this feature will be added any time soon, if ever.
However, after digging into the framework sources, I think, I could come up with a viable solution to the problem.
The core idea is that we can utilize the Razor view engine to look up an arbitrary view for us: e.g. a partial view which defines some local functions we want to use globally. Once we manage to get hold of a reference to this view, nothing prevents us from calling its public methods.
The GlobalRazorHelpersFactory class below encapsulates this idea:
public interface IGlobalRazorHelpersFactory
{
dynamic Create(string helpersViewPath, ViewContext viewContext);
THelpers Create<THelpers>(ViewContext viewContext) where THelpers : class;
}
public class GlobalRazorHelpersOptions
{
public Dictionary<Type, string> HelpersTypeViewPathMappings { get; } = new Dictionary<Type, string>();
}
public sealed class GlobalRazorHelpersFactory : IGlobalRazorHelpersFactory
{
private readonly ICompositeViewEngine _viewEngine;
private readonly IRazorPageActivator _razorPageActivator;
private readonly ConcurrentDictionary<Type, string> _helpersTypeViewPathMappings;
public GlobalRazorHelpersFactory(ICompositeViewEngine viewEngine, IRazorPageActivator razorPageActivator, IOptions<GlobalRazorHelpersOptions>? options)
{
_viewEngine = viewEngine ?? throw new ArgumentNullException(nameof(viewEngine));
_razorPageActivator = razorPageActivator ?? throw new ArgumentNullException(nameof(razorPageActivator));
var optionsValue = options?.Value;
_helpersTypeViewPathMappings = new ConcurrentDictionary<Type, string>(optionsValue?.HelpersTypeViewPathMappings ?? Enumerable.Empty<KeyValuePair<Type, string>>());
}
public IRazorPage CreateRazorPage(string helpersViewPath, ViewContext viewContext)
{
var viewEngineResult = _viewEngine.GetView(viewContext.ExecutingFilePath, helpersViewPath, isMainPage: false);
var originalLocations = viewEngineResult.SearchedLocations;
if (!viewEngineResult.Success)
viewEngineResult = _viewEngine.FindView(viewContext, helpersViewPath, isMainPage: false);
if (!viewEngineResult.Success)
{
var locations = string.Empty;
if (originalLocations.Any())
locations = Environment.NewLine + string.Join(Environment.NewLine, originalLocations);
if (viewEngineResult.SearchedLocations.Any())
locations += Environment.NewLine + string.Join(Environment.NewLine, viewEngineResult.SearchedLocations);
throw new InvalidOperationException($"The Razor helpers view '{helpersViewPath}' was not found. The following locations were searched:{locations}");
}
var razorPage = ((RazorView)viewEngineResult.View).RazorPage;
razorPage.ViewContext = viewContext;
// we need to save and restore the original view data dictionary as it is changed by IRazorPageActivator.Activate
// https://github.com/dotnet/aspnetcore/blob/v3.1.6/src/Mvc/Mvc.Razor/src/RazorPagePropertyActivator.cs#L59
var originalViewData = viewContext.ViewData;
try { _razorPageActivator.Activate(razorPage, viewContext); }
finally { viewContext.ViewData = originalViewData; }
return razorPage;
}
public dynamic Create(string helpersViewPath, ViewContext viewContext) => CreateRazorPage(helpersViewPath, viewContext);
public THelpers Create<THelpers>(ViewContext viewContext) where THelpers : class
{
var helpersViewPath = _helpersTypeViewPathMappings.GetOrAdd(typeof(THelpers), type => "_" + (type.Name.StartsWith("I", StringComparison.Ordinal) ? type.Name.Substring(1) : type.Name));
return (THelpers)CreateRazorPage(helpersViewPath, viewContext);
}
}
After introducing the singleton IGlobalRazorHelpersFactory service to DI, we could inject it in views and call the Create method to acquire an instance of the view which contains our helper functions.
By using the #implements directive in the helper view, we can even get type-safe access:
#inherits Microsoft.AspNetCore.Mvc.Razor.RazorPage
#implements IMyGlobalHelpers
#functions {
public void MyAwesomeGlobalFunction(string someParam)
{
<div>#someParam</div>
}
}
(One can define the interface type to view path mappings explicitly by configuring the GlobalRazorHelpersOptions in the ordinary way - by services.Configure<GlobalRazorHelpersOptions>(o => ...) - but usually we can simply rely on the naming convention of the implementation: in the case of the IMyGlobalHelpers interface, it will look for a view named _MyGlobalHelpers.cshtml at the regular locations. Best to put it in /Views/Shared.)
Nice so far but we can do even better! It'd be much more convenient if we could inject the helper instance directly in the consumer view. We can easily achieve this using the ideas behind IOptions<T>/HtmlLocalizer<T>/ViewLocalizer:
public interface IGlobalRazorHelpers<out THelpers> : IViewContextAware
where THelpers : class
{
THelpers Instance { get; }
}
public sealed class GlobalRazorHelpers<THelpers> : IGlobalRazorHelpers<THelpers>
where THelpers : class
{
private readonly IGlobalRazorHelpersFactory _razorHelpersFactory;
public GlobalRazorHelpers(IGlobalRazorHelpersFactory razorHelpersFactory)
{
_razorHelpersFactory = razorHelpersFactory ?? throw new ArgumentNullException(nameof(razorHelpersFactory));
}
private THelpers? _instance;
public THelpers Instance => _instance ?? throw new InvalidOperationException("The service was not contextualized.");
public void Contextualize(ViewContext viewContext) => _instance = _razorHelpersFactory.Create<THelpers>(viewContext);
}
Now we have to register our services in Startup.ConfigureServices:
services.AddSingleton<IGlobalRazorHelpersFactory, GlobalRazorHelpersFactory>();
services.AddTransient(typeof(IGlobalRazorHelpers<>), typeof(GlobalRazorHelpers<>));
Finally, we're ready for consuming our global Razor functions in our views:
#inject IGlobalRazorHelpers<IMyGlobalHelpers> MyGlobalHelpers;
#{ MyGlobalHelpers.Instance.MyAwesomeGlobalFunction("Here we go!"); }
This is a bit more complicated than the original App_Code + static methods feature but I think this is the closest we can get. According to my tests, the solution also works nicely with runtime compilation enabled. I haven't had the time so far to do benchmarks but, in theory, it should generally be faster than using partial views as the shared view is looked up only once per consumer view and after that it's just plain method calls. I'm not sure about tag helpers though. It'd be interesting to do some benchmarks comparing them. But I leave that up to the adopter.
(Tested on .NET Core 3.1.)
Update
You can find a working demo of this concept in my ASP.NET boilerplate project:
Infrastructure (relevant files are only those whose name contains GlobalRazorHelpers)
Registration
Helper interface sample
Helper implementation sample
Usage sample
The #helper directive was removed since it was incomplete and its current design did not fit in the new 'ASP.NET 5 way'. One of the reasons is that helpers should be declared in the App_Code folder while ASP.NET 5 has no concept of special folders. Therefore the team decided to temporarily remove the feature.
There are plans to bring it back in the future though. See this and this.
You can easily replace that "feature" with a ViewComponent (and a TagHelper if you want). ASP.NET Core is much more friendly to web designers, and the ViewComponents allow you to write HTML without any (weird to most) razor code.
For example:
Create a SayComponent : ViewComponent class:
public class SayComponent : ViewComponent
{
public void Render(string message)
{
return View(message);
}
}
Create a View file under Views/Shared/Say/Default.cshtml with just
#model string
<div>Message: #Model.</div>
And call it:
#await Component.RenderAsync("Say", "some message")
For a better experience, add this to your _ViewImports.cshtml file:
#addTagHelper *, YourSolutionName
And then you can use it as a tag helper:
<vc:say message="some message"></vc:say>
How about using partials to recreate reusable tags?
MyProject/Views/Shared/_foo.cshtml
#model string
<div>#Model</div>
MyProject/Views/Courses/Index.cshtml
#{
Layout = "_Layout";
}
<div>
<partial name="_foo" model="foo" />
<partial name="_foo" model="bar" />
<partial name="_foo" model="baz" />
</div>

NancyFX, extension method for HtmlHelpers<T> is not recognised in Razor view

I am trying to create an extension method for HtmlHelpers<T> using NancyFX.
The code is something like this
public static IHtmlString DropdownList<T>(HtmlHelpers<T> htmlHelper, string elementId, string elementName, IEnumerable<DropdownListItem> dropdownListItems)
{
var builder = new StringBuilder();
if (dropdownListItems != null && dropdownListItems.ToList().Any())
{
dropdownListItems.ToList().ForEach(t => builder.AppendFormat(#"<option value=""{0}"" {1}>{2}</option>", t.Value, t.Selected ? "selected" : string.Empty, t.Text));
}
var htmlMarkup = string.Format(#"<select id=""{0}"" name=""{1}"">{2}</select>", elementId, elementName, builder);
return new NonEncodedHtmlString(htmlMarkup);
}
This method is not recognised in cshtml view. I have tried both with namespace and without.
When using namespace, I have registered it in razor section in web.config.
NancyFX version I am using: 0.16.1.0
Any working example on how to use extension methods with razor views in NancyFX will be really appreciated.
Oh fogged it! Rookie error. There's a tiny mistake in the extension method. Missed keyword this for the first parameter.

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.

Html tags inside DisplayAttribute

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.