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:
CustomModelMetadataProvider
is correctly registered as it contains other code which is getting called."data-autocomplete-url"
.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)