WebApi & AngularJS Game Shop
Selamlar;
Bugün online bir oyun mağzası için ürünlerin çekildiği webapi ile asenkron restful bir servis yazıcaz. Daha sonra angularJs javascript framework’ü kullanılan bir mvc ara yüzünde bu ürünleri listeleyip, filitreleyip satın alıcaz.
Amaç angularJs’in restful servisler ile çalışma mantığını görmek ayrıca önceden bahsetmediğim diger özeliklerine derinlemesine incelemektir. Dilerseniz örneğimize geçelim.
Öncelikle database’den başlıyalım. DAL adı ile yeni bir CodeFirst projesi yaratılır. İlgili data modeller aşağıdadır. Görüldüğü gibi oyun kategorisi ve oyun tabloları vardır. Bir de games adında bir DbContext bulunmaktadır. Tüm database işlemleri bu dll üzerinden yapılmaktadır.
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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
namespace DAL { using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Data.Entity.Spatial; [Table("tblCategory")] public partial class tblCategory { public tblCategory() { tblGames = new HashSet<tblGames>(); } public int ID { get; set; } [StringLength(50)] public string CategoryName { get; set; } public bool? isEnable { get; set; } public virtual ICollection<tblGames> tblGames { get; set; } } } namespace DAL { using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Data.Entity.Spatial; public partial class tblGames { public int ID { get; set; } [StringLength(50)] public string Name { get; set; } public decimal? Price { get; set; } public int? CategoryID { get; set; } [StringLength(50)] public string ImageUrl { get; set; } public virtual tblCategory tblCategory { get; set; } } } namespace DAL { using System; using System.Data.Entity; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; public partial class Games : DbContext { public Games() : base("name=Games") { } public virtual DbSet<tblCategory> tblCategory { get; set; } public virtual DbSet<tblGames> tblGames { get; set; } protected override void OnModelCreating(DbModelBuilder modelBuilder) { modelBuilder.Entity<tblCategory>() .Property(e => e.CategoryName) .IsUnicode(false); modelBuilder.Entity<tblCategory>() .HasMany(e => e.tblGames) .WithOptional(e => e.tblCategory) .HasForeignKey(e => e.CategoryID); modelBuilder.Entity<tblGames>() .Property(e => e.Name) .IsUnicode(false); modelBuilder.Entity<tblGames>() .Property(e => e.Price) .HasPrecision(18, 3); modelBuilder.Entity<tblGames>() .Property(e => e.ImageUrl) .IsUnicode(false); } } } |
Şimdi de WebApi, cross domain restfull servisimizi WebApiAngular adında yeni bir projede yaratalım. Cross domain için NuGet’in Library Package Manager dan Package Manager Console’a :
yazılır.
Tabi henüz işimiz bitmedi. Projemizdeki /App_Start/WebApi.config içine aşağıdaki kod satırı eklenmesi gerekmektedir.
config.EnableCors();
Ayrıca GamesController class’ının başına ya da kullanılacak tüm methodların başına tek tek aşağıda görülen attribute’un eklenmesi gerekir.
1 |
[EnableCors(origins: "*", headers: "*", methods: "*")] |
WebApi servisimizde kullanılan methodlar aşağıda görüldüğü gibidir:
- GetAllGames adında asenkron bir method vardır. Default olarak kategory’si 1 olan yani xbox360 kategorisine ait oyunlar çekilip listelenmektedir. Ayrıca yüklemenin seneryo gereği uzun sürmesi için 3sn lik bekleme yapılmıştır. Amaç datalar çekilirken loader.gif’in kullanıcıya gösterilmesi, yükleme bittikten sonrada saklanmasıdır.
- GetAllGames (int categoryID) adında ikinci bir method vardır. Amaç istenen kategorideki oyunların listesini gene asenkron olarak vermektir.
- BuyGames(int ID) adında üçüncü bir method daha vardır. Bu method sadece post durumunda çağrılabilmektedir. Burada amaç ID si gönderilen oyun sepete eklenmektedir. Dikkat ederseniz WebApi’da session mantığı pek yoktur. Yazmayı denerseniz herbir request’de yeni bir session açıldığını görebilirsiniz. Yani her request’de yeni bir sessionID oluşmaktadır. Bu nedenle önceden var olan user’a erişilememektedir. Buna çözüm olarak oyunların ID si ve adedi bir hastable’da tutulmuş ve bu hastable da global olarak applicationda saklanmıştır. Sepete yeni bir ürün eklendiğinde adet 1 olarak atanmıştır. Eğer zaten ürün var ise değeri 1 artırılmaktadır. Methodun sonunda ilgili ID’li oyunun adı çekilmekte ve sepetteki en son adedi oyun adı ile birlikte asenkron olarak string şeklinde gönderilmektedir.
Burada ufak bir terslik vardır. Herbir User için ayrıştırmaya gidilmemiştir. Yani iki farklı user aynı oyunu alırsa adet 2 gözükecektir. Yorumlar kısmına user bazında nasıl ayrıştırma yapabileceğimizi ve konu ile ilgili fikirlerinizi bekliyorum.
WebApiAngular:
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 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
using System.Collections.Generic; using System.Data.Entity; using System.Linq; using System.Threading; using System.Web.Http; using System.Web.Http.Cors; using DAL; using WebApiAngular.Models; using System.Threading.Tasks; using System.Web; using System.Collections; namespace WebApiAngular.Controllers { [EnableCors(origins: "*", headers: "*", methods: "*")] public class GamesController : ApiController { // GET api/values public async Task<List<GamesModel>> GetAllGames() { using (Games dbContext = new Games()) { List<GamesModel> models = new List<GamesModel>(); var data = await dbContext.tblGames.Where(ga => ga.CategoryID == 1).ToListAsync(); foreach (tblGames item in data) { GamesModel model = new GamesModel(); model.CategoryID = item.CategoryID; model.ID = item.ID; model.ImageUrl = item.ImageUrl; model.Name = item.Name; model.Price = item.Price; models.Add(model); } Thread.Sleep(3000); return models; } return new List<GamesModel>(); } public async Task<List<GamesModel>> GetAllGames(int categoryID) { using (Games dbContext = new Games()) { List<GamesModel> models = new List<GamesModel>(); var data = await dbContext.tblGames.Where(ga => ga.CategoryID == categoryID).ToListAsync(); foreach (tblGames item in data) { GamesModel model = new GamesModel(); model.CategoryID = item.CategoryID; model.ID = item.ID; model.ImageUrl = item.ImageUrl; model.Name = item.Name; model.Price = item.Price; models.Add(model); } Thread.Sleep(3000); return models; } return new List<GamesModel>(); } Hashtable htblGlobalValues = null; [HttpPost] public async Task<string> BuyGames(int ID) { if (HttpContext.Current.Application["GlobalValueKey"] != null) { htblGlobalValues = HttpContext.Current.Application["GlobalValueKey"] as Hashtable; } else { htblGlobalValues = new Hashtable(); } if (htblGlobalValues[ID]!= null) { htblGlobalValues[ID] = ((int)htblGlobalValues[ID]) + 1; } else { htblGlobalValues[ID] = 1; } HttpContext.Current.Application["GlobalValueKey"] = htblGlobalValues; using (Games dbContext = new Games()) { var gameName = await dbContext.tblGames.Where(tg => tg.ID == ID).ToListAsync(); return ID.ToString() + "'li " + gameName.FirstOrDefault().Name + " Ürünün Toplam Adedi: " + htblGlobalValues[ID].ToString(); } } } } |
WebApi’dan gönderilen GamesModel aşağıdaki gibidir:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
using System; using System.Collections.Generic; using System.Linq; using System.Web; namespace WebApiAngular.Models { public class ServicesModel { } public class GamesModel { public int ID { get; set; } public string Name { get; set; } public decimal? Price { get; set; } public int? CategoryID { get; set; } public string ImageUrl { get; set; } } } |
Ayrıca WebApi’dan sonuçları xml veya json olarak alabilmek için Global.asax’a alttaki kodları eklemek gerekir.
1 2 |
GlobalConfiguration.Configuration.Formatters.XmlFormatter.MediaTypeMappings.Add(new QueryStringMapping("xml", "true", "application/xml")); GlobalConfiguration.Configuration.Formatters.JsonFormatter.SupportedMediaTypes.Add(new MediaTypeHeaderValue("text/html")); |
Şimdi de oyunların gösterildiği ara yüzü tasarlayalım:
AngularView adında yeni bir mvc projesi yaratıyoruz. Index.cshtml kodları aşağıda görüldüğü gibidir.
- Öncelikle siteye jquery ve angular.min.js scriptleri eklenir.
- Ürünlerin listeleneceği ProductController yaratılır. ProductController’da $scope.loading property’si başta true atanır. Daha sonra data çekilince false atanır. Amaç aşağıda görüldüğü gibi view’da loader gif’i yükleme zamanında ng-show‘a bağlı olarak property’e true atanarak göstermek, yükleme bitince property’e false atanarak gizelemektir.
<strong ng-show=”loading”><img ng-src=”/Content/ajax-loader.gif” /></strong>
- Dropdown’ın dolacağı datalar categoryTypes property’si ile atanmaktadır. İlk başta default olarak xbox360 oyunlarının webapi servisinden $http.get ile çekilir ve products property’si doldurulur. Ayrıca dropdown değiştiği zaman ng-change=”getCategory()” şeklinde bir function tetiklenir. $scope.getCategory ile ilgili function ProductController’da tanımlanmakta categoryID’ye göre servisden çekilen oyun datası $scope.products property’sine atanmaktadır.
$scope.buyGame function’ı da ilgili oyunu sepete eklemek için webapi servisine ilgili oyun ID’yi post etmektedir. İlgili kayıt daha önceden eklenmiş ise adet 1 arttırılır yok ise yeni kayıt oluşturulup 1 değeri atanır.
- View kısmında ng-app=”MyApp” ve ng-controller=”ProductController” olan bir div tanımlanır. İçine categoryTypes’dan dönen datalar ile oyun kategorilerinin belirlendiği dropdown list ng-repeat=”p in categoryTypes” ile doldurulur.
1 2 3 4 5 6 7 |
<div ng-app="MyApp" ng-controller="ProductController as Product"> <select ng-model="selectedCategory" ng-change="getCategory()" id="fromSelect"> <option value="">Xbox 360</option> <option ng-repeat="p in categoryTypes" value="{{p.Value}}"> {{p.Name}} </option> </select> <br /><br /> |
- Datalar çekilirken hata olması durumunda $scope.error propert’sine hatalar yazdırılır. Aşağıda görüldüğü gibi bu hatalar dropdownlist altında gösterilir. Ayrıca gene bunun altında yüklenme sırasındaki loader.gif gösterilir.
1 2 |
<strong class="error">{{ error }}</strong> <strong ng-show="loading"><img ng-src="/Content/ajax-loader.gif" /></strong> |
- Oyun ismine göre filitreleme yapılabilecek text aşağıda görüldüğü gibi ng-model=”Name” ile ilgili kolona bağlanır.
1 |
<p><b>Filitrele:</b> <input type="text" ng-model="Name" placeholder="Filter by" autofocus></p> |
- Oyunların konucağı tablonun kolonları aşağıda görüldüğü gibi products property’sine göre tekrarlanır. Price’a göre azdan çoğa göre sıralanır. Ve Name’e göre filitrelenir.
<td ng-repeat=”product in products | orderBy:’Price’ | filter:Name “>
- Kolonun içine ürünün Adı aşağıda görüldüğü gibi custom filter ile yazdırılır. Burada oyun isminin önüne ID’si de yazdırılmaktadır.<div><b>{{ product.Name | displayName : product.ID}}</b></div>
1 2 3 4 5 |
app.filter('displayName', function () { return function (product,value) { return "["+ value +"] " + product ; } }); |
- Oyunun resmi ng-src ile konur. <img ng-src=”/Content/GamesImage/{{ product.ImageUrl }}” />
- Oyunun fiyatı currency filter ile yazdırılır. {{ product.Price | currency :”€”}}
- En sona da custom controller olarak adlandırabileceğimiz satın alma buttonu aşağıda görüldüğü gibi directive ile yaratılır. <btn-buy> </btn-buy> Directive’in önce ismi belirlenir.”btnBuy”. Daha sonra restrict :”E” ile element olarak tanımlanır. Bir başka yolda restirict:”A” olarak tanımlamaktır. Bu durumda directive nesneye attribute olarak konur. template ile konucağı yerdeki html çıktı belirlenir. Mesela burada bir div’in içine Buy textinde button konmuştur. Click event’inde buyGame() function’ı çağrılır ve id’si ‘Engin text’inin sonuna herbir kayıt için otomatik artan {{$index}} parametresi konmuştur.Örnek Engin1,Engin2 gibi. Ayrıca istenirse template yerine templateUrl kullanılarak belirtilen html çıktı başka bir html dosyasına konarak yolu belirlenebilir.Örnek : templateUrl: ‘my-customer.html’
1 2 3 4 5 6 7 8 |
var app = angular.module("MyApp", []); app.directive("btnBuy", function () { return { restrict: "E", transclude: true, template: "<div><input type='button' value='Buy' class='btnClass' ng-click='buyGame(product.ID)' id='Engin{{$index}}'/></div>" }; }); |
Index.cshtml :
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 |
@{ ViewBag.Title = "Index"; } <script src="~/Scripts/jquery-1.10.2.min.js"></script> <script src="~/Scripts/angular.min.js"></script> <link href="~/Content/Site.css" rel="stylesheet" /> <h2>Product Store</h2> <script> var app = angular.module("MyApp", []); app.directive("btnBuy", function () { return { restrict: "E", transclude: true, template: "<div><input type='button' value='Buy' class='btnClass' ng-click='buyGame(product.ID)' id='Engin{{$index}}'/></div>" }; }); app.filter('displayName', function () { return function (product,value) { return "["+ value +"] " + product ; } }); function ProductController($scope, $http) { $scope.loading = true; $scope.categoryTypes = [{ Name: 'Ps4', Value: 2 }, { Name: 'Xbox One', Value: 3 }]; $http.get('http://localhost:17276/api/games/').success(function (data) { $scope.products = data; $scope.loading = false; }) .error(function () { $scope.error = "An Error has occured while loading posts!"; $scope.loading = false; }); $scope.getCategory = function () { var ID = $scope.selectedCategory; $scope.loading = true; $http.get('http://localhost:17276/api/games/' + ID).success(function (data) { $scope.products = data; $scope.loading = false; }); }; $scope.buyGame = function (ID) { $scope.loading = true; $http.post('http://localhost:17276/api/games?ID=' + ID).success(function (data) { $scope.loading = false; alert(data); }) } } </script> <div ng-app="MyApp" ng-controller="ProductController as Product"> <select ng-model="selectedCategory" ng-change="getCategory()" id="fromSelect"> <option value="">Xbox 360</option> <option ng-repeat="p in categoryTypes" value="{{p.Value}}"> {{p.Name}} </option> </select> <br /><br /> <strong class="error">{{ error }}</strong> <strong ng-show="loading"><img ng-src="/Content/ajax-loader.gif" /></strong> <table> <tr> <p><b>Filitrele:</b> <input type="text" ng-model="Name" placeholder="Filter by" autofocus></p> <td ng-repeat="product in products | orderBy:'Price' | filter:Name " style="padding:10px"> <div><b>{{ product.Name | displayName : product.ID}}</b></div> <img ng-src="/Content/GamesImage/{{ product.ImageUrl }}" /> <div><h4><b><font color="red">{{ product.Price | currency :"€"}}</font></b></h4></div> <btn-buy> </btn-buy> </td> </tr> </table> </div> |
Yukarıdaki örnekten de anlaşılacağı gibi angularJs ile restful bir servisi konuşturulmuştur. İlgili datalar çeşitli parametrelere göre çekilmiş ve post işlemi yapılmıştır. Directive ile satın alma butonu olan custom bir kontrol yazılmıştır. Böylece tekrarlayan tüm oyunlarda aynı kodu tekrar tekrar yazmaktan ve kod kalabalığından kurtulunmuştur. Custom filter ile oyun isimleri id’si başına gelecek şekilde yazılmıştır. Böylece özel yazılan filterlar ile data gösteriminde isteğe bağlı bir çok customize yapılabileceği görülmüştür. Koşula göre nesneleri göstermi ve gizlenmesine en güzel örnek olarak yükleme gifi verilmiştir. Bu çok daha farklı şekillerde kullanılabilir. Örneğin fiyatı 60€ dan fazla olan ürünlerin gösterilmemesi gibi düşünülebilir. Nesnelerde olan değişikliklere göre çeşitli functionların tetiklemesine örnek dropdownlist’in değişmesi durumunda ilgili kategorideki oyunların webapi servisi ile getirilmesi sağlanmıştır. Ayrıca webapi servislerinde asenkron kullanım ve dataların remde saklanması gibi konular üzerinde durulmuştur.
Geldik bir makalenin daha sonuna.
Yeni bir makalede görüşmek üzere.Hoşçakalın.
Source Code: http://www.borakasmer.com/projects/WebApiAngular.rar
Örnek WebApi Url (İptal): http://webapigames.azurewebsites.net/api/games?xml=true
Örnek GamesShop Url (İptal): http://gamesshops.azurewebsites.net/
Son Yorumlar