Introduction
This is an article / tip that I am trying to giving an idea of creating a general auto complete helper for asp.net MVC 5 developers
When i started my new project in ASP.NET MVC, I had requirement to provide a input control like auto complete for many number of domain entities (department, employee, product, categories etc.) and for general purpose. I just wanted to use auto completes instead of dropdown list
This is useful when there is a requirement to provide auto suggestion kind of data entry to end user
Two helpers with various overloads in terms of having attributes, auto complete type, is required, on select call back and source URL. One helper used for strongly type view and other one for view with no model binding
How it works
Helper creates two html input elements one type of hidden and other type of text. Id and name of hidden would be the developer give name of autocomplete or model propertyname. Id and name for the textbox would be the name of hidden _AutoComplete (ex: hidden name is ProductID, textbox name is ProductID_Auto Complete). Selected text shown in text box and selected value goes to hidden control. On submission of the current form both the values from hidden and text will be submited to server.
If we specify the source url to the control, the it gets the data from specified url. In other case if we specify the type of auto complete the data loads from a predefined url.
A JavaScript callback function can be specify to execute after select an item from autocomplete
Client side required validation can be added by using parameter IsRequired = true
Test requirements
- Visual Studio 2013
- ASP.NET MVC 5
- jquery-1.10.2
- jquery-ui-1.8.23.custom.min AutoComplete plugin
Using the code
Code blocks consists of following resources
-
Department, Employee ( c# classes domain entities of this attached source )
-
AutocompleteHelper ( c# statick class to define the helpers)
-
TestAutoCompleteController ( c# MVC controller class to return data to auto completes request)
-
CustomAutoComplete script ( Jquery script to handle auto complete request and responses from client)
-
View ( Index.cshtml to design the sample view)
First define an Enum to specify the type of data to load in terms of entity names
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Text;
using System.Web.Mvc;
using System.Web.Mvc.Html;
namespace AutoCompleteHelper_MVC.Extensions
{
public enum AutoCompleteType
{
None,
Department,
Employee,
}
public static class AutoCompleteHelper
{
public static MvcHtmlString Autocomplete(this HtmlHelper helper, string name, string value, string text, string actionUrl, bool? isRequired = false, IDictionary<string, object> viewhtmlAttributes = null, string onselectfunction = "")
{
return GetAutocompleteString(helper, name, value, text, AutoCompleteType.None, actionUrl, isRequired, viewhtmlAttributes, onselectfunction: onselectfunction);
}
public static MvcHtmlString Autocomplete(this HtmlHelper helper, string name, string value, string text, AutoCompleteType autoCompleteType, bool? isRequired = false, IDictionary<string, object> viewhtmlAttributes = null,string onselectfunction="")
{
string actionUrl=string.Empty;
UrlHelper url = new UrlHelper(helper.ViewContext.RequestContext);
string acpath = url.Content("~/TestAutoComplete/");
if (autoCompleteType == AutoCompleteType.Department)
actionUrl = acpath+"GetDepartments";
else if (autoCompleteType == AutoCompleteType.Employee)
actionUrl = acpath + "GetEmployees";
return GetAutocompleteString(helper, name, value, text, autoCompleteType, actionUrl, isRequired: isRequired, viewhtmlAttributes: viewhtmlAttributes,onselectfunction: onselectfunction);
}
private static MvcHtmlString GetAutocompleteString(HtmlHelper helper, string name, string value, string text, AutoCompleteType autoCompleteType, string actionUrl = "", bool? isRequired = false, IDictionary<string, object> viewhtmlAttributes = null, string onselectfunction = "")
{
if(viewhtmlAttributes==null)
viewhtmlAttributes=new Dictionary<string, object>();
viewhtmlAttributes.Add("data-autocomplete", true);
viewhtmlAttributes.Add("data-autocompletetype", autoCompleteType.ToString().ToLower());
viewhtmlAttributes.Add("data-sourceurl", actionUrl);
viewhtmlAttributes.Add("data-valuetarget", name);
if (!string.IsNullOrEmpty(onselectfunction))
{
viewhtmlAttributes.Add("data-electfunction", onselectfunction);
}
if (isRequired.HasValue && isRequired.Value)
{
viewhtmlAttributes.Add("data-val", "true");
viewhtmlAttributes.Add("data-val-required", name + " is required");
}
var hidden = helper.Hidden(name, value);
var textBox = helper.TextBox(name + "_AutoComplete", text, viewhtmlAttributes);
var builder = new StringBuilder();
builder.AppendLine(hidden.ToHtmlString());
builder.AppendLine(textBox.ToHtmlString());
return new MvcHtmlString(builder.ToString());
}
public static MvcHtmlString AutocompleteFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, string DisplayProperty, string actionUrl, bool? isRequired = false, IDictionary<string, object> viewhtmlAttributes = null, string onselectfunction = "")
{
return GetAutocompleteForString(helper, expression, DisplayProperty, AutoCompleteType.None, actionUrl, isRequired, viewhtmlAttributes, onselectfunction: onselectfunction);
}
public static MvcHtmlString AutocompleteFor<TModel, TValue>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, string DisplayProperty, AutoCompleteType autoCompleteType, bool? isRequired = false, IDictionary<string, object> viewhtmlAttributes = null, string onselectfunction = "")
{
string actionUrl = string.Empty;
UrlHelper url = new UrlHelper(helper.ViewContext.RequestContext);
string acpath = url.Content("~/TestAutoComplete/");
if (autoCompleteType == AutoCompleteType.Department)
actionUrl = acpath + "GetDepartments";
else if (autoCompleteType == AutoCompleteType.Employee)
actionUrl = acpath + "GetEmployees";
return GetAutocompleteForString(helper, expression, DisplayProperty, autoCompleteType, actionUrl, isRequired: isRequired, viewhtmlAttributes: viewhtmlAttributes,onselectfunction: onselectfunction);
}
private static MvcHtmlString GetAutocompleteForString<TModel, TValue>(HtmlHelper<TModel> helper, Expression<Func<TModel, TValue>> expression, string DisplayText, AutoCompleteType autoCompleteType, string actionUrl = "", bool? isRequired = false, IDictionary<string, object> viewhtmlAttributes = null, string onselectfunction = "")
{
if(viewhtmlAttributes==null)
viewhtmlAttributes=new Dictionary<string, object>();
viewhtmlAttributes.Add("data-autocomplete", true );
viewhtmlAttributes.Add("data-autocompletetype", autoCompleteType.ToString().ToLower() );
viewhtmlAttributes.Add("data-sourceurl", actionUrl );
if (!string.IsNullOrEmpty(onselectfunction))
{
viewhtmlAttributes.Add("data-electfunction", onselectfunction);
}
Func<TModel, TValue> method = expression.Compile();
object value = null;
if (helper.ViewData.Model != null)
value = method((TModel)helper.ViewData.Model);
string modelpropname = ((MemberExpression)expression.Body).ToString();
modelpropname = modelpropname.Substring(modelpropname.IndexOf('.') + 1);
viewhtmlAttributes.Add("data-valuetarget", modelpropname);
if (isRequired.HasValue && isRequired.Value)
{ viewhtmlAttributes.Add("data-val", "true");
viewhtmlAttributes.Add("data-val-required", modelpropname + " is required");
}
MvcHtmlString hidden = helper.HiddenFor(expression);
MvcHtmlString textBox = helper.TextBox(modelpropname+"_AutoComplete", DisplayText, viewhtmlAttributes);
var builder = new StringBuilder();
builder.AppendLine(hidden.ToHtmlString());
builder.AppendLine(textBox.ToHtmlString());
return new MvcHtmlString(builder.ToString());
}
}
}
Models/Department.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace AutoCompleteHelper_MVC.Models
{
public class Department
{
public int DepartmentID { get; set; }
public string DepartmentName { get; set; }
public static List<Department> TotalDepartments
{
get
{
return new List<Department>{
new Department{DepartmentID=1,DepartmentName="Accounts"},
new Department{DepartmentID=2,DepartmentName="Advertisement"},
new Department{DepartmentID=3,DepartmentName="Sales"},
new Department{DepartmentID=4,DepartmentName="Shipment"},
new Department{DepartmentID=5,DepartmentName="Production"},
new Department{DepartmentID=6,DepartmentName="Marketing"}
};
}
}
public static List<Department> GetDepartmentsLikeName(string namestring)
{
var x=TotalDepartments.Where<Department>(d=>d.DepartmentName.ToLower().StartsWith(namestring.ToLower()));
return x.ToList();
}
}
}
Models/Employee.cs
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web;
namespace AutoCompleteHelper_MVC.Models
{
public class Employee
{
public int EmployeeID { get; set; }
[Required]
public string EmployeeName { get; set; }
public static List<Employee> TotalEmployees
{
get
{
return new List<Employee>{
new Employee{EmployeeID=1,EmployeeName="Addison",DepartmentID=1},
new Employee{EmployeeID=2,EmployeeName="Ashwin",DepartmentID=1},
new Employee{EmployeeID=3,EmployeeName="Alden",DepartmentID=1},
new Employee{EmployeeID=4,EmployeeName="Anthony",DepartmentID=6},
new Employee{EmployeeID=5,EmployeeName="Bailee",DepartmentID=5},
new Employee{EmployeeID=6,EmployeeName="Baileigh",DepartmentID=4},
new Employee{EmployeeID=7,EmployeeName="Banjamin",DepartmentID=2},
new Employee{EmployeeID=8,EmployeeName="Cadan",DepartmentID=3},
new Employee{EmployeeID=9,EmployeeName="Cadimhe",DepartmentID=2},
new Employee{EmployeeID=10,EmployeeName="Carissa",DepartmentID=1},
new Employee{EmployeeID=11,EmployeeName="Ceara",DepartmentID=2},
new Employee{EmployeeID=12,EmployeeName="Cecilia",DepartmentID=1}
};
}
}
public int DepartmentID { get; set; }
public static List<Employee> GetEmployeesLikeName(string namestring)
{
var x = TotalEmployees.Where<Employee>(d => d.EmployeeName.ToLower().StartsWith(namestring.ToLower()));
return x.ToList();
}
}
}
Controllers/ TestAutoCompleteController.cs
using AutoCompleteHelper_MVC.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace AutoCompleteHelper_MVC.Controllers
{
public class TestAutoCompleteController : Controller
{
public ActionResult Index()
{
var empObj = new Employee { EmployeeID = 1, EmployeeName = "Michale" };
return View(empObj);
}
public ActionResult GetEmployees(string searchHint)
{
return Json(Employee.GetEmployeesLikeName(searchHint),JsonRequestBehavior.AllowGet);
}
public ActionResult GetDepartments(string searchHint)
{
return Json(Department.GetDepartmentsLikeName(searchHint), JsonRequestBehavior.AllowGet);
}
public ActionResult GetGeneralItems(string searchHint)
{
return Json(Employee.GetEmployeesLikeName(searchHint), JsonRequestBehavior.AllowGet);
}
}
}
Views/TestAutoComplete/Index.cshtml
This view contains four controls to test all available options
-
First one is auto complete with data source url
-
Second is auto complete for employee type
-
Third is auto complete for Department type
-
Fourth is to load the data in to auto complete with employee id and name
@using AutoCompleteHelper_MVC.Extensions
@using AutoCompleteHelper_MVC.Models
@model AutoCompleteHelper_MVC.Models.Employee
@{
ViewBag.Title = "Index";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<form>
<div class="form-horizontal">
<h4>AutoComplete</h4>
<hr />
@Html.ValidationSummary(true)
<div class="form-group">
@Html.Label("With Source URL:", new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(m => m.EmployeeName)
@Html.ValidationMessageFor(m => m.EmployeeName)
</div>
</div>
<div class="form-group">
@Html.Label("With Source URL:", new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.Autocomplete("Empgeneral", "", "", actionUrl: "/TestAutoComplete/GetEmployees", isRequired: true, onselectfunction: "onEmpSelection")
@Html.ValidationMessage("Empgeneral_AutoComplete")
</div>
</div>
<div class="form-group">
@Html.Label("With Employee type:", new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.Autocomplete("Emp", "", "", AutoCompleteType.Employee)
</div>
</div>
<div class="form-group">
@Html.Label("With Department type:", new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.Autocomplete("Dept", "", "", AutoCompleteType.Department)
</div>
</div>
<div class="form-group">
@Html.Label("Binding to view model:", new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.AutocompleteFor(m => m.EmployeeID, @Model.EmployeeName, AutoCompleteType.Employee, isRequired: true)
@Html.ValidationMessage("EmployeeID_AutoComplete")
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="submit" class="btn btn-default" />
</div>
</div>
</div>
</form>
@section scripts{
<script>
function onEmpSelection(event, ui)
{
alert( }
</script>
}
Scripts/CustomAutoComplete.js
$(document).ready(function () {
BindAutoComplete();
});
function BindAutoComplete() {
$('[data-autocomplete]').each(function (index, element) {
var sourceurl = $(element).attr('data-sourceurl');
var autocompletetype = $(element).attr('data-autocompletetype');
$(element).autocomplete({
source: function (request, response) {
$.ajax({
url: sourceurl,
dataType: "json",
data: { searchHint: request.term },
success: function (data) {
response($.map(data, function (item) {
if (autocompletetype == 'none') {
return {
label: item.EmployeeName,
value: item.EmployeeName,
selectedValue: item.EmployeeID
};
}
else if (autocompletetype == 'department') {
return {
label: item.DepartmentName,
value: item.DepartmentName,
selectedValue: item.DepartmentID
}; }
else if (autocompletetype == 'employee') {
return {
label: item.EmployeeName,
value: item.EmployeeName,
selectedValue: item.EmployeeID
}; }
}));
},
error: function (data) {
alert(data);
},
});
},
select: function (event, ui) {
var valuetarget = $(this).attr('data-valuetarget');
$("input:hidden[name='" + valuetarget + "']").val(ui.item.selectedValue);
var selectfunc = $(this).attr('data-electfunction');
if (selectfunc != null && selectfunc.length > 0) {
window[selectfunc](event, ui);
}
},
change: function (event, ui) {
var valuetarget = $(this).attr('data-valuetarget');
$("input:hidden[name='" + valuetarget + "']").val('');
},
});
});
}
Points of Interest
Interested in Microsoft .Net Technologies, specially in asp.net and sharing my Ideas, Articles and tips
Tested screens
Accessing the selected item detail through control specific onselect callback function
Validation on submission
Fiddler is showing the submited values from four autocompletes