Web Workers Nedir? Mvc ve AngularJs İle Nasıl Kullanılır?
Bugün Web Workers nedir ve gerçekten ne zaman ihtiyacımız olur sorularına Mvc ve AngularJS kullanarak yazacağımız birkaç örnek ile cevap arayacağız.
Öncelikle gelin sorunu ele alalım ve ihtiyaca yönelik çözümü bu yolda arıyalım:
Boş bir Mvc projesi Visual Studio 2015 ile yaratılır. Amaç belirlenen sayı kadar bir değişkeni saydırma ve bunu yine belirlenen sayı kadar tekrarlama olacaktır. Her bir tekrardan sonra, kaçıncı sırada olduğunu bildiren bir bildiri ekrana basılacaktır.
İstenen 2 değişkenin girileceği html input elementleri, aşağıda görüldüğü gibi default değerleri ile tanımlanmıştır. Ayrıca işlemin başlamasını sağlayan ve bir de ekrana mesaj basan 2 button konmuştur. Son olarak bildirimlerin yazılacağı “divWorkers” html elementi oluşturulmuştur.
1 2 3 4 5 |
<div id=divWorkers></div> <div>Toplam Saydırma:</div><input type="text" id="txtCounter" value="5"> <div>Toplam Bekleme:</div><input type="text" id="txtWait" value="10000000"></br></br> <input type="button" onclick="Send();" value="SAYMA" /> <input type="button" onclick="MESAJ();" value="MESAJ" /> |
Şimdi gelin “Sayma” işleminde çağrılacak Javascript kodunu yazalım: Aşağıdaki koda baktığımızda “5”‘defa “10milyon” kez sayım yapılacak, ve 5 seferin her birinde ekrana kaçıncı seferde olduğu bilgisi yazılacaktır. Bu koda bakılınca, dikkat edilmesi gereken şey, bu işlem sırasında başka bir işlemin yapılamıyacağıdır. Örneğin “SAYMA” buttonuna basıldıktan sonra “MESAJ” buttonuna basılır ise,ilgili alert ilk işlem bitmeden sonra karşımıza çıkmaktadır.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function Send() { document.getElementById("divWorkers").innerHTML = ""; var counter = document.getElementById('txtCounter').value; var waiter = document.getElementById('txtWait').value; var currentSet = 1; for (var i = 0; i <= waiter; i++) { if (i == waiter) { document.getElementById("divWorkers").innerHTML += "<br />" + currentSet + ". set bitti."; currentSet++; i = 0; if (currentSet > counter) { break; } } } document.getElementById("divWorkers").innerHTML += "Worker saymayı bitirdi."; } function MESAJ() { alert("GUZEL TURKIYEM...."); } |
İsterseniz problemi daha iyi anlamak için yukarıdaki kodun devamına bir kutu koyalım. Bu kutu klavyenin yön tuşlarına basılınca hareket ettirilebilsin. Yani yukarı + aşağı + sağ ve sol. Şimdi isterseniz önce html’i oluşturalım: “square” kutusunun ve ona ait css lerin kodları aşağıda görüldüğü 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 25 |
<style> #animationWrapper { position: relative; height: 50px; } #square { position: absolute; left: 0px; top: 0px; width: 50px; height: 50px; background-color: rgba(0, 0, 220, 0.3); } </style> <div id=divWorkers></div> <div>Toplam Saydırma:</div><input type="text" id="txtCounter" value="5"> <div>Toplam Bekleme:</div><input type="text" id="txtWait" value="10000000"></br></br> <input type="button" onclick="Send();" value="SAYMA" /> <input type="button" onclick="MESAJ();" value="MESAJ" /> <div id="animationWrapper"> <div id="square" style="top: 0px;"></div> </div> |
Şimdi sıra geldi kutuyu hareket ettireceğimiz Javascript kodlarına: Aşağıda görüldüğü gibi “onkeyup” ile basılan tuşun “keyCode“‘una bakılmış ve yön tuşlarından biri ise(37,38,39,40) “switch case” yapısı ile ilgili kutuya yön verilmiştir. “recursive” şekilinde kendi kendini çağıran ilgili “moveSquare()” function’ı “requestAnimationFrame()” ile çağrılarak ilgili kutunun position’ının belirlenen “MOVEMENT_STEP” değeri kadar animatif olarak değişmesi 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 |
$(document).ready(function () { var SQUARE_SIZE = 50; var MOVEMENT_STEP = 3; var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; // Hareket ettirilecek kare konumlandırılır. var square = document.getElementById("square"); var direction = 12; // stop square.style.top = 0; square.style.left = 0; square.style.height = SQUARE_SIZE; square.style.width = SQUARE_SIZE; function moveSquare() { var left = parseInt(square.style.left, 10); var top = parseInt(square.style.top, 10); var right = left + SQUARE_SIZE; var bottom = top + SQUARE_SIZE; switch (direction) { case 37: // sol if (left > 0) { square.style.left = left - MOVEMENT_STEP + 'px'; } break; case 38: // yukarı if (top > 0) { square.style.top = top - MOVEMENT_STEP + 'px'; } break; case 39: //sağ if (right < document.documentElement.clientWidth) { square.style.left = left + MOVEMENT_STEP + 'px'; } break; case 40: // aşağı if (bottom < document.documentElement.clientHeight) { square.style.top = top + MOVEMENT_STEP + 'px'; } break; default: break; } requestAnimationFrame(moveSquare); } window.onkeydown = function (event) { if (event.keyCode >= 37 && event.keyCode <= 40) { // yön tuşlarına basılmış demek direction = event.keyCode; } }; // start the square animating requestAnimationFrame(moveSquare); }); |
Önemli Not: Aşağıda görüldüğü gibi en başta kutu sağ doğru hareket ettirilmeye başlanmıştır. Tam bu esnada sayma işlemine başlanıldığında herşeyin donduğu ve kutunun hareket ettirilemediği görülmektedir. Bu durum belirlenen sayma işlemi bitene kadar devam etmekte ve ancak tüm işlem bittikten sonra kutu klavyenin yön tuşlarına cevap verebilmektedir. Aynı zamanda ekrana yazılacak bildiriler tek tek değilde, yine işlem bitiminde topluca yazılabilmektedir.
Sorunu anladığımıza göre artık çözüme geçebiliriz.
Amaç: Eğer yapılması istenen iş çok zaman alıcak bir iş ise, ilgili işlemin bitmesi donuk bir ekrandan izlenmemelidir. O anda yapılan işlemden bağımsız olarak, ekrandaki diğer işlemlere devam edilebilmelidir. Bu açıdan bakılınca ilk akla gelen thread konusu olmaktadır. İşte biz bugun javascript’in thread’i olan HTML5 ile gelen Web Workers’ı işleyeceğiz. Web worker lar ilgili kodun, javascript ‘in üzerinde çalıştığı browser’ın kullandığı thread’in dışında, farklı başka bir thread’de çalıştırılmasını sağlarlar. 2 tipi mevcuttur.
- Dedicated Worker : Sadece 1 parent’a yani yaratıldığı parent’a bağlıdır.
- Shared Worker : Aynı domaindeki birden fazla parent veya worker’ lar ile çalışabilir. 1 tane bağlantısı kalsa bile worker thread’i sonlanmıyacaktır.
Bu iki madde de anlatılmak istenen esas konu worker’lar harici javascript dosyaları ile çalışırlar. Ya bunlardan 1 tanesi ile çalışabilirsiniz. Ya da farklı farklı birkaç javascript dosyası ile de iletişim halinde olabilirsiniz. İşte ihtiyaca göre oluşan farklılık budur.
Gelin bu kadar teoriden sonra koda geçip neler oluyor bir bakalım:
- Öncelikle ilgili browser’ın “window.Worker”‘ı destkeleyip desteklemediği kontrol edilir.
- “aDedicatedWorker = new Worker(“Scripts/test.js?v=1”);” aDedicatedWorker adında yeni bir worker oluşturulur. Harici çalıştırılacak javascript dosyasının(“test.js“) yolu belirtilir.
- test.js asıl sayma işleminin harici olarak yapıldığı yerdir. Aşağıda detaylıca inceleyeceğiz.
- Kritik yerlerden birisi burası “aDedicatedWorker.onmessage()” kısaca test.js’den post edilecek paketlerin, yakalandığı yer burasıdır. Bir çeşit listenerdır. İlgili gelen data burada “event.data“‘dir. test.js’den gönderilen bildiriler burada “divWorkers” divinin innerHTML’ine basılır. Kısaca herbir seferden sonraki bildirim, test.js’den post edilip burada “onmessage()” function’ında yakalanır ve bu yakalanan mesaj “divWorkers” divin html’ine farklı bir thread’den basılır.
- “SAYMA” button’una tıklanınca ilgili textbox’daki değerler alınıp “aDedicatedWorker.postMessage()” function’ı ile “waiter” ve “counter” şeklinde 2 parametre ile ilgili “test.js”‘e gönderilir. Burada önemli bir nokta var.
- Not: İlgili parametreler gönderilirken, data kopyalanarak gönderilir. Yani verinin kendisi(referansı) değil oluşturulan bir kopyası taşınır. Browser’a göre farklılık gösterese de, kopylama işleminde structured cloning algoritması kullanılmaktadır. Bu şu demektir, gönderilen parametreler parent’da değişse de, başka bir thread(test.js)’de yapılan işlemler bundan etkilenmez.
- Bu başlatılan worker istenir ise “aDedicatedWorker.terminate();” function’ı ile iptal edilebilir. Bu işlem aşağıdaki örneğe göre “Cancel()” ile yapılabilmektedir.
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 |
var aDedicatedWorker = undefined; if (window.Worker) { function Send() { document.getElementById("divWorkers").innerHTML = ""; if (typeof (aDedicatedWorker) == "undefined") { console.log("Sonra"); aDedicatedWorker = new Worker("Scripts/test.js?v=1"); } aDedicatedWorker.onmessage = function (event) { document.getElementById("divWorkers").innerHTML += "<br />" + event.data; } var counter = document.getElementById('txtCounter').value; var waiter = document.getElementById('txtWait').value; aDedicatedWorker.postMessage({ 'Waiter': waiter, 'Counter': counter }); } function MESAJ() { alert("GUZEL TURKIYEM...."); } function Cancel() { document.getElementById("divWorkers").innerHTML += 'Worker çalışnası durduruldu.'; aDedicatedWorker.terminate(); aDedicatedWorker = undefined; } } |
test.js: Ana sayfadan bağımsız, farklı bir thread’de istenen işin yapıldığı yerdir.
- Yine burada da parent’dan yapılan post işleminin dinlenildiği “this.onmessage = function (event)” function’ı bulunmaktadır. İlgili “event“‘de parent’dan gönderilen “Waiter” ve “Counter” değişkenleri bulunmaktadır. İlgili data bu worker’a ‘Json‘ olarak gönderilmiştir.
- Parent’a işlemin başladığı “postMessage(‘Worker Saymaya baslıyor.’)” function’ı ile bildirilmiştir. Parent tarafında ilgili mesaj, yukarıda görüldüğü gibi gene “aDedicatedWorker.onmessage = function (event) ” function’ı ile yakalanmaktadır. Ve ana ekranda, yani parent’da “<div id=divWorkers></div>” içerisine basılmaktadır.
- Sonraki adımda gönderilen parametreye göre, default olarak 5 defa tekrar eden ve her seferinde 1 den 10Milyon’a kadar süren bir counter yani sayaç işlemi yapılmaktadır.
- Her 10milyon’da, yeni bir “postMessage(currentSet + ‘. set bitti.’);” post işlemi yapılmakta ve kaçıncı sırada olunulduğu bilgisi, parent’a bildirilmektedir.
- Tüm işlem bitince “postMessage(‘Worker saymayı bitirdi.’);” ilgili post işlemi ile işlemin bittiği, parent’a haber verilmektedir.
test.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
this.onmessage = function (event) { postMessage('Worker Saymaya baslıyor.'); var currentSet = 1; var data = event.data for (var i = 0; i <= data.Waiter; i++) { if (i == data.Waiter) { postMessage(currentSet + '. set bitti.'); currentSet++; i = 0; if (currentSet > data.Counter) { break; } } } postMessage('Worker saymayı bitirdi.'); close(); } |
İlgili WebWork’ın çalışabilmesi için IIS’den yayınlanması gerekmektedir. Yukarıda görüldüğü gibi ben IIS’den 82.portdan yayımlama yaptım.
Aşağıda projenin örnek bir çalışma videosu görülmektedir. Dikkat edilirse ilgili sayma işlemi farklı bir threadde sürdürülmekte ve ana yani parent ekranda, kullanıcının tüm komutlarına cevap verebilmektedir. Böylece ilgili işlem arkada çalışırken, ekran donmamakta, kullanıcı etkileşimi normal seyirinde başka bir threadde devam etmektedir.
- Workerlar çalıştırılma sırasında timer işlemlerini yapabilirler. Yani setInterval, clearInterval , setTimeout, clearTimeout gibi komutları kullanabilirler.
- XMLHttpRequest nesnelerine erişebilirler. Parent’e etkilemeden sync veya async request işlemler yapabilirler.
- Workerlar harici çalışan farklı bir thread’deki javascript olduklarından “DOM, Window, document ve parent” gibi nesnelere erişemezler.
Tüm Kod Index.cshtml: test.js’de herhangi bir değişiklik yoktur.
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 |
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>Index</title> <style> #animationWrapper { position: relative; height: 50px; } #square { position: absolute; left: 0px; top: 0px; width: 50px; height: 50px; background-color: rgba(0, 0, 220, 0.3); } </style> <script src="~/Scripts/jquery-3.1.1.min.js"></script> <script> var aDedicatedWorker = undefined; if (window.Worker) { function Send() { document.getElementById("divWorkers").innerHTML = ""; if (typeof (aDedicatedWorker) == "undefined") { console.log("Sonra"); aDedicatedWorker = new Worker("Scripts/test.js?v=1"); } aDedicatedWorker.onmessage = function (event) { document.getElementById("divWorkers").innerHTML += "<br />" + event.data; } var counter = document.getElementById('txtCounter').value; var waiter = document.getElementById('txtWait').value; aDedicatedWorker.postMessage({ 'Waiter': waiter, 'Counter': counter }); } function MESAJ() { alert("GUZEL TURKIYEM...."); } function Cancel() { document.getElementById("divWorkers").innerHTML += 'Worker çalışnası durduruldu.'; aDedicatedWorker.terminate(); aDedicatedWorker = undefined; } } $(document).ready(function () { var SQUARE_SIZE = 50; var MOVEMENT_STEP = 3; var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; // Set up the animated square var square = document.getElementById("square"); var direction = 12; // stop square.style.top = 0; square.style.left = 0; square.style.height = SQUARE_SIZE; square.style.width = SQUARE_SIZE; function moveSquare() { var left = parseInt(square.style.left, 10); var top = parseInt(square.style.top, 10); var right = left + SQUARE_SIZE; var bottom = top + SQUARE_SIZE; switch (direction) { case 37: // left if (left > 0) { square.style.left = left - MOVEMENT_STEP + 'px'; } break; case 38: // up if (top > 0) { square.style.top = top - MOVEMENT_STEP + 'px'; } break; case 39: //right if (right < document.documentElement.clientWidth) { square.style.left = left + MOVEMENT_STEP + 'px'; } break; case 40: // down if (bottom < document.documentElement.clientHeight) { square.style.top = top + MOVEMENT_STEP + 'px'; } break; default: break; } requestAnimationFrame(moveSquare); } window.onkeydown = function (event) { if (event.keyCode >= 37 && event.keyCode <= 40) { // is an arrow key direction = event.keyCode; } }; // start the square animating requestAnimationFrame(moveSquare); }); </script> </head> <body> <div id=divWorkers></div> <div>Toplam Saydırma:</div><input type="text" id="txtCounter" value="5"> <div>Toplam Bekleme:</div><input type="text" id="txtWait" value="10000000"></br></br> <input type="button" onclick="Send();" value="SAYMA" /> <input type="button" onclick="MESAJ();" value="MESAJ" /> <input type="button" onclick="Cancel();" value="IPTAL ET" /> <div id="animationWrapper"> <div id="square" style="top: 0px;"></div> </div> </body> </html> |
Peki AngularJS’ile Web Workerlar Nasıl çalışır:
Index.cshtml: AngularJs’li Mvc uygulaması IIS ortamına publish işlemi yapılmadan Web Workerların çalışması sağlanmıştır. Aşağıda görüldüğü gibi tüm değişkenler ve functionlar “$scope” altında toplanmıştır. Yine ilgili worker “$scope” altında tanımlanmıştır. Harici Javascript dosyası yine bu örnekde de “test.js”‘dir. Angular’da “window.onKeydown” yerine ayrı bir directive yazılabilirdi. Ama ben bu örnek için, esas odak noktasından uzaklaşmak istemedim. Web Workerların AngularJs altında çalışma mantığı görüldüğü gibi normal bir html sayfasından çok da farklı değildir.
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 |
@{ Layout = null; } <!DOCTYPE html> <html> <head> <script src="~/Scripts/angular.min.js"></script> <meta name="viewport" content="width=device-width" /> <title>Index</title> <style> #animationWrapper { position: relative; height: 50px; } #square { position: absolute; left: 0px; top: 0px; width: 50px; height: 50px; background-color: rgba(0, 0, 220, 0.3); } </style> <Script> var app = angular.module('app', []); app.controller('controller', function ($scope) { var SQUARE_SIZE = 50; var MOVEMENT_STEP = 3; var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame; // Set up the animated square var square = document.getElementById("square"); $scope.direction = 12; // stop square.style.top = 0; square.style.left = 0; square.style.height = SQUARE_SIZE; square.style.width = SQUARE_SIZE; $scope.Waiter = 5; $scope.Counter = 500000000; $scope.message = function () { alert("GUZEL TURKIYEM...."); } $scope.myWorker = null; $scope.functionWorker = function () { document.getElementById("divWorkers").innerHTML = ""; $scope.myWorker = new Worker("/Scripts/test.js"); $scope.myWorker.postMessage({ 'Waiter': $scope.Counter, 'Counter': $scope.Waiter }); $scope.myWorker.onmessage = function (event) { document.getElementById("divWorkers").innerHTML += "<br />" + event.data; }; }; $scope.moveSquare = function () { var left = parseInt(square.style.left, 10); var top = parseInt(square.style.top, 10); var right = left + SQUARE_SIZE; var bottom = top + SQUARE_SIZE; switch ($scope.direction) { case 37: // left if (left > 0) { square.style.left = left - MOVEMENT_STEP + 'px'; } break; case 38: // up if (top > 0) { square.style.top = top - MOVEMENT_STEP + 'px'; } break; case 39: //right if (right < document.documentElement.clientWidth) { square.style.left = left + MOVEMENT_STEP + 'px'; } break; case 40: // down if (bottom < document.documentElement.clientHeight) { square.style.top = top + MOVEMENT_STEP + 'px'; } break; default: break; } requestAnimationFrame($scope.moveSquare); } $scope.Cancel=function() { document.getElementById("divWorkers").innerHTML += '<br/>Worker çalışnası durduruldu.'; $scope.myWorker.terminate(); $scope.myWorker = undefined; } window.onkeydown = function (event) { if (event.keyCode >= 37 && event.keyCode <= 40) { // is an arrow key $scope.direction = event.keyCode; } }; requestAnimationFrame($scope.moveSquare); }); </Script> </head> <body ng-app="app" ng-controller="controller"> <div id=divWorkers></div> <div>Toplam Saydırma:</div><input type="text" id="txtCounter" ng-model="Counter"> <div>Toplam Bekleme:</div><input type="text" id="txtWait" ng-model="Waiter"><br /><br /> <button type="button" class="btn btn-primary" data-ng-click="functionWorker()">SAYMA</button> <button type="button" class="btn btn-primary" ng-click="message()">MESAJ</button> <button type="button" class="btn btn-primary" ng-click="Cancel()">IPTAL</button> <div id="animationWrapper"> <div id="square" style="top: 0px;"></div> </div> </body> </html> |
test.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
this.onmessage = function (event) { postMessage('Worker Saymaya baslıyor.'); var currentSet = 1; var data = event.data; for (var i = 0; i <= data.Waiter; i++) { if (i == data.Waiter) { postMessage(currentSet + '. set bitti.'); currentSet++; i = 0; if (currentSet > data.Counter) { break; } } } postMessage('Worker saymayı bitirdi.'); close(); } |
Örnek Demo (İptal): http://webworkerblock.azurewebsites.net/
Bu makalede Html5 ile gelen Web Workerları inceledik. Bir Html sayfada yüklü işlemlerin yapılması gerektiği durumlarda, yani tamamlanması vakit alan proseslerde, kullanıcı tarafında hayatın nasıl devam edebileceğini, var olan işlemlerin arkada farklı bir thread ile nasıl yönetilebileceğini, web workerlar kullanılarak başlatılan işlerin arkada çalışması esnasında, kullanıcının diğer işlerine nasıl devam edebileceğini hep beraber inceledik. Böylece geldik bir makalenin daha sonuna :)
Yeni bir makalede görüşmek üzere hoşçakalın.
Kaynak: https://html5demos.com/worker/#view-source, https://stackoverflow.com/questions/5605588/how-to-use-requestanimationframe
Oldukça açıklayıcı ve faydalı bir yazı olmuş. Çalışmalarınız için teşekkür ederim.
Ben teşekkür ederim Muhammed.
Hocam konu ile alakalı değil ama ben bir web servise bağlandığım zaman eğer o web servisi aktif değilse uygulamam hiç çalışmıyor mesela web servisi aktif değilse bir mesaj göstersem bilgiler gelmiyor tarzında .
Teşekkür ederim iyi çalışmalar dilerim :)
http://i.hizliresim.com/zaqWWj.png -> Mvc Projemde böyle çağırıyorum
http://i.hizliresim.com/GPXqqZ.png -> Web Servisimde bu şekilde hocam
Selam Emre;
WebServisini front’da mesela Angular ile çağır. Ve Result’ı bir model’e bağla. Eğer model boş ise uyarı verirsin. Aşağıdaki Angular2 kodundan feyz alabilirsin.
iyi çalışmalar.
this.http.request('./app/person.json')
.subscribe(response => this.People = response.json(),
err => console.error(err),
() => console.log("OK"));
Elinize sağlık hocam.
Teşekkürler Tayfun :)
Hocam çok açıklayıcı bir yazı olmuş elinize sağlık
Teşekkürler Enes…