Düzenlenen Bir Kaydın, Azure Üzerindeki Bir SignalR Servisi ile Tüm Clientlar İçin Kilitlenmesi
Selamlar,
Bugün, daha önceki makalede backend tarafından incelediğimiz, bir kaydın herhangi bir client tarafından düzenlenmesi durumunda, başka biri tarafından düzenlenememesi konusunu, bu sefer de Front-End taraftından inceleyeceğiz. Yani amaç düzenlenen bir ürünün, başkası tarafından aynı anda düzenlenmesini engellemek.
Ali yazar, Veli bozar Küp suyunu çeker azar azar. ― Barış Manço
Azure SignalR Service:
Azure üzerinde signalR servisin çalıştırılması hakkında, daha önceden yazdığım şu makaleyi okuyabilirsiniz.
Öncelikle gelin Azure üzerinde signalR servisimizi oluşturalım:
1-) Aşağıda görüldüğü gibi Azure Marketplace’den signalR aranır, ve çıkan sonuçlardan SignalR Service seçilir.
2-) Aşağıda görüldüğü gibi SignalR service seçilip, create tuşuna basılır.
3-) Aşağıda görüldüğü gibi SignalR servisinin adı, grubu, bölgesi (Türkiye için en mantıklı bölge North Europe’dur), ödeme planına göre adet ve çalışma modu seçilip oluşturulur.
Tüm özellikler doldurulduktan sonra, aşağıda görüldüğü gibi yapılan seçimler toplu olarak gösterilir ve son onay için Create butonuna basılarak, ilgili LockRowHub signalR servisi Azure üzerinde oluşturulmuş olunur.
İlgili SignalR servisin oluşturulma anında, aşağıdaki gibi bir bildiri ekranı ile karşılaşılır.
SignalR service Azure üzerinde ayağa kalktıktan sonra, aşağıdaki gibi bir ekran ile karşılaşılır. Bağlanan Client Hub sayısı ve Backend’de açılan “Server Hub” sayısı, buradan kolaylıkla monitor edilebilir. Settings’den Keys alanına gelinir ise, ilgili servisin Connection String’ine erişilir.
Keys alanına gelindiğinde aşağıdaki gibi bir ekran ile karşılaşılır. İlgili SignalR servisin yolu : “lockrow.service.signalr.net“‘dir. Ayrıca Connection String gene burada tanımlanmıştır. Back-End’de .Net Core projesinde, SignalR Azure servisine’e, buradaki Connection String ile erişecektir. İstenir ise, Connection String içinde geçen, “Primary Key”, güvenlik ihlali durumunda tekrardan yaratılarak (Regenarate Primary key) connection string’in değişmesi sağlanır.
.Net Core 5.0.101 SignalR Hub Ve WebApi
Şimdi gelin, backend tarafda .Net 5.0 ilgili End Pointlerimizi ve SignalR Hub’ımızı oluşturalım: Aşağıdaki komut ile ilgili webapi projesi oluşturulur.
1 |
dotnet new webapi -o signalqrlock |
Product.cs: Düzenlenecek bir product sınıfı, aşağıda görüldüğü gib oluşturulur.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
using System; namespace signalrlock { public class Product { public int ID { get; set; } public string Name { get; set; } public double Price {get; set;} public DateTime CreatedDate { get; set; } } } |
Resim Kaynağı: https://www.videomaker.com/wp-content/uploads/2017/12/381-Laptop-BG-secondary-7-1392×783.jpg
Controller/ProductController(1): Bu örnekde, bir DB connection yapılmayacaktır. Herbir client için random, static ProductList’den 5 farklı ürün çekilip, geriye dönülecektir. Ayrıca verilecek her bir ürünün fiyat bilgisi de, client bazlı random olarak farklılık gösterecektir. Kısaca A client’ı için PS5 fiyatı ile, B client’ı için PS5 fiyatı farklı olacaktır.
Son olarak, makalenin devamında anlatılacak olan signalR Hub sınıfa connect olan herbir client’ın, kendine özel bir connectionID’si vardır. Herbir Client’a özel Product listesi, “ProductDB” Dictionary List’ine, unique connectionID’leri ile birlikte atılır. Bu liste, bir nevi herbir client’a ait özel küçük bir DB’yi temsil etmektedir.
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 |
public class ProductsController : ControllerBase { public static Dictionary<string, List<Product>> ProductDB = new(); public static List<Product> Products = new(); private static readonly string[] ProductList = new[] { "Macbook16 Pro", "Bose Q35", "PS5", "PS4", "Xbox", "GoPro", "Magic_Keyboard", "MX_Anywhere", "Dell_P2715Q", "IPhone 12 PRO" }; [HttpGet("{connectionID}")] public List<Product> Get(string connectionID) { var rng = new Random(); Products = Enumerable.Range(1, 10).Select(index => new Product { ID = index, Name = ProductList[rng.Next(ProductList.Length)], Price = rng.Next(1000) + 500, CreatedDate = DateTime.Now }).GroupBy(item => item.Name) .Select(grp => grp.First()).Take(5) .ToList(); ProductDB.Add(connectionID, Products); return Products; } } |
Örnek Dictionary<string, List<Product>> ProductDB:
Controller/ProductController(2): Aşağıda görüldüğü gibi ProductController’a, GetProductByName(), UpdateProduct() Actionları eklenmiştir. SignalR yapıya dahil olunca, bu methodlar güncellenecektir.
- GetProductByName(): Bir ürünün ilgili client için, detay bilgisinin çekildiği methoddur.
- “ProductDB.TryGetValue(connectionID, out List<Product> _Products)“: Dictionary liste içinde connect olan Client’ın SignalR ConnectionID’si için, ürün listesi var ise, _Products değişkenine atanır.
- “Product product = _Products.First(pr => pr.Name == name)“: Eğer _Products listesi dolu ise, detayı çekilecek ürünün ismine göre filter işlemi yapılır ve bulunan ilk ürün geriye dönülür.
- UpdateProduct(): Güncellenen bir ürünün, var ise tüm clientlar için güncellendiği methoddur.
- “ProductDB.ToList().ForEach(proList => proList.Value.ForEach(pro =>“: Tüm clientların ürün listesi gezilir ve ilgili ürün ismine göre aranır.
- “pro.Price = pro.Name == product.Name ? product.Price : pro.Price“: Eğer ilgili ürün bulunur ise, bir client’ın ilgili ürün için yaptığı fiyat güncellemesi tüm clientlara uygulanır. Böylece aynı ürün için random farklı fiyatlara sahip olan clientların, ilgili ürün için fiyatları eşitlenmiş olunur.
- “ProductDB.ToList().ForEach(proList => proList.Value.ForEach(pro =>“: Tüm clientların ürün listesi gezilir ve ilgili ürün ismine göre aranır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
[HttpGet("GetProductByName/{name}/{connectionID}")] public Product GetProductByName(string name, string connectionID) { if (ProductDB.TryGetValue(connectionID, out List<Product> _Products)) { Product product = _Products.First(pr => pr.Name == name); return product; } return new Product(); } [HttpPost("UpdateProduct/{connectionID}")] public void UpdateProduct([FromBody] Product product, string connectionID) { if (product != null) { ProductDB.ToList().ForEach(proList => proList.Value.ForEach(pro => { pro.Price = pro.Name == product.Name ? product.Price : pro.Price; })); } } |
SignalR HUB: Düzenlenen bir ürünün diğer kullanıcılarda kitlenmesini, güncelleme işlemi biten ürünün son güncel bilgisinin diğer kullanıcılara gönderilmesini, bağlanan kullanıcının clientID’sinin kendisine gönderilmesini, ayrılan kullanıcının ürün listesinin, ProductDB’den çıkarılmasını sağlayan yapıdır.
Connect olan client’ın SignalR ConnectionID’si, client-side taraftaki “GetConnectionId()” function’ına gönderilmiştir. Yani Server-Side taraftan, o an client’ın açık olan browser’ındaki function, trigger edilir.
Bir kayıt düzenlenirken, vazgeçilip Temizle button’una basılır ise, diğer kullanıcılarda kitli olan ilgili kayıdın, kilidinin açılması için ClearProduct() methodu çağrılır ve ClientSide taraftaki “PushProduct()” methodu, “false” parametres ile tetiklenerek, “Güncelle” buttonunun aktif hale gelmesi ve kilidin açılması sağlanır.
Bir client sayfayı yenilediği ya da kapattığı zaman SignalR Hub sınıfında Disconnect olur. Bu durumda kendisine ait olan product listesinin, “ProductDB” Dictionary’den çıkarılması için, aşağıdaki method yazılmıştır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public class LockerHub : Hub { public override async Task OnConnectedAsync() { await Clients.Caller.SendAsync("GetConnectionId", this.Context.ConnectionId); } public async Task ClearProduct(Product product) { await Clients.Others.SendAsync("PushProduct", product, false); } public override async Task OnDisconnectedAsync(Exception exception) { ProductsController.ProductDB.Remove(this.Context.ConnectionId); Console.WriteLine("DisconnectID:" + this.Context.ConnectionId); await base.OnDisconnectedAsync(exception); } } |
Bilmek, ileriyi görmek; ileriyi görmek, güçlü olmaktır. ―Aristoteles
Şimdi gelin servis tarafından signalR Hub sınıfının nasıl çağrılabileceğine bakalım?
signalR Hub Dependency injection:
Controllers/IHubProductDispatcher.cs: IHubContext‘in Productcontroller’ın Constructor’ında çağrılabilmesi için aşağıdaki interface yaratılmıştır. “PushProduct()” methodu ile ya üzerinde işlem yapılan product’ın kitlenmesi, ya da son güncel halinin diğer clientlara gönderilerek kilidin kaldırılması sağlanır.
1 2 3 4 5 6 7 8 9 |
using System.Threading.Tasks; namespace signalrlock.Controllers { public interface IHubProductDispatcher { Task PushProduct(Product product,string connectionID,bool isCancel = true); } } |
Controller/HubProductDispatcher.cs: Aşağıda görüldüğü gibi “HubContext<LockerHub>“, dependency injection ile sisteme dahil edilmiştir.
“await this._hubContext.Clients.AllExcept(connectionID).SendAsync(“PushProduct”, product, isDisabled)” : İlgili method ile, ürünün üzerinde işlem yapan clientın haricindeki tüm clientlara ürünün o anki hali “product” parametresi ile, ürünün kitlenecek ya da serbest bırakılacağı bilgisi, “isDisabled” parametresi ile Client-Side tarafdaki “PushProduct()” function’ına asenkron olarak gönderilmektedir.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
using Microsoft.AspNetCore.SignalR; using System.Threading.Tasks; namespace signalrlock.Controllers { public class HubProductDispatcher:IHubProductDispatcher { private readonly IHubContext<LockerHub> _hubContext; public HubProductDispatcher(IHubContext<LockerHub> hubContext) { _hubContext = hubContext; } public async Task PushProduct(Product product,string connectionID,bool isDisabled = true) { await this._hubContext.Clients.AllExcept(connectionID).SendAsync("PushProduct", product, isDisabled); } } } |
Resim Kaynağı: https://blogin.co/uploads/images/knowledge-sharing-culture-startup.jpg.pagespeed.ce.1ifZTVR9tV.jpg
Startup.cs: Cross Domain, signalR, Swagger ve Routing gibi tanımlamaların yapıldığı yer burasıdır.
Aşağıda görüldüğü gibi, signalR servisinin “HubProductDispatcher” sınıfı ile Depedency Injection ile projelere katılabilmesi için Singleton, yani sadece bir kere ayağa kaldırılacak ve tüm clientlar tarafından aynı nesnenin kullanılması sağlanacak kodlar yazılmıştır.
Not: Ayrıca AddJsonProtocol ile signalR servisden client-side’a gönderilen modelin propertylerinin, farklılaşıp camelcase’e dönüştürülmesi engellenmiştir.
WebApi servis sonucu, Client-Side’dan geri dönülen modellerin propertylerinin, CamelCase’e dönüştürülüp küçük harf ile başlaması engellenmiştir. Kısacası model’in Server-side’da nasıl ise, Client-Side tafata da aynısının olması amaçlanmıştır.
Aşağıda görüldüğü gibi Cross Domain problemi çözülmüştür. Yani Angular front-end tarafdan => Server Side’a erişim, güvenlik nedeni ile yasaktır. Bunun için alttaki izinler ile, sadece “localhost:4200” portundan erişimine izin verilmiştir.
Configure methodunda kullanılacak Cors Policy, “ApiCorsPolicy” olarak tanımlanmıştır. Ayrıca routing amaçlı, browser’dan “lockerHub” keyword’ü ile gelindiğinde => “LockerHub” signalR sınıfına yönlendirilir.
startup.cs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 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 |
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.HttpsPolicy; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.OpenApi.Models; using signalrlock.Controllers; namespace signalrlock { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.AddSingleton<IHubProductDispatcher, HubProductDispatcher>(); services.AddSignalR() .AddJsonProtocol(options => { options.PayloadSerializerOptions.PropertyNamingPolicy = null; }); //Normalde Hub class'dan dönen gelen data da property isimlerinin ilk harfi küçük geliyor,kendi çeviriyor. Olduğu gibi bıraksın diye bu satırı eklendi. services.AddControllersWithViews().AddJsonOptions(opts => opts.JsonSerializerOptions.PropertyNamingPolicy = null); services.AddCors(options => options.AddPolicy("ApiCorsPolicy", builder => { builder.WithOrigins("http://localhost:4200") .AllowAnyMethod() .AllowAnyHeader() .Build(); })); services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "signalrlock", Version = "v1" }); }); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseSwagger(); app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "signalrlock v1")); } app.UseHttpsRedirection(); app.UseRouting(); app.UseAuthorization(); app.UseCors("ApiCorsPolicy"); app.UseEndpoints(endpoints => { endpoints.MapHub<LockerHub>("/lockerHub"); endpoints.MapControllers(); }); } } } |
Angular 11 UI:
Şimdi sıra geldi ürünlerin listelendiği ve güncellendiği Fornt-End Angular projesine. Aşağıdaki komut ile angular projesi yaratılır.
1 |
ng new signalrlockUI |
src/app/models: Listelenecek ve düzenlenecek Product model aşağıdaki gibi tanımlanır.
1 2 3 4 5 6 |
export class Product { ID: number; Name: string; Price: number; CreatedDate: Date; } |
Setup: Projede kurulması gereken paketler, aşağıdaki gibidir.
1 2 3 4 |
npm install bootstrap npm install jquery npm install --save ag-grid-community ag-grid-angular npm install @aspnet/signalr |
src/app/app.component.html:
Aşağıda görüldüğü gibi, Submit buttonuna basılınca “saveForm()” function’ı çağrılmaktadır. Ayrıca otomatik tanımlama kapatılmıştır.
Aşağıda görüldüğü gibi, güncellenecek Product fieldlarından, Name ve Price alanları tanımlanmıştır.
Sayfada listelenecek product kayıtları için ag-grid component kullanılmışdır. Aşağıda görüldüğü gibi [rowData] ile tanımlanaca data source kaynağı, [columnDefs] ile product modelinde gösterilecek kolonlar tanımlanmıştır.
rowSelection=”single” ile her seferinde tek bir satırın seçilmesi sağlanmıştır. [frameworkComponents] kullanım amacı custom oluşturulan componentların “ag-grid” içerisinde kullanılamsının sağlanmasıdır. (gridReady) eventinde “onGridReady()” function’ı, çağrılacaktır.
Aşağıda görüldüğü gibi footer’da, Temizle ve Kaydet buttonları tanımlanmıştır. Temizlenin (click) eventinde, “clearForm()” function’ı çağrılmıştır. Kaydet zaten submit buttonudur.
app.component.html:
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 |
<router-outlet></router-outlet> <h1>Ürün Sayfası</h1> <form class="form-horizontal" #myForm="ngForm" (ngSubmit)="saveForm(myForm.form)" autocomplete="off"> <div class="card-body"> <div class="row"> <div class="col"> <div class="form-group row"> <label for="txtName" class="col-sm-4 col-form-label">Adı</label> <div class="col-sm-8"> <input type="text" class="form-control" id="txtName" name="txtName" placeholder="" [(ngModel)]="product.Name"> </div> </div> <div class="form-group row"> <label for="txtPrice" class="col-sm-4 col-form-label">Fiyat</label> <div class="col-sm-6"> <input type="text" class="form-control" id="txtPrice" name="txtPrice" placeholder="" [(ngModel)]="product.Price"> </div> </div> </div> </div> </div> <div class="card-footer"> <button type="button" (click)="clearForm()" class="btn btn-danger" id="btnClear" name="btnClear">Temizle</button> <button type="submit" [disabled]="selectedTable!=null" class="btn btn-info float-right" id="Kaydet">Kaydet</button> </div> </form> <div class="row justify-content-sm-center"> <div class="col-sm-11"> <ag-grid-angular id="myGrid" style="width: 100%; height: 300px;" class="ag-theme-alpine" [rowData]="rowData" [columnDefs]="columnDefs" [defaultColDef]="defaultColDef" rowSelection="single" [frameworkComponents]="frameworkComponents" (gridReady)="onGridReady($event)" > </ag-grid-angular> </div> |
src/app/app.component.ts:
Eklenen kütüphaneler aşağıdaki gibidir.
Aşağıda görüldüğü gibi :
- “_hubConnection”: signalR sınıfa bağlanılacak bir client nesnesidir.
- “_connectionId”: Herbir client’ın signalR Hub sınıfından aldığı, unique connectionID değeridir.
- “signalRServiceIp”: SignalR sınıfa erişim için kullanılan url adresidir.
- “gridApi, gridColumnApi” : Grid nesnesine ve grid kolonlarına erişim için kullanılır.
- “rowData=[ ]” : Grid’e atanacak datanın tutulduğu dizidir.
- “Product”: Düzenlenecek ürünün atandığı modeldir.
- “frameworkComponents”: Makalenin devamında anlatılacak, BtnCellRenderer yani Güncelle buttonu burada tanımlanır.
-
“constructor(private service: ProductService) {“: Constructor’da ürün listesini ve detayını getirecek, düzenleyecek servis dependency injection ile sisteme dahil edilir.
- “this._hubConnection = new HubConnectionBuilder()” : SignalR Hub classına bağlanacak connector.
- “this._hubConnection.start().then(” : Browser üzerinden, SignalR Hub sınıfına bağlanılınca çalışan function.
- ” this._hubConnection.on(‘GetConnectionId’, (connectionId: string) => {“: Client, SignalR Hub sınıfına bağlanınca, server side tarafta yukarıda tanımlanan “OnConnectedAsync()” methodu tetiklenir. O da bağlanan client’ın browserındaki bu “GetConnectionId()” function’ını tetikler. Ve ilgili client kendisine ait unique ConnectionID’i alır.
- “this._hubConnection.on(‘PushProduct’, (product: Product, isDisabled: boolean) => {“: Bir client, bir ürünü düzenleyeceği zaman, server side tarafdaki, “GetProductByName()” methoduna get yapılır. Burdanda signalR Hub sınıfına ait “PushProduct()” methodu tetiklenir. O da, ürünü düzenleyen client haricindeki tüm clientların burada tanımlanan “PushProduct()” function’ını tetikleyerek, güncellenecek kayıdın Güncelle button’unu pasif hale getirir. Bu koşulda isDisabled parametresi true’dur. Eğer bu parametre false ise, kayıt güncellendi demektir. O zaman da, Güncelle buttonu tıklanılabilir hale getirilip, güncel satır bulunup eskisi ile değiştirilir.
- “sendCancelProduct()” : Client-Side tarafda düzenlenen bir kaydın, vaz geçilip temizle buttonuna basılınması ile çağrılan methoddur. Server-Side taraftaki signalR Hb sınıfının “ClearProduct()” methodu tetiklenmiştir. O methodda diğer clientların tamamında “PushProduct()” function’ı tetiklemekte ve “isDisabled” değişkenini false göndererek, ilgili kayıt için kitli olan “Güncelle” buttonunu aktif hale getirilmesi sağlanmaktadır.
- “getProductByName()” : Seçilen güncellenecek ürünün, adına göre detayının çekildiği functiondır. Servisden çekilen data, Product değişkenine atanmaktadır.
- “getProductList()” : Client, Hub Sınıfına bağlandığı zaman “GetProductList()” methodu ile tüm ürün listesini çeker ve “rowData[ ]” dizisine doldurur.
- “saveForm()”: Form üzerinde “Kaydet” buttonuna basıldığında ya da Form submit olduğunda, çağrılan functiondır. Bu makalede sadece ürün güncelleme durumu üzerinde durulmuş, bu nedenle servis katmanında sadece “UpdateProduct()” function’ı çağrılmıştır.
- “this.rowData = this.rowData.filter(pro => pro.Name != this.product.Name):” Ayrıca güncellenen ürünün ilgili liste içerisinde de yenilenmesi için, aşağıdaki gibi rowData önce filitrelenmiş daha sonra da güncel product, ilgili listeye eklenmiştir. Not: Yorumlanan satırlar, bir liste içerisinde değiştirilecek satırı bulup değiştirme işlemini daha performanslı yapsa da, rowData[]’ya tekrardan yeni bir değer atanmadıkça, “ag-grid” tarafından değişim anlaşılmamaktadır.
- clearForm(): Bir ürünün güncellenmesi anında, Temizle buttonuna basıldığında çağrılan functiondır. Formun edit kısmı temizlenir ve “sendCancelProduct()” function’ı çağrılarak, diğer tüm clientlarda ilgili product’ın serbest bırakılması sağlanır. Bunun için signalR Hub sınıfının “ClearProduct()” methodu çağrılır.
- “getSelectedRow()” : Düzenleme amaçlı bir ürün seçildiği zaman, çağrılan functiondır. Burada, “getProductByName()” function’ı çağrılmıştır. Server side taraftaki “GetProductByName()” methodu buradan çağrılarak, seçilen ürünün detayının DB’den çekilmesi sağlanmış, ayrıca diğer clientların “PushProduct()” function’ı signalR Hub sınıfı ile tetiklenerek, ilgili kaydın kitlenmesi sağlanmıştır.
Aşağıda görüldüğü gibi ag-grid’in tüm kolonları title ve beklenen field’a göre tanımlanmıştır. Ayrıca ag-grid’e özel kolon olarak “btnCellRenderer” component’ı tanımlanmıştır. Click’lenme durumunda, hangi eventin çağrılacağı(getSelectedRow()) label’ı, stil gibi bir çok tanımlama yapılmıştır. Aslında, standart “Güncelle” buttonudur.
app.component.ts:
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 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
import { Component, OnInit } from '@angular/core'; import { HubConnection, HubConnectionBuilder, LogLevel } from '@aspnet/signalr'; import * as signalR from '@aspnet/signalr'; import { ProductService } from './services/productService'; import { Product } from './models/product'; import { BtnCellRenderer } from './button-cell-renderer.component'; import { Button } from 'protractor'; import { FormGroup } from '@angular/forms'; @Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'] }) export class AppComponent implements OnInit { title = 'signalrlockUI'; _hubConnection: HubConnection; _connectionId: string; signalRServiceIp: string = "http://localhost:1923/lockerHub"; private gridApi; private gridColumnApi; rowData = []; public product: Product = new Product(); selectedTable: string = null; frameworkComponents: any; constructor(private service: ProductService) { this.frameworkComponents = { btnCellRenderer: BtnCellRenderer }; } ngOnInit(): void { this._hubConnection = new HubConnectionBuilder() .withUrl(`${this.signalRServiceIp}`, { skipNegotiation: true, transport: signalR.HttpTransportType.WebSockets }) .build(); this._hubConnection.start().then( () => console.log("Hub Connection Start")) .catch(err => console.log(err)); this._hubConnection.on('GetConnectionId', (connectionId: string) => { this._connectionId = connectionId; console.log("ConnectionID :" + connectionId); this.getProductList(this._connectionId); }); this._hubConnection.on('PushProduct', (product: Product, isDisabled: boolean) => { console.log("Product:" + JSON.stringify(product)); console.log("isDisabled:" + isDisabled); var item = this.rowData.find(rd => rd.Name == product.Name); (document.getElementById("btn_" + item.ID) as HTMLButtonElement).disabled = isDisabled; if (!isDisabled)//Güncelenen bir kayıt geldi demek.. { //Var olan kayıt güncellenir. ag-grid bu değişimi fark etmemektedir. /* for (var i = 0; i < this.rowData.length; i++) { if (this.rowData[i] == item) { this.rowData[i] = product; } } */ //Var olan kayıt güncellenir. this.rowData = this.rowData.filter(pro => pro != item); this.rowData.push(product); } console.log("Lock Data :" + JSON.stringify(item)); }); } sendCancelProduct(product: Product) { this._hubConnection.invoke('ClearProduct', product); } public getProductByName(name: string) { this.service .GetProductByName(name, this._connectionId) .then((result) => { this.product = result; //console.log(result); }) .catch((err) => { console.log('Hata:' + JSON.stringify(err)); }); } public getProductList(connectionID: string) { this.service .GetProductList(connectionID) .then((result) => { this.rowData = result; //console.log(result); }) .catch((err) => { console.log('Hata:' + JSON.stringify(err)); }); } public saveForm(form: FormGroup) { let data = JSON.stringify(this.product); this.service.UpdateProduct(data, this._connectionId); //İlgili Ürün Güncellenir. ag-grid bu değişimi fark etmemektedir. /* for (var i = 0; i < this.rowData.length; i++) { if (this.rowData[i].Name == this.product.Name) { this.rowData[i] = this.product; } } */ //İlgili Ürün Güncellenir this.rowData = this.rowData.filter(pro => pro.Name != this.product.Name); this.rowData.push(this.product); this.product = new Product(); } public clearForm() { this.sendCancelProduct(this.product); this.product = new Product(); /*var buttons = (document.getElementsByName("btnEdit") as NodeListOf<HTMLButtonElement>); for (var i = 0; i < buttons.length; i++) { buttons[i].disabled = false; }*/ } public getSelectedRow(e) { // alert(e.rowData.Name); this.getProductByName(e.rowData.Name); } onGridReady(params) { this.gridApi = params.api; this.gridColumnApi = params.columnApi; //params.api.sizeColumnsToFit(); } columnDefs = [ { field: 'id', cellRenderer: 'btnCellRenderer', cellRendererParams: { onClick: this.getSelectedRow.bind(this), label: 'Güncelle', btnClass: 'far fa-edit fa-sm', imageButton: false, id: "btn" }, minWidth: 150, }, { headerName: "ID", field: "ID" }, { headerName: "Adı", field: "Name" }, { headerName: "Fiyat", field: "Price" }, { headerName: "Oluşturma Tarihi", field: "CreatedDate" }, ]; defaultColDef = { flex: 1, sortable: true, filter: true } <span style="color: #ff6600;">} </span> |
button-cell-renderer.component.ts: ad-grid’in bir kolonunun özelleştirilerek, içine render edilen bir component’dır. Güncelle button’u, burada tanımlanır.
- “this.id” : Güncelle buttonunun ID’si “btn_“+ gönderilen product’ın ID’si şeklinde tanımlanır. Amaç güncellenen bir ürüne ait Güncelle button’unun, seçilen ürün ID’sinden bulunarak pasif ya da aktif hale getirilmesidir.
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 |
import { Component, OnDestroy } from "@angular/core"; import { ICellRendererAngularComp } from "ag-grid-angular"; import { IAfterGuiAttachedParams } from "ag-grid-community"; @Component({ selector: 'btn-cell-renderer', template: ` <button [hidden]="imageButton" type="button" class="{{btnClass}}" (click)="onClick($event)" id={{id}} name="btnEdit">{{label}}</button> <i [hidden]="!imageButton" style="cursor:pointer; font-size: 1.3em;" class="{{btnClass}}" (click)="onClick($event)" title="{{label}}"></i> `, }) export class BtnCellRenderer implements ICellRendererAngularComp, OnDestroy { params; label: string; btnClass: string; imageButton: boolean; id: string; agInit(params): void { this.params = params; this.label = this.params.label || null; this.btnClass = this.params.btnClass || ''; this.imageButton = this.params.imageButton || false; this.id = this.params.id +"_"+ this.params.node.data.ID; } refresh(params?: any): boolean { return true; } onClick($event) { if (this.params.onClick instanceof Function) { const params = { event: $event, rowData: this.params.node.data } this.params.onClick(params); } } ngOnDestroy() { } } |
src/app/service/productService.ts: Aşağıdaki serviste, üç method bulunmaktadır. Ürün sayfasında kullanılan, tüm servis işlemleri bu sayfa üzerinden yapılmaktadır.
GetProductByName(): Seçilen bir ürünün unique ismine göre detayının, client’a özel olan connectionID’si ile ona ait olan dizi gurubundan çekilmesi sağlanmıştı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 |
import { Injectable } from '@angular/core'; import { HttpClient, HttpHeaders } from "@angular/common/http"; @Injectable({ providedIn: 'root' }) export class ProductService { public baseUrl: string = "http://localhost:1923/"; constructor(private httpClient: HttpClient) { } public GetProductList(connectionID:string): Promise<any> { let headers = new HttpHeaders({ "Content-Type": "application/json" }); const options = { headers: headers }; var url = `${this.baseUrl}products/${connectionID}`; return this.httpClient .get(url, options) .toPromise() .then( (res: any) => { return res; } ) .catch(x => { if (x.status == 401) { window.location.href = "http://localhost:4200"; } return Promise.reject(x); }); } public GetProductByName(name: string, connectionID: string): Promise<any> { let headers = new HttpHeaders({ "Content-Type": "application/json" }); const options = { headers: headers }; var url = `${this.baseUrl}products/GetProductByName/${name}/${connectionID}`; return this.httpClient .get(url, options) .toPromise() .then( (res: any) => { return res; } ) .catch(x => { if (x.status == 401) { window.location.href = "http://localhost:4200"; } return Promise.reject(x); }); } public UpdateProduct(data: string, connectionID: string) { let headers = new HttpHeaders({ "Content-Type": "application/json", }); var url = `${this.baseUrl}products/UpdateProduct/${connectionID}`; const options = { headers: headers }; return this.httpClient .post(url, data, options) .toPromise(); } } |
app.module.ts:
- İlgili “ProductService” provider olarak projeye dahil edilir.
- “AppComponent ve ag-grid’e custom olarak eklenen Güncelle button’u, “BtnCellRenderer” componentleri declarations’da tanımlanmıştır.
- Ayrıca imports’da, AgGridModule’ü [BtnCellRenderer] ile tanımlanmıştı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 |
import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { HttpClientModule } from '@angular/common/http'; import { AppComponent } from './app.component'; import { ProductService } from './services/productService'; import { AgGridModule } from "ag-grid-angular"; import { AppRoutingModule } from './app-routing.module'; import { BtnCellRenderer } from './button-cell-renderer.component'; @NgModule({ declarations: [ AppComponent, BtnCellRenderer ], imports: [ BrowserModule, FormsModule, AppRoutingModule, HttpClientModule, AgGridModule.withComponents([BtnCellRenderer]), ], providers: [ProductService], bootstrap: [AppComponent] }) export class AppModule { } |
.Net Core Bir Projeye Azure SignalR Service Entegrasyonu
Makalenin başında Azure üzerinde tanımlanan signalR Servis hizmetini, yazılan bu projeye implemente edeceğiz. Amaç, performans ve yüksek trafikde Auto scale edilerek, gelen yükün kaldırılması, trafik azalınca da daha az bir kaynak ile projenin çalışmasına devam edilmesidir. Ayrıca bakım, monitor ve disaster recovery gibi maliyetlerden de Azure signalR Services kullanılarak kurtulunmuş olunur.
1-) Öncelikle, projeye alttaki paket indirilir.
1 |
dotnet add package Microsoft.Azure.SignalR |
2-) Alttaki komut ile UserSecret, proje içerisine dahil edilir.
1 |
dotnet user-secrets init |
3-) Makalenin başında tanımlanan, Azure üzerinde tanımlı signalR Servisin connection string değeri, aşağıdaki yorumlu alana konarak ilgili komut çalıştırılır.
1 |
dotnet user-secrets set Azure:SignalR:ConnectionString "<Your connection string>" |
4-) Aşağıda görüldüğü gibi startup.cs altındaki ConfigureServices’de, “.AddAzureSignalR()” methodu eklenir.
Bu işlem adımlarından sonra, signalR Hub class’ı local makinada çalışsa da, clientların connect olması, tüm bağlı clientlara push notify gibi sunucuya yoğun yük getiren işlemlerin tamamı, Azure üzerinden yürütülmeye başlanacaktır.
Geldik bir makalenin daha sonuna. Bu makalede, iş hayatında bolca karşımıza çıkabilecek olan, bir kayıt üzerinde işlem yapılırken, bir başka kişinin ilgili kayıt üzerinde değişiklik yapamaması konusuna, Front-End tarafından bir bakış attık. Tabi ki burada alınan önlemlere bel bağlamamak, ve önceki makalede de anlattığım gibi aynı konun, bir de Back-End yani server side tarafta da kontorolünü sağlamak gerekmektedir.
Yeni bir makalede görüşmek üzere hepinize hoşçakalın.
Source Code: https://github.com/borakasmer/SignalRLock
Source:
Son Yorumlar