AutoComplete using customized UIHint attribute in ASP.Net MVC

In one project I worked on recently, I wanted to display list of suppliers which is very very long list, it was more than 6000 items. to display such number of items in dropdown list in a page of course will affect page load time.

The best solution was using Autocomplete jQuery plugin, it is light, powerful, and covers my needs but unfortunately I need to display that suppliers list in many pages for different view models.

One of possible solutions copy/paste autocomplete code snippet in each view.This solution didn't make me satisfied and I thought of course there is better way and more elegant to do it.

One possible alternative is Editor Template, in most cases we need the Id value not the item full details. for simplicity I'm using list of programming languages as a data source.

I override UIHint attribute to pass the URL of our datasource, the URL will be used as source for autocomplete jQuery plugin.
public class AutoCompleteUIHintAttribute : UIHintAttribute
{
   private readonly UrlHelper _urlHelper = new UrlHelper(Current.Request.RequestContext);
   public AutoCompleteUIHintAttribute(string url) :
        base("AutoComplete") //Editor template name
   {        
       DataSourceUri = url;
   }   
   public AutoCompleteUIHintAttribute(string action, string controller) :
        base("AutoComplete")
   {
        DataSourceUri = _urlHelper.RouteUrl(new { action = action, controller = controller });
   }
   public AutoCompleteUIHintAttribute(string action, string controller, string area) :
        base("AutoComplete")
   {
        DataSourceUri = _urlHelper.RouteUrl(new { action = action, controller = controller, area = area });
   }
   public string DataSourceUri { get; set; }
}
In customized attribute we have three overloads of its constructor, first one accepts the full url as string. We can use it if our datasource is external or remote datasource. Second accepts action name and controller name and third accepts same values plus area name, these can be used if our datasource inside the current web application.

In our view model what we need is just decorate the LanguageId property with the AutoCompleteUIHint attribute.
 public class MyTestModel
 {
     [DisplayName("Language")]
     //Here I used constructor that accepts action name and controller name
     [AutoCompleteUIHint("datasource","home")]
     public int LanguageId { get; set; }        
 }
in view, we just use EditorFor helper method
@Html.EditorFor(m => m.LanguageId)
in controller, we have action method return languages as JsonResult
public JsonResult DataSource(string term)
{
     //_datasource is a predefined list of programming languages
     var langauges = _dataSource.Where(x => x.Name.ToLower().StartsWith(term.ToLower()));
     return Json(langauges.Select(i => new
     {
         id = i.ID,
         value = i.Name
     }// autocomplete jquery plugin accepts result in this format(id & value)
     ), JsonRequestBehavior.AllowGet);
}
Now we should write HTML version of our editor template. Add new file with name AutoComplete.cshtml to Views/Shared/EditorTemplates folder then copy following code into new file.
@using AutoCompleteUIHint.Core;
@{
    //get the full qualified name of property decorated with our attribute
    var propertyName = ViewData.TemplateInfo.GetFullHtmlFieldName("");
    //javascript doesn't work properly with ids contain dot
    var propertyId = propertyName.Replace(".", "_");
    //generate random Id in case our view contains many instances
    var textBoxId = Guid.NewGuid().ToString();
}

@functions{
    //generate text box for autocomplete feature
    private MvcHtmlString TextBox(string name)
    {
        var tagBuilder = new TagBuilder("input");
        tagBuilder.MergeAttribute("id", textBoxId);
        tagBuilder.MergeAttribute("type", "text");

        return MvcHtmlString.Create(tagBuilder.ToString());
    }
    //genarete hidden field to keep selected value(id) of selected language ID
    private MvcHtmlString Hidden(string name)
    {
        var tagBuilder = new TagBuilder("input");
        tagBuilder.MergeAttribute("type", "hidden");
        tagBuilder.MergeAttribute("id", name.Replace(".", "_"));
        tagBuilder.MergeAttribute("name", name);

        return MvcHtmlString.Create(tagBuilder.ToString());
    }

    //get url of datasource, that has been generated from Attribute constructor
    private string GetUrl()
    {
        var attribute = ViewData.ModelMetadata.ContainerType
                        // Get the property we are displaying for (LanguageId)
                        .GetProperty(ViewData.ModelMetadata.PropertyName)
                        // Get all attributes of type AutoCompleteUIHintAttribute
                        .GetCustomAttributes(typeof(AutoCompleteUIHintAttribute), false)
                        // Cast the result as AutoCompleteUIHintAttribute
                        .Select(a => a as AutoCompleteUIHintAttribute)
                        // Get the first one or null
                        .FirstOrDefault(a => a != null);

        return attribute?.DataSourceUri;
    }
}
// render text box
@Html.Raw(TextBox(textBoxId))
// render hidden field
@Html.Raw(Hidden(propertyName))

<script>
    $(function () {
        $("#@textBoxId").autocomplete({
            source: '@Url.Content(GetUrl())',// datasource url 
            minLength: 1,// type at least 1 character
            select: function (event, ui) {
                $('#@propertyId').val(ui.item.id);// set hidden field value with the id of selected language.
            }
        });
    });
</script>
when we run our application and start typing we get the following result
when select a language and click submit button, you will get selected language ID and Name
Your feedback are very welcome and you can download full code here

References

Comments

  1. i think this approach has one issue that you put javascript inside EditorTemplate. which may be load before the JqueryUi load. i think you must but javascript inside main view not EditorTemplate

    ReplyDelete
    Replies
    1. You are right, but it just simple version to show how to solve loading a huge list problem using Editor Template. I know it needs a lot of enhancement!!

      Delete

Post a Comment

Popular posts from this blog

Android : How to change progress bar color at runtime programmatically?

How to fire RowCommand event of nested GridView?

ASP.Net MVC : Conditional Validation using ValidationAttribute