Erhan Ballıeker

Microsoft Build 2019 Türkiye Etkinliği

Selamlar,

Geçtiğimiz 18 haziran günü Microsoft Türkiye’de Build 2019′ un Türkiye ayağını gerçekleştirdik. Çok değerli konuşmacı ve katılımcı dostlarımızla çok faydalı bir etkinlik olduğunu düşünüyorum. Birçok konuya değindiğimiz bu etkinlikte ben de WebAssembly ve Blazor hakkında bilgiler paylaştım.

Bir önceki yazımda WebAssembly ile alakalı yazımı bulabilirsiniz. Bunun üzerine bu yazımda da sizlere Blazor bahsetmek istiyorum.

WebAssembly’yi .Net dünyasında kullanmak için henüz official pre-relase halinde bulunan Blazor a başvuracağız. Blazor ile Visual Studio da 3 farklı şekilde geliştirme yapmak mümkün. Bunların detayından aşağıda bahsedip örnekler göstereceğim.

  1. Client-Side Blazor
  2. Asp.Net Core Hosted Blazor
  3. Server-Side Blazor

Client-Side Blazor

Bir önceki yazımda bahsettiğim gibi high-level dillerde yazılan kodun wasm a dönüşüp, browser içerisinde javascript sandbox ı içerisinde çalıştırılabilmesi mümkün. Hatta bu zaten WebAssembly nin en güçlü özelliklerinden biri.

Blazor için bu senaryo aşağıdaki şekilde gerçekleşiyor.

yazdığımız C#/Razor dosyaları compile olup .dll haline geliyor. daha sonra bu .dll dosyalarımız mono.wasm runtime ın da çalışabilir hale geliyor. mono.wasm da, javascript runtime üzerinde çalışıyor. Yani özetle şuan bir .dll dosyamız wasm olarak browserda çalışmak için mono ya biraz muhtaç. Ama bunlar arka planda olan şeyler, kullanırken mono vs uğraşmıyorsunuz. Ama yine de bu mono dan itibaren başlayan xamarin in gücünü ve arka planındaki güzelliği de bilmek açısından değerli 🙂

compiletowasm

Projemizde bir mvc projesinde kullandığımız .cshtml sayfalarına benzer olarak, Client-side Blazor da .razor uzantılı dosyaları kullanıyoruz. Bu dosyalar derlendiğinde elde ettiğmiz dll, mono-runtime ile browser içerisinde javascript runtime ın da çalışabilir bir wasm a dönüşüyor.

Tamamen javascript kadar güvenli olduğundan tekrar söz etmek istiyorum, çünkü aşağıdaki resme baktığımızda bir .net web developer ın çok da alışık olmadığı bir takım dosyaların browser tarafına yüklendiğini göreceğiz. Bir çok yazılımcının aklına, browsera a yüklenen .net dll lerini gördüğünde güvenlik ile alakalı sıkıntılar yaratıp yaratmayacağı geliyor. Ama dediğimiz gibi herşey en sonunda javascript kendisinin çalıştığı, sandbox environment ında çalıştığı için en az javascript kadar güvenli.

Capture

Yukarıda ki resme baktığınızda, C# kodumuzun browser içerisinde wasm olarak çalışması için ne kadar fazla dll in de browser a yüklendiğini görüyoruz.

Mono.Security.dll, Mono.WebAssembly.Interop.dll, mono.wasm, solutionname..dll, System.dll, System.Core.dll gibi hem mono hem de .net e bağlı birçok dll in browser a yükleniyor. Bu sayede yazdığımız C# kodları, client side tarafında çalıır hale geliyorlar.

Peki direk filmin sonunu göstermiş olduk ama biraz daha başa sararak herşeye daha detaylıca bakalım.

Öncelikle  ister Blazor ile ister diğer pre-release olan tüm yeni feature ları denemek için neler yapacağımıza bir bakalım.

Blazor .net core 3.0 ile beraber sadece Server-Side desteği ile official olarak gelecek. Client-Side ve Core Hosted taraflarının release olmasına biraz daha vari ama denemekten hatta basit projeleri bu yöntemler ile de yapmaktan çekinmenize hiçbir sebep yok.

Yeni .net core feature larını denemek için aşağıdaki yöntemleri izlemenizi tavsiye ederim.

  1. Öncelikle şuradan Visual Studio 2019 Preview ı download edin. Yeni featureları mevcut stabil VS 2019 da da aktif edebilirsiniz ama, preview ı indirip tüm yeni-henüz release olmamış özellikleri buradan daha hızlı update ler ile takip etmenizde fayda var.
  2. Sonrasında şuradan son güncel .net-core 3.0 sdk sını indirmeniz gerekiyor.

Bu adımlar aslında birçok feature ı deneminiz için yeterli ama Blazor için iki adımımız daha var.

  1. Buradan Asp.Net Core Blazor template ini download edip kurmanız gerekiyor. Oldukça basit bir işlem, bir VSIX dosyası (Visual Studio Extension) kurmaktan farklı değil
  2. Son olarak da, aşağıdaki komutu çalıştırmanı yeterli olacaktır.
    1. dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.0.0-preview6.19307.2

Yukarıdaki tüm adımları tamamladığımızda, Visual Studio 2019 Preview ı açtığımızda, File-> New Project seçip yeni bir Asp.Net Core Project seçtiğimizde karçımıza aşağıdaki gibi bir ekran çıkacak. Yeni gelen, ve gelecek olan tüm proje template lerini burada görebilirsiniz.

Captureas.PNG

yukarıdaki 4 adımı da tamamladığımızda tüm yeni Blazor template leri karçımıza çıkıyor.

Blazor dışında, gRPC service, Worker Service (eski IHostedServices veya Background Services ın yeni hali) gibi yeni templateleri de denemenizde fayda var. Bunlar ile ilgili de en kısa sürede yazmayı planlıyorum.

Şimdilik Blazor a geri dönelim. Seçeneklerden Blazor (Client-Side) seçip Create e bastığımızda karşımıza aşağıdaki gibi bir solution açılacak.

Captureasasa.PNG

Yukarıda gördüğünüz gibi, Asp.Net Core projelerinde olduğu gibi bir Startup.cs, ve Program.cs dosyalarımız mevcut. Değişik gelecek ilk şey, .razor uzantılı dosyalar.Bunlar aslında klasik MVC projelerindeki .cshtml lerden çok da farklı değil. Gerisi tamamen bildiğiniz bir ASp.Net Core proje yapısı.

App.razor dosyasının içerisine baktğımızda aşağıdaki gibi tek bir satır kod görüyoruz.

Burada bir Router tanımlı. Assembly olarak da sadece projedeki Program.cs dosyasının bulunduğu assembly gösterilmiş. Sayfalar arasındaki geçişler sırasında postback olmadan geçişler olduğunu göreceksiniz. Bu router ın tanımlandığı yer.

Router AppAssembly="typeof(Program).Assembly" 

Program.cs dosyasının içerisi de aşağıdaki gibi. Normal bir Asp.Net Core projesi geliştirmesinde bulunduysanız, burada WebHostBuilder yada, HostBuilder kullanıldığını görmüşsünüzdür. Burada fark olarak kullandığımız HostBuilder, BlazorWebAssemblyHost oluyor. Use Startup yerine de yine Blazor için olan, UseBlazorStartup extension metodunu görüyoruz.

 public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) =>
            BlazorWebAssemblyHost.CreateDefaultBuilder()
                .UseBlazorStartup();
    }

Yukarıda ki Program.cs tarafında kullanılmasını söylediğimiz Blazor Startup dosyasıda aşağıdaki gibi. Yine klasik bir core projesinden çok farklı değil. Sadece app ismindeki component imiz, builder a ekleniyor. App.Razor tarafında ki routing tarafının eklendiği ksım. Bir çok hali hazırda yazılmış komponentler mevcut, sizler kendi component lerinizi yazabilirsiniz. Bunlara başka bir yazıda detaylıca değineceğim.

   public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
        }

        public void Configure(IComponentsApplicationBuilder app)
        {
            app.AddComponent("app");
        }
    }

Projede ki en temel kısımları gördükten sonra, .razor dosyalarının içlerine bir bakalım. Örneğin sol taraftaki Navigasyon menüsü için, proje içerisinde NavMenu.razor adında bir dosya mevcut. Bunun içeriği aşağıdaki gibi.



… @functions { bool collapseNavMenu = true; string NavMenuCssClass => collapseNavMenu ? “collapse” : null; void ToggleNavMenu() { collapseNavMenu = !collapseNavMenu; } }

Yukarıdaki gibi normal bir html içerisine, tıpkı cshtml deik gibi C# kodlarımızıda ekleyebilyoruz. Burada ekstra olarak farklı olan şey sayfanın en sonunda function section ı içerisinde yazmış olduğumuz C# kodları. İşte bunlar tam olarak javascript metodları yazar gibi C# metodlarını yazığ sayfa içerisinde kullanacağımız kısım. Counter.razor dosyasına bakarken daha dikkatli inceleyeceğiz.

sayfaların en üst tarafında route ları belirtiyoruz. Örneğin aşağıda Index.razor ı görüyoruz, ekstra bir root a sahip olmadığından direkt olarak “/” ile işaretlenmiş durumda. bu route u verirken sadece başına @page yazmamız gerekiyor.

@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

Sayfalar içerisinde başka sayfaları (componentleri – .razor dosyalarını) çağırmak da çok basit. yukarıda gördüğünü gibi Index içerisinde SurveyPrompt dosyası ayrı bir html tag i gibi veriliyor sadece bu kadar. Eğer komponentler içerisine parametre geçmek de istiyorsa bunu da yukarıdaki Title attribute u ile yağtığımız gibi, componenti tanımlarken ki Property adını yazıp verebiliyoruz atamak istediğimiz değeri.

Örneğin SurveyPromt.razor dosyasının alt tarafında yazılan function sekmesi şöyle;

@functions {
      // Demonstrates how a parent component can supply parameters
       [Parameter] string Title { get; set; }
}

Başında [Parameter] attribute ile işaretlenmiş bir Property mevcut. Bu parametreyide aynı isimle başka bir component içerisinden çağırırıken yukarı da index sayfasında yaptığımız gibi, bir html element ine attribute atarmış gibi tanımlıyoruz bu kadar.

Aşağıda bir de counter dosyasına bakalım. basit bir IncrementCount metodu tanımlanmış, currentCOunt değerini bir arttırıyor. Bu metodu html elementine atamak da sadece başına bir @ işareti koyup adını yazmak kadar basit. herşey normal cshtml dosyalarında razor engine kullanırken ki syntax gibi aslında. ama yapabildiklerimiz çok çok daha fazla =)

@page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" onclick="@IncrementCount">Click me</button>

@functions {
    int currentCount = 0;

    void IncrementCount()
    {
        currentCount++;
    }
}

Bu projeyi çalışıtırığp çıktısına baktığımızda da en üstte gösterdiğim gibi bir çok dll in browser tarafına yüklendiğini görüyoruz. Mono runtime ı sayesinde de dll lerimiz javascript runtime ında çalışabilen wasm a dönüşüyor. Benim gibi javascript i çok seven biri değilseniz, WebAssembly ve Blazor ın yeri sizde de bambaşka olacaktır. =)

Bir sonraki yazımda Server-Side ve Core-Hosted seçeneklerini inceleyeceğiz.

Bir sonraki yazımda görüşmek üzere.

WebAssembly ve Microsoft Asp.Net Core Blazor

Selamlar,

Bu yazımda sizlere Mono, Docker vb çılgınlık seviyesinde bir yenilikten bahsetmek istiyorum;

WebAssembly

Tarihi çok da eskilere gitmemekle beraber öncesinde bir grubun poc olarak başlattığı fakat sonradan çarşının karışabileceği belli olduğundan birçok dünya devininde geliştirilmesine yatırımlar yaptığı bir teknoloji haline geldi webassembly.

Geçmişi tam olarak şöyle;

wasmhistory.PNG

2017 yılında işe Apple, Google, Mozilla, Facebook ve benzeri devlerin de devreye girmesiyle hayalin gerçeğe dönüşmesi işi başladı.

Ne olduğunu tek bir cümlede anlatmak zor, ama özetle şu;

  • Modern browserlar üzerinde hiçbir plugin e gerek kalmadan (Flash, Silverlight vb.) çalışacak olan, client side (– server side desteği ile de,  ki .NetCore 3.0 içerisinde bu şekilde gelecek malesef, signalr destekli olarak. -) web yazılımına yeni özellikler ve performans katmayı amaçlayan yeni bir binary kod tipi.

Kendi sitelerinde söyledikleri de tam olarak şu aslında;

  • Binary formata sahip, stack-based bir virtual machine. High-Level dillerin (C#, Java vb.) kendisine compile edilebilmesi ve client ve server uygulamaları için web üzerinden deploy edilmesi ile asıl farkı yaratıyor.

Bu tek cümle yetersiz kaldığı kadar güçsüz de kalıyor. Az daha örneklersek şöyle oluyor;

  • High-Level bir dilde yadığınız kodu, .wasm a compile edip (bu kısma aşağıda web assembly yi nasıl yazarız kısmında değineceğim) web e deploy ederek, client side kodu gibi kullanabiliyorsunuz.

Aslında nasıl node.js ile front-end yazan arkadaşlarımız bir anda backendci ve fullstack oluverdilerse, bu sayede backend ci arkadaşlar da artık javascript yazmadan frontend ci olabilecekler (html + css hariç :)).

Peki o zaman ilk soru şu; Hangi browserlarda çalışır?

Bunun için şu adresten herhangi bir t anında hangi browserlarda çalışabilir olduğunu kontrol edebilirsiniz.

WebAssembly i anlamak için aslında önce javascript in nasıl çalıştığını anlamak gerekiyor. Modern browserlarda server-client arası işleyiş aşağıdakine benzer şekilde oluyor.

jvinanutshell

Html ve css ile browserların design tooları web sitenizi gösteriyor. Kullanıcı ile bu sayfalar arasında etkileşim oluşturabilmek için javascript kodları yazıyoruz. Browser http üzerinden server e istek gönderdiğinde, browser ın websitemizi göstermesi için gerekli olan tüm dosyalar server dan browser a iletiliyor yukarıda ki resimde de görebileceğiniz üzere.

Javascript bildiğiniz üzere tüm client-side tarafta(node.js ile beraber server tarafında da) işlemlerimizi yapmak için kullandığımız bir dil. Interpreted (yorumlanan) bir dil javascript, bir derlenme sürecinden geçip hedef makina nın doğrudan anlayıp çalıştırabileceği bir kod a dönüşmüyor önceden, yani ilgili cihaz da ilgili runtime da bir intertpreter(yorumlayıcı) ile yorumlanıp sonrasında runtime ın anlayıp execute edebileceği bir hale geliyor.

Tüm browserlarda (desktop, mobil vs) javascript çalıştırmak için aşağıdaki gibi bir Javascript Runtime a sahip. Bu sandbox olarak browser için de bulunan runtime da javascript kodları çalışırken bir yandan da browser tarafında bazı api lara ulaşma hakkı oluyor. Herşeye ulaşma imkanı yok, aksi halde bu ciddi güvenlik sorunları doğururdu. Aşağıdaki resimde özetle Javascript in bir browser üzerinde javascript runtime ile çalışıp browser üzerinde ulaşabildiği api ları görüyoruz.

jvruntime

Buraya kadar bu günlere geldik, yazılan tüm web projeleri client-side gereksinimlerini javascript ve yukarıdaki model ile çözebildi. Halen de çözebilir, gelecek zaman için javascript adına kötü haberler yok tabii ki.

Ama bu resme artık şöyle bir arkadaş eklendi;

jvwwa

Javascript ile aynı güvenlik prensiplerine sahip, aynı javascript runtime ı içerisinde çalışan ama interpreted olmayan, doğrudan runtime ın anlayıp çalıştırabileceği yeni bir binary kod tipi olan ve üstelik başka high-level dillerden kendisine compile edilebilir olan WASM (WebAssembly)” oyuna resmi olarak dahil oldu.

WebAssembly kodunun tipi aşağıdaki gibi birşey;

jsvsawasm

Sol tarafta bildiğiniz javascript, high-level yani insanların okuyup anlayabileceği kolayca yazabileceği bir dil, sağ tarafta ise bir wasm (web assembly) kodu görüyoruz. Ve önceden söylediğim gibi ikisi de aynı runtime içerisinde çalışarak, aynı güvenlik sistemleri içerisinde hem browser tarafında hemde node.js gibi teknolojilerle server tarafında çalışabilir halde.

 

WebAssembly nin gelecek planları içerisinde kendi runtime ın da çalıştırabilmek javascript runtime ına bağlı kalmamak gibi bir hedefi de var. Browser larda yine javascript runtime kullanılacak fakat, örneğin bir android cihaz da browser a gerek kalmadan doğrudan kendi runtime içerisinde çalıştırılabilmesi gibi bir hedef de söz konusu.

Wasm’ı tahmin edebilebileceğiniz gibi doğrudan yazmak biraz zor gibi duruyor 🙂 ama isteyen yazabilir, diğer farklı yazım olanaklarına bakmadan önce hızlıca “neden wasm kullanmalıyım?” sorusunu bir düşünelim.

Cevabını aşağıdaki gibi listeyelebiliriz.

  • Interpreted bir dil olmayıp, doğrudan runtime tarafından execute edilebilecek olmasından dolayı neredeyse native hız da çalışabilecek bir kod tipi olması.
  • Diğer High-Level kodların(C#, Java, Python, C/C++ ..) WebAssembly(wasm) a derlenebilir olması.
  • Browserlar tarafından doğrudan desteklenir olması, kullanmak için hiçbir plugin e gerek olmaması.
  • Javascript Runtime Sandbox ı içerisinde çalıştığı için, javascript yazarken ki güvenliğin birebir aynısına sahip olması.
  • Javascript kodu ile beraber çalışabilir olması. Javascript tarafından webassembly modüllerini çağırıp parametreler geçebiliriz, aynı şekilde wasm modülleri tarafından da javascript fonksiyonları çağırabiliriz.

İki büyük yanlış anlaşılmayı da gidermek adına burada da söylemem de fayda var.

  • WebAssembly javascript yerine gelmedi. Aksine onu tamamlayıcı, onunla beraber çalışabilir bir dil. Javascript hayatına olduğu gibi devam edecek tabii ki.
  • WebAssembly ile yazmak demek artık server side ihtiyacı yok demek değil asla.Bunun yerine şunu söylersek doğru olur, önceleri javascript ile client side tarafında yapamayacağımız yoğun hesaplama işlemleri içeren bir sistemi artık server side a ihtiyaç duymadan client-side tarafta yapabiliriz. Örneklersek;
    • Video/Audio Editing, Streaming, Calling
    • Game
    • Virtual/Augmented Reality
    • AI (Image Recognition vs..)

WebAssembly nin performansı sayesinde yukarıdaki örneklerden oyun a örnek olarak aşağıdaki oyuna bir göz atabilirsiniz.

https://www.funkykarts.rocks/demo.html 

Bu oyun yerçekimini simülasyonu hesaplamalarını tamamen client-side tarafında webassembly ile yapan bir oyun.

Diğer yapılanlar için buraya bakabilirsiniz.

WebAssembly Yazmanın Yolları

WebAssembly ile kod geliştirmek için farklı yöntemler mevcut. Bunların neler olduğuna bakalım.

WAT Formatında Kod

Doğrudan wasm formatında kod yazmak çok gerçekçi ve mümkün olmadığından, bununla eş düzeyde wat formatında, insanın okuyup yazmasına çok daha müsait formatta wat kodumuzu yazıp wasm a derleyebiliriz.

İşleyiş şu şekilde oluyor.

writewat

Sol tarafta gördüğünüz gibi WAT formatında kodunuzu yazıp The WebAssembly Binary Toolkit ile runtime ın anlayabileceği WASM formatına dönüştürebilirsiniz.Bu dönüşüm işlemleri için hali hazırda başka tool larda yazılmakta fakat şu an en popülerlerinden birisi bu.

Peki bunu gerçekten deneyelim.

https://webassembly.studio/ sitesine giderek, yeni bir proje oluşurup bunu web ortamında build edip javascript içerisinde çağırıp sonuçları gözlemleyebilirsiniz.

webassembly.studio sitesine gittiğinizde karşınıza bir popup da istediğiniz proje tipini soran bir popup çıkacak.Burada bir çok highlevel dilin yanında Empty Wat Project seçerek, wat formatında kodunuzu yazabilirsiniz.

emptywat

Empty Wat Project seçtiğinizde karşınıza aşağıdaki gibi bir solution yapısı çıkıyor. src altında main.html, main.js ve main.wat dosyaları olduğunu görüyoruz. Burada .wat dosyasında wat formatında kodumuzu yazıp, Build&Run diyerek sonuçları aynı ekran üzerinde gözlemlememiz mümkün.

Aşağıdaki $add fonksiyonu basitce iki adet sayıyı ekleyip geri dönen bir function. Burada kod yazmak istediğiniz de inetllisense bile mevcut. sytnax a alışmak biraz vakit alıcak olsa da, doğrudan wasm yazmayla zorluk derecesi kıyaslanamaz bile 🙂

mainwat

Aşağıda ise, javascript doyasından bir wasm modülünün nasıl çağırıldığını görüyoruz. Bu da aslında WebAssembly yazmanın bir başka yolu diyebiliriz. Modülü instantiate ettikten sonra ilgili fonksiyonu export ederek kullanabiliriz.

mainjs

main.html tarafında ise bildiğimizi dışında hiçbirşey yok. aşağıdaki gibi basit bir html koduna sadece main.js eklenmiş.

mainhtml

Build&Run diyerek bu kodları çalıştırdığımızda çıktı aşağıdaki gibi ekranın sağ alt köşesinde gözüküyor.

Capture.PNG

Ek olarak bir de solution tarafında bir output folder ı görüyoruz.Burada wat kodumuzun dönüşmüş olduğu wasm kodunun nasıl olduğuna bakmak için, main.wasm doyasına sağ tıklayarak alttaki seçeneklerden View as Binary yi seçersek, ekran da bize yazdığımız wat kodumuzun wasm a çevrilmiş halini gösterecektir.

wasmbinary

Microsoft Blazor Tool Chain

Higl-level bir kodun wasm a derlenerek js runtime da çalışabildiğini söylemiştim, ki zaten bu javascripte göre en büyük artılarından bir tanesi webassembly nin.

Bir çok dil için farklı Tool lar mevcut. Biz bir C# / Razor kodumuzun nasıl webassembly e dönüştüğü kısmına bakacağız.

Aşağıdaki resimde ana fikri görüyoruz. Detaylarına değineceğim.

compiletowasm

Burada anlatılan aslında şu;

  • Yazdığımız C# kodu normal hayatta olduğu gibi bir .dll dosyasına dönüştürülür.
  • Bu .dll dosyası, wasm formatına dönüştürülerek mono.wasm runtime u üzerinde çalışır.
  • mono da, aslında js runtime u üzerinde çalışır.

Yani aslında yazdığımız kod .dll e dönüştükten sonra, mono sayesinde, ilgili formata dönüştürülüp mono runtime üzerinde çalışır. yani kodumuz direk js runtime üzerinde değil, mono runtime ında çalışır, mono runtime ı da js runtime üzerinde çalışabildiğinden, en nihayetinde yazdığımız C# kodu, mono sayesinde javascript runtime üzerinde wasm tipinde çalışan bir kod haline gelir.

Sadece bu bile Mono yu yani Xamarin i(mono project i başlatan ekip sonuçta, hatta başlatan kişi 🙂 ) çok fazla sevmek için bile tek başına yetebilecek bir sebep.

Blazor ın Client-side ve server side tarafında çalışma şekilleri farklı. Bunların detaylarına bir sonraki yazımda gireceğim. Asp.Net Core 3.0 ile beraber ne şekilde kullanabiliyor olacağız, bunları inceleyeceğiz.

WebAssembly tarafına ufak bir girişten ve Blazor için C#/Razor kodunun browser da çalışır hale gelmesi akışını sadece akış olarak inceledikten sonra, bir sonraki yazımda sizlere Visual Studio üzerinde Blazor ile neler yapabiliyoruz bunları da göstermek istiyorum.

Bir sonraki yazımda görüşmek üzere.

 

Asp.Net Core API Backend ve Xamarin.Forms İle Kelime Oyunu Bölüm 4 (Game Component – Custom Grid – Android Grid View – Circle Progress Bar)

Selamlar,

Son bir kaç yazıdır Xamarin.Forms ile geliştirdiğimiz kelime oyunun dan ve kullandığımız teknolojilerden bahsetmiştim.Şimdi de oyun ekranından bahsetmek üzere karşınızdayım.

Burada oyun componenti için custom renderer yazarken nelerle karşılaştık ne şekilde aştık bunlardan bahsetmek istiyorum.

Önce oyun ekranını bir hatırlayalım

WhatsApp Image 2019-05-31 at 22.10.46

Burada iki adet custom component mevcut.

  • Custom Circle Progress Bar (Zaman geri sayacı olarak kullandığımız)
  • Custom Grid

Önce Circle ProgressBar dan bahsedeyim.

public class CircleProgressBar : BoxView
    {
        public readonly BindableProperty BackColorProperty = BindableProperty.Create(nameof(BackColor), typeof(Color), typeof(CircleProgressBar), Color.Transparent);
        public readonly BindableProperty ForeColorProperty = BindableProperty.Create(nameof(ForeColor), typeof(Color), typeof(CircleProgressBar), Color.Transparent);
        public readonly BindableProperty BarHeightProperty = BindableProperty.Create(nameof(BarHeight), typeof(double), typeof(CircleProgressBar), default(double));
        public readonly BindableProperty MinimunProperty = BindableProperty.Create(nameof(Minimun), typeof(int), typeof(CircleProgressBar), default(int));
        public readonly BindableProperty MaximunProperty = BindableProperty.Create(nameof(Maximun), typeof(int), typeof(CircleProgressBar), default(int),propertyChanged: (b, o, n) =>
        {
            var bar = (CircleProgressBar)b;
            bar.Maximun = (int)n;
        });
        public readonly BindableProperty ValueProperty = BindableProperty.Create(nameof(Value), typeof(int), typeof(CircleProgressBar), default(int),
            BindingMode.TwoWay,
               (BindableProperty.ValidateValueDelegate)null,
               (obj, oldValue, newValue) => {
                   var bar = obj as CircleProgressBar;
                   if (bar.BindingContext is MainPageViewModel context)
                   {
                       bar.Value = context.RemainingTime;
                   }
               },
               (BindableProperty.BindingPropertyChangingDelegate)null,
               (BindableProperty.CoerceValueDelegate)null,
               (BindableProperty.CreateDefaultValueDelegate)null);

        public readonly BindableProperty AnimationDurationProperty = BindableProperty.Create(nameof(AnimationDuration), typeof(int), typeof(CircleProgressBar), default(int));
        public readonly BindableProperty TextSizeProperty = BindableProperty.Create(nameof(TextSize), typeof(int), typeof(CircleProgressBar), default(int));
        public readonly BindableProperty TextMarginProperty = BindableProperty.Create(nameof(TextMargin), typeof(int), typeof(CircleProgressBar), default(int));
        public readonly BindableProperty TextProperty = BindableProperty.Create(nameof(Text), typeof(string), typeof(CircleProgressBar), string.Empty, propertyChanged: (b, o, n) =>
        {
            var bar = (CircleProgressBar)b;
            if (bar.BindingContext is MainPageViewModel context)
            {
                bar.Text = context.RemainingTime.ToString();
            }
        });
        public readonly BindableProperty TextColorProperty = BindableProperty.Create(nameof(TextColor), typeof(Color), typeof(CircleProgressBar), Color.Black);

        public CircleProgressBar()
        {
        }

        public Color BackColor
        {
            get { return (Color)GetValue(BackColorProperty); }
            set { SetValue(BackColorProperty, value); }
        }

        public Color ForeColor
        {
            get { return (Color)GetValue(ForeColorProperty); }
            set { SetValue(ForeColorProperty, value); }
        }

        public double BarHeight
        {
            get { return (double)GetValue(BarHeightProperty); }
            set { SetValue(BarHeightProperty, value); }
        }

        public int Minimun
        {
            get { return (int)GetValue(MinimunProperty); }
            set { SetValue(MinimunProperty, value); }
        }

        public int Maximun
        {
            get { return (int)GetValue(MaximunProperty); }
            set { SetValue(MaximunProperty, value); }
        }

        public int Value
        {
            get { return (int)GetValue(ValueProperty); }
            set { SetValue(ValueProperty, value); }
        }

        public int AnimationDuration
        {
            get { return (int)GetValue(AnimationDurationProperty); }
            set { SetValue(AnimationDurationProperty, value); }
        }

        public int TextSize
        {
            get { return (int)GetValue(TextSizeProperty); }
            set { SetValue(TextSizeProperty, value); }
        }

        public int TextMargin
        {
            get { return (int)GetValue(TextMarginProperty); }
            set { SetValue(TextMarginProperty, value); }
        }

        public string Text
        {
            get { return GetValue(TextProperty).ToString(); }
            set { SetValue(TextProperty, value); }
        }

        public Color TextColor
        {
            get { return (Color)GetValue(TextColorProperty); }
            set { SetValue(TextColorProperty, value); }
        }
    }

Burada göreceğiniz gibi aslında circular progress bar oluşturmak için BoxView dan miras aldık. Gerekli bir kaç property i bindable olarak tanımladıktan sonra platforms spesifik taraflara geçip Custom Renderer larımızı yazmaya başladık

Aşağıda iOS Custom Renderer kodlarını görüyorsunuz.

BoxView ios tarafından temel de UIView kullandığı için ister BoxRenderer dan isterseniz VisualElementRenderer dan miras alarak custom renderer a başlayabilirsiniz.BoxRenderer da en nihayetinde VisualElementRenderer dan miras alıyor zaten.

[assembly: ExportRenderer(typeof(CircleProgressBar), typeof(CircleProgressBarRenderer))]    
namespace...iOS.CustomRenderers
{
    public class CircleProgressBarRenderer : VisualElementRenderer
    {
        CAShapeLayer backgroundCircle;
        CAShapeLayer indicatorCircle;
        UILabel indicatorLabel;
        CGSize indicatorLabelSize;
        int indicatorFontSize;

        double startAngle = 1.5 * Math.PI;

        public CircleProgressBarRenderer()  { }

        protected override void OnElementChanged(ElementChangedEventArgs e)
        {
            base.OnElementChanged(e);

            if (Element != null)
            {
                indicatorFontSize = Element.TextSize;

                backgroundCircle = new CAShapeLayer();

                CreateBackgroundCircle();

                CreateIndicatorCircle();

                CreateIndicatorLabel();
            }
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == "Text")
            {
                if (Element.BindingContext is MainPageViewModel model)
                {
                    indicatorLabel.Text = model.RemainingTime.ToString();
                    var val = CalculateValue();
                    animateBackwards(val);
                }
            }
        }

        public override void LayoutSubviews()
        {
            base.LayoutSubviews();

            double radius = CreatePathAndReturnRadius();

            double heightRatio = (radius - Element.TextMargin) / indicatorLabelSize.Height;
            double widthRatio = (radius - Element.TextMargin) / indicatorLabelSize.Width;
            double ratio = 1;
            if (heightRatio < widthRatio)                                   ratio = (radius - Element.TextMargin) / indicatorLabelSize.Height;             else                 ratio = (radius - Element.TextMargin) / indicatorLabelSize.Width;             indicatorFontSize = (int)Math.Round(indicatorFontSize * ratio, 0, MidpointRounding.ToEven);             indicatorLabel.Font = UIFont.SystemFontOfSize(indicatorFontSize);             indicatorLabel.InvalidateIntrinsicContentSize();             indicatorLabelSize = indicatorLabel.IntrinsicContentSize;             indicatorLabel.Frame = new CGRect((Frame.Width / 2) - (indicatorLabelSize.Width / 2), (Frame.Height / 2) - (indicatorLabelSize.Height / 2), indicatorLabelSize.Width, indicatorLabelSize.Height);             this.AddSubview(indicatorLabel);             animate();         }         private double CalculateValue()         {             double min = Element.Minimun;             double max = Element.Maximun;             double current = Element.Value;             double range = max - min;             return current / range > 1 ? 1 : current / range;
        }

        private void CreateIndicatorLabel()
        {
            indicatorLabel = new UILabel();
            indicatorLabel.AdjustsFontSizeToFitWidth = true;
            indicatorLabel.Font = UIFont.SystemFontOfSize(indicatorFontSize);
            indicatorLabel.Text = Element.Text.ToString();
            indicatorLabel.TextColor = Element.TextColor.ToUIColor();
            indicatorLabel.TextAlignment = UITextAlignment.Center;
            indicatorLabelSize = indicatorLabel.IntrinsicContentSize;
        }

        private void CreateIndicatorCircle()
        {
            indicatorCircle = new CAShapeLayer();
            indicatorCircle.StrokeColor = Element.ForeColor.ToCGColor();
            indicatorCircle.FillColor = UIColor.Clear.CGColor;
            indicatorCircle.LineWidth = new nfloat(Element.BarHeight);
            indicatorCircle.Frame = this.Bounds;
            indicatorCircle.LineCap = CAShapeLayer.CapButt;
            this.Layer.AddSublayer(indicatorCircle);
        }

        private void CreateBackgroundCircle()
        {
            backgroundCircle.StrokeColor = Element.BackColor.ToCGColor();
            backgroundCircle.FillColor = UIColor.Clear.CGColor;
            backgroundCircle.LineWidth = new nfloat(Element.BarHeight);
            backgroundCircle.Frame = this.Bounds;
            this.Layer.AddSublayer(backgroundCircle);
        }

        private double CreatePathAndReturnRadius()
        {
            var radius = (Math.Min(Frame.Size.Width, Frame.Size.Height) - backgroundCircle.LineWidth - 2) / 2;
            var circlePath = new UIBezierPath();
            circlePath.AddArc(new CGPoint(Frame.Size.Width / 2, Frame.Size.Height / 2), (nfloat)radius, (nfloat)startAngle, (nfloat)(startAngle + 2 * Math.PI), true);
            backgroundCircle.Path = circlePath.CGPath;
            indicatorCircle.Path = circlePath.CGPath;
            backgroundCircle.StrokeEnd = new nfloat(1.5);
            indicatorCircle.StrokeEnd = new nfloat(1.5);//new nfloat(CalculateValue());
            return radius;
        }

        private void animate()
        {
            var animation = new CABasicAnimation();
            animation.KeyPath = "strokeEnd";
            animation.Duration = Element.AnimationDuration / 1000;
            animation.From = new NSNumber(0.0);
            animation.To = new NSNumber(CalculateValue());
            animation.TimingFunction = CAMediaTimingFunction.FromName(CAMediaTimingFunction.EaseOut);
            indicatorCircle.StrokeStart = new nfloat(0.0);
            indicatorCircle.StrokeEnd = new nfloat(CalculateValue());
            indicatorCircle.AddAnimation(animation, "appear");
        }

        private void animateBackwards(double val)
        {
            var animation = new CABasicAnimation();
            animation.KeyPath = "strokeEnd";
            animation.Duration = Element.AnimationDuration / 1000;
            animation.From = new NSNumber(val);
            animation.To = new NSNumber(val - 0.00185);
            animation.TimingFunction = CAMediaTimingFunction.FromName(CAMediaTimingFunction.EaseOut);
            indicatorCircle.StrokeStart = new nfloat(0.0);
            indicatorCircle.StrokeEnd = new nfloat(1.5);
            indicatorCircle.AddAnimation(animation, "appear");
        }
    }
}

iOS tarafında CAShapeLayer sınıfı ekranda birşeyler çizmek için kullanacak olduğunuz namespace.

Biz de ekran xamarin forms tarafında renk, font size vs gibi bazı propertyleri alarak, bir yuvarlak simit oluşturuyoruz. Tam ortasına denk gelecek şekilde hesaplayarak bir adet UILabel i simitin ortasına yerleştiriyoruz.

Sonrasında iki farklı animasyon olarak, birincisi ekran ilk açıldığında sıfırdan maksimum noktasına gelecek şekilde animate edip, daha sonra her bir saniye geçtiğinde çizilmesi gereken kısmı hesaplayıp ters yönde bir animasyon çalıştırıyoruz.

OnElementPropertyChanged olayında da hem UILabel ın text ini hem de geri şekilde yapılacak olan anismayonumuzu oynatıyoruz. OnElementPropertyChanged olayı bildiğiniz gibi bir custom renderer yazarken, forms tarafında oluşturmuş olduğunuz bindable propertylerın değerlerinin değişmesi sonucu platform spesifik taraflarda bu olayı tetikliyor. Dolayısı ile bir custom renderer yazdığınız zaman, oluşturmuş olduğunuz componentin bazı değerleri runtime içerisinde değişiyor ve buna göre aksiyonlar almanız gerekiyorsa bu event içerisinde ilgili aksiyonlarınızı tetikleyebilirsiniz.

Android tarafında da durum şöyle.

[assembly: ExportRenderer(typeof(CircleProgressBar), typeof(CircleProgressBarRenderer))]

namespace ...Droid.CustomRenderers
{
    public class CircleProgressBarRenderer : ViewRenderer<CircleProgressBar, ProgressBar>
    {
        private ProgressBar pBar;
        private Drawable pBarBackDrawable;
        private Drawable pBarForeDrawable;
        public CircleProgressBarRenderer(Context context) : base(context)
        {
            SetWillNotDraw(false);
        }

        protected override void OnElementChanged(ElementChangedEventArgs e)
        {
            base.OnElementChanged(e);
            if (Control == null)
            {
                pBar = CreateNativeControl();
                SetNativeControl(pBar);
                CreateAnimation();
            }
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (e.PropertyName == "Text")
            {
                if (Element.BindingContext is MainPageViewModel model)
                {
                    CreateAnimationCountDown(model.RemainingTime);
                    Draw(new Canvas());
                }
            }
        }

        protected override ProgressBar CreateNativeControl()
        {
            pBarBackDrawable = DrawableCompat.Wrap(Resources.GetDrawable("CircularProgress_background"));
            pBarForeDrawable = DrawableCompat.Wrap(Resources.GetDrawable("CircularProgress_drawable"));

            DrawableCompat.SetTint(pBarBackDrawable, Element.BackColor.ToAndroid());
            DrawableCompat.SetTint(pBarForeDrawable, Element.ForeColor.ToAndroid());

            var nativeControl = new ProgressBar(Context, null, Android.Resource.Attribute.ProgressBarStyleHorizontal)
            {
                Indeterminate = false,
                Max = Element.Maximun,
                ProgressDrawable = pBarForeDrawable,
                Rotation = -90f,
                LayoutParameters = new LayoutParams(LayoutParams.MatchParent, LayoutParams.MatchParent),
            };

            nativeControl.SetBackground(pBarBackDrawable);

            return nativeControl;
        }

        public override void Draw(Canvas canvas)
        {
            base.Draw(canvas);
        }

        protected override void OnDraw(Canvas canvas)
        {
            base.Invalidate();
            base.OnDraw(canvas);

            Rect bounds = new Rect();
            TextPaint paint = new TextPaint();
            paint.Color = Element.TextColor.ToAndroid();
            paint.TextSize = Element.TextSize;
            paint.GetTextBounds(Element.Text.ToString(), 0, Element.Text.ToString().Length, bounds);
            if (((this.Width / 2) - (Element.TextMargin * 4)) < bounds.Width())
            {
                float ratio = (float)((this.Width / 2) - Element.TextMargin * 4) / (float)bounds.Width();
                paint.TextSize = paint.TextSize * ratio;
                paint.GetTextBounds(Element.Text.ToString(), 0, Element.Text.ToString().Length, bounds);
            }

            int x = this.Width / 2 - bounds.CenterX();
            int y = this.Height / 2 - bounds.CenterY();
            canvas.DrawText(Element.Text.ToString(), x, y, paint);
        }

        private void CreateAnimation()
        {
            ObjectAnimator anim = ObjectAnimator.OfInt(pBar, "progress", Element.Minimun, Element.Value);
            anim.SetDuration(Element.AnimationDuration);
            anim.SetInterpolator(new DecelerateInterpolator());
            anim.Start();
        }

        private void CreateAnimationCountDown(int val)
        {
            ObjectAnimator anim = ObjectAnimator.OfInt(pBar, "progress", val, val - 1);
            anim.SetDuration(Element.AnimationDuration);
            anim.SetInterpolator(new DecelerateInterpolator());
            anim.Start();
        }
    }
}

Android tarafında ise işler kısmen daha kolay çünkü zaten bir ProgressBar widget ı var. Bu tarafta ise bu widget ın ortasına Text i basmak biraz zor oldu, OnDraw metodunu override edip tam ortaya gelecek şekilde bir canvas çizerek devam ettik.

Nihayetinde yukarıdaki resimde ki gibi, renkleri gibi bazı özellikleri ile oynayabildiğimiz ileri geri animasyonla hareke edebilen, güzel bir circle progress bar ımız olmuş oldu her iki platformda da.

Gelelim Grid View ın kendisine. Burada en çok dikkat çeken şey benim için şu oldu.

xamarin.forms.platform.ios içerisinde ki GetControl metodu ve bu metodun android tarafında olmayışı.

Çünkü ilerleyişe şu şekilde başladık;

  • Xamarin.Forms tarafında normal Grid den miras alan bir component yazıp, istediğimiz ekstra özellikleri buraya ekleyelim
  • Zaten harf dizilimlerini de burada yaptıktan sonra, Platform spesifik taraflara geçerek, Touch eventleri ezip ilgili harf e denk gelen o touch ile ilgili işlemlerimizi yapalım

Bu işleyiş iOS tarafında çok güzel çalıştı. Forms tarafında yaptığımız CustomGrid, içi dolu bir şekilde iOS tarafında GetControl dediğimizde elimizdeydi. Ekrandaki tüm UIView lar içerisinde dönüp istediğimiz işlemi istediğimiz şekilde yapabildik. Her şey çok güzeldi.

taa ki android tarafında ki renderer kısmına geçene kadar.

İki taraftada ViewRenderer kullanmamıza rağmen, Android tarafta nereden miras alırsak alalım, elimize Forms tarafında içini butonlarla doldurduğumuz halde bir grid gelmedi. Bir şekilde her ne denediysek, forms taraftaki içi dolu Grid e ulaşamadık.

Bu yüzden tüm GridView baştan oluşturup ekrana sıfırdan bir komponent yazıyormuş eklemek zorunda kaldık.

GridView ın forms tarafı şu şekilde.

 public class CustomGrid : Grid
    {
        public CustomGrid()
        {
            SwipedButtonList = new List();
        }

        public List SwipedButtonList { get; set; }

        public static readonly BindableProperty IsFilledProperty = BindableProperty.Create(nameof(IsFilled),
           typeof(bool),
           typeof(CustomGrid),
           false,
           BindingMode.TwoWay,
           null,
           null,
           null,
           null,
           null);


        public bool IsFilled
        {
            get { return (bool)this.GetValue(IsFilledProperty); }
            set { this.SetValue(IsFilledProperty, (object)value); }
        }


        public static readonly BindableProperty SelectWordProperty = BindableProperty.Create(nameof(BackgroundColor),
               typeof(string),
               typeof(CustomGrid),
               string.Empty,
               BindingMode.TwoWay,
               null,
               (obj, oldValue, newValue) => {
                   var old = oldValue;
                   var n = newValue;
                   var grid = obj as CustomGrid;

                   (grid.BindingContext as MainPageViewModel).SelectedWord = n.ToString();
               },
               null,
               null,
               null);


        public string SelectedWord
        {
            get { return (string)this.GetValue(SelectWordProperty); }
            set { this.SetValue(SelectWordProperty, (object)value); }
        }
    }

iOS tarafındaki CustomRenderer; 

public class CustomGridRenderer : ViewRenderer
    {
        UIView myGrid;
        List selectedButtonList = new List();

        protected override void OnElementChanged(ElementChangedEventArgs e)
        {
            base.OnElementChanged(e);

            if (Control != null)
            {
                Control.BackgroundColor = UIColor.White;
            }
            else
            {
                myGrid = GetControl();
               
                if (myGrid != null)
                {
                    foreach (UIView sview in myGrid.Subviews)
                    {
                        var ctrl = (sview as ButtonRenderer).Control;
                        if (ctrl != null)
                        {
                            ctrl.UserInteractionEnabled = false;
                        }
                    }
                }
            }
        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (Control != null)
            {
                Control.BackgroundColor = UIColor.White;
            }
            else
            {
                myGrid = GetControl();
               
               ...
                }
            }
        }

        public override void TouchesBegan(NSSet touches, UIEvent evt)
        {
            base.TouchesBegan(touches, evt);
        }

        public override void TouchesMoved(NSSet touches, UIEvent evt)
        {
            base.TouchesMoved(touches, evt);
            var grid = Element as CustomGrid;
            int totalSwipedButtonCount = grid.SwipedButtonList.Count;

           for (int i = 0; i < myGrid.Subviews.Length; i++)            
           {
             foreach (UITouch touch in touches)                 
                {                     
                   var ctrl = (myGrid.Subviews[i] as ButtonRenderer).Control;                     
                    if (ctrl.PointInside(touch.LocationInView(ctrl), evt) && !(ctrl.AccessibilityValue == "blue"))                  
                    {                         
                       ...
                    }
                    else if (ctrl.PointInside(touch.LocationInView(ctrl), evt))
                    {
                       ...
                    }
                }
            }
        }

        public override async void TouchesEnded(NSSet touches, UIEvent evt)
        {
            base.TouchesEnded(touches, evt);

            var grid = Element as CustomGrid;
            var context = Element.BindingContext as MainPageViewModel;

            var foundWord = context.Words.FirstOrDefault(x => x.Word == grid.SelectedWord);

            if (Element != null)
            {
                if (Element is CustomGrid _grid)
                {
                   ...
                }
            }

            ...
           
        }
    }

Buradaki çok gerekli olmayan kodları kaldırdım, zira en önemli nokta yukarıda da kalın olarak işaretlediğim gibi, o anki touch noktasının Grid içerisinde ki hangi butona denk geldiğini bulmak oldu sadece.

Bunu da

var ctrl = (myGrid.Subviews[i] as ButtonRenderer).Control;

if (ctrl.PointInside(touch.LocationInView(ctrl), evt) && !(ctrl.AccessibilityValue == “blue”)) { …. }

control üzerindeki PointInside metodunu ve touch dan gelen LocationInView metodu nın birlikte kullanımı ile çok basit bir şekilde yapabiliyorsunuz.

Yani sizde içerisinde birden çok ui element i olan ve parmak ile gezildikçe o anki element üzerinde bir şey yapmanız gerekirse iOS tarafında, özet işleyiş şu şekilde oluyor;

TouchesMoved eventi içerisinde tüm UIView larda gezerken yukarıdaki iki metodu kullanarak bu işi hallediyorsunuz.

Gelelim Android tarafına. Yukarıda bahsetiğim gibi android tarafında sıfırdan bir GridView oluşturup, kendi Adapter i ile içerisini doldurup, onun üzerinden yürüdük.

iOS dan farklı olarak burada ayrı ayrı gelen touch eventleri yerine tek bir DispatchTouchEvent event i mevcut. Buraya geçilen MotionEvent ile o anki motion ı yakalayıp işlemlerinizi istediğiniz hareket altında yazabilirsiniz.

        GridView mainView;
        

        public CustomGridRenderer(Context context)
            : base(context)
        {

        }

        protected override void OnElementPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            base.OnElementPropertyChanged(sender, e);

            if (Control != null)
            {
                var count = Control;
            }

            if (e.PropertyName == "IsFilled")
            {
                var linearLayout = new LinearLayout(Context);
                linearLayout.LayoutParameters = new LayoutParams(100, 100)
                {
                    Width = LayoutParams.MatchParent,
                    Height = LayoutParams.MatchParent
                };

                var density = Resources.DisplayMetrics.Density;

                mainView = new GridView(Context);
                mainView.SetNumColumns(4);
                mainView.SetHorizontalSpacing((int)(10 * density));
                mainView.SetVerticalSpacing((int)(10 * density));

                var adapter = new ButtonAdapter(Context);
                foreach (var item in Element.Children)
                {
                    //add buttons to grid.
                    var customButton = item as CustomButton;
                    adapter.CustomButtons.Add(customButton);
                    var btn = new Button(Context)
                    {
                        Text = customButton.Text,
                        Tag = customButton.WordIndex,
                        TextSize = 30,
                        Clickable = false
                    };

                    Typeface tf = Typeface.CreateFromAsset(Context.Assets, "HVD_Comic_Serif_Pro.ttf");
                    btn.SetTypeface(tf, TypefaceStyle.Normal);

                    btn.LayoutParameters = new LayoutParams(30, 30)
                    {
                        Width = LayoutParams.MatchParent,
                        Height = LayoutParams.MatchParent
                    };
                    btn.SetHeight(60);
                    btn.SetBackgroundResource(Resource.Drawable.Button_Border);
                    btn.SetTextColor(Android.Graphics.Color.Black);
                    adapter.Buttons.Add(btn);

                  ...
                }

                adapter.SecreenDensity = density;
                adapter.FormsGrid = Element;
                mainView.Adapter = adapter;
                linearLayout.AddView(mainView);

                SetNativeControl(linearLayout);
            }
        }

        public override bool DispatchTouchEvent(MotionEvent e)
        {
            var grid = Element as CustomGrid;
            int totalSwipedButtonCount = grid.SwipedButtonList.Count;
            var insideGrid = Control.GetChildAt(0) as GridView;

            switch (e.Action)
            {
                case MotionEventActions.Up:
                    
                     ...
                    break;
                case MotionEventActions.Move:
                    int x = (int)Math.Round(e.GetX());
                    int y = (int)Math.Round(e.GetY());

                    for (int i = 0; i < insideGrid.ChildCount; i++)
                     {                         
                       var child = insideGrid.GetChildAt(i) as Button;                         
                         if (child != null)         
                             {               
                                
                             if (x > child.Left && x < child.Right && y > child.Top && y < child.Bottom && child.ContentDescription != "done")                             
                                                               
                                   ...
                        }
                    }

                    break;
                default:
                    break;
            }

            return base.DispatchTouchEvent(e);
        }

        public class ButtonAdapter : BaseAdapter
        {
           
          ...
        }
    }

Burada da önce bir LinearLayout oluşturuyoruz. Bunun içerisine bir GridView atıyoruz. Bu Grid in içerisini de butonlarla dolduruyoruz.

iOS tarafında yaptığımız gibi ilgili touch ın hangi ui elemente denk geldiğini bulmak içinse bu tarafta şu şekilde ilerliyoruz.

for (int i = 0; i < insideGrid.ChildCount; i++) {

var child = insideGrid.GetChildAt(i) as Button;

if (x > child.Left && x < child.Right && y > child.Top && y < child.Bottom …)

Grid içerisindeki tüm ChildCount sayısı kadar bir for döngüsü içerisinde dönüp, İçerisinde döndüğümüz grid in o anki “i” index li elemanını yakalayıp, bu elemanın Left-Right-Top-Bottom ı, touch ın x ve y değerleri içerisinde mi buna bakıyoruz.

Yani aslında iOS da bir iki metod ile yaptığımız işlemi burada daha gözler önünde yapıyoruz.

Bu yazımda da bahsetmek istediklerim bu kadar

Bir sonraki yazımda görüşmek üzere.

Asp.Net Core API Backend ve Xamarin.Forms İle Kelime Oyunu Bölüm 3 (Asp.Net Core IHostedService ve BackgroundService ile Background Tasks)

Selamlar,

Bu yazım sizlere Asp.Net Core projenizde Background service yazmanın yöntemlerinden ve bizim yazdığımız oyun için bunu nasıl kullandığımızdan bahsetmek isiyorum.

Öncelikle bizim nasıl kullandığımızdan önce bir asp.net core projenizde arka planda belli zaman aralıklarında bir şeyler yapmak istediğiniz de neler yapabilirsiniz buna bakalım.

Bunun için başvuracağınız ilk arkadaş IHostedService.

Microsoft.Extensions.Hosting paketinde olan ve zaten Microsoft.AspNetCore.App metapackage ı ile elimize çoktan geçmiş olan bu arkadaş iki metot içeren bir interface

namespace Microsoft.Extensions.Hosting
{
    //
    // Summary:
    //     Defines methods for objects that are managed by the host.
    public interface IHostedService
    {
        //
        // Summary:
        //     Triggered when the application host is ready to start the service.
        //
        // Parameters:
        //   cancellationToken:
        //     Indicates that the start process has been aborted.
        Task StartAsync(CancellationToken cancellationToken);
        //
        // Summary:
        //     Triggered when the application host is performing a graceful shutdown.
        //
        // Parameters:
        //   cancellationToken:
        //     Indicates that the shutdown process should no longer be graceful.
        Task StopAsync(CancellationToken cancellationToken);
    }
}

StartAsync ve StopAsync bu kadar.

WebHostBuilder (WebHost – IWebHostBuilder) ile uygulamanız ayağa kalkıyor ise bu StartAsync metodu server ayağa kalktıktan hemen sonra (IApplicationLifetime.ApplicationStarted tetiklendikten sonra) tetikleniyor.

GenericHost (HostBuilder) ile uygulamanızı host ediyor iseniz bu sefer de IApplicationLifetime.ApplicationStarted metodu tetiklenmeden hemen önce bu StartAsync metodumuz tetikleniyor.

Aynı şekilde StopAsync de host shutdown olduktan sonra tetikleniyor. Burada IDisposable interface ini implemente etmek ve içeride kullandığımız diğer sınıfları da (ör: Timer gibi) burada dispose etmekte fayda var.

Örnek bir kullanım görelim.

internal class TimedHostedService : IHostedService, IDisposable
{
    private readonly ILogger _logger;
    private Timer _timer;

    public TimedHostedService(ILogger logger)
    {
        _logger = logger;
    }

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Background Service is starting.");

        _timer = new Timer(DoWork, null, TimeSpan.Zero, 
            TimeSpan.FromSeconds(5));

        return Task.CompletedTask;
    }

    private void DoWork(object state)
    {
        _logger.LogInformation("Timed Background Service is working.");
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _logger.LogInformation("Timed Background Service is stopping.");

        _timer?.Change(Timeout.Infinite, 0);

        return Task.CompletedTask;
    }

    public void Dispose()
    {
        _timer?.Dispose();
    }
}

Yukarıda bir timer vasıtası ile 5 sn de bir log atıcak basit bir IHostedService implementastonu görüyoruz. shutdown olduğunda timer infinite state e alınıp dispose implementasyonunda da dispose ediliyor.

Peki bunun dışında muhemelen ihtiyacımız olacak olan bir detaydan bahsetmek isterim. Bir IHostedService imizde uygulamamıza Scoped lifetime ına sahip olarak register etmiş olduğumuz bir service kullanmak istersek, bunun için scope default olarak üretilmiyor, bunu bizim manuel yapmamız gerekiyor. Aşağıdaki gibi..

 private void DoWork()
    {
        _logger.LogInformation(
            "Consume Scoped Service Hosted Service is working.");

        using (var scope = Services.CreateScope())
        {
            var scopedProcessingService = 
                scope.ServiceProvider
                    .GetRequiredService();

            scopedProcessingService.DoWork();
        }
    }

Asp.Net Core 2.0 ile gelen IHostedService in 2.1 de daha kolay kullanımı için bir arkadaş daha var. Bu da BackgroundService, IHostedService implementasyonunu baştan sona yapmak istemezseniz bu arkadaşı Asp.Net Core 2.1 den sonra kullanmaya çekinmeyiniz, birçok senaryoda işinizi görecektir. Bu sayede sadece ExecuteAsync metodunu doldurmanız yeterli. Default olarak CancellationToken timeout süresi 5 sn. ama bu süreyide değiştirmemiz mümkün.

public class MyBackgroundService: BackgroundService
{
    private readonly ILogger _logger;

    public MyBackgroundService(ILogger logger)
    {
        ...
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        _logger.LogDebug($"Service is starting.");

        stoppingToken.Register(() =>
            _logger.LogDebug($"background task is stopping."));

        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogDebug($"task doing background work.");

            await Task.Delay(_settings.CheckUpdateTime, stoppingToken);
        }

        _logger.LogDebug($"background task is stopping.");
    }

    ...
}

Cancellation token time out süresini aşağıdaki gibi değiştirebilirsiniz.

WebHost.CreateDefaultBuilder(args) .UseShutdownTimeout(TimeSpan.FromSeconds(10));

Peki biz bu uygulamada bu background service i ne için ve nasıl kullandık kısmına gelirsek. Bizim asıl ihtiyacımız olan şey, belli bir sürede birşeyler çalıştırmanın yanında o belli bir sürenin kendisi idi 🙂 Her bir oyun için 60 yada 90 saniyelik sürelerimiz var. Ve bu süreleri HostedService mizde sürekli olarak geri sayıyoruz. oyun bitikten sonra bazı işlemler için 30 saniyelik te bir bekleme süresi var. Yani bir kullanıcı oyunu açtığında ve canlı oyuna katılmak istediğinde, o an oyun oynanıyor ise oyunun bitmesine kaç saniye kaldığını yada oyun bitmiş ve 30 sn lik bekleme süresinde isek de yeni oyunun başlamasına kaç saniye kaldığını göstermemiz gerekti.

Aşağıdaki resimde ki gibi.

WhatsApp Image 2019-05-31 at 22.10.46 (1)

Kullanıcı oyuna katılmak istediğinde ona hosted service in o an geri sayarken ki saniyenisi dönüyoruz, signalr ile. Ve kalan süreyi bir kere client aldıktan sonra artık kendi cihazındaki timer ile sayma işlemi devam ediyor.

Bizim HostedService miz şu şekilde.

 internal class TimedHostedService : IHostedService, IDisposable
    {
        private readonly ILogger _logger;
        private readonly IHubContext _hubContext;
        public IServiceProvider Services { get; }
        private Timer _timer;

        public TimedHostedService(ILogger logger, IServiceProvider services, IHubContext hubContext)
        {
            _logger = logger;
            _hubContext = hubContext;
            Services = services;
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Timed Background Service is starting.");

            _timer = new Timer(GetNewGameAndBroadcast, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));

            return Task.CompletedTask;
        }

        private async void GetNewGameAndBroadcast(object state)
        {
            if (TimerService.WaitDuration == 25)
            {
                _logger.LogInformation("Timed Background Service is working.");
                using (var scope = Services.CreateScope())
                {
                    var gameService = scope.ServiceProvider.GetRequiredService();
                    var game = await gameService.NewGame(4);
                    var gameJson = JsonConvert.SerializeObject(game);
                    TimerService.gameJson = gameJson;
                    try
                    {
                        await _hubContext.Clients.All.SendAsync("GameReceived", gameJson);
                    }
                    catch (Exception ex)
                    {
                         ...
                    }
                }
            }
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            _logger.LogInformation("Timed Background Service is stopping.");

            _timer?.Change(Timeout.Infinite, 0);

            return Task.CompletedTask;
        }

        public void Dispose()
        {
            _timer?.Dispose();
        }
    }

Burada her bir saniyede ilgili metodu çalıştırıp eğer oyun bekleme süresinde ise zaten online olan kullanıcılar için yeni oyunu 25 saniye kala üretip IHubContext i kullanarak tüm kullanıcılara broadcast ediyoruz, bu durumda her online olan kişi süre bittiğinde aynı oyuna başlıyor, oyun sırasında dahil olanlarda bu oluşturulmuş olan oyunu alıp kalan saniyeden oyuna devam ediyorlar.

Bu yazımda bahsetmek istediklerim bu kadar. Bir sonraki yazımda asıl oyun için gerekli olan Grid i nasıl oluşturduk neler yaptık bundan bahsedeceğim.

Bir sonraki yazımda görüşmek üzere.

Asp.Net Core API Backend ve Xamarin.Forms İle Kelime Oyunu Bölüm 2 (Asp.Net Core SignalR ve Azure SignalR Service)

Selamlar,

Xamarin Forms Mobile App ve Asp.Net Core backend tabanlı kelime oyunumuzdan bir önceki yazımda bahsetmiştim. Bu 1 aylık macerada yaşadıklarımızı ve kullandıklarımızı örneklerle anlatmaya devam ediyorum

Bu yazımda kullanıcıları aynı üzerinde oynatmak ve online olarak göstermek için faydalandığımız teknolojiden – ki herhangi bir .net projenizde bir şekilde bir realtime ihtiyacınız varsa kullanmanızı önereceğim teknoloji olan –  SignalR dan bahsedeceğim.

Aslında daha da doğrusu Asp.Net Signalr Core dan bahsedeceğim. SignalR ve SignalR Core farklı iki API. Asp.Net Core ile beraber yeniden geliştirilmeye başlandı.

Biliyorsunuz ki bir uygulama da realtime ihtiyacımız olduğunda kullanabileceğimiz birçok yöntem var, bunlar;

  • Web Socket
  • Server Sent Events (SSE)
  • Long Polling
  • Short Polling

vs gibi birden çok seçeneğimiz var. En tavsiye edilen en yeni yöntem tabii ki websocket kullanmak. Ama yazdığınız uygulama her ne ise (web-mobil-desktop vs) kullanacak olan clientları ve kullanacakları donanım ve yazılımları bilemeyeceğiniz için sadece kalkıpta tüm iletişimi WebSocket ile kurmak çok doğru olmayacaktır, size kullanıcılar kaybettirecektir.

Peki SignalR ne yapıyor?

SignalR önce gerçek zamanlı haberleşme isteyen client ın kontrolünü yapıyor. İlk denediği seçenek WebSocket kullanarak haberleşmek eğer client bu teknolojiyi desteklemiyorsa sırası ile SSE, LongPolling vs deneyerek en sonunda doğru yöntemi bulup iletişimi sağlıyor.

Bir asp.net core projemizde signalr kullanmakta çok basit artık. Biliyorsunuz ki Asp.Net core da artık koca MVC akışı bi pipeline olarak proje ekleniyor. SignalR da bu şekilde projeye ekleniyor ve kullanmaya başlıyorsunuz.

Öncelikle biz Asp.Ne Core API projemizde bunun için neler yaptık buna bakalım.

Microsoft.AspNetCore.SignalR paketini kullandık. Bu paketi eğer projenizde Microsoft.AspNetCore.App paketi varsa indirmenize gerek yok zaten bu paket mevcut halde gelmiş oluyor.

Bundan sonra yapmanız gereken Signalr ın projenizle ilişki kurduğu Hub sınıfını oluşturmak.

Bizim projemiz içerisinde GameHub adında bir hub ımız mevcut.Aşağıdaki gibi bir kısmını inceleyelim.

public class GameHub : Hub
    {
        public IServiceProvider Services { get; }
        public GameHub(IServiceProvider services)
        {
            Services = services;
        }

        public async void ConnectToHub(string username)
        {
            await Clients.Caller.SendAsync("ConnectedToHub", username, Context.ConnectionId);
            await Clients.AllExcept(Context.ConnectionId).SendAsync("NewUserConnectedToHub");
        }

        ...

Burada constructor tarafında kullandığımız ServiceProvider a çok takılmayın şimdilik. Bu aşağıda başka metodlar da application service ler tarafında yazdığımız ve container a register ettiğimiz serviceleri almak için kullandığımız yöntem. constructor injection yapamazdık çünkü Hub sınıfı parametresiz Constructor ı bulup çağıracaktır.

ConnectToHub metodunda iki şey yaptık.

  • Bu metodu çağıran Client ın kendisin deki –Clients.Caller diyerek-  Client tarafta ki ConnectedToHub metodunu çalıştırmasını söyledik. Parametre olarak ta signalr ın kendi oluşturmuş olduğu Context.ConnectionId yi geçtik, çünkü kullanıcıyı kendi db mizde online olarak update etmek ve bu kullanıcıyla doğrudan haberleşmelerde bu ContextId yi kullanmamız gerekeceği için o bağlantı boyunca saklamak istedik.
  • Kullanıcı bir şekilde disconnected olduğunda signalr client tarafta zaten böyle bir event i tetikleyecek bizde kullanıcıyı offline olarak işaretleyip sakladığımı ConnectionId sini sileceğiz, taa ki yeniden bağlanıp yeni connectionId sini öğrenene kadar.
  • İkinci satırda ise, uygulamada zaten mevcut oturum açmış insanların telefonunda online user sayını arttırabilmek yani yeni birinin geldğini söyleyebilmek adına, Clients.AllExcept(Context.ConnectionId) diyerek yani bu metodu çağıran kişi dışında herkese bir sinyal göndererek client tarafta NewUserConnectedToHub metodunu çalıştırmak istediğimizi söyledik.

Hub tarafında bu ve benzeri başka metodların tanımı dışında birşey yok.

Gelelim bu signalr ı Asp.net core un request pipeline ına nasıl eklediğimize.

public void ConfigureServices(IServiceCollection services)
        {
            // CORS
            services.AddCors();

            // MVC
            var mvc = services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

            //SignalR 
            services.AddSignalR()
                    .AddAzureSignalR("Endpoint=...;AccessKey=...;Version=...;");

              ........

Startup tarafındaki ConfigureServices metoduna yukarıda gördüğünüz gibi AddSignalR diyerek SignalR service lerini kullanacağımızı söyledik. Biz bu projede signalr ın backend tarafı scale edilme yönetmi olarak Azure SignalR Service i kullandığımız için AddAzureSignalR diyerek içerisine azure tarafında oluşturmuş olduğumuz service url i ve accesskey i verdik. Azure SignalService tarafına daha sonra gelicem. Şimdilik SignalR ile devam edelim.

 public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            ...
            app.UseMvc();
            app.UseAzureSignalR(routes =>
            {
                routes.MapHub("/game");
            });
            ...
        }

Configure metodunda ise UseAzureSignalR diyerek AzureSignalService kullanacağımızı ve hub ımızı register etmiş olduk. Eğer AzureSignalService kullanmadan sadece SignalR ı eklemek isteseydik kod aşağıdaki gibi olacaktı.

app.UseSignalR(builder => builder.MapHub("/game"));

SignalR Client (Xamarin Forms)

Api tarafında Signalr ı ne şekilde kurup kullandığımızdan bahsettim. Xamarin Forms projemizde de signalr kullanmak için yaptığımız ilk şey aşağıdaki paketi tüm forms projelerine indirmekti

Microsoft.AspNetCore.SignalR.Client (1.1.0)

Bundan sonra uygulamanın App.cs tarafında aşağıdaki gibi HubConnectionBuilder  ı kullanarak kullanıcıyı hub a bağladık.

 public App()
        {
            InitializeComponent();

            Connection = new HubConnectionBuilder()
               .WithUrl("http://appserversitename.net/game")
               .Build();

            Connection.Closed += async (error) =>
            {
                IsConnectedToHub = false;
                await Task.Delay(new Random().Next(0, 15) * 1000);
                MessagingCenter.Instance.Send(this, "Disconnected");

                var disconnectResult = await ApiClient.UserApi.DisconnectUser(new Models.Models.Dto.Request.User.DisconnectUserRequestModel { Username = InMemorySettings.GetCurrentUser().Username });

                await Connection.StartAsync();
            };

            RegisterSignalrEvents();

            ....

Connection ın kapanması durumunda signalr bizim için bir event tetikliyor burada da istediğimiz diğer işlemleri yazdık.

RegisterSignalREvents metodumuz da ise, API tarafında kullanıcı tarafında şu metodları tetikle dediğimiz metodları oluşturduk. örnek olarak API tarafı client tarafında ConnectedToHub metodu tetiklediğinde ne olacağını, Connection.On metodu ile parametreleri de vererek, içeriye yazdık.

private void RegisterSignalrEvents()
        {
            Connection.On<string, string>("ConnectedToHub", (user, connectionId) =>
             {
                 Xamarin.Forms.Device.BeginInvokeOnMainThread(async () =>
                 {
                     try
                     {
                         await ApiClient.UserApi.ConnectUser(new Models.Models.Dto.Request.User.ConnectUserReqModel { ConnectionId = connectionId, Username = user });

                         var onlineUserCount = await ApiClient.UserApi.GetOnlineUsers();

                         Device.BeginInvokeOnMainThread(() =>
                         {
                             MessagingCenter.Instance.Send(this, "OnlineUserCountReceived", onlineUserCount);
                         });

                         InMemorySettings.GetCurrentUser().ConnectionId = connectionId;
                         MessagingCenter.Instance.Send(this, "Connected");

                         IsConnectedToHub = true;
                     }
                     catch (Exception ex)
                     {
                           ...
                     }
                 });
             });

Burada basitçe kullanıcıyı db de online olarak set edip, signalrdan gelen connection id sini saklayıp, uygulama tarafında başka yerleri xamarin.forms un messaging center ını kullanarak tetikliyoruz. Aşağıdaki gibi o an kaş kişi oynuyor bunu görmemiz için kullandığımız yöntem bu idi.

WhatsApp Image 2019-05-31 at 22.10.45 (1)

ApiClient.. şeklindeki haberleşme yapısına benzer yapıyı nasıl kurduğumuza bakmak için aşağıdaki yazılarımı okuyabilirsiniz.

Client tarafında bundan fazla bir numara yok.

Gelelim Azure SignalR Service i neden kullandığımıza. Daha önceleri SignalR kullandıysanız bir gerçek hayat senaryosunda ve büyük bir projede signalr ı ya azure service bus ile ya redis ile yada mssqlserver ile desteklemişsinizdir. Bunun temel sebepleri hem signalrı scalable kılmak hemde daha persistant bir yapı oluşturmak diyebiliriz. İşte artık Azure Signalr Service ile bunları düşünmekten kurtuluyorsunuz. Bu şöyle oluyor. Artık kullanıcılar eskiden olduğu gibi doğrudan sizin appserver ına bağlanmıyorlar, sizin appserver ınızdan bir url alıp (negotiation url) Signalr Service e bağlanıyorlar, sizin app server ınızda buraya bağlanıyor ve nihayetinde client ile appserver ınız arasında realtime persistant bir connection kurulmuş oluyor.

Aşağıdaki görsel bunu anlatıyor.

Capture.PNG

Asp.net Core SignalR için, Signalr Service, hub başına 5 adet websocket açıyor. Bu service i ücretsiz olarak maximum 20 concurrent user için deneyebilirsiniz. Bu da zaten test etmeniz için yeterli bir sayı. Daha sonra concurrent kullanıcınız arttıkça paralı tarifeye geçip binlerce kullanıcıyı concurrent birşekilde yönetme ve realtime birbirini bağlama işini Azure a bırakabilirsiniz.

Azure portal e girdiğiniz de All Services den Web sekmesini tıklayarak ücretsiz bir SignalR Service oluşturun ve en kısa sürede denemeye çalışın derim.

Capture.PNG

Bu yazımda da yazmış olduğumuz kelime oyunu için realtime kısmını hem server hem de client tarafta nasıl çözdük ve azure signal service i neden nasıl kullandığımızı anlattım.

Bir sonraki yazımda görüşmek üzere.

Asp.Net Core API Backend ve Xamarin.Forms İle Kelime Oyunu Bölüm 1 (Proje Yapısı, Teknolojiler ve Lottie)

Selamlar,

Eğlenceli bir seri ile karşınızdayım. Xamarin’i 2012 den beri aktif olarak takip eden ve gönülden destekleyen biri olarak her fırsatta gücünü göstermek için elimden geleni yapıyorum. Muhtemelen bir xamarin ekibini bir de DreamTheater ı bu kadar yürekten savunup reklamını yapmışımdır.  =)

Haziran ayının ilk haftası içerisinde store a atmaya hazırlandığımız ve 1 ay gibi kısa bir sürede (Normal mesai dışında) geliştirdiğimiz xamarin.forms ile yazılmış ve backend tarafında Asp.Net Core kullandığımız uygulamamızın arayüzlerinin bir kısmını, bu uygulamayı yaparken kullandığımız teknolojileri sizlerle örnekler yaparak paylaşmak istedim.

Uygulamanın ara yüz örnekleri aşağıdaki gibi.

Evet store da onlarcası olan bir kelime oyununu yaptık =) Maksat ya bu da tutarsa dan fazlası. İnsanlara xamarin ile kısa sürede yapılabilecekleri göstermek de sebepleri arasında. Tabi ekstra özellikler de koyacağız zamanla ama şuanki haliyle hem kendi kendinize hem de online kişilerle aynı anda oynayabilme seçenekleri ile storeda kilerden fonksiyonalite olarak pek bir fazlası yok, ama ui konusunda daha gösterişli olduğumuzu düşünüyoruz:)

Gelelim 1 ay gibi kısa bir sürede bu oyunu neler kullanarak tamamladık.

Solution tarafında temel projelerimiz şu şekilde idi.

  • 1 Asp.net core api projesi.
  • 1 Xamarin.Forms projesi. (Standard lib, ios ve android)
  • Geçen yazılarımda göstermiş olduğum gibi bir API client projesi
  • Boggle algoritmasını kullanarak oyunları oluşturup sakladığımız bir Console App
  • Gerisi de yardımcı Utils tadında class libraryler diyebilirim.

Daha detaylı olarak kullandığımız teknolojiler ise aşağıdak gibi;

  • Asp.Net Core SignalR
  • Azure SignalR Service
  • Azure MSSQL Database
  • EF Core
  • Asp.Net Core
  • Xamarin.Forms
  • Lottie

Evet temel seviyede bunlardan daha farklı kullandığımız birşey yoktu aslında. Klasik bir katmanlı yapı içerisinde data önden arkaya gidip geliyor 🙂

Uygulamadan bahsedecek olursak, projenin tamamlanması için önemli iki konu vardı

Birincisi parmağınızla harflerin üzerinde gezerek kelime oluşturucağınız komponent in yapımı

İkincisi de bu komponent te kelimelerin doğru şekilde yerleştirilemsi tabii ki 🙂

İkinci konuyu Boggle algoritması ile çözebiliyorsunuz. Detaylarına girmiyeceğim tabii ki ama araştırmak isteyen olursa şuradan başlayabilir.

İkinci sorunumuzun çözümü iOS tarafında basit idi, ama android tarafında 0 dan GridView oluşturup ekrana basmak durumunda kaldık. Detayına sonradan gireceğim ama şunu söylemeden edemeyeceğim CustomRenderer yazarken iOS tarafındaki şu sihirli metodu android tarafında çok arayacaksınız.

Xamarin.Forms.Platform.iOS dll i altındaki ;

protected internal virtual UIView GetControl();

Xamarin forms projelerinizde CustomRenderer yazarken artık Native taraftasınız bildiğiniz gibi. Bu GetControl, Xamarin Forms tarafındaki oluşturmuş olduğunuz componenti size ios tarafında komple UIView olarak veriyor. zaten herşey orada UIView olduğu için herşey çok güzel. Android tarafta ise 128198321 tane Widget ve Layout olduğu için(bu kadar olmasa da fazlaca)  işler daha da karışabiliyor.

Peki bazı sayfaların detaylarına girip nerede neyi nasıl kullandık kısmına gelmeden önce, yukarıda ayrıca yazmış olduğum Lottie kütüphanesinden bahsetmek istedim.

Bu o kadar başarılı bir kütüphane ki ister native ister crossplatform bir uygulamay yazıyor olun, herhangi bir yerde herhangi bir animasyon oynatmak en basit işleriniz arasına giriyor bunun sayesinde.

Bu projede bizde bolca kullandık. Bu yüzden bu yazıyı Lottie ye örneklendirme yaparak bitirmek istiyorum.

Xamarin.Forms projelerinizin tümüne yani hem kod paylaşımı yaptığınız .NetStandard hem de platform spesifik projelere projelere aşağıdaki kütüphaneyi nugetten indiriyorsunuz.

Com.Airbnb.Xamarin.Forms.Lottie

İster kendi tasarımcılarınızın çizdiği animasyonlar olsun, isterseniz hazır animasyon olsunlar Lottie ile bu animasyonu oynatmak için onun istediği formatta bir json dosyası olarak vermeniz yeterli. Bu formatı tasarımcılarınız nasıl çıkartacaklarını bilirler yada hazır bulduğunuz animasyonlar için zaten download ederken bu şekilde gelecektir.

Hazır animasyonlar için de bir ton güzel animasyonun olduğu aşağıdaki linki incelemenizi öneririm

https://lottiefiles.com/

Burada illa ki ihtiyacınız olana benzer bir animasyon bulup projenize ekleyebilirsiniz diye düşünüyorum

Peki devam edelim Lottie yi xamarin forms tarafında kullanmamıza. Nuget paketini indirdikten sonra yapacağımız şey şu;

Her iki platform spesifik projeye gidip yani iOS ta AppDelegate e, Android de ise MainActivity ye gidip normal bir plugin kullanımında yaptığımız gibi aşağıdaki kodu yazıyoruz.

AnimationViewRenderer.Init();

LoadApplication(new App()) den önce bu kodu da yerleştirdikten sonra artık kullanmaya hazırız.

Projenizde bir Xaml sayfasına açın. Namespace lerin olduğu kısma aşağıdaki namespace i ekleyin.

xmlns:forms=”clr-namespace:Lottie.Forms;assembly=Lottie.Forms”

Artık projede Lottie animasyonu kullanmaya hazırız. lottifiles sitesine gidip istediğiniz bir animasyonu indirin. Aşağıdaki gibi istediğiniz animasyonun detayına gittiğinizde bazı görsel ve hız özelliklerini de değiştirerek bilgisayarınıza download edin.

Capture

json formatında pc nize indirdiğiniz bu dosyayı iOS projenizde direk Root a (AppDelegate ile aynı yere), Android projenizde ise Assets Folder(yok ise kendiniz oluşturabilirsiniz) ı içerisine ekleyin.

Daha sonra namespaci eklediğiniz xaml sayfasına dönün ve animasyonun ekran içerisinde oynamasını istediğiniz yere aşağıdaki kodu yazın

  <forms:AnimationView WidthRequest="50" 
                       HeightRequest="20"
                       HorizontalOptions="Center" 
                       Scale="4"
                       x:Name="loadingAnimationView"
                       Animation="wave_loading.json" 
                       Loop="true" 
                       AutoPlay="true"/>      

Burada projenize eklediğiniz json dosyasının adını uzantısı ile birlikte Animation propertysine vermeniz yeterli. Burada Loop true diyerek ve AutoPlay true diyerek animasyonun ekran açılınca hemen oynamaya başlamasını ve bittiğinde baştan tekrar başlamasını söylemiş olduk. Bunların tamamını animasyonun oynamasını istediğiniz senaryonuza göre kendiniz customize edebilirsiniz.

OnClick, OnPause, OnPlay vs gibi bir çok yardımcı event ten bir çok propertysine kadar istediğiniz şekilde animasyonu evirip çevirmenize olanak sağlıyor Lottie.

Biz uygulamada ana sayfadan tutunda, günlük ödül verdiğimiz popup a kadar birçok yerde Lottie yi kullandık.

Bu uygulama da kullandıklarımızla ile ilgili şeylerden örnekler le bahsetmeye devam edeceğim.

Bir sonraki örneğimizde görüşmek üzere.

Tüm .Net Client Projelerimizde Ortak Kullanabileceğimiz Bir API Client yazalım Bölüm 2

Selamlar,

Bir önceki yazımda hazırlığını yapmış olduğumuz projemizin asıl kısmına gelelim.

Projemize ApiClient adında bir sınıf ekliyorum ve içeriğini aşağıdaki gibi dolduruyorum. Bir göz gezdirip detaylarına değinelim.


 public class ApiClient
        {
            #region fields

            private static ApiClient shared;
            private static object obj = new object();

            #endregion

            #region properties

            internal static IServiceCollection Services { get; set; }
            internal static IServiceProvider ServiceProvider { get; private set; }
            private static ApiClient Shared
            {
                get
                {
                    if (shared == null)
                    {
                        lock (obj)
                        {
                            if (shared == null)
                            {
                                shared = new ApiClient();
                            }
                        }
                    }

                    return shared;
                }
            }

            #endregion

            #region api spesific properties

            private IRandomUserApi _IRandomUserApi { get => ServiceProvider.GetRequiredService(); }

            //Exposed public static props via ApiClient.Shared 
            public static IRandomUserApi RandomUserApi{ get => Shared._IRandomUserApi; }

            #endregion

            #region ctor

            private ApiClient()
            {
                if (Services == null)
                    Services = new ServiceCollection();

                Init();
            }

            #endregion

            #region internal methods

            private void Init()
            {
                ConfigureServices(Services);
                ServiceProvider = Services.BuildServiceProvider();
            }

            private void ConfigureServices(IServiceCollection services)
            {
                services.AddTransient<ITokenService, TokenService>();

                #region AnonymousApi Configuration

                services.AddRefitClient()
                .ConfigureHttpClient(c =>
                {
                    c.BaseAddress = new Uri("http://wordy.azurewebsites.net/");
                    c.Timeout = TimeSpan.FromSeconds(10);
                })
                .AddTransientHttpErrorPolicy(p => p.RetryAsync())
                .AddHttpMessageHandler(serviceProvider =>
                {
                    var token = serviceProvider.GetRequiredService().GetToken();
                    return new AuthTokenHandler(token);
                });

                #endregion

            }

            #endregion
        }

Yukarıdakilerle alakalı olarak şurayı da okumanızı öneririm.

Burada önceki yazımda projeme eklediğim kütüphaneleri kullanmaya başlıyorum artık.

Yukarıda dönen hikaye şu;

  • Singleton bir ApiClient objem var. Tüm refit interfacelerini birer property üzerinden dışarıya expose ediyorum.
  • ConfigureServices metodunu neredeyse Asp.Net Core daki gibi birebir aynı yapmaya çalıştım. IoC conteiner ı oluşturup, refit interfacelerimi ve polly policy lerimi ilgili enpoint e register ediyor
  • Son olarak aynı şekilde yazmış olduğum DelegatingHandler larıda client ıma ekliyorum ve herşey kullanıma hazır hale geliyor.

Bundan sonra herhangi bir clien projesinde şunu yapabilirim

ApiClient.RandomUserApi.GetUser(“3”);

dediğimde Client projem her ne olursa olsun ister bir core web app ister xamarin ister başka birşey,  httpclient factory üzerinden refit, polly, delegating handler lar da kullanarak güzel yapı kurmuş oluyorum.

Bunları istediğimiz gibi şekillendirip güncelleyip, istediğimiz gibi konfigüre edebiliriz ve hiçbir client projemize dokunmamış oluruz.

Ek olarak burada ITokenService ve TokenService diye bir service yazdım. Bunu da şuna örnek olarak kullanabiliriz. Örneğin kullanıcı mobil uygulama da login olduğunda aldığımız token Xamarin.Essentials s Preference paketi ile saklayarak AuthTokenDelegating handler a parametre olarak verebilmenin örneği olsun diye koydum.

Farkli client projeler de bu ITokenService implemenatasyonlarını ayrı yazarak başka türlü yerlerde saklayıp okuyabiliriz.

Xamarin projesi için bu işimizi görecektir.

   public interface ITokenService
    {
        string GetToken();
    }

Xamarin forms projesi için implementasyon;

    public class TokenService : ITokenService
    {
        public string GetToken()
        {
            if (Preferences.ContainsKey(Constants.AuthToken))
                return Preferences.Get(Constants.AuthToken, string.Empty);
            else
                return string.Empty;
        }
    }

Bunu muhtemelen bu ApiClient projesinde değil de client projelerde register etmek daha doğru olacaktır gerçek hayat senaryolarında.

Bir sonraki yazımda görüşmek üzere.

Tüm .Net Client Projelerimizde Ortak Kullanabileceğimiz Bir API Client yazalım.

Selamlar,

Daha önceden bahsetmiştim böyle bir konuya girmek istediğimi.

Yeni gelen HttpClientFactory ile de beraber daha önceden yazmış olduğum Resilient Network Services serisinin daha kısasını ve güncellenmiş halini kütüphanelerin detayların da çok fazla boğumladan yeniden incelemek istiyorum.

Amacımız şu;

Bir ApiClient Standard kütüphanesi oluşturalım. Solution ne kadar Client projemiz var ise Xamarin, Web, Console, UWP, WPF farketmez hepsi bu kütüphane üzerinden network haberleşmelerini yapsın(Http üzerinden)

Bu işlem sırasında da önceki yazılarımızda kullandığımız refit ve polly hatta akavache yi de kullanalım. ModernHttpClient a artık çok gerek yok, çünkü proje özelliklerinden Default mu yoksa platform spesifik sınıflarımı kullanmak istediğimizi zaten belirtebiliyoruz aşağıdaki gibi.Capture.PNG

Burada HttpClient implementation ın Managed mı Default mu yoksa Android mi olduğunu seçebiliyoruz. iOS tarafında da durum aynı şekilde.

Peki çok uzatmadan bu kütüphanemizi yazmaya başlayalım ve Asp.Net Core Web App ve Xamarin projelerimizde de kullanalım bakalım.

Bir adet boş solution açıp içerisine önce bir adet Asp.Net Core Web App ve bir adet de xamarin.forms projeleri ekleyelim.

Daha sonra Add New Project diyerek Bir adet .netstandard class library ekleyelim.

Capture.PNG

Bu kütüphanenin diğer tüm client projeleri tarafından kullanılabileceğine eminiz çünkü .netstandard kütüphanesi ekledik.

Projede kullanmak istediğimiz ve resilient network services kısmında bize yardımcı olacak 3 temel şey var.

  • Polly
  • Refit
  • HttpClientFactory

Tüm bunların kullanımı için öncelikle aşağıdaki paketleri projeye teker teker ekliyoruz.

  • Microsoft.Extensions.DependencyInjection
  • Microsoft.Extensions.Http.Polly
  • Refit.HttpClientFactory
  • Xamarin.Essentials (xamarin tarafında kullanmak üzere şimdilik çok önemi yok)

Projede kullanmak için bir api yazmaktansa yine open apilardan randomuser  kullanalım.

Bunun için daha önceki yazılarımda çokça detayına girdiğim için burada konuyu uzatmadan hemen refit interface imi oluşturucam. Sonrasında da örnek bir tane delegating Handler oluşturucam

Bunlar aşağıdaki gibi;

Random user api si ile haberleşecek olan Refit Interface i;
Projede Endpoints adında bir klasör açıp içerisine aşağıdaki interface i atıyorum

Burada amaç tüm farklı endpointleri ayrı ayrı interfaceler altında toplayıp hepsini ayrı ayrı konfigüre edebilmek.

 [Headers("Content-Type : application-json")]
    public interface IRandomUserApi
    {
        [Get("/")]
        Task<string> GetUser(string results);
    }

DelegatingHandler ım.

 public class AuthTokenHandler : DelegatingHandler
    {
        private readonly string _token;
        private const string TOKEN = "Authorization";
        public AuthTokenHandler(string token)
        {
            _token = token;
        }

        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            if (!request.Headers.Contains(TOKEN) && string.IsNullOrEmpty(_token))
            {
                return new HttpResponseMessage(HttpStatusCode.BadRequest)
                {
                    Content = new StringContent("Missing auth token.")
                };
            }
            else if (!request.Headers.Contains(TOKEN) && !string.IsNullOrEmpty(_token))
            {
                request.Headers.Add(TOKEN, $"Bearer {_token}");
            }

            var response = await base.SendAsync(request, cancellationToken);

            return response;
        }
    }

Bu iki kod bloğu ile ilgili daha detaylara girmek isterseniz aşağıdaki yazılarımı okuyabilirsiniz.

Projenin temel kısımları hazır oldu. Bundan sonraki yazımda asıl sınıfımız olan ApiClient sınıfını yazıp client projelerimizde kullanacağız.

Bir sonraki yazımda görüşmek üzere.

Microsoft Xamarin Türkiye Meetup – Xamarin Forms Shell

Selamlar,

Dün Microsoft Türkiye’de gerçekleştirmiş olduğumuz etkinlikte katılımcılara yeni gelen Xamarin Forms Shell den bahsetmişim. Bu yazımda buna değinmek istedim.

Öncelikle şunu belirtmek isterim ki Xamarin.Forms Shell şuan production da kullanılmaya hazır değil diye düşünüyorum. Geliştirmeler halen devam ediyor, üzerinde daha birçok değişiklik olacaktır. Ama tanımaktan denemekten zarar gelmez. Değişiklikleride hepberaber takip ederiz.

Xamarin Forms Shell i eğer VisualStudio 2019 kurduysanız bir extension paketi kurarak File-> NewProject dediğinizde Template olarak görebilirsiniz.

Template i şuradan indirebilirsiniz.

Bu visual studio extension paketini kurduğunuz da Yeni bir xamarin forms projesi açtığınızda aşağıdaki gibi karşınıza Shell Template i gelecektir.

Capture.PNG

Bunu seçip devam ettiğiniz deki kısma geleceğiz. Ama önce hali hazırda Visual Studio 2019 kurmamış olanların Shell i nasıl deneyeceklerine gelelim.

Visual Studio 2017 de yine bir xamarin forms projesi açıp başlayın.

Sonrasında yapmanız gereken ilk şey platform spesifik projelere gidip Xamarin.Forms.Forms.Init.. den önce

  • global::Xamarin.Forms.Forms.SetFlags(“Shell_Experimental”, “Visual_Experimental”, “CollectionView_Experimental”, “FastRenderers_Experimental”);

kodunu eklemeniz. Bu kod sayesinde şuan experimental olarak geçen tüm diğer özellikleri de test etmeye başlayabilirsiniz. Örneğin CollectionView uzun zamandır beklenen bir Layout idi, FlexLayout ile kısmen sorunlarımız çözüyorduk ama bunu da denemekte fayda olacaktır şimdiden.

Bundan sonra projesine bir Xaml sayfası ekleyip onu Shell sınıfından türetmeniz yeterli olacaktır. Artık sizde bu Shell dosyası üzerinde oynayabilir, çıktıları gözlemleyebilirsiniz.

Gelelim Shell in özelliklerine;

Shell in başlıca amacı şu;

  • Tüm uygulama genelindeki navigasyonları tek bir çatı altında toplayıp, sanki bir storyboard hazırlar gibi hazırlayabilmeniz.

Shell ile beraber web e yakın bir routing mekanizması da geldi. Ama varolan tüm navigaston mekanizması yani Navigaion.Push..Pop.. vs hepsi halen geçerli tabi

Şuanda temel 4 farklı tag imiz var.

Bunlar şunlar;

  • ShellItem
  • ShellSection
  • ShellContent
  • MenuItem

Bınların uygulamaya etkileri  şöyle oluyor.

Temel menülerin dizilimi yukarıda kullanacağınız tag lara göre 3 e ayrılıyor.

  • Her bir ShellItem Sol menüde ki bir Link e karşılık geliyor.
  • Bunların içinde tanımlanmış Herbir Shell Section bottombar olarak karşımıza çıkıyor.
  • Bunun da içerisinde bir yada birden çok Shell Content tanımlarsak bunlar da topbar olarak karşımıza geliyor.

Yani aşağıdaki gibi bir xaml ın menü hiyerarşisi şu şekilde oluyor.

 
<Shell xmlns="http://xamarin.com/schemas/2014/forms" 	
	   xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" 	
	   xmlns:local="clr-namespace:TailwindTraders.Mobile.Features.Shell" 	
	   x:Class="TailwindTraders.Mobile.Features.Shell.TheShell" 	
	   Title="TailwindTraders"> 
<ShellItem Title="Home“ Route=“Home”> 
	<ShellSection Route=“Section1“>  
		<ShellContent Route=“index”> 
			<local:HomePage1 /> 
		</ShellContent>
		<ShellContent Route=“index”> 
			<local:HomePage2 /> 
		</ShellContent>
	 </ShellSection> 
	 	<ShellSection Route=“Section2“>  
		<ShellContent Route=“index”> 
			<local:HomePage3 /> 
		</ShellContent>
	 </ShellSection> 
</ShellItem> 
</Shell>

Sol tarafta tek bir menü linki. Buna tıkladığımızda, bu sayfada altta iki tab ı olan bir bottom tab. çünkü iki adet ShellSection konulmuş. İlk taba tıkladığımızda da yukarıda 2 tane tab ı olan topbar göreceğiz çünkü bunun içerisine de iki tane shell content konulmuş.

Bunun yanı sıra Flyout dediğimiz tag in Header ve Footer ile istediğimiz gibi oynayıp sol menünün görünümünü düzenleyebiliyoruz.

Routing mekanizması şu şekilde değişiyor.

RouteScheme: Url scheme such as “http” or “app”

RouteHost: The Domain portion of the URI

Route: The base segment of your application’s URI

▪Exp: “app://microsoft.com/myapp/home/section/index”

▪(App.Current.MainPage as Shell).GoToAsync(“app:///newapp/greeting?msg=Hello”);

İstersek sayfalar arasında data taşımak için querystring bile kullanabiliyoruz 🙂

[QueryProperty("Message", "msg"]
    public partial class GreetingPage : ContentPage
    {
        public GreetingPage()
        {
            InitializeComponent();
        }
 
        string _message;
        public string Message
        {
            get { return _message; }
            set
            {
                _message = value;
            }
        }
    }

Dediğim gibi şuan için yeni başlayan projelerinizde asla başlamamanızla birlikte deneysel olarak alışmanızda fayda olduğunu düşündüğümden çok fazla detaylarına girmeden Shell den bahsetmek istedim.

Daha fazla detaylara şuradan ulaşabilirsiniz.

Bir sonraki yazımda görüşmek üzere.