In the previous tutorial, we have talked about how to create jQuery Autocomplete in .Net Core MVC project. In this post, we will try to do it with AngularJS in regular .Net Framework MVC project.
Note: Code can be downloaded at my Github.
1. How to Create AngularJS Autocomplete in .Net Framework MVC
Note: This demo is using AngularJS integrated with .Net Framework MVC project, not Angular 2.0 or higher. I will walk through each step to create this demo project.
Step 1: Create an ASP.Net Web Application (.Net Framework)
Open Visual Studio 2019, search for ASP.Net Web Application (.Net Framework) and create on Next
Name the project AutoCompleteAngularJSDemo, select .Net Framework 4.5.2 and click on Create:
Select MVC and then click on Create
Below is the project structure after creating successfully:
Step 2: Add AngularJS to .Net Framework MVC project
Download below files from my Github:
Under Scripts folder, create 2 sub-folders: app and vendors. Move all JavaScript files under Scripts folder into vendors folder. In app folder, create sub-folders and files like below:
In app.js: This file is to initialize the AngularJS app in .Net Framework MVC project
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | ( function () { 'use strict' ; angular.module( 'AutoCompleteAngularJSDemo' , [ 'common.core' , 'common.ui' ]) .config(config) .run(run); config.$inject = [ '$locationProvider' , '$httpProvider' ]; function config($locationProvider, $httpProvider) { $locationProvider.html5Mode( true ); //initialize get if not there if (!$httpProvider.defaults.headers.get) { $httpProvider.defaults.headers.get = {}; } // Answer edited to include suggestions from comments // because previous version of code introduced browser-related errors //disable IE ajax request caching $httpProvider.defaults.headers.get[ 'If-Modified-Since' ] = 'Mon, 26 Jul 1997 05:00:00 GMT' ; // extra $httpProvider.defaults.headers.get[ 'Cache-Control' ] = 'no-cache' ; $httpProvider.defaults.headers.get[ 'Pragma' ] = 'no-cache' ; } run.$inject = [ '$rootScope' , '$location' , '$http' , '$route' ]; function run($rootScope, $location, $http, $route) { //handle page refreshes } })(); |
In common.core.js: This file is to inject all core library into the AngularJS app
1 2 3 4 5 6 | ( function () { 'use strict' ; angular.module( 'common.core' , [ 'ngRoute' ]); })(); |
In common.ui.js: This file is to inject all ui library into the AngularJS app
1 2 3 4 5 | ( function () { 'use strict' ; angular.module( 'common.ui' , [ 'ui.bootstrap' ]); })(); |
In rootCtrl.js: This file is the root controller of the project, will be injected in _Layout.cshtml page
1 2 3 4 5 6 7 8 9 10 11 12 | ( function (app) { 'use strict' ; app.controller( 'rootCtrl' , rootCtrl); rootCtrl.$inject = [ '$scope' ]; function rootCtrl($scope) { } })(angular.module( 'AutoCompleteAngularJSDemo' )); |
In homeCtrl.js: This file is the controller of the Home/Index.cshtml page
1 2 3 4 5 6 7 8 9 10 11 12 | ( function (app) { 'use strict' ; app.controller( 'homeCtrl' , homeCtrl); homeCtrl.$inject = [ '$scope' ]; function homeCtrl($scope) { } })(angular.module( 'AutoCompleteAngularJSDemo' )); |
In apiService.js: This file is a service to call (get/post) API methods
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | ( function (app) { 'use strict' ; app.factory( 'apiService' , apiService); apiService.$inject = [ '$http' , '$rootScope' ]; function apiService($http, $rootScope) { var service = { get: get, post: post }; function get(url, config, success, failure) { return $http.get(url, config) .then( function (result) { success(result); }, function (error) { failure(error); }); } function post(url, data, success, failure) { return $http.post(url, data) .then( function (result) { success(result); }, function (error) { failure(error); }); } return service; } })(angular.module( 'common.core' )); |
In inputDropdown.directive.js: This file is the directive for the Autocomplete part
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | ( function (app) { 'use strict' ; app.directive( 'inputDropdown' , inputDropdown); inputDropdown.$inject = [ 'apiService' ]; function inputDropdown(apiService) { return { restrict: 'E' , scope: { inputName: '@' , inputPlaceholder: '@' , customModel: '=ngModel' , selectedItem: '=selectedItem' , remoteUrl: '@' , required: '@' }, templateUrl: '/scripts/app/shared/inputDropdown.html' , link: function ($scope, $element, $attrs) { $scope.dropdownVisible = false ; $scope.inputChange = function () { var value = btoa(unescape(encodeURIComponent($scope.customModel))); apiService.get($scope.remoteUrl + '/' + value, null , categoryLoadCompleted, categoryLoadFailed); }; $scope.inputFocus = function () { var value = btoa(unescape(encodeURIComponent($scope.customModel))); apiService.get($scope.remoteUrl + '/' + value, null , categoryLoadCompleted, categoryLoadFailed); }; function categoryLoadCompleted(result) { $scope.dropdownItems = result.data; if (result.data.length > 0) showDropdown(); else hideDropdown(); } function categoryLoadFailed(result) { hideDropdown(); } $scope.inputBlur = function (event) { if ($scope.pressedDropdown) { // Blur event is triggered before click event, which means a click on a dropdown item wont be triggered if we hide the dropdown list here. $scope.pressedDropdown = false ; return ; } hideDropdown(); }; $scope.dropdownPressed = function () { $scope.pressedDropdown = true ; } var showDropdown = function () { $scope.dropdownVisible = true ; }; var hideDropdown = function () { $scope.dropdownVisible = false ; } $scope.selectItem = function (item) { $scope.selectedItem = item; $scope.customModel = item.Name; hideDropdown(); $scope.dropdownItems = [item]; }; } } } })(angular.module( 'common.ui' )); |
In inputDropdown.html: This file is the template for the Autocomplete
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | < div class = "input-dropdown" style = "width:100%;" > < input type = "text" class = "form-control" name = "{{inputName}}" id = "{{inputName}}" placeholder = "{{inputPlaceholder}}" ng-model = "customModel" ng-change = "inputChange()" ng-focus = "inputFocus()" ng-blur = "inputBlur($event)" validate-on = "blur" ng-required = "{{required}}" /> < ul ng-show = "dropdownVisible" > < li ng-repeat = "item in dropdownItems" ng-click = "selectItem(item)" ng-mouseenter = "setActive($index)" ng-mousedown = "dropdownPressed()" ng-class = "{'active': activeItemIndex === $index}" > < span class = "name" >{{item.Name}}</ span > < br /> < span class = "sub-name" >{{item.Sub}}</ span > </ li > </ ul > </ div > |
The Scripts folder will look like below after finishing all steps above:
Once you are done with the Scripts folder, we need to include all newly created JavaScript files in the Bundle. Update App_Start/BundleConfig.cs as below:
In App_Start/BunderConfig.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | using System.Web; using System.Web.Optimization; namespace AutoCompleteAngularJSDemo { public class BundleConfig { // For more information on bundling, visit https://go.microsoft.com/fwlink/?LinkId=301862 public static void RegisterBundles(BundleCollection bundles) { bundles.Add( new ScriptBundle( "~/bundles/jquery" ).Include( "~/Scripts/vendors/jquery-{version}.js" )); bundles.Add( new ScriptBundle( "~/bundles/jqueryval" ).Include( "~/Scripts/vendors/jquery.validate*" )); // Use the development version of Modernizr to develop with and learn from. Then, when you're // ready for production, use the build tool at https://modernizr.com to pick only the tests you need. bundles.Add( new ScriptBundle( "~/bundles/modernizr" ).Include( "~/Scripts/vendors/modernizr-*" )); bundles.Add( new ScriptBundle( "~/bundles/bootstrap" ).Include( "~/Scripts/vendors/bootstrap.js" )); bundles.Add( new ScriptBundle( "~/bundles/angularjs" ).Include( "~/Scripts/vendors/angular.js" , "~/Scripts/vendors/angular-route.js" , "~/Scripts/vendors/ui-bootstrap-tpls-3.0.6.js" )); bundles.Add( new ScriptBundle( "~/bundles/app" ).Include( "~/Scripts/app/modules/common.core.js" , "~/Scripts/app/modules/common.ui.js" , "~/Scripts/app/app.js" , "~/Scripts/app/controllers/rootCtrl.js" , "~/Scripts/app/controllers/home/homeCtrl.js" , "~/Scripts/app/services/apiService.js" , "~/Scripts/app/shared/inputDropdown.directive.js" )); bundles.Add( new StyleBundle( "~/Content/css" ).Include( "~/Content/bootstrap.css" , "~/Content/site.css" )); } } } |
Now, we need to initialize AngularJS app in html and specify the controller for each page.
In _Layout.cshtml: This is to initialize AngularJS app and specify rootCtrl for the shared layout
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | <!DOCTYPE html> <html ng-app= "AutoCompleteAngularJSDemo" > <head> < base href= "/" /> <meta charset= "utf-8" /> <meta name= "viewport" content= "width=device-width, initial-scale=1.0" > <title>@ViewBag.Title - My ASP.NET Application</title> @Styles.Render( "~/Content/css" ) @Scripts.Render( "~/bundles/modernizr" ) </head> <body ng-controller= "rootCtrl" > <div class = "navbar navbar-inverse navbar-fixed-top" > <div class = "container" > <div class = "navbar-header" > <button type= "button" class = "navbar-toggle" data-toggle= "collapse" data-target= ".navbar-collapse" > <span class = "icon-bar" ></span> <span class = "icon-bar" ></span> <span class = "icon-bar" ></span> </button> @Html.ActionLink( "Application name" , "Index" , "Home" , new { area = "" }, new { @ class = "navbar-brand" }) </div> <div class = "navbar-collapse collapse" > <ul class = "nav navbar-nav" > <li>@Html.ActionLink( "Home" , "Index" , "Home" )</li> <li>@Html.ActionLink( "About" , "About" , "Home" )</li> <li>@Html.ActionLink( "Contact" , "Contact" , "Home" )</li> </ul> </div> </div> </div> <div class = "container body-content" > @RenderBody() <hr /> <footer> <p>© @DateTime.Now.Year - My ASP.NET Application</p> </footer> </div> @Scripts.Render( "~/bundles/jquery" ) @Scripts.Render( "~/bundles/angularjs" ) @Scripts.Render( "~/bundles/app" ) @Scripts.Render( "~/bundles/bootstrap" ) @RenderSection( "scripts" , required: false ) </body> </html> |
In Home/Index.cshtml: specify homeCtrl for this page as well as add AutoComplete field:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | @{ ViewBag.Title = "Home Page" ; } <div ng-controller= "homeCtrl" > <div style= "padding-top:50px; padding-bottom: 50px;" > <div class = "row" > <div class = "col-lg-3" ></div> <div class = "col-lg-6" > <h3>Autocomplete with .Net 4.5 MVC and AngularJS</h3> <div class = "form-group" > <label for = "inputUser" >Users</label> <input-dropdown input-name= "inputUser" ng-model-options= "{ allowInvalid: true}" input-placeholder= "" required= "false" ng-model= "user.Name" selected-item= "user" remote-url= "/api/AutoComplete/SearchUsersByFilter" > </input-dropdown> </div> <div class = "form-group" > Selected User: {{user}} </div> </div> <div class = "col-lg-3" ></div> </div> </div> </div> |
Step 3: Add Web API to .Net Framework MVC project
This API feature is to help getting data for the AutoComplete field while user is typing.
In Controller folder, create a subfolder called WebAPI, create an ApiController inside this folder and call it AutoCompleteController. The folder will look like below:
In AutoCompleteController.cs: this Controller has SearchUsersByFilter to get data for the autocomplete field while user is typing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | using AutoCompleteAngularJSDemo.Models; using System; using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Web.Http; namespace AutoCompleteAngularJSDemo.Controllers.WebAPI { [RoutePrefix("api/AutoComplete")] public class AutoCompleteController : ApiController { [AllowAnonymous] [HttpGet] [Route("SearchUsersByFilter/{filter?}")] public HttpResponseMessage SearchUsersByFilter(string filter = null) { HttpResponseMessage response = null; if (!string.IsNullOrEmpty(filter)) { byte[] data = Convert.FromBase64String(filter); filter = Encoding.UTF8.GetString(data); } List< SearchItemVM > searchItems = SearchItemsByFilter(filter); HttpRequestMessage request = new HttpRequestMessage(); var configuration = new HttpConfiguration(); request.Properties[System.Web.Http.Hosting.HttpPropertyKeys.HttpConfigurationKey] = configuration; response = request.CreateResponse(HttpStatusCode.OK, searchItems); return response; } private List< SearchItemVM > SearchItemsByFilter(string filter) { List< SearchItemVM > users = new List< SearchItemVM >(); users.Add(new SearchItemVM() { ID = 1, Name = "Lucas Nguyen", Sub = "[email protected]", Type = "User" }); users.Add(new SearchItemVM() { ID = 2, Name = "Eric Lu", Sub = "[email protected]", Type = "User" }); users.Add(new SearchItemVM() { ID = 3, Name = "Rachel Horseman", Sub = "[email protected]", Type = "User" }); return users.Where(u => u.Name.ToLower().Contains(filter.ToLower())).ToList(); } } } |
Create a SearchItemVM in Models folder to hold the data for AutoComplete and pass it to front-end.
In Models/SearchItemVM
1 2 3 4 5 6 7 8 9 10 | namespace AutoCompleteAngularJSDemo.Models { public class SearchItemVM { public int ID { get ; set ; } public string Name { get ; set ; } public string Sub { get ; set ; } public string Type { get ; set ; } } } |
Update App_Start/WebApiConfig.cs for the API routing as below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | using System.Web.Http; namespace AutoCompleteAngularJSDemo { public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi" , routeTemplate: "api/{controller}/{action}/{id}" , defaults: new { id = RouteParameter.Optional } ); } } } |
We need to invoke WebApiConfig in Global.asax also:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Http; using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; namespace AutoCompleteAngularJSDemo { public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); WebApiConfig.Register(GlobalConfiguration.Configuration); GlobalConfiguration.Configuration.EnsureInitialized(); } } } |
Done, the AutoComplete is implemented and it will show suggestions while user is typing. You can run the project and test the feature.
2. Conclusion
Many applications are using Autocomplete fields to enhance better User Experience. I hope this tutorial help you in your current or future projects. Please let me know your thoughts in the comment section below. See you next time!