Question

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?

Was it helpful?

Solution

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)
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top