I have some hardcoded value which I want to change to read the value from web.config.
[Display(Name = "SomeValue")]
I tried with
[Display(Name = ConfigurationManager.AppSettings["SomeValue"].ToString())]
An attribute argument must be a constant expression, ...
To do this properly, you would create a new attribute to use instead of DisplayAttribute, and a new ModelMetadataProvider which correctly reads that attribute and the other display attributes.
To do it simply, you create a new HtmlHelper extension that reads the attribute, then access Web.config to find that AppSettings key, and returns the value.
Model
public class SomeClass
{
[Display(Name ="IdColText")]
public int Id { get; set; }
}
Extension
using System;
using System.Configuration;
using System.Linq.Expressions;
using System.Web.Mvc;
namespace ProjectName
{
public static class DisplayConfigNameExtension
{
public static MvcHtmlString DisplayConfigNameFor<TModel, TResult>(this HtmlHelper<TModel> html, Expression<Func<TModel, TResult>> expression)
{
ModelMetadata metadata = ModelMetadata.FromLambdaExpression(expression, new ViewDataDictionary<TModel>());
string configName = metadata.DisplayName;
return MvcHtmlString.Create(ConfigurationManager.AppSettings[configName]);
}
}
}
View
#model SomeClass
// The following is required for the HtmlHelper extension
#using ProjectName;
#Html.DisplayConfigNameFor(m => m.Id)
This will also work if using the DisplayNameAttribute instead of DisplayAttribute, and should also work for classes with MetadataTypeAttribute.
Related
i have a .net core project where in one of my views i have to display the
DisplayName in English and in Arabic in the same view
only i get one language as the view have localized resource file
i created an extension method which take the model and return the DisplayName metadata
actually my method and the original #html helper return one language
here the method i hope some one can modify it so it return the original English metadata instead of the localized value
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Internal;
using System;
using System.Linq.Expressions;
namespace iSee.IHtmlHelpers
{
public static class HtmlExtensions
{
public static IHtmlContent DisplayNameForEn<TModel, TValue>(
this IHtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression
)
{
var modelExplorer = ExpressionMetadataProvider.FromLambdaExpression(expression, htmlHelper.ViewData, htmlHelper.MetadataProvider);
var metadata = modelExplorer.Metadata;
var DisplayName = metadata.DisplayName;
return new HtmlString(DisplayName);
}
}
}
You could do it with code like the example below.
public static IHtmlContent DisplayNameFor<TModel, TValue>(
this IHtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression,CultureInfo culture
)
{
if (culture == null)
{
culture = CultureInfo.CurrentCulture;
}
var displayAttribute = (expression.Body as MemberExpression)?.Member.GetCustomAttributes()
.FirstOrDefault(tt => tt is DisplayAttribute) as DisplayAttribute;
if (displayAttribute == null)
{
return new HtmlString("");
}
var resourceType = displayAttribute.ResourceType;
var name = displayAttribute.Name;
if(resourceType == null)
{
return new HtmlString(name);
}
var resourceManager = new global::System.Resources.ResourceManager(resourceType);
var displayName = resourceManager.GetString(name, culture);
return new HtmlString(displayName);
}
In general you want to take the DisplayAttribute.ResourceType and the DisplayAttribute.Name then use the Resource Manager along with the culture info of the language you want to translate it.
EDIT 2:
A possible Exception on .NET Core would be due to the changes on how the .resx files are used in .NET Core. Making the method above more suitable for .NET Framework applications and the IStringLocalizer<T> solution from the first EDIT more suitable for the ASP.NET Core
EDIT:
You can also use the IStringLocalizer<T> feature of ASP.NET Core find some reference here: Globalization and Localization. For example you could update the above method to use the IStringLocalizer
var culture = new CultureInfo("en-gb");
var localizer = factory.Create(typeof(Startup));
var specificLoc = localizer.WithCulture(culture);
Thanks to #Nick Polideropoulos the answer above was helpful and inspired me to find an answer for the question
here the answer in case some one else search for
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using System;
using System.ComponentModel.DataAnnotations;
using System.Linq.Expressions;
using System.Reflection;
namespace iSee.IHtmlHelpers
{
public static class HtmlExtensions
{
public static IHtmlContent DisplayNameForLatin<TModel, TValue>(
this IHtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression)
{
var memberExpression = expression.Body as MemberExpression;
if (memberExpression == null)
{
return new HtmlString("");
}
var displayAttribute = memberExpression.Member.GetCustomAttribute<DisplayAttribute>();
return new HtmlString(displayAttribute.Name);
}
}
}
So I've got a partial view that displays a model containing some info for a person at an organization. One of the properties is a list of titles which has a UIHint attribute on it to determine what display template to use for it.
Lets say that the model looks like this:
public class Info
{
[UIHint("Titles")]
[DataType("Titles")]
public virtual IEnumerable<string> Titles { get; set; }
}
Let's say that the template for Info looks like:
#model Info
#Html.DisplayFor(x=> x.Titles)
Now we have a very specific type of person-at-org instance that we want to display using the same template but we want to use a different display template for the Titles property so we create a subclass of a Info model:
public class SpecificInfo : Info
{
[UIHint("SpecificTitles")]
[DataType("SpecificTitles")]
public override IEnumerable<string> Titles { get; set; }
}
But it's still trying to use the "Titles" display template presumably because the expression passed into the DisplayFor helper thinks that it's accessing the property on the Info class.
Is there any way to get that helper to use the correct display template? I've been thinking that a possible solution would be to create my own DisplayFor extension method that figures out what the runtime type of the model is and uses reflection to find the property and check to see if we are specifying a template there but I can't shake the feeling that there might be an easier way to do it.
You are right setting #model Info in your view makes DisplayeFor use htmlHelper<Info>. It has only acces to Info attributes. You can spcify in view which template to use:
#Html.DisplayFor(x=> x.Titles, Model is SpecificInfo ? "SpecificInfo" : "Info")
but then there is no reason for UIHintAttribute.
As you write you can also write custom DisplayFor method and you can use htmlHelper.ViewData.Model to get actual model attribute(as you suggested in comment ;)):
public static MvcHtmlString CustomDisplayFor<TModel, TValue>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, TValue>> expression)
{
MemberExpression memberExpression = (MemberExpression)expression.Body;
var propertyName = memberExpression.Member is PropertyInfo ? memberExpression.Member.Name : (string)null;
var prop = htmlHelper.ViewData.Model.GetType().GetProperty(propertyName);
var customAttributeData = prop.GetCustomAttributesData().FirstOrDefault(a => a.AttributeType.Name == "UIHintAttribute");
if (customAttributeData != null)
{
var templateName = customAttributeData.ConstructorArguments.First().Value as string;
return DisplayExtensions.DisplayFor<TModel, TValue>(htmlHelper, expression, templateName);
}
return DisplayExtensions.DisplayFor<TModel, TValue>(htmlHelper, expression);
}
View:
#Html.CustomDisplayFor(x => x.Titles)
I'm using erichynds Multi Select Widget to create a nice style for my MultiSelectList. But my issue (appears) to be unrelated.
I am trying to loop through each DemographicQuestionFilter question, list out the DemographicResponseFilter response and be able to get/post the selected items along with my DemographicFilterViewModel model. The issue I am having is that when I set the filters for item-1 (index 0) in the list it works fine, when I set item-2 (index 1) it only works if item-1 is also set, if item-1 is not set then the DemographicFilters object is null. I'm assuming I can switch up types, or that I'm missing something basic here.
How can I make it so that the list containing the selected items for n Question is not dependent n-1 also having a selected item?
Here are my ViewModel objects:
Parent:
public class DemographicFilterViewModel
{
public int TaskID { get; set; }
public List<DemographicQuestionFilter> DemographicFilters { get; set; }
}
Child:
public class DemographicQuestionFilter
{
public string Question { get; set; }
public List<DemographicResponseFilter> Responses { get; set; }
public List<SelectListItem> selectListItems { get; set; }
public List<int> SelectedItems { get; set; }
}
Grandchild:
public class DemographicResponseFilter
{
public int ResponseID { get; set; }
public string Response { get; set; }
}
View:
#Html.HiddenFor(m => m.TaskID)
if (Model.DemographicFilters != null)
{
for (int i = 0; i < Model.DemographicFilters.Count; i++)
{
#Html.HiddenFor(model => model.DemographicFilters[i].SelectedItems)
#Html.DisplayTextFor(m => m.DemographicFilters[i].Question)
<br />
#Html.ListBoxFor(model => model.DemographicFilters[i].SelectedItems, new MultiSelectList(Model.DemographicFilters[i].Responses, "ResponseID", "Response", Model.DemographicFilters[i].SelectedItems), new { Multiple = "multiple" })
<br />
<br />
}
}
Here is what is rendered to the screen (just so you can try to follow what I am doing):
http://i.imgur.com/ZefpLy1.png?1
Edit: The issue is when the View posts back to the controller, the View displays correctly, but on HttpPost the values in [n]SelectedItems are dependent on [n-1]SelectedItems having a value,
If [i]SelectedItems is blank (nothing selected) then every [>i]SelectedItems is null, even when the values are correctly set in the HttpGet...
HTMLHelper Extension:
#region Usings
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Web.Mvc.Html;
using System.Web.Mvc;
#endregion
namespace Extensions
{
public static class HtmlHelperExtensions
{
public static MvcHtmlString HiddenEnumerableFor<TModel, TEnumType>(this HtmlHelper<TModel> htmlHelper, Expression<Func<TModel, IEnumerable<TEnumType>>> expression)
{
return htmlHelper.Hidden(htmlHelper.NameFor(expression).ToHtmlString(),
string.Join(",", expression.Compile().Invoke(htmlHelper.ViewData.Model) ?? new TEnumType[0]));
}
}
}
HiddenFor cannot be used for ListBoxFor so here is the workaround I tried to fix the issue.
Replaced
#Html.HiddenFor(model => filter.SelectedItems)
With
#Html.Hidden(string.Format("DemographicFilters[{0}].SelectedItems", i), "-1")
Problem with this approach is that your DemographicFilters.SelectedItems will have an extra row -1 added to it, need to add code to exclude -1 row.
My answer is an extension on what Vasanth Sundaralingam posted in his answer, that HiddenFor won't work with arrays. I went ahead and created a function that behaves like a hiddenFor for enumerable properties.
#functions
{
public MvcHtmlString HiddenEnumerableFor<TModel, TEnumType>(
HtmlHelper<TModel> htmlHelper,
Expression<Func<TModel, IEnumerable<TEnumType>>> expression)
{
return htmlHelper.Hidden(htmlHelper.NameFor(expression).ToHtmlString(),
string.Join(",", expression.Compile().Invoke(htmlHelper.ViewData.Model) ?? new TEnumType[0]));
}
}
Replace
#Html.HiddenFor(model => filter.SelectedItems)
With
#HiddenEnumerableFor(Html, m => m.DemographicFilters[i].SelectedItems)
You could also convert this into an extension method by adding it to a static class, and adding this before the first parameter. That way it would look like very similar to HiddenFor
#Html.HiddenEnumerableFor(m => m.DemographicFilters[i].SelectedItems)
I am trying to create a custom ModelMetadataProvider to provide unobtrusive attributes for the JQuery UI Autocomplete widget.
I have a custom attribute that looks like this:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public sealed class AutocompleteAttribute : Attribute, IMetadataAware
{
public void OnMetadataCreated(ModelMetadata metadata)
{
metadata.TemplateHint = "Autocomplete";
}
}
and an editor template that looks like this:
#{
var attributes = new RouteValueDictionary
{
{"class", "text-box single-line"},
{"autocomplete", "off"},
{"data-autocomplete-url", "UrlPlaceholder" },
};
}
#Html.TextBox("", ViewContext.ViewData.TemplateInfo.FormattedModelValue, attributes)
I have a viewModel with a property of type string that includes the AutocompleteAttribute like this:
public class MyViewModel
{
[Autocomplete]
public string MyProperty { get; set; }
}
When I use this viewModel in my view I check the generated html and I am getting an <input> tag which has an attribute like this: data-autocomplete-url="UrlPlaceholder".
What I want to do next is to be able to specify the URL in my view that uses my viewModel like this:
#model MyViewModel
#{ ViewBag.Title = "Create item"; }
#Html.AutoCompleteUrlFor(p => p.MyProperty, UrlHelper.GenerateUrl(null, "Autocomplete", "Home", null, Html.RouteCollection, Html.ViewContext.RequestContext, true))
// Other stuff here...
<div>
#Html.ActionLink("Back to List", "Index")
</div>
My AutoCompleteForUrl helper just saves the generated URL in a dictionary, using the property name as a key.
Next I have created a custom ModelMetadataProvider and registered it in global.asax using this line of code ModelMetadataProviders.Current = new CustomModelMetadataProvider();.
What I want to do is to insert the URL to be used by the JQuery UI Autocomplete widget into the metadata.AdditionalValues dictionary to be consumed by the Autocomplete editor template.
My custom ModelMetadataProvider looks like this:
public class CustomModelMetadataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(IEnumerable<System.Attribute> attributes, Type containerType, Func<object> modelAccessor, Type modelType, string propertyName)
{
var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
if (metadata.TemplateHint == "Autocomplete")
{
string url;
if(htmlHelpers.AutocompleteUrls.TryGetValue(metadata.propertyName, out url)
{
metadata.AdditionalValues["AutocompleteUrl"] = url;
}
}
return metadata;
}
}
and my updated editor template looks like this:
#{
object url;
if (!ViewContext.ViewData.ModelMetadata.TryGetValue("AutocompleteUrl", out url))
{
url = "";
}
var attributes = new RouteValueDictionary
{
{"class", "text-box single-line"},
{"autocomplete", "off"},
{"data-autocomplete-url", (string)url },
};
}
#Html.TextBox("", ViewContext.ViewData.TemplateInfo.FormattedModelValue, attributes)
The problem is, the TemplateHint property never equals "Autocomplete" in my custom model metadata provider so my logic to generate the URL never gets called. I would have thought that at this point the TemplateHint property would be set as I have called the base implementation of CreateMetadata of the DataAnnotationsModelMetadataProvider.
Here's what I can confirm:
The CustomModelMetadataProvider is correctly registered as it contains other code which is getting called.
The correct editor template is getting picked up as the Html that is generated contains an attribute called "data-autocomplete-url".
If I put a breakpoint in the Autocomplete template, Visual Studio goes to the debugger.
So can anyone shed any light on this for me please? What am I misunderstanding about the ModelMetadataProvider system?
After looking through the ASP.NET MVC 3 source code I have discovered that the reason for this is because the CreateMetadata method is called prior to the OnMetadataCreated method of any IMetadataAware attributes that are applied to the model.
I have found an alternative solution that allows me to do what I wanted.
First of all I updated my AutocompleteAttribute:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class AutocompleteAttribute : Attribute, IMetadataAware
{
public const string Key = "autocomplete-url";
internal static IDictionary<string, string> Urls { get; private set; }
static AutocompleteAttribute()
{
Urls = new Dictionary<string, string>();
}
public void OnMetadataCreated(ModelMetadata metadata)
{
metadata.TemplateHint = "Autocomplete";
string url;
if (Urls.TryGetValue(metadata.PropertyName, out url))
{
metadata.AdditionalValues[Key] = url;
Urls.Remove(metadata.PropertyName);
}
}
}
and my Html helper method for setting the url in my views looks like this:
public static IHtmlString AutocompleteUrlFor<TModel, TValue>(this HtmlHelper<TModel> html, Expression<Func<TModel, TValue>> expression, string url)
{
if (string.IsNullOrEmpty(url))
throw new ArgumentException("url");
var property = ModelMetadata.FromLambdaExpression(expression, html.ViewData).PropertyName;
AutocompleteAttribute.Urls[property] = url;
return MvcHtmlString.Empty;
}
And then all I have to do in my editor template is this:
#{
object url;
ViewData.ModelMetadata.AdditionalValues.TryGetValue(AutocompleteAttribute.Key, out url);
var attributes = new RouteValueDictionary
{
{"class", "text-box single-line"},
{"autocomplete", "off"},
{ "data-autocomplete-url", url },
};
}
#Html.TextBox("", ViewContext.ViewData.TemplateInfo.FormattedModelValue, attributes)
I am building a custom HTML.LabelFor helper that looks like this :
public static MvcHtmlString LabelFor<TModel, TValue>(this HtmlHelper<TModel> self, Expression<Func<TModel, TValue>> expression, Boolean showToolTip)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, self.ViewData);
...
}
To be able to get the proper name for the property I am using the following code :
metadata.DisplayName
And on the property of the ModelView class I got :
[DisplayName("Titel")]
The problem is that I also need a description. There is an Attribute called Display and that has Name and Description but I do not see how to extract this with the metadata variable in the above code?
Disclaimer: The following works only with ASP.NET MVC 3 (see the update at the bottom if you are using previous versions)
Assuming the following model:
public class MyViewModel
{
[Display(Description = "some description", Name = "some name")]
public string SomeProperty { get; set; }
}
And the following view:
<%= Html.LabelFor(x => x.SomeProperty, true) %>
Inside your custom helper you could fetch this information from the metadata:
public static MvcHtmlString LabelFor<TModel, TValue>(
this HtmlHelper<TModel> self,
Expression<Func<TModel, TValue>> expression,
bool showToolTip
)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, self.ViewData);
var description = metadata.Description; // will equal "some description"
var name = metadata.DisplayName; // will equal "some name"
// TODO: do something with the name and the description
...
}
Remark: Having [DisplayName("foo")] and [Display(Name = "bar")] on the same model property is redundant and the name used in the [Display] attribute has precedence in metadata.DisplayName.
UPDATE:
My previous answer won't work with ASP.NET MVC 2.0. There are a couples of properties that it is not possible to fill by default with DataAnnotations in .NET 3.5, and Description is one of them. To achieve this in ASP.NET MVC 2.0 you could use a custom model metadata provider:
public class DisplayMetaDataProvider : DataAnnotationsModelMetadataProvider
{
protected override ModelMetadata CreateMetadata(
IEnumerable<Attribute> attributes,
Type containerType,
Func<object> modelAccessor,
Type modelType,
string propertyName
)
{
var metadata = base.CreateMetadata(attributes, containerType, modelAccessor, modelType, propertyName);
var displayAttribute = attributes.OfType<DisplayAttribute>().FirstOrDefault();
if (displayAttribute != null)
{
metadata.Description = displayAttribute.Description;
metadata.DisplayName = displayAttribute.Name;
}
return metadata;
}
}
which you would register in Application_Start:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterRoutes(RouteTable.Routes);
ModelMetadataProviders.Current = new DisplayMetaDataProvider();
}
and then the helper should work as expected:
public static MvcHtmlString LabelFor<TModel, TValue>(
this HtmlHelper<TModel> self,
Expression<Func<TModel, TValue>> expression,
bool showToolTip
)
{
var metadata = ModelMetadata.FromLambdaExpression(expression, self.ViewData);
var description = metadata.Description; // will equal "some description"
var name = metadata.DisplayName; // will equal "some name"
// TODO: do something with the name and the description
...
}