Thursday 4 June 2015

JavaScript AngularJS Google Maps Geocoding API Bootstrap Location Manager UI + C# WebApi EntityFramework Backend

UPDATE 06/06/2015: The code for locations directive and location service was a bit buggy so I updated it to be correct, everything else should be fine now.

What we are going to build:



Please review the links from the video, if you have covered other posts on this blog you will know that I'm using grunt, bower, requirejs stack. Below is the source code of the widget:

https://developer.mozilla.org/en-US/d...
https://angularjs.org/
http://angular-ui.github.io/angular-g...
https://developers.google.com/maps/do...
https://angular-ui.github.io/bootstrap/
http://getbootstrap.com/

1. application constants


 define(['factories/factories', 'angular'], function (factories, angular) {  
   'use strict';  
   factories.factory('ApplicationConstants', ['$window', '$log', function ApplicationConstants($window, $log) {  
     return {  
       apiUrl: 'YOUR API URL',  
       googleApiKey: 'YOUR API KEY'
     };  
   }]);  
 });  

2. utils


 define(['factories/factories', 'angular'], function (factories, angular) {  
   'use strict';  
   factories.factory('Utils', [ function Utils() {  
     return {  
       guid: function () {  
         function s4() {  
           return Math.floor((1 + Math.random()) * 0x10000)  
            .toString(16)  
            .substring(1);  
         }  
         return s4() + s4() + '-' + s4() + '-' + s4() + '-' +  
          s4() + '-' + s4() + s4() + s4();  
       }  
     };  
   }]);  
 });  

3. location service


 define(['services/services'], function (services) {  
   'use strict';  
   services.factory('LocationService', ['$http', 'ApplicationConstants', '$q', '$log', '$window', '$timeout',  
     function ($http, ApplicationConstants, $q, $log, $window, $timeout) {  
       return {  
         geocodeAddress: function (address) {  
           var deffered = $q.defer();  
           var geocoder = new $window.google.maps.Geocoder();  
           var request = { 'address': address };  
           //google limits to ~250ms request frequency  
           $timeout(function () {  
             geocoder.geocode(request, function (results, status) {  
               $log.debug('results', results);  
               $log.debug('status', status);  
               if (status == 'OK') {  
                 deffered.resolve(results);  
               } else {  
                 deffered.reject(status);  
               }  
             });  
           }, 300);  
           return deffered.promise;  
         },  
         getCountries: function () {  
           return $http({  
             method: 'GET',  
             url: ApplicationConstants.apiUrl + 'api/Countries',  
             headers: { 'Content-Type': 'application/json' },  
             params: {}  
           });  
         },  
         getCities: function (countryId) {  
           return $http({  
             method: 'GET',  
             url: ApplicationConstants.apiUrl + 'api/Cities/Country',  
             headers: { 'Content-Type': 'application/json' },  
             params: { 'countryId': countryId }  
           });  
         }  
       };  
     }]);  
 });  

Please note that to use here the geocoder correctly you have to initialize the google api script as angular-google-maps directives recommend to do in their documentation of getting started.

4. locations directive


 define(['directives/directives', 'lodash', 'angular'], function (directives, _, angular) {  
   'use strict';  
   directives.directive('locations',  
     ['$log', 'ApplicationConstants', '$timeout', 'Utils', 'uiGmapGoogleMapApi', 'LocationService', 'blockUI', '$window', '$compile',  
     function ($log, ApplicationConstants, $timeout, Utils, uiGmapGoogleMapApi, LocationService, blockUI, $window, $compile) {  
       return {  
         restrict: 'E',  
         templateUrl: "/public/js/directives/templates/locations.html",  
         scope: {  
           locations: '='  
         },  
         link: function (scope, element, attr) {  
           scope.map = undefined;  
           scope.countries = undefined;  
           scope.cities = undefined;  
           scope.selection = {  
             selectedCountryId: null,  
             selectedCityId: null,  
             addressLine: undefined,  
             locationName: undefined,  
             locationDescription: undefined,  
             latitude: undefined,  
             longitude: undefined  
           };  
           scope.$watch('selection', function (newVal, oldVal) {  
             $log.debug('location selection', newVal);  
           }, true);  
           scope.sortOrder = 0;  
           scope.showAddressLine = false;  
           scope.showResolveBtn = false;  
           scope.showAddBtn = false;  
           scope.showLocationName = false;  
           scope.showCoordinates = false;  
           scope.showGeocoderError = false;  
           scope.showLocationDescription = false;  
           scope.geocoderMessage = '';  
           scope.isAdded = false;  
           scope.accordionStatus = {  
             isListingOpen: false,  
             isAddOpen: true,  
           };  
           scope.$watch('accordionStatus.isAddOpen', function (newVal, oldVal) {  
             if (newVal) {  
               $timeout(function () {  
                 $log.debug('add open')  
                 angular.element('#listingPanel').css('height', '55px').css('overflow', 'auto');  
                 scope.map = { center: { latitude: 33, longitude: -33 }, zoom: 2 };  
                 scope.marker.options.labelContent = '';  
                 scope.marker.options.visible = true;  
                 scope.marker.options.labelVisible = false,  
                 scope.marker = scope.getNewMarker();  
                 for (var i = 0; i < scope.locations.length; ++i) {  
                   scope.locations[i].markerOptions.visible = false;  
                   scope.locations[i].markerWindowOptions.visible = false;  
                 }  
                 scope.showMarker = true;  
               });  
             }  
           });  
           scope.$watch('accordionStatus.isListingOpen', function (newVal, oldVal) {  
             if (newVal) {  
               $timeout(function () {  
                 $log.debug('listing open');  
                 scope.cities = null;  
                 scope.selection.locationName = '';  
                 scope.selection.locationDescription = '';  
                 scope.selection.addressLine = '';  
                 scope.selection.selectedCountryId = null;  
                 scope.selection.selectedCityId = null;  
                 scope.selection.latitude = undefined;  
                 scope.selection.longitude = undefined;  
                 scope.showAddForm = false;  
                 scope.showAddressLine = false;  
                 scope.showResolveBtn = false;  
                 scope.showAddBtn = false;  
                 scope.showLocationName = false;  
                 scope.showLocationDescription = false;  
                 scope.showCoordinates = false;  
                 scope.marker.options.visible = false;  
                 scope.showLocationDescription = false;  
                 scope.marker.options.labelContent = '';  
                 scope.markerWindowTitle = '';  
                 scope.marker.options.labelVisible = false;  
                 angular.element('#listingPanel').css('height', '660px').css('overflow', 'scroll');  
                 for (var i = 0; i < scope.locations.length; ++i) {  
                   scope.locations[i].markerOptions.visible = true;  
                   scope.locations[i].markerOptions.labelVisible = true;  
                   scope.locations[i].markerWindowOptions.visible = true;  
                 }  
                 if (!scope.mapControl) {  
                   scope.mapControl = {};  
                 }  
                 $log.debug('locations:', scope.locations);  
                 scope.centerMapToMarkers();  
               });  
             } else {  
               $timeout(function () {  
                 $log.debug('listing close');  
                 angular.element('#listingPanel').css('height', '60px').css('overflow', 'auto');  
               });  
             }  
           });  
           scope.currentZoom = 2;  
           scope.zoomOut = function () {  
             scope.mapControl.getGMap().setZoom(scope.mapControl.getGMap().getZoom() - 1);  
           }  
           scope.centerMapToMarkers = function () {  
             $timeout(function () {  
               $log.debug('center to markers');  
               var map = scope.mapControl.getGMap();  
               var markerControls = _.keys(scope.mapControl);  
               $log.debug('map', map);  
               $log.debug('marker controls', markerControls);  
               var markers = [];  
               $log.debug('map ctrl:', scope.mapControl);  
               for (var i = 0; i < markerControls.length; ++i) {  
                 $log.debug(i, markerControls[i]);  
                 $log.debug(i, scope.mapControl[markerControls[i]]['getGMarkers']);  
                 if (markerControls[i] !== 'current' && markerControls[i] !== 'refresh' && !angular.isFunction(scope.mapControl[markerControls[i]])) {  
                   markers.push(scope.mapControl[markerControls[i]]['getGMarkers']()[0]);  
                 }  
               }  
               $log.debug('markers', markers)  
               map.setZoom(16);  
               var bounds = new $window.google.maps.LatLngBounds();  
               $log.debug(bounds);  
               for (var i = 0; i < markers.length; ++i) {  
                 bounds.extend(markers[i].getPosition());  
                 markers[i].setAnimation(-1);  
               }  
               map.setCenter(bounds.getCenter());  
               map.fitBounds(bounds);  
               map.setZoom(map.getZoom() - 1);  
             });  
           };  
           scope.mapControl = { 'current': {} };  
           scope.getNewMarker = function () {  
             scope.markerId = Utils.guid();  
             return {  
               id: scope.markerId,  
               coordinates: {  
                 latitude: 33,  
                 longitude: -33  
               },  
               options: {  
                 animation: 1,  
                 draggable: true,  
                 icon: '/public/js/images/doctor_icon_new.png',  
                 visible: true,  
                 labelContent: '',  
                 labelAnchor: "33 0",  
                 labelClass: "marker-labels",  
                 labelVisible: false,  
               },  
               events: {  
                 position_changed: function (marker, eventName, model, args) {  
                   $timeout(function () {  
                     $log.debug('positionChanged');  
                     $log.debug(marker.getPosition());  
                     scope.selection.latitude = $window.Math.round(marker.getPosition().lat() * 100000) / 100000;  
                     scope.selection.longitude = $window.Math.round(marker.getPosition().lng() * 100000) / 100000;  
                     $log.debug(marker);  
                     $log.debug(eventName),  
                     $log.debug(model);  
                     $log.debug(args);  
                     marker.setAnimation(1);  
                   });  
                 },  
                 dragend: function (marker, eventName, model, args) {  
                   $timeout(function () {  
                     marker.setAnimation(1);  
                     $log.debug('marker dragend');  
                     $log.debug(eventName);  
                     $log.debug(model);  
                     $log.debug(args);  
                     var latitude = $window.Math.round(marker.getPosition().lat() * 100000) / 100000;  
                     var longitude = $window.Math.round(marker.getPosition().lng() * 100000) / 100000;  
                     $log.debug(latitude);  
                     $log.debug(longitude);  
                     scope.selection.latitude = latitude;  
                     scope.selection.longitude = longitude;  
                     scope.marker.options = {  
                       icon: '/public/js/images/doctor_icon_new.png',  
                       draggable: true,  
                       labelContent: scope.selection.locationName,  
                       labelAnchor: "33 0",  
                       labelClass: "marker-labels",  
                       visible: true  
                     };  
                   });  
                 }  
               }  
             };  
           };  
           scope.markerId = undefined;  
           scope.showMarker = false;  
           scope.marker = scope.getNewMarker();  
           scope.markerWindowOptions = {  
             visible: true  
           };  
           scope.onMarkerClick = function () {  
             scope.markerWindowOptions.visible = !scope.markerWindowOptions.visible;  
           };  
           scope.closeMarkerWindow = function (locationId) {  
             if (!locationId) {  
               scope.markerWindowOptions.visible = false;  
             } else {  
               scope.locations[_.findIndex(scope.locations, function (location) { return location.id === locationId })].markerWindowOptions.visible = false;  
             }  
           };  
           scope.markerWindowTitle = '';  
           scope.$watchCollection('locations', function (newVal, oldVal) {  
             $timeout(function () {  
               $log.debug('locations', newVal);  
               if (newVal && newVal.length > 0 && scope.isAdded) {  
                 scope.accordionStatus.isListingOpen = true;  
               } else {  
                 scope.accordionStatus.isListingOpen = false;  
               }  
             });  
           });  
           scope.resolve = function () {  
             $timeout(function () {  
               var address = scope.countries[_.findIndex(scope.countries, function (country) { return country.id === scope.selection.selectedCountryId; })].name  
                 + " " +  
                 scope.cities[_.findIndex(scope.cities, function (city) { return city.id === scope.selection.selectedCityId })].name  
                 + " " +  
                 scope.selection.addressLine;  
               $log.debug('address', address);  
               blockUI.start('Resolving location ...')  
               LocationService.geocodeAddress(address).then(function (geocodeResult) {  
                 $log.debug('geocoder data', geocodeResult);  
                 scope.marker.options.visible = true;  
                 scope.showGeocoderError = false;  
                 scope.geocoderMessage = '';  
                 scope.showLocationName = true;  
                 scope.showLocationDescription = true;  
                 scope.showMarker = true;  
                 scope.map.center.latitude = scope.selection.latitude = scope.marker.coordinates.latitude = $window.Math.round(geocodeResult[0].geometry.location.k * 100000) / 100000;  
                 scope.map.center.longitude = scope.selection.longitude = scope.marker.coordinates.longitude = $window.Math.round(geocodeResult[0].geometry.location.D * 100000) / 100000;  
                 scope.map.zoom = 16;  
                 blockUI.stop();  
               }, function (status) {  
                 switch (status) {  
                   case 'ZERO_RESULTS':  
                     scope.showGeocoderError = true;  
                     scope.geocoderMessage = 'search returned zero results, please try again different search parameters.';  
                     break;  
                   case 'OVER_QUERY_LIMIT':  
                     scope.showGeocoderError = true;  
                     scope.geocoderMessage = 'search query limit reached, please try again later...';  
                     break;  
                   case 'REQUEST_DENIED':  
                     scope.showGeocoderError = true;  
                     scope.geocoderMessage = 'search was denied, please try again different search parameters.';  
                     break;  
                 }  
                 blockUI.stop();  
                 $log.debug('geocoder error', error);  
               }  
               );  
             }, 300);  
           };  
           scope.selectLocation = function (locationId) {  
             $timeout(function () {  
               $log.debug('mapControl', scope.mapControl);  
               var markerControls = _.keys(scope.mapControl);  
               var markers = [];  
               for (var i = 0; i < markerControls.length; ++i) {  
                 $log.debug(i, markerControls[i]);  
                 if (markerControls[i] !== 'current' && markerControls[i] !== 'refresh' && !angular.isFunction(scope.mapControl[markerControls[i]])) {  
                   markers.push(scope.mapControl[markerControls[i]]['getGMarkers']()[0]);  
                 }  
               }  
               var location = scope.locations[_.findIndex(scope.locations, function (loc) { return loc.id === locationId })];  
               $log.debug('markers', markers);  
               $log.debug('location', location);  
               var indexToSelect = -1;  
               var currentMarkerPosition = null  
               for (var i = 0; i < markers.length; ++i) {  
                 markers[i].setAnimation(-1);  
                 markers[i].setIcon('/public/js/images/doctor_icon_added.png');  
                 currentMarkerPosition = markers[i].getPosition();  
                 $log.debug('marker position', currentMarkerPosition);  
                 $log.debug('location position', location.coordinates);  
                 $log.debug('test1', $window.Math.round(currentMarkerPosition.lng() * 100000) / 100000, location.coordinates.longitude)  
                 $log.debug('test2', $window.Math.round(currentMarkerPosition.lat() * 100000) / 100000, location.coordinates.latitude)  
                 if ($window.Math.round(currentMarkerPosition.lng() * 100000) / 100000 === location.coordinates.longitude  
                   &&  
                   $window.Math.round(currentMarkerPosition.lat() * 100000) / 100000 === location.coordinates.latitude  
                   ) {  
                   indexToSelect = i;  
                 }  
               }  
               $log.debug(indexToSelect);  
               markers[indexToSelect].setAnimation(1);  
               markers[indexToSelect].setIcon('/public/js/images/doctor_icon_new.png');  
               scope.map = { center: { latitude: markers[indexToSelect].getPosition().lat(), longitude: markers[indexToSelect].getPosition().lng() }, zoom: scope.mapControl.getGMap().getZoom() + 1 };  
             });  
           };  
           scope.deleteLocation = function (locationId) {  
             if (confirm('Are you sure that you want to delete this location ?')) {  
               _.remove(scope.locations, function (location) { return location.id === locationId });  
               scope.centerMapToMarkers();  
             }  
           };  
           scope.add = function () {  
             $timeout(function () {  
               if (!scope.mapControl) {  
                 scope.mapControl = {};  
               }  
               scope.mapControl[scope.markerId] = {}  
               scope.isAdded = true;  
               $log.debug('new location', location);  
               if (scope.locations.length === undefined) {  
                 scope.locations = [];  
               }  
               scope.locations.push({  
                 id: scope.markerId,  
                 countryId: scope.countries[_.findIndex(scope.countries, function (country) { return country.id === scope.selection.selectedCountryId })].id,  
                 cityId: scope.cities[_.findIndex(scope.cities, function (city) { return city.id === scope.selection.selectedCityId })].id,  
                 address: scope.selection.addressLine,  
                 name: scope.selection.locationName,  
                 description: scope.selection.locationDescription,  
                 coordinates: {  
                   latitude: _.clone(scope.selection.latitude),  
                   longitude: _.clone(scope.selection.longitude)  
                 },  
                 markerOptions: {  
                   animation: -1,  
                   draggable: false,  
                   icon: '/public/js/images/doctor_icon_added.png',  
                   visible: true,  
                   labelContent: scope.selection.locationName,  
                   labelAnchor: "33 0",  
                   labelClass: "marker-labels",  
                   labelVisible: true,  
                 },  
                 markerWindowOptions: {  
                   visible: true  
                 },  
                 sortOrder: scope.sortOrder++  
               });  
               scope.accordionStatus.isAddOpen = false;  
               scope.accordionStatus.isListingOpen = true;  
             });  
           };  
           scope.locationDescriptionChange = function () {  
             $timeout(function () {  
               scope.markerWindowTitle = '';  
               if (scope.selection.locationDescription !== '') {  
                 scope.markerWindowTitle = scope.selection.locationDescription;  
                 scope.markerWindowOptions.visible = true;  
               } else {  
                 scope.showAddBtn = false;  
               }  
               if (scope.selection.locationName !== '' && scope.selection.locationDescription !== '') {  
                 scope.showAddBtn = true;  
               }  
             });  
           };  
           scope.locationNameChange = function () {  
             $timeout(function () {  
               scope.marker.options.labelContent = '';  
               scope.marker.options.labelVisible = false;  
               $log.debug(scope.selection.locationName);  
               if (scope.selection.locationName !== '') {  
                 scope.marker.options.labelVisible = true;  
                 scope.marker.options.labelContent = scope.selection.locationName;  
               } else {  
                 scope.showAddBtn = false;  
                 scope.marker.options.labelContent = '';  
                 scope.marker.options.labelVisible = false;  
               }  
               if (scope.selection.locationName !== '' && scope.selection.locationDescription !== '') {  
                 scope.showAddBtn = true;  
               }  
             });  
           };  
           scope.addressLineChange = function () {  
             $timeout(function () {  
               if (scope.selection.addressLine !== '') {  
                 scope.showResolveBtn = true;  
                 scope.showCoordinates = true;  
                 scope.showGeocoderError = false;  
                 scope.geocoderMessage = '';  
               } else {  
                 scope.selection.locationName = '';  
                 scope.selection.locationDescription = '';  
                 scope.selection.latitude = '';  
                 scope.selection.longitude = '';  
                 scope.showResolveBtn = false;  
                 scope.showCoordinates = false;  
                 scope.showAddBtn = false;  
                 scope.showLocationName = false;  
                 scope.showLocationDescription = false;  
                 scope.showGeocoderError = false;  
                 scope.geocoderMessage = '';  
                 scope.showMarker = false;  
               }  
             });  
           };  
           scope.cityChange = function () {  
             $timeout(function () {  
               if (scope.selection.selectedCityId) {  
                 scope.showAddressLine = true;  
                 scope.showResolveBtn = false;  
                 scope.selection.addressLine = undefined;  
                 scope.showGeocoderError = false;  
                 scope.geocoderMessage = '';  
               } else {  
                 scope.selection.locationName = '';  
                 scope.selection.locationDescription = '';  
                 scope.selection.addressLine = '';  
                 scope.selection.latitude = '';  
                 scope.selection.longitude = '';  
                 scope.showAddForm = false;  
                 scope.showAddressLine = false;  
                 scope.showResolveBtn = false;  
                 scope.showCoordinates = false;  
                 scope.showAddBtn = false;  
                 scope.showLocationName = false;  
                 scope.showLocationDescription = false;  
                 scope.showGeocoderError = false;  
                 scope.geocoderMessage = '';  
                 scope.showMarker = false;  
               }  
             });  
           };  
           scope.countryChange = function () {  
             $timeout(function () {  
               if (scope.selection.selectedCountryId) {  
                 blockUI.start('Loading Cities For ' + scope.countries[_.findIndex(scope.countries, function (country) { return country.id === scope.selection.selectedCountryId; })].name + ' ...');  
                 LocationService.getCities(scope.selection.selectedCountryId).then(function (citiesData) {  
                   $timeout(function () {  
                     $log.debug(citiesData.data);  
                     scope.cities = citiesData.data;  
                     scope.showGeocoderError = false;  
                     scope.geocoderMessage = '';  
                     blockUI.stop();  
                   });  
                 }, function (error) {  
                   blockUI.stop();  
                   $log.debug(error);  
                 })  
               } else {  
                 scope.showGeocoderError = false;  
                 scope.geocoderMessage = '';  
                 scope.showLocationDescription = false;  
                 scope.selection.locationName = '';  
                 scope.selection.locationDescription = '';  
                 scope.selection.addressLine = '';  
                 scope.selection.latitude = '';  
                 scope.selection.longitude = '';  
                 scope.selection.selectedCityId = null;  
                 scope.showAddForm = false;  
                 scope.showAddressLine = false;  
                 scope.showResolveBtn = false;  
                 scope.showCoordinates = false;  
                 scope.showAddBtn = false;  
                 scope.showLocationName = false;  
                 scope.showMarker = false;  
               }  
             });  
           };  
           uiGmapGoogleMapApi.then(function (maps) {  
             $log.debug('maps ready', maps);  
             $timeout(function () {  
               scope.map = { center: { latitude: 33, longitude: -33 }, zoom: 2 };  
               //scope.mapControl['current'] = {};  
               blockUI.start('Loading Countries ...');  
               LocationService.getCountries().then(function (countriesData) {  
                 $log.debug('Countries', countriesData.data);  
                 $timeout(function () {  
                   scope.countries = countriesData.data;  
                   //scope.mapControl = {};  
                   scope.sortOrder = 0;  
                   if (scope.locations && scope.locations.length > 0) {  
                     for (var i = 0; i < scope.locations.length; ++i) {  
                       $log.debug('marker options', scope.locations[i].markerOptions);  
                       scope.locations[i].markerOptions.visible = false;  
                       scope.locations[i].markerOptions.labelVisible = false;  
                       scope.locations[i].markerWindowOptions.visible = false;  
                       scope.mapControl[scope.locations[i].id] = {};  
                       ++scope.sortOrder;  
                       $compile(angular.element('#' + scope.locations[i].id).contents())(scope);  
                     }  
                   }  
                   scope.marker = scope.getNewMarker();  
                   scope.isAdded = false;  
                   scope.accordionStatus.isAddOpen = true;  
                   scope.accordionStatus.isListingOpen = false;  
                 });  
                 blockUI.stop();  
               }, function (error) {  
                 blockUI.stop();  
                 $log.debug(error);  
               });  
             });  
           });  
         }  
       };  
     }]);  
 });  

5. location directive template


 <div class="container-fluid" id="locationsWrapper">  
   <div class="col-md-8">  
     <ui-gmap-google-map center='map.center' zoom='map.zoom' control="mapControl">  
       <ui-gmap-marker coords="marker.coordinates" options="marker.options" events="marker.events" idkey="marker.id" control="mapControl['current']">  
         <ui-gmap-window options="markerWindowOptions" closeclick="closeMarkerWindow()" show="showLocationDescription">  
           <div>{{markerWindowTitle}}</div>  
         </ui-gmap-window>  
       </ui-gmap-marker>  
       <ui-gmap-marker ng-repeat="location in locations" coords="location.coordinates" options="location.markerOptions" idkey="location.id" control="mapControl[location.id]">  
         <ui-gmap-window options="location.markerWindowOptions" closeclick="closeMarkerWindow(location.id)" show="location.markerWindowOptions.visible">  
           <div>{{location.description}}</div>  
         </ui-gmap-window>  
       </ui-gmap-marker>  
     </ui-gmap-google-map>  
   </div>  
   <div class="col-md-4">  
     <accordion close-others="true">  
       <accordion-group is-open="accordionStatus.isAddOpen">  
         <accordion-heading>  
           <div class="noselect"><span class="glyphicon glyphicon-plus" style="margin-right:5px;"></span><span>Add Location</span><i class="pull-right glyphicon" ng-class="{'glyphicon-chevron-down': accordionStatus.isAddOpen, 'glyphicon-chevron-right': !accordionStatus.isAddOpen}"></i></div>  
         </accordion-heading>  
         <div id="addLocationFormWrapper" class="container-fluid">  
           <ng-form name="addLocationForm">  
             <div class="form-group">  
               <label class="text-left" for="country" name="lblCountry" id="lblCountry">Country</label>  
               <select name="country" id="country" class="form-control text-left" ng-model="selection.selectedCountryId" required ng-options="country.id as country.name for country in countries" ng-disabled="countries == null" ng-change="countryChange()" tooltip="Select country" tooltip-placement="bottom" tooltip-class="default">  
                 <option value="">--Select--</option>  
               </select>  
             </div>  
             <div class="form-group">  
               <label class="text-left" for="city" name="lblCity" id="lblCity">City</label>  
               <select name="city" id="city" class="form-control text-left" ng-model="selection.selectedCityId" required ng-options="city.id as city.name for city in cities" ng-disabled="selection.selectedCountryId == null" ng-change="cityChange()" tooltip="Select city" tooltip-placement="bottom" tooltip-class="default">  
                 <option value="">--Select--</option>  
               </select>  
             </div>  
             <div class="form-group" ng-show="showAddressLine">  
               <label name="lblAddressLine" id="lblAddressLine" class="control-label text-left" for="addressLine">Street, Building's Number and Postcode</label>  
               <textarea name="addressLine" class="form-control text-left" id="addressLine" ng-model="selection.addressLine" ng-change="addressLineChange()" tooltip="Provide address" tooltip-placement="bottom" tooltip-class="default"></textarea>  
             </div>  
             <div class="form-group" ng-show="showResolveBtn">  
               <div class="alert alert-danger alert-dismissible alert-dismissible" role="alert" ng-show="showGeocoderError"><span class="glyphicon glyphicon-info-sign"></span>&nbsp;Address couldn't be resolved due to:&nbsp;{{geocoderMessage}}</div>  
               <button name="btnLocalizeOnMap" id="btnLocalizeOnMap" type="button" class="btn btn-primary text-left" ng-click="resolve()" tooltip="Click to localize on the map" tooltip-placement="bottom" tooltip-class="default">Resolve</button>  
             </div>  
             <div class="form-group" ng-show="showCoordinates">  
               <div class="col-md-12"><span class="help-text">Please drag the marker to change the coordinates.</span></div>  
               <div class="col-md-12">  
                 <label class="text-left" for="latitude" name="lblLatitude" id="lblLatitude">Latitude</label>  
                 <input class="form-control input-sm" name="latitude" id="latitude" ng-model="selection.latitude" ng-disabled="true" style="width:80px" tooltip="Latitude" tooltip-placement="bottom" tooltip-class="default" />  
                 <label class="text-left" for="longitude" name="lblLongitude" id="lblLongitude">Longitude</label>  
                 <input class="form-control input-sm" name="longitude" id="longitude" ng-model="selection.longitude" ng-disabled="true" style="width:80px;" tooltip="Longitude" tooltip-placement="bottom" tooltip-class="default" />  
               </div>  
             </div>  
             <div class="form-group" ng-show="showLocationName" style="margin-top: 10px;">  
               <label name="lblLocationName" id="lblLocationName" class="control-label text-left" for="locationName">Location's Name</label>  
               <textarea name="locationName" class="form-control text-left" id="locationName" ng-model="selection.locationName" ng-change="locationNameChange()" tooltip="Your name of the location" tooltip-placement="bottom" tooltip-class="default"></textarea>  
             </div>  
             <div class="form-group" ng-show="showLocationName">  
               <label name="lblLocationDescription" id="lblLocationDescription" class="control-label text-left" for="locationDescription">Location's Description</label>  
               <textarea name="locationDescription" class="form-control text-left" id="locationDescription" ng-model="selection.locationDescription" ng-change="locationDescriptionChange()" tooltip="Your description of location" tooltip-placement="bottom" tooltip-class="default"></textarea>  
             </div>  
             <div class="form-group" ng-show="showAddBtn">  
               <button name="btnAddLocation" id="btnAddLocation" type="button" class="btn btn-success text-left" ng-click="add()" tooltip="Add location to current locations" tooltip-placement="bottom" tooltip-class="default"><span class="glyphicon glyphicon-plus"></span>&nbsp;Add</button>  
             </div>  
           </ng-form>  
         </div>  
       </accordion-group>  
       <accordion-group is-open="accordionStatus.isListingOpen" style="height:55px;overflow:auto;" id="listingPanel">  
         <accordion-heading>  
           <div class="noselect" ng-click="alert('showall')"><span class="glyphicon glyphicon-globe" style="margin-right:5px;"></span><span>Current Locations</span><i class="pull-right glyphicon" ng-class="{'glyphicon-chevron-down': accordionStatus.isListingOpen, 'glyphicon-chevron-right': !accordionStatus.isListingOpen}"></i></div>  
         </accordion-heading>  
         <div class="container-fluid">  
           <div ng-repeat="location in locations">  
             <div class="panel panel-default">  
               <div class="panel-heading">  
                 <div class="row">  
                   <div class="col-md-12">  
                     <span class="glyphicon glyphicon-globe"></span>&nbsp;{{location.name}}  
                   </div>  
                 </div>  
               </div>  
               <div class="panel-body">  
                 <div class="row">  
                   <div class="col-md-12">{{location.description}}</div>  
                 </div>  
                 <div class="row">&nbsp;</div>  
                 <div class="row">  
                   <div class="col-md-7">  
                     <div class="btn-group-vertical" role="group">  
                       <button type="button" class="btn-primary btn-sm" ng-click="selectLocation(location.id)" tooltip="Zoom in on location" tooltip-placement="bottom" tooltip-class="default"><span class="glyphicon glyphicon-search"></span>&nbsp;<span class="glyphicon glyphicon-plus-sign"></span></button>  
                       <button type="button" class="btn-primary btn-sm" ng-click="zoomOut()" tooltip="Zoom out on location" tooltip-placement="bottom" tooltip-class="default"><span class="glyphicon glyphicon-search"></span>&nbsp;<span class="glyphicon glyphicon-minus-sign"></span></button>  
                     </div>  
                   </div>  
                   <div class="col-md-5">  
                     <button type="button" class="btn-danger btn-sm" ng-click="deleteLocation(location.id)" tooltip="delete location" tooltip-placement="bottom" tooltip-class="default"><span class="glyphicon glyphicon-remove"></span></button>  
                   </div>  
                 </div>  
               </div>  
               <div class="panel-footer">  
                 <div class="row">  
                   <div class="col-md-12"><small>{{location.address}}</small></div>  
                 </div>  
                 <div class="row">  
                   <div class="col-md-12"><small>{{location.city.name}}&nbsp;{{location.country.name}}</small></div>  
                 </div>  
               </div>  
             </div>  
             <br />  
           </div>  
         </div>  
       </accordion-group>  
     </accordion>  
   </div>  
 </div>  

6. C# WebAPI Controllers


 using System;  
 using System.Collections.Generic;  
 using System.Data;  
 using System.Data.Entity;  
 using System.Data.Entity.Infrastructure;  
 using System.Linq;  
 using System.Net;  
 using System.Net.Http;  
 using System.Threading.Tasks;  
 using System.Web.Http;  
 using System.Web.Http.Description;  
 using API.DomainModel;  
   
 namespace API.Controllers  
 {  
   [RoutePrefix("api/Coutries")]  
   public class CountriesController : ApiController  
   {  
     private Entities db = new Entities();  
   
     [HttpGet]  
     [AllowAnonymous]  
     [ResponseType(typeof(IQueryable<Country>))]  
     public async Task<IHttpActionResult> GetCountries()  
     {  
       return Ok(await db.Countries.Select(c => new { c.Id, c.Name} ).ToListAsync());  
     }  
   
     // GET: api/Countries/5  
     [HttpGet]  
     [Authorize(Roles = "admin")]  
     [ResponseType(typeof(Country))]  
     public async Task<IHttpActionResult> GetCountry(Guid id)  
     {  
       Country country = await db.Countries.FindAsync(id);  
       if (country == null)  
       {  
         return NotFound();  
       }  
   
       return Ok(country);  
     }  
   
     // PUT: api/Countries/5  
     [HttpPut]  
     [Authorize(Roles = "admin")]  
     [ResponseType(typeof(void))]  
     public async Task<IHttpActionResult> PutCountry(Guid id, Country country)  
     {  
       if (!ModelState.IsValid)  
       {  
         return BadRequest(ModelState);  
       }  
   
       if (id != country.Id)  
       {  
         return BadRequest();  
       }  
   
       db.Entry(country).State = EntityState.Modified;  
   
       try  
       {  
         await db.SaveChangesAsync();  
       }  
       catch (DbUpdateConcurrencyException)  
       {  
         if (!CountryExists(id))  
         {  
           return NotFound();  
         }  
         else  
         {  
           throw;  
         }  
       }  
   
       return StatusCode(HttpStatusCode.NoContent);  
     }  
   
     // POST: api/Countries  
     [HttpPost]  
     [Authorize(Roles = "admin")]  
     [ResponseType(typeof(Country))]  
     public async Task<IHttpActionResult> PostCountry(Country country)  
     {  
       if (!ModelState.IsValid)  
       {  
         return BadRequest(ModelState);  
       }  
   
       db.Countries.Add(country);  
   
       try  
       {  
         await db.SaveChangesAsync();  
       }  
       catch (DbUpdateException)  
       {  
         if (CountryExists(country.Id))  
         {  
           return Conflict();  
         }  
         else  
         {  
           throw;  
         }  
       }  
   
       return CreatedAtRoute("DefaultApi", new { id = country.Id }, country);  
     }  
   
     // DELETE: api/Countries/5  
     [HttpDelete]  
     [Authorize(Roles = "admin")]  
     [ResponseType(typeof(Country))]  
     public async Task<IHttpActionResult> DeleteCountry(Guid id)  
     {  
       Country country = await db.Countries.FindAsync(id);  
       if (country == null)  
       {  
         return NotFound();  
       }  
   
       db.Countries.Remove(country);  
       await db.SaveChangesAsync();  
   
       return Ok(country);  
     }  
   
     protected override void Dispose(bool disposing)  
     {  
       if (disposing)  
       {  
         db.Dispose();  
       }  
       base.Dispose(disposing);  
     }  
   
     private bool CountryExists(Guid id)  
     {  
       return db.Countries.Count(e => e.Id == id) > 0;  
     }  
   }  
 }  
 using System;  
 using System.Collections.Generic;  
 using System.Data;  
 using System.Data.Entity;  
 using System.Data.Entity.Infrastructure;  
 using System.Linq;  
 using System.Net;  
 using System.Net.Http;  
 using System.Threading.Tasks;  
 using System.Web.Http;  
 using System.Web.Http.Description;  
 using API.DomainModel;  
   
 namespace API.Controllers  
 {  
   [RoutePrefix("api/Cities")]  
   public class CitiesController : ApiController  
   {  
     private Entities db = new Entities();  
   
     //GET: api/Cities/Country/1  
     [HttpGet]  
     [Route("Country")]  
     [AllowAnonymous]  
     [ResponseType(typeof(IQueryable<City>))]  
     public async Task<IHttpActionResult> GetCitiesByCountry(Guid countryId)  
     {  
       return Ok(await db.Cities.Where(c => c.CountryId == countryId).Select(city => new { city.Id, city.CountryId, city.Name }).ToListAsync());  
     }  
   
     // GET: api/Cities  
     [HttpGet]  
     [Authorize(Roles="admin")]  
     [ResponseType(typeof(IQueryable<City>))]  
     public async Task<IHttpActionResult> GetCities()  
     {  
       return Ok(await db.Cities.Select(city => new { city.Id, city.CountryId, city.Name}).ToListAsync());  
     }  
   
     // GET: api/Cities/5  
     [HttpGet]  
     [Authorize(Roles="admin")]  
     [ResponseType(typeof(City))]  
     public async Task<IHttpActionResult> GetCity(Guid id)  
     {  
       City city = await db.Cities.FindAsync(id);  
       if (city == null)  
       {  
         return NotFound();  
       }  
   
       return Ok(city);  
     }  
   
     // PUT: api/Cities/5  
     [HttpPut]  
     [Authorize(Roles = "admin")]  
     [ResponseType(typeof(void))]  
     public async Task<IHttpActionResult> PutCity(Guid id, City city)  
     {  
       if (!ModelState.IsValid)  
       {  
         return BadRequest(ModelState);  
       }  
   
       if (id != city.Id)  
       {  
         return BadRequest();  
       }  
   
       db.Entry(city).State = EntityState.Modified;  
   
       try  
       {  
         await db.SaveChangesAsync();  
       }  
       catch (DbUpdateConcurrencyException)  
       {  
         if (!CityExists(id))  
         {  
           return NotFound();  
         }  
         else  
         {  
           throw;  
         }  
       }  
   
       return StatusCode(HttpStatusCode.NoContent);  
     }  
   
     // POST: api/Cities  
     [HttpPost]  
     [Authorize(Roles = "admin")]  
     [ResponseType(typeof(City))]  
     public async Task<IHttpActionResult> PostCity(City city)  
     {  
       if (!ModelState.IsValid)  
       {  
         return BadRequest(ModelState);  
       }  
   
       db.Cities.Add(city);  
   
       try  
       {  
         await db.SaveChangesAsync();  
       }  
       catch (DbUpdateException)  
       {  
         if (CityExists(city.Id))  
         {  
           return Conflict();  
         }  
         else  
         {  
           throw;  
         }  
       }  
   
       return CreatedAtRoute("DefaultApi", new { id = city.Id }, city);  
     }  
   
     // DELETE: api/Cities/5  
     [HttpDelete]  
     [Authorize(Roles = "admin")]  
     [ResponseType(typeof(City))]  
     public async Task<IHttpActionResult> DeleteCity(Guid id)  
     {  
       City city = await db.Cities.FindAsync(id);  
       if (city == null)  
       {  
         return NotFound();  
       }  
   
       db.Cities.Remove(city);  
       await db.SaveChangesAsync();  
   
       return Ok(city);  
     }  
   
     protected override void Dispose(bool disposing)  
     {  
       if (disposing)  
       {  
         db.Dispose();  
       }  
       base.Dispose(disposing);  
     }  
   
     private bool CityExists(Guid id)  
     {  
       return db.Cities.Count(e => e.Id == id) > 0;  
     }  
   }  
 }  

7. C# Entity Framework


 namespace API.DomainModel  
 {  
   using System;  
   using System.Data.Entity;  
   using System.Data.Entity.Infrastructure;  
     
   public partial class Entities : DbContext  
   {  
     public Entities()  
       : base("name=Entities")  
     {  
     }  
     
     protected override void OnModelCreating(DbModelBuilder modelBuilder)  
     {  
       throw new UnintentionalCodeFirstException();  
     }  
     
     public virtual DbSet<City> Cities { get; set; }  
     public virtual DbSet<Country> Countries { get; set; }  
   }  
 }  
 namespace API.DomainModel  
 {  
   using System;  
   using System.Collections.Generic;  
     
   public partial class City  
   {  
     public City()  
     {  
     }  
     
     public System.Guid Id { get; set; }  
     public Nullable<System.Guid> CountryId { get; set; }  
     public string Name { get; set; }  
     
     public virtual Country Country { get; set; }  
   }  
 }  
   
 namespace API.DomainModel  
 {  
   using System;  
   using System.Collections.Generic;  
     
   public partial class Country  
   {  
     public Country()  
     {  
       this.Cities = new HashSet<City>();   
     }  
     
     public System.Guid Id { get; set; }  
     public string Name { get; set; }  
     
     public virtual ICollection<City> Cities { get; set; }  
   }  
 }  
   

7. C# WebApi Config Related
 using API.DomainModel;  
 using API.Identity;  
 using API.Providers;  
 using Microsoft.Owin;  
 using Owin;  
 using System;  
 using System.Collections.Generic;  
 using System.Data.Entity;  
 using System.Linq;  
 using System.Web;  
 using System.Web.Http;  
   
 [assembly: OwinStartup(typeof(API.Startup))]  
   
 namespace API  
 {  
   public class Startup  
   {  
     public void Configuration(IAppBuilder app)  
     {  
            HttpConfiguration config = new HttpConfiguration();
            app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
            config.Formatters.Add(new BrowserJsonFormatter());
            WebApiConfig.Register(config);
            app.UseWebApi(config);
     }  
   }  
 }  
 using Newtonsoft.Json;  
 using System;  
 using System.Collections.Generic;  
 using System.Linq;  
 using System.Net.Http.Formatting;  
 using System.Net.Http.Headers;  
 using System.Web;  
   
 namespace API  
 {  
   public class BrowserJsonFormatter : JsonMediaTypeFormatter  
   {  
     public BrowserJsonFormatter()  
     {  
       this.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html"));  
       this.SerializerSettings.Formatting = Formatting.Indented;  
     }  
   
     public override void SetDefaultContentHeaders(Type type, HttpContentHeaders headers, MediaTypeHeaderValue mediaType)  
     {  
       base.SetDefaultContentHeaders(type, headers, mediaType);  
       headers.ContentType = new MediaTypeHeaderValue("application/json");  
     }  
   }  
 }  

8. Database SQL


 CREATE TABLE [dbo].[Countries](  
     [Id] [uniqueidentifier] NOT NULL CONSTRAINT [DF_Countries_Id] DEFAULT (newid()),  
     [Name] [nvarchar](50) NOT NULL,  
  CONSTRAINT [PK_Countries] PRIMARY KEY CLUSTERED   
 (  
     [Id] ASC  
 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]  
 ) ON [PRIMARY]  
   
 CREATE TABLE [dbo].[Cities](  
     [Id] [uniqueidentifier] NOT NULL CONSTRAINT [DF_Cities_Id] DEFAULT (newid()),  
     [CountryId] [uniqueidentifier] NULL,  
     [Name] [nvarchar](255) NULL,  
  CONSTRAINT [PK_Cities] PRIMARY KEY CLUSTERED   
 (  
     [Id] ASC  
 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]  
 ) ON [PRIMARY]  
   
 ALTER TABLE [dbo].[Cities] WITH CHECK ADD CONSTRAINT [FK_Cities_Countries] FOREIGN KEY([CountryId])  
 REFERENCES [dbo].[Countries] ([Id])  
 ON UPDATE CASCADE  
 ON DELETE CASCADE  
   
 ALTER TABLE [dbo].[Cities] CHECK CONSTRAINT [FK_Cities_Countries]  
   

This is I believe all what is required (transfered minimal code from my project) besides entering the entries to the database. If any questions then feel free to comment. I hope someone will find this code somehow useful. Here is the usage of the directive somewhere in your system:

 <locations locations="myLocations"></locations>  

Wednesday 3 June 2015

Asp.NET Identity SQL schema script

So this time I'm working on a project involving EntityFramework and ASP.NET Identity membership system. It is ok once you use Code First strategy rather than Database First to EntityFramework to generate your schema for the ASP.Identity. I'm using Database First and had problems with generating the required schema on-fly so I decided to post here SQL ASP.NET Identity's schema creation script to be run on your database just in case.


 /****** DATABASE ******/  
 USE [YourDatabase]  
   
 SET ANSI_NULLS ON  
   
 SET QUOTED_IDENTIFIER ON  
   
 /****** USER ******/  
   
 CREATE TABLE [dbo].[AspNetUsers](  
     [Id] [nvarchar](128) NOT NULL,  
     [Email] [nvarchar](256) NULL,  
     [EmailConfirmed] [bit] NOT NULL,  
     [PasswordHash] [nvarchar](max) NULL,  
     [SecurityStamp] [nvarchar](max) NULL,  
     [PhoneNumber] [nvarchar](max) NULL,  
     [PhoneNumberConfirmed] [bit] NOT NULL,  
     [TwoFactorEnabled] [bit] NOT NULL,  
     [LockoutEndDateUtc] [datetime] NULL,  
     [LockoutEnabled] [bit] NOT NULL,  
     [AccessFailedCount] [int] NOT NULL,  
     [UserName] [nvarchar](256) NOT NULL,  
  CONSTRAINT [PK_dbo.AspNetUsers] PRIMARY KEY CLUSTERED   
 (  
     [Id] ASC  
 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]  
 ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]  
   
   
 CREATE TABLE [dbo].[AspNetRoles](  
     [Id] [nvarchar](128) NOT NULL,  
     [Name] [nvarchar](256) NOT NULL,  
  CONSTRAINT [PK_dbo.AspNetRoles] PRIMARY KEY CLUSTERED   
 (  
     [Id] ASC  
 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]  
 ) ON [PRIMARY]  
   
 /****** ROLE ******/  
   
 CREATE TABLE [dbo].[AspNetUserRoles](  
     [UserId] [nvarchar](128) NOT NULL,  
     [RoleId] [nvarchar](128) NOT NULL,  
  CONSTRAINT [PK_dbo.AspNetUserRoles] PRIMARY KEY CLUSTERED   
 (  
     [UserId] ASC,  
     [RoleId] ASC  
 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]  
 ) ON [PRIMARY]  
   
 /****** USER - ROLES ******/  
   
 ALTER TABLE [dbo].[AspNetUserRoles] WITH CHECK ADD CONSTRAINT [FK_dbo.AspNetUserRoles_dbo.AspNetRoles_RoleId] FOREIGN KEY([RoleId])  
 REFERENCES [dbo].[AspNetRoles] ([Id])  
 ON DELETE CASCADE  
   
 ALTER TABLE [dbo].[AspNetUserRoles] CHECK CONSTRAINT [FK_dbo.AspNetUserRoles_dbo.AspNetRoles_RoleId]  
   
 ALTER TABLE [dbo].[AspNetUserRoles] WITH CHECK ADD CONSTRAINT [FK_dbo.AspNetUserRoles_dbo.AspNetUsers_UserId] FOREIGN KEY([UserId])  
 REFERENCES [dbo].[AspNetUsers] ([Id])  
 ON DELETE CASCADE  
   
 ALTER TABLE [dbo].[AspNetUserRoles] CHECK CONSTRAINT [FK_dbo.AspNetUserRoles_dbo.AspNetUsers_UserId]  
   
 /****** USER - LOGINS ******/  
   
 CREATE TABLE [dbo].[AspNetUserLogins](  
     [LoginProvider] [nvarchar](128) NOT NULL,  
     [ProviderKey] [nvarchar](128) NOT NULL,  
     [UserId] [nvarchar](128) NOT NULL,  
  CONSTRAINT [PK_dbo.AspNetUserLogins] PRIMARY KEY CLUSTERED   
 (  
     [LoginProvider] ASC,  
     [ProviderKey] ASC,  
     [UserId] ASC  
 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]  
 ) ON [PRIMARY]  
   
 ALTER TABLE [dbo].[AspNetUserLogins] WITH CHECK ADD CONSTRAINT [FK_dbo.AspNetUserLogins_dbo.AspNetUsers_UserId] FOREIGN KEY([UserId])  
 REFERENCES [dbo].[AspNetUsers] ([Id])  
 ON DELETE CASCADE  
   
 ALTER TABLE [dbo].[AspNetUserLogins] CHECK CONSTRAINT [FK_dbo.AspNetUserLogins_dbo.AspNetUsers_UserId]  
   
 /****** USER - CLAIMS ******/  
   
 CREATE TABLE [dbo].[AspNetUserClaims](  
     [Id] [int] IDENTITY(1,1) NOT NULL,  
     [UserId] [nvarchar](128) NOT NULL,  
     [ClaimType] [nvarchar](max) NULL,  
     [ClaimValue] [nvarchar](max) NULL,  
  CONSTRAINT [PK_dbo.AspNetUserClaims] PRIMARY KEY CLUSTERED   
 (  
     [Id] ASC  
 )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]  
 ) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]  
   
 ALTER TABLE [dbo].[AspNetUserClaims] WITH CHECK ADD CONSTRAINT [FK_dbo.AspNetUserClaims_dbo.AspNetUsers_UserId] FOREIGN KEY([UserId])  
 REFERENCES [dbo].[AspNetUsers] ([Id])  
 ON DELETE CASCADE  
   
 ALTER TABLE [dbo].[AspNetUserClaims] CHECK CONSTRAINT [FK_dbo.AspNetUserClaims_dbo.AspNetUsers_UserId]  

Monday 25 May 2015

AngularJS - preloading whole project / displaying loading image during AngularJS's bootstrap

It is very quick post but useful. I'll demonstrate how to display loading image before and during AngularJS's first load of the project - we do not want to use ng-cloack directive in this case. The trick is simple - the main goal is to divide project into two div elements - one for loading image and the other for the whole rest of the project. The first one will be displayed initially but the second one will be hidden using CSS. Because there is no AngularJS initially and we want to access the first div with the loading image to hide it when the app loads the only option is to define a directive restricted as a class. Consider following code:

html-template

   <div class="jumbotron vertical-center app-loading">  
     <div class="container-fluid">  
       <div class="row text-center">  
         <img src="~/Public/release/images/preloader.gif">  
       </div>  
       <div class="row text-center">  
         Application is loading. Please wait...  
       </div>  
     </div>  
   </div>  
   <div class="container-fluid" id="appContainer" style="display:none;">  
     <div class="col-xs-12" ng-view></div>  
   </div>  

app-loading directive

 define(['directives/directives', 'angular'], function (directives, angular) {  
   'use strict';  
   directives.directive("appLoading", [function () {  
     return ({  
       link: link,  
       restrict: "C"  
     });  
     function link(scope, element, attributes) {  
       element.remove();  
       scope = element = attributes = null;  
       angular.element('#appContainer').show();  
     }  
   }]);  
 });  

This solves our problem. Once AngularJS fully bootstraps on our application the appLoading directive is being executed, preloader hidden and contents of the app from ng-view displayed (could be a controller or something else).

AngularJS - simple image switcher / cycler / swapper with preload

This time, taking previous post into consideration, I'll show basic web control build as a directive. It will consist of a single image (being preloaded) which will be switched to the next one from the list of all enclosed images to render. We will specify base delay for switching the images, list of images and some other stuff. The main dependency of this new directive is the preloaded-image directive from previous post. This control is to be used across the project since it is the directive. Here is the code:

image-cycler


 define(['directives/directives', 'angular', 'lodash'], function (directives, angular, _) {  
   'use strict';  
   directives.directive('imageCycler', ['$log', 'ApplicationConstants', '$timeout', '$interval', '$compile', '$window',  
     function ($log, ApplicationConstants, $timeout, $interval, $compile, $window) {  
       return {  
         restrict: 'E',  
         scope: {  
           containerId: '@',  
           images: '=',  
           baseDelay: '@',  
           external: '@',  
           placeholderText: '@',  
           placeholderWidth: '@',  
           placeholderHeight: '@',  
           tooltipText: '@',  
         },  
         templateUrl: '/public/js/directives/templates/imageCycler.html',  
         link: function (scope, element, attrs) {  
           scope.isImagePreloading = false;  
           var delay = parseInt(scope.baseDelay) + (($window.Math.random() * parseInt(scope.baseDelay)) | 0);  
           scope.$on(ApplicationConstants.events.imagePreloadStart, function (event, imageId) {  
             $timeout(function () {  
               if (_.has(scope.images, imageId)) {  
                 scope.isImagePreloading = true;  
               }  
             });  
           });  
           scope.$on(ApplicationConstants.events.imagePreloadEnd, function (event, imageId) {  
             $timeout(function () {  
               if (_.has(scope.images, imageId)) {  
                 scope.isImagePreloading = false;  
               }  
             });  
           });  
           scope.$watch('isImagePreloading', function (newValue, oldValue) {  
             $log.debug(newValue, oldValue);  
             if (newValue === true) {  
               $timeout(scope.updateImage, delay);  
             }  
           });  
           var currentImage = 0;  
           scope.updateImage = function () {  
             var baseUrl = '/public/'  
             switch (ApplicationConstants.env) {  
               case 'release':  
                 baseUrl += 'release/images/';  
                 break;  
               case 'dev':  
                 baseUrl += 'js/images/';  
                 break;  
             }  
             if (scope.external) {  
               baseUrl = '';  
             }  
             var imageIds = _.keys(scope.images);  
             if (scope.isImagePreloading) {  
               return;  
             }  
             if (currentImage === imageIds.length) {  
               currentImage = 0;  
             }  
             angular.element('#imageContainer-' + scope.containerId).empty();  
             var preloadedImage = '<preloaded-image image-id="' + imageIds[currentImage] + '" image-src="' + baseUrl + scope.images[imageIds[currentImage]] + '" placeholder-width="' + scope.placeholderWidth + '" placeholder-height="' + scope.placeholderHeight + '" placeholder-text="' + scope.placeholderText + '" tooltip="' + scope.tooltipText + '" tooltip-placement="bottom" tooltip-animation="false" tooltip-class="default"></preloaded-image>'  
             angular.element('#imageContainer-' + scope.containerId).append(preloadedImage)  
             $compile(angular.element('#imageContainer-' + scope.containerId).contents())(scope);  
             ++currentImage;  
           };  
           $timeout(function () {  
             scope.updateImage();  
           });  
         }  
       };  
     }]);  
 });  

As you can see in the code - it depends on the events broadcasted from preloaded-image directive from previous post and running $timeout with delay when image is preloaded / displayed. It inserts the new preloaded image directive's html to the current element and then compiles it allowing angular do it's work.

image-cycler-tpl


 <div id="imageContainer-{{containerId}}"></div>  

usage: controller


 define(['controllers/controllers'], function (controllers) {  
   'use strict';  
   controllers.controller('IndexCtrl', ['$scope', '$location', '$window', function ($scope, $location, $window) {  
     $scope.Math = $window.Math;  
     $scope.images = {  
       'img_1': 'img_1.gif',  
       'img_2': 'img_2.gif',  
       'img_3': 'img_3.gif',  
       'img_4': 'img_4.gif',  
       'img_5': 'img_5.gif',  
       'img_6': 'img_6.gif',  
       'img_7': 'img_7.gif'  
     };  
   }]);  
 });  

usage: view


 <div class="panel-body">  
     <image-cycler container-id="images-cycler" images="images" base-delay="10000" preloader-text="image" placeholder-width="300" placeholder-height="250" tooltip-text="This application helps you plan and carry out your dietary plans."></image-cycler>  
 </div>  


Wednesday 20 May 2015

Working AngularJS Giphy API integration + preloading GIF images directive


This post describes how to integrate Giphy's API with AngularJS framework. As an example I'll show implementation of a AngularJS's directive which will provide random Giphy's image specified by tags to the end user with preload.

From Giphy's API documentation one can determine requirements for the environment of the API. Following items are worth noticing:
  • apiUrl = http://api.giphy.com/v1/gifs/
  • apiKey = dc6zaTOxFJmzC -> public beta key for testing
  • apiRandomEndpoint = random?api_key=dc6zaTOxFJmzC&tag=tag1+tag2
  • notification about CORS requirement for making requests to the endpoints.
Please note that the apiUrl uses http and redirects to another addres also in http. Because we want to use the https we will configure the redirect URL from the original api URL to be via https. The plan is straightforward - the directive for displaying the random gif, provider for obtaining giphy's image details given the request, embedded as $resource using jsonp for CORS. In my setup I use grunt, bower, requirejs together with angular so the code included might be strange at first sight but easily transplantable.

ApplicationConstants config for the angular's app's module - nothing fancy here - just the config properties of the giphy's API and default search tags.

 define(['factories/factories', 'angular'], function (factories, angular) {  
   'use strict';  
   factories.factory('ApplicationConstants', ['$window', function ApplicationConstants($window) {  
     return {  
       'env': $window.location.href.indexOf('localhost') > -1 ? 'dev' : 'release',  
       'giphyDefaultRandomQuery': 'bicycle',  
       'giphyApiUrl': $window.location.href.indexOf('localhost') > -1 ? 'http://tv.giphy.com/v1/gifs/' : 'https://tv.giphy.com/v1/gifs/',  
       'giphyApiKey': 'dc6zaTOxFJmzC',  
           };  
   }]);  
 });  

The provider for the data of the giphy's image given the configuration and using jsonp for CORS. Notice callback=JSON_CALLBACK  added to the endpoint's URL and other configuration options for idividual objects. Our aim is to obtain $resource as the output to be manipulated later using angular's promises API in the directive.

 define(['providers/providers'], function (providers) {  
   'use strict';  
   providers.provider('giphyRandom', function giphyRandomProvider() {  
     this.$get = ['$resource', function ($resource) {  
       return {  
         query: '',  
         setQuery: function (query) {  
           this.query = query;  
         },  
         invoke: function (giphyApiUrl, giphyApiKey, giphyDefaultRandomQuery) {  
           var query = this.query;  
           var queryToRun;  
           if (!query) {  
             queryToRun = encodeURIComponent(giphyDefaultRandomQuery);  
           } else {  
             queryToRun = encodeURIComponent(query);  
           }  
           return $resource(  
             giphyApiUrl + 'random?api_key=' + giphyApiKey + '&tag=' + queryToRun + '&callback=JSON_CALLBACK',  
             {},  
             {  
               jsonpQuery: {  
                 method: 'JSONP',  
                 transformResponse: function (data, headers) {  
                   //Here is possible to restict the data with only required properties but we want whole giphy's model.  
                   //console.log(data);  
                   return data;  
                 },  
                 responseType: 'json',  
                 headers: { 'Content-Type': 'application/json' }  
               },  
             }  
           );  
         }  
       };  
     }];  
   });  
 });  

Next, we want to specify our directive for the giphy's image. It'll utilize mainly the provider and the application's constants to configure the provider. The data is read from the obtained $resource using angular's promises API. As you can see, I'm using lodash library here.

 define(['directives/directives', 'lodash'], function (directives, _) {  
   'use strict';  
   directives.directive('giphyRandomImage', ['$log', 'giphyRandom', 'ApplicationConstants', '$timeout', '$compile', function ($log, giphyRandomProvider, ApplicationConstants, $timeout, $compile) {  
     return {  
       restrict: 'E',  
       transclude: true,  
       templateUrl: "/public/js/directives/templates/giphyRandomImage.html",  
       scope: {  
         query: '@'  
       },  
       link: function (scope, element, attr) {  
         //giphy's model  
         scope.giphy = {};  
         if (scope.query) {  
           giphyRandomProvider.setQuery(scope.query);  
         } else {  
           giphyRandomProvider.setQuery(ApplicationConstants.giphyDefaultRandomQuery);  
         }  
         var giphyRandom = giphyRandomProvider.invoke(  
           ApplicationConstants.giphyApiUrl,  
           ApplicationConstants.giphyApiKey,  
           ApplicationConstants.giphyDefaultRandomQuery).jsonpQuery();  
         giphyRandom.$promise.then(function (giphyData) {  
           $timeout(function(){  
             _.assign(scope.giphy, { 'fixed_height_downsampled_height': giphyData.data.fixed_height_downsampled_height });  
             _.assign(scope.giphy, { 'fixed_height_downsampled_url': giphyData.data.fixed_height_downsampled_url });  
             _.assign(scope.giphy, { 'fixed_height_downsampled_width': giphyData.data.fixed_height_downsampled_width });  
             _.assign(scope.giphy, { 'fixed_height_small_height': giphyData.data.fixed_height_small_height });  
             _.assign(scope.giphy, { 'fixed_height_small_still_url': giphyData.data.fixed_height_small_still_url });  
             _.assign(scope.giphy, { 'fixed_height_small_url': giphyData.data.fixed_height_small_url });  
             _.assign(scope.giphy, { 'fixed_height_small_width': giphyData.data.fixed_height_small_width });  
             _.assign(scope.giphy, { 'fixed_width_downsampled_height': giphyData.data.fixed_width_downsampled_height });  
             _.assign(scope.giphy, { 'fixed_width_downsampled_url': giphyData.data.fixed_width_downsampled_url });  
             _.assign(scope.giphy, { 'fixed_width_downsampled_width': giphyData.data.fixed_width_downsampled_width });  
             _.assign(scope.giphy, { 'fixed_width_small_height': giphyData.data.fixed_width_small_height });  
             _.assign(scope.giphy, { 'fixed_width_small_still_url': giphyData.data.fixed_width_small_still_url });  
             _.assign(scope.giphy, { 'fixed_width_small_url': giphyData.data.fixed_width_small_url });  
             _.assign(scope.giphy, { 'fixed_width_small_width': giphyData.data.fixed_width_small_width });  
             _.assign(scope.giphy, { 'id': giphyData.data.id });  
             _.assign(scope.giphy, { 'image_frames': giphyData.data.image_frames });  
             _.assign(scope.giphy, { 'image_height': giphyData.data.image_height });  
             _.assign(scope.giphy, { 'image_mp4_url': giphyData.data.image_mp4_url });  
             _.assign(scope.giphy, { 'image_original_url': giphyData.data.image_original_url });  
             _.assign(scope.giphy, { 'image_url': giphyData.data.image_url });  
             _.assign(scope.giphy, { 'image_width': giphyData.data.image_width });  
             _.assign(scope.giphy, { 'type': giphyData.data.type });  
             _.assign(scope.giphy, { 'url': giphyData.data.url });  
             scope.$apply();  
             $compile(element.contents())(scope);  
           });  
         }).catch(function (error) {  
           $log.debug("Error getting Giphy Resource's Data", error);  
         });  
       }  
     };  
   }]);  
 });  

Now, having the directive it is required in our case to have the template for it (as you can see I'm using AngularUIBootstrap attributes here):

 <div>  
   <preloaded-image   
            image-id="{{giphy.id}}"   
            image-src="{{giphy.image_url}}"   
            placeholder-text="giphy"  
            placeholder-width="300"   
            placeholder-height="150"  
            tooltip="giphy.com"  
            tooltip-placement="bottom"  
            tooltip-animation="false"  
            tooltip-class="default">  
   </preloaded-image>  
 </div>  

As you probably have noticed, there are two new directives specified in the template, preloaded-image, Preloaded-image directive first:

 define(['directives/directives', 'angular'], function (directives, angular) {  
   'use strict';  
   directives.directive('preloadedImage', ['$log', '$document', '$timeout', '$window', 'ApplicationConstants', '$rootScope', '$compile',  
     function ($log, $document, $timeout, $window, ApplicationConstants, $rootScope, $compile) {  
       return {  
         restrict: 'E',  
         scope: {  
           imageId: '@',  
           imageSrc: '@',  
           placeholderWidth: '@',  
           placeholderHeight: '@',  
           placeholderText: '@',  
         },  
         templateUrl: '/public/js/directives/templates/preloadedImage.html',  
         link: function (scope, element, attrs) {  
           scope.imageMimeType = '';  
           scope.showPlaceholder = false;  
           scope.showLoadingImage = true;  
           scope.showProgressBar = false;  
           scope.progressBarValue = 0;  
           scope.resolveImageMimeType = function () {  
             var parts = [];  
             parts = scope.imageSrc.split('.');  
             if (parts.length === 0 && !(parts[parts.length - 1] === 'jpg' || parts[parts.length - 1] === 'png' || parts[parts.length - 1] === 'gif')) {  
               $log.debug('Image error', 'Wrong image url - not returning proper image of jpg, gif, png.');  
             } else {  
               switch (parts[parts.length - 1]) {  
                 case 'jpg':  
                   return 'image/jpeg';  
                 case 'gif':  
                   return 'image/gif';  
                 case 'png':  
                   return 'image/png';  
               }  
             }  
           };  
           scope.onLoadStart = function () {  
             scope.$apply(function () {  
               $rootScope.$broadcast(ApplicationConstants.events.imagePreloadStart, scope.imageId);  
               scope.showLoadingImage = false;  
               scope.showProgressBar = true;  
             });  
           };  
           scope.onProgress = function (e) {  
             scope.$apply(function () {  
               scope.progressBarValue = e.loaded / e.total * 100;  
             });  
           };  
           scope.showImage = function () {  
             var image = $document[0].createElement('img');  
             image.className = 'img-responsive img-rounded center-block';  
             image.src = 'data:' + scope.imageMimeType + ';base64,' + scope.xhrBase64DataEncode(xhr.responseText);  
             element.append(image);  
           };  
           scope.onLoadEnded = function () {  
             scope.$apply(function () {  
               $rootScope.$broadcast(ApplicationConstants.events.imagePreloadEnd, scope.imageId);  
               scope.showProgressBar = false;  
               scope.showImage();  
             });  
           };  
           scope.onError = function () {  
             angular.element('#placeholder-' + scope.imageId).attr('ng-src', 'http://placehold.it/' + scope.placeholderWidth + 'x' + scope.placeholderHeight + '&text=' + scope.placeholderText);  
             scope.$apply(function () {  
               $rootScope.$broadcast(ApplicationConstants.events.imagePreloadEnd, scope.imageId);  
               scope.isPreloading = false;  
               scope.showPlaceholder = true;  
             });  
           };  
           var xhr = new $window.XMLHttpRequest();  
           xhr.onloadstart = scope.onLoadStart;  
           xhr.onprogress = scope.onProgress;  
           xhr.onload = scope.onLoadEnded;  
           xhr.onerror = scope.onError;  
           scope.loadImage = function () {  
             $timeout(function () {  
               xhr.open("GET", scope.imageSrc, true);  
               xhr.overrideMimeType('text/plain; charset=x-user-defined');  
               xhr.send();  
             });  
           };  
           scope.xhrBase64DataEncode = function (inputStr) {  
             var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";  
             var outputStr = "";  
             var i = 0;  
             while (i < inputStr.length) {  
               //all three "& 0xff" added below are there to fix a known bug   
               //with bytes returned by xhr.responseText  
               var byte1 = inputStr.charCodeAt(i++) & 0xff;  
               var byte2 = inputStr.charCodeAt(i++) & 0xff;  
               var byte3 = inputStr.charCodeAt(i++) & 0xff;  
               var enc1 = byte1 >> 2;  
               var enc2 = ((byte1 & 3) << 4) | (byte2 >> 4);  
               var enc3, enc4;  
               if (isNaN(byte2)) {  
                 enc3 = enc4 = 64;  
               }  
               else {  
                 enc3 = ((byte2 & 15) << 2) | (byte3 >> 6);  
                 if (isNaN(byte3)) {  
                   enc4 = 64;  
                 }  
                 else {  
                   enc4 = byte3 & 63;  
                 }  
               }  
               outputStr += b64.charAt(enc1) + b64.charAt(enc2) + b64.charAt(enc3) + b64.charAt(enc4);  
             }  
             return outputStr;  
           };  
           $timeout(function () {  
             scope.imageMimeType = scope.resolveImageMimeType();  
             scope.loadImage();  
           });  
         }  
       };  
     }]);  
 });  

Now, the template for the preloader directive:

 <div id="{{imageId}}">  
   <loading-image id="loadingImage-{{imageId}}" ng-show="showLoadingImage"></loading-image>  
   <progressbar id="progressbar-{{imageId}}" ng-show="showProgressBar" value="progressBarValue" typeof="success">{{progressBarValue}}%</progressbar>  
   <img id="placeholder-{{imageId}}" ng-show="showPlaceholder" class="img-responsive img-rounded center-block" />  
 </div>  

Note: I won't enclose here the loading-image directive because it is just a simple AJAX animated loading image to be chosen from web.

Finally, here is the usage of this all somewhere else, like for example in HomeController's view of the angular's application using bootstrap's panel:

  <div class="panel panel-default">  
       <div class="panel-heading">  
         <h3 class="panel-title">Food and Diet</h3>  
       </div>  
       <div class="panel-body">  
         <giphy-random-image query="food"></giphy-random-image>  
       </div>  
  </div>  

First post

New beginnings. This blog was concieved as the place to post various things here related to the blog's topic, interesting problems which I have encoutered and solved, technology descriptions, development practices and so on. I hope soon I'll be able to post here something more useful for others than this introduction.