ModelMetadata TemplateHint always null - c#

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)

Related

POST data to action using HtmlFieldPrefix yields empty model

In my application, I have a few modal windows. Each of them takes the same partial view and the same view model, but displays other data. For that, I generate a dynamic HtmlFieldPrefix as to not have multiple IDs of the same name. Example:
#foreach (var product in Model.Products)
{
string buyModalId = product.BuyModel.BindingPrefix;
#await Html.PartialForAsync("_BuyForm", product.BuyModel, buyModalId)
}
BindingPrefix contains a dynamically added string (for example buy-product-{ID}). In my view I also have a hidden field that is supposed to POST the binding prefix back:
#Html.Hidden(nameof(Model.BindingPrefix), Model.BindingPrefix)
(Source: Asp.Net MVC Dynamic Model Binding Prefix)
That does not work, however, since the binding prefix is null when POSTing, too. Hence await TryUpdateModelAsync(model, model.BindingPrefix); in my controller fails.
The code for the Html.PartialForAsync method is the following:
public static Task<IHtmlContent> PartialForAsync(this IHtmlHelper htmlHelper, string partialViewName, object model, string prefix)
{
var viewData = new ViewDataDictionary(htmlHelper.ViewData);
var htmlPrefix = viewData.TemplateInfo.HtmlFieldPrefix;
viewData.TemplateInfo.HtmlFieldPrefix += !Equals(htmlPrefix, string.Empty) ? $".{prefix}" : prefix;
var part = htmlHelper.PartialAsync(partialViewName, model, viewData);
return part;
}
(Source: MVC 6 VNext how to set HtmlFieldPrefix?)
What am I missing? Why is my model still null? When removing the binding prefix, the binding works flawlessly - but the browser throws warnings regarding multiple same IDs.
Found the answer by using a custom model binder inside my model:
public override void BindModel(ModelBindingContext bindingContext)
{
var providers = bindingContext.ValueProvider as System.Collections.IList;
var formProvider = providers?.OfType<JQueryFormValueProvider>().FirstOrDefault();
if (formProvider != null)
{
var (_, value) = formProvider.GetKeysFromPrefix(string.Empty).First();
bindingContext.BinderModelName = value;
bindingContext.FieldName = value;
bindingContext.ModelName = value;
}
base.BindModel(bindingContext);
}

Set/Change Display(Name="") Attribute of a Property in Controller/ViewComponent

According to this answer from 2010, regarding mvc-2, it wasn't possible. What about now, in asp.net-core 2.2?
My usecase:
I have a BaseViewModel that is being used by 2 views: TableView (for users) and TableManagentView (for admins). The BaseViewModel is invoked by a ViewComponent. Here are some samples:
BaseViewModel:
public class BaseViewModel {
[Display(Name = "Comment")
public string UserComment { get; set; }
}
TableView:
#await Component.InvokeAsync(nameof(Base), new { myObject = myObject, stringName = "User"})
TableManagementView:
#await Component.InvokeAsync(nameof(Base), new { myObject = myObject, stringName = "Admin"})
Base:
public class Base : ViewComponent
{
public IViewComponentResult Invoke(BaseViewModel myObjet, string invokingView)
{
// here i want to do something like that
if (invokingView == "User") {
myObject.UserComment.SetDisplayName("My Comment");
}
if (invokingView == "Admin") {
myObject.UserComment.SetDisplayName("User's Comment");
}
return View("BaseViewComponent", myObject);
}
}
BaseViewComponent:
#Html.DisplayNameFor(model => model.UserComment)
The BaseViewModel is simplified, but there are a lot more attributes. The reason I want to do this is to avoid code duplication in both tables. The only thing that should change are the label names.
I've tried reflection, but without success:
public IViewComponentResult Invoke(BaseViewModel myObject, string invokingView)
{
MemberInfo property = typeof(BaseViewModel).GetProperty("UserComment");
property.GetCustomAttributes(typeof(DisplayAttribute)).Cast<DisplayAttribute>().Single().Name = "test";
return View("BaseViewComponent", myObject);
}
The Name doesn't change and remains "Comment" from the initial setting.
If it's not possible to set the attribute name programmatically, what other solutions do I have? I'm thinking about ViewBag/ViewData or TempData, but this solution doesn't appeal to me. What would be the pro's and con's of that?
Extending on the comment I left, one way you could solve this is by having your BaseViewModel being an abstract class and have concrete classes deriving from it. So UserViewModel and AdminViewModel. These two concrete classes would then be the models for both TableView and TableManagentView and would be responsible for telling the "outside world" how to label fields.
The base class has two main aspects (apart from your normal fields): An abstract Dictionary<string, string> which will contain the labels and a method to get the label from the list: string GetLabel(string propName). So something like this:
public abstract class BaseViewModel
{
protected abstract Dictionary<string, string> Labels { get; }
public string UserComment { get; set; }
public string GetLabel(string propName)
{
if (!Labels.TryGetValue(propName, out var label))
throw new KeyNotFoundException($"Label not found for property name: {propName}");
return label;
}
}
Then you create the two deriving classes User and Admin:
public sealed class UserViewModel : BaseViewModel
{
protected override Dictionary<string, string> Labels => new Dictionary<string, string>
{
{ nameof(UserComment), "User label" }
};
}
public sealed class AdminViewModel : BaseViewModel
{
protected override Dictionary<string, string> Labels => new Dictionary<string, string>
{
{ nameof(UserComment), "Admin label" }
};
}
They only implement the Dictionary<string, string> and set the appropriate text for each field on the base class.
Next, changing your BaseViewComponent to this:
View:
#model DisplayNameTest.Models.BaseViewModel
<h3>Hello from my View Component</h3>
<!-- Gets the label via the method on the base class -->
<p>#Model.GetLabel(nameof(BaseViewModel.UserComment))</p>
<p>#Model.UserComment)</p>
ComponentView class (simpler now)
public IViewComponentResult Invoke(BaseViewModel viewModel)
{
return View(viewModel);
}
Finally, changing your views TableView and TableManagentView to this:
#model WebApp.Models.AdminViewModel
#{
Layout = null;
}
<h1>Admin View</h1>
<div>
#await Component.InvokeAsync("Base", Model)
</div>
and the Controller to:
public IActionResult Index()
{
var adminViewModel = new AdminViewModel { UserComment = "some comment from admin" };
return View(adminViewModel);
}
Now when you navigate to TableView, you'll pass a UserViewModel to the BaseViewComponent and it will figure it out the correct label. Introducing new fields will just now require you to change your viewmodels, adding a new entry to the Dictionary.
It's not perfect, but I think it's an okay way to solve it. I'm by far not an MVC expert so maybe others can come up with a more natural way to do it as well. I also prepared a working sample app and pushed to GitHub. You can check it out here: aspnet-view-component-demo. Hope it helps somehow.

How can I make Html.CheckBoxFor() work on a string field?

I'm using ASP.NET MVC3 with Razor and C#. I am making a form builder of sorts, so I have a model that has a collection of the following object:
public class MyFormField
{
public string Name { get; set; }
public string Value { get; set; }
public MyFormType Type { get; set; }
}
MyFormType is just an enum that tells me if the form field is a checkbox, or textbox, or file upload, or whatever. My editor template looks something like this (see the comment):
~/Views/EditorTemplates/MyFormField.cshtml
#model MyFormField
#{
switch (Model.Type)
{
case MyFormType.Textbox:
#Html.TextBoxFor(m => m.Value)
case MyFormType.Checkbox:
#Html.CheckBoxFor(m => m.Value) // This does not work!
}
}
I tried casting/converting the m.Value to a bool in the lambda expression for CheckBoxFor(), but that threw an error. I would just manually construct a checkbox input, but CheckBoxFor() seems to do two things that I can't seem to replicate:
Creates a hidden input that somehow gets populated by the checkbox. This appears to be what the model binder picks up.
Generates the name form the object so that the model binder gets the value into the right property.
Does anyone know a way around using CheckBoxFor() on a string, or a way to replicate its functionality manually, so that I can make this work?
You could also add a property on your viewmodel:
public class MyFormField
{
public string Name { get; set; }
public string Value { get; set; }
public bool CheckBoxValue
{
get { return Boolean.Parse(Value); }
}
public MyFormType Type { get; set; }
}
Your view would be something like this:
#model MyFormField
#{
switch (Model.Type)
{
case MyFormType.Textbox:
#Html.TextBoxFor(m => m.Value)
case MyFormType.Checkbox:
#Html.CheckBoxFor(m => m.CheckBoxValue) // This does work!
}
}
Use Boolean.TryParse if you want to avoid exceptions.
One way is to create your own htmlhelper extension method.
public static MvcHtmlString CheckBoxStringFor<TModel>(this HtmlHelper<TModel> html, Expression<Func<TModel, string>> expression)
{
// get the name of the property
string[] propertyNameParts = expression.Body.ToString().Split('.');
string propertyName = propertyNameParts.Last();
// get the value of the property
Func<TModel, string> compiled = expression.Compile();
string booleanStr = compiled(html.ViewData.Model);
// convert it to a boolean
bool isChecked = false;
Boolean.TryParse(booleanStr, out isChecked);
TagBuilder checkbox = new TagBuilder("input");
checkbox.MergeAttribute("id", propertyName);
checkbox.MergeAttribute("name", propertyName);
checkbox.MergeAttribute("type", "checkbox");
checkbox.MergeAttribute("value", "true");
if (isChecked)
checkbox.MergeAttribute("checked", "checked");
TagBuilder hidden = new TagBuilder("input");
hidden.MergeAttribute("name", propertyName);
hidden.MergeAttribute("type", "hidden");
hidden.MergeAttribute("value", "false");
return MvcHtmlString.Create(checkbox.ToString(TagRenderMode.SelfClosing) + hidden.ToString(TagRenderMode.SelfClosing));
}
The usage is the same as CheckBoxFor helper (e.Value is a string)
#Html.CheckBoxStringFor(e => e.Value)
Use the Checkbox, this simple way works fine
#Html.CheckBox("IsActive", Model.MyString == "Y" ? true : false)
I had this problem as well but was unable to modify the view model. Tried mdm20s solution but as i suspected it does not work on collection properties (it does not add the indexes to the names and ids like the native html helpers). To overcome this you can use the Html.CheckBox instead. It adds the proper indexes and you can pass the value of the checkbox yourself.
If you really want to use an expression you can always write a wrapper similar to mdm20s but replace everything after the TryParse with
return Html.CheckBox("propertyName", isChecked). Obviously you will need to add using System.Web.Mvc.Html as well.

Extract Display name and description Attribute from within a HTML helper

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
...
}

DataAnnotation with custom ResourceProvider

I have created a custom ResourceProvider to pull localization information from a database. I now want to use DataAnnotation to add validation to the model.
DataAnnotation has ErrorMessageResourceType and ErrorMessageResourceName properties but ErrorMessageResourceType only accepts System.Type (i.e. a compiled resource file)
Is there any way to get DataAnnotation to use the custom ResourceProvider?
I realize this is an old question, but wanted to add a bit. I found myself in the same situation and there doesn't appear to be any documentation/blogumentation on this topic. Nevertheless, I figured out a way to use a custom resource provider, with one caveat. The caveat is that I'm in an MVC application so I still have HttpContext.GetLocalResourceObject() available. This is the method that asp.net uses to localize items. The absence of the resource object doesn't stop you from writing our own solution, even if its a direct query of the DB tables. Nevertheless, I thought it was worth pointing out.
While I'm not terribly happy with the following solution, it seems to work. For each validation attribute I want to use I inherit from said attribute and overload the IsValid(). The decoration looks like this:
[RequiredLocalized(ErrorMessageResourceType= typeof(ClassBeginValidated), ErrorMessageResourceName="Errors.GenderRequired")]
public string FirstName { get; set; }
The new attribute looks like this:
public sealed class RequiredLocalized : RequiredAttribute {
public override bool IsValid(object value) {
if ( ! (ErrorMessageResourceType == null || String.IsNullOrWhiteSpace(ErrorMessageResourceName) ) ) {
this.ErrorMessage = MVC_HtmlHelpers.Localize(this.ErrorMessageResourceType, this.ErrorMessageResourceName);
this.ErrorMessageResourceType = null;
this.ErrorMessageResourceName = null;
}
return base.IsValid(value);
}
}
Notes
You need to decorate your code with the derived attribute, not the standard one
I'm using ErrorMessageResourceType to pass the type of the class being validated. By that I mean if I'm in a customer class and validating the FirstName property I would pass typeof(customer). I'm doing this because in my database backend I'm using the full class name (namespace + classname) as a key (the same way a page URL is used in asp.net).
MVC_HtmlHelpers.Localize is just a simple wrapper for my custom resource provider
The (semi-stolen) helper code looks like this ....
public static string Localize (System.Type theType, string resourceKey) {
return Localize (theType, resourceKey, null);
}
public static string Localize (System.Type theType, string resourceKey, params object[] args) {
string resource = (HttpContext.GetLocalResourceObject(theType.FullName, resourceKey) ?? string.Empty).ToString();
return mergeTokens(resource, args);
}
private static string mergeTokens(string resource, object[] args) {
if (resource != null && args != null && args.Length > 0) {
return string.Format(resource, args);
} else {
return resource;
}
}
I have used fluent validation to achieve this. It saves me lots of time. This is what my Globalized validator looks like. It does mean that you don't use data anotations, but sometimes data anotations get a bit big and messy.
Here is an example:
(Errors.Required, Labels.Email and Errors.AlreadyRegistered are in my blobal resources folder.)
public class CreateEmployerValidator : AbstractValidator<CreateEmployerModel> {
public RegisterUserValidator() {
RuleFor(m => m.Email)
.NotEmpty()
.WithMessage(String.Format(Errors.Required, new object[] { Labels.Email }))
.EmailAddress()
.WithMessage(String.Format(Errors.Invalid, new object[] { Labels.Email }))
.Must(this.BeUniqueEmail)
.WithMessage(String.Format(Errors.AlreadyRegistered, new object[] { Labels.Email }));
}
public bool BeUniqueEmail(this IValidator validator, string email ) {
//Database request to check if email already there?
...
}
}
Like I said, it is a move away form data annotations, only because I already have too many annotations on my methods already!
I'll add my findings since I had to fight with this. Maybe it will help someone.
When you derive from RequiredAttribute, it seems to break client side validation. So to fix this I implemented IClientValidatable and implemented the GetClientValidationRules method. Resources.GetResources is static helper method I have that wraps around HttpContext.GetGlobalResourceObject.
The custom required attribute:
public class LocalizedRequiredAttribute : RequiredAttribute, IClientValidatable
{
public LocalizedRequiredAttribute(string resourceName)
{
this.ErrorMessage = Resources.GetResource(resourceName);
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
yield return new ModelClientValidationRule
{
ErrorMessage = this.ErrorMessage,
ValidationType= "required"
};
}
}
Usage:
[LocalizedRequired("SomeResourceName")]
public string SomeProperty { get; set; }
And my Resources helper if anyone is interested:
public class Resources
{
public static string GetResource(string resourceName)
{
string text = resourceName;
if (System.Web.HttpContext.Current != null)
{
var context = new HttpContextWrapper(System.Web.HttpContext.Current);
var globalResourceObject = context.GetGlobalResourceObject(null, resourceName);
if (globalResourceObject != null)
text = globalResourceObject.ToString();
}
return text;
}
}

Categories

Resources