Progressive Web Apps(PWA)’lar gelişen web teknolojileri ve web browser’lar ile değişik deneyimleri ve kazançları kullanıcılara sağlamak adına tercih edilebilecek bir uygulama modeli. PWA’lar için, Web uygulamalarının kullanıcı deneyim avantajları ve masaüstü(desktop) uygulamalarının performans kazanımları, tarayıcı çatısı altında birleşerek, işletim sistemi farklılıklarının da ortadan kalktığı bir uygulama modeli de diyebiliriz.

Biraz daha basite indirgeyerek, tarayıcıların işletim sistemi gibi ele alındığı ve tarayıcı API’larının yeteneklerini kullanarak işletim sistemlerinden bağımsız geliştirilen, tarayıcılara yüklenen web uygulamaları diyebiliriz. Mesela twitter.com, maps.google.com gibi hepimizin oldukça sık kullandığı web siteleri PWA uygulama modeli ile geliştirilen siteler(uygulamalar). Bu sayede Twitter, işletim sistemi özelinde ayrı uygulamalar geliştirmeden, tek bir kod alt yapısı ile masaüstü uygulama(-kısmen) deneyimini sunabiliyor.

PWA özelinde daha fazla ayrıntıya girmeden, Blazor WebAssembly uygulama modeli ile nasıl Progressive Web Apps(PWA) geliştirebiliriz kısaca buna bakmaya çalışacağız. ASP.NET Core çatısı altındaki Blazor uygulama modelinin, WebAssembly yaklaşımı ile bu tarz uygulamalar geliştirmek mümkün. Açıkçası Blazor WebAssembly’nin ne olduğunun çok fazla ayrıntısına girmeyeceğim, ama kısa bir bilgi olması adına; tarayıcıda .NET Runtime’ın WebAssembly üzerinde çalışabilmesini sağlayan bir framework diyebiliriz. JavaScript’e ek olarak C# kodları yazarak da, tarayıcıda çalışan önyüz uygulamalarını Blazor WebAssembly ile geliştirebiliyoruz.

PWA ve Blazor ile ilgili bu kısa girişten sonra küçük bir örnek ile bazı kavramları biraz daha somutlaştırmak istiyorum, hem de daha derinlere dalmak isteyenlere başlangıç noktası ve bazı anahtar kelimeler olur, yani umarım.

Yine belli bir senaryo çatısı altında, aşağıdaki başlıklar ile Blazor’u, Blazor WebAssembly ve PWA yaklaşımlarını biraz anlatmaya çalışacağım. Aslında hepsi ayrı yazı olacak şekilde ele alınacak konular ama bir bütünlük içerisinde, kullanımlarını ve neler yapabilirizi daha iyi anlamak için bir arada paylaşmak istedim. Umarım faydalı olur…

Senaryomuz; bir API kaynağından döviz kurlarının karşılığını alıp gösteren, network durumuna göre offline durumda da çalışabilen basit bir WEB(PWA) uygulaması olsun. (Offff, çok yaratıcıyım… 🤦🏻‍♂️😀)

Küçük bir anektot olması adına, Blazor WebAssembly uygulamalarında, .NET Core’da işletim sistemi bilgisini almak için kullanılan
System.Runtime.InteropServices.RuntimeInformation.OSDescription, .NET 5.0.0-preview 8 ile Browser olarak sonuç dönmekte.

Blazor WebAssembly için PWA proje şablonu var mı?

PWA yaklaşımı ile bir Blazor WebAssembly projesi oluşturmak için, standart dotnet new komutunu kullanabiliyoruz. –pwa argümanı ile oluşturduğumuz Blazor WebAssembly projemiz, PWA desteği ile oluşuyor.

dotnet new blazorwasm -o {PROJECT_NAME} --pwa

Visual Studio’da görsel olarak daha kolay tabi ki. 🎉

PWA destekli bir Blazor WebAssembly projesi oluşturduğumuz da, normal Blazor WebAssembly proje çıktısı ile hemen hemen aynı içeriğe sahip bir proje yapısı ortaya çıkıyor.

Buradaki önemli fark, manifest.json ve service-worker.js dosyaları. manifest.json’ı, PWA’nın bir tanım dosyası gibi düşünebiliriz. Index.html’de <head /> tag’i arasında eklenmiş olduğunu da göreceksiniz.

Uygulamanın adı, uygulamanın nasıl açılacağı, icon’ları gibi çeşitli tanımları bu dosya içerisinde yapıyoruz. Bu tanımlara göre uygulama tarayıcıya yüklenebilir duruma gelebiliyor.

{
  "name": "Progressive Web Application with Blazor WASM",
  "short_name": "blazorwasm",
  "start_url": "./",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#03173d",
  "icons": [
    {
      "src": "icon-512.png",
      "type": "image/png",
      "sizes": "512x512"
    }
  ]
}

Bir diğer önemli fark da service-worker.js dosyası. Bu JavaScript dosyasını, tarayıcının özelliklerini ve belli API’larını web uygulamalarına sunmak için açılan bir kapı olarak düşünebiliriz. Service Worker’lar, şu an için Offline çalışmayı, cache kontrolünü ve bildirim göndermeyi sağlayan fonksiyonlar gibi özelliklere sahipler. PWA uygulamalarının gelişmesi ve gücü tamamen buradaki fonkisyonların kapasitesine kalmış bir durum. Ama belli bir standart ile gelişmekte oldukları için de tüm tarayıcılarda hemen hemen aynı(?) şekilde çalışabiliyorlar. Service Worker’lar, tarayıcıda açtığımız bir sayfanın thread’inde değil, ayrı bir thread de arka planda çalışmakta. Belli senkronizasyon fonksiyonları ve performans getirileri bu sayede sağlanmış oluyor.

manifest.json ve Service Worker kavramları Blazor WebAssembly’ye özel kavramlar değil. Hangi geliştirme alt yapısını kullanırsanız kullanın, PWA geliştirme modelinde olan kavramlar.

PWA ve bu kavramlarını daha iyi anlamak adına buradaki anlatıma da göz atmanızı tavsiye ederim.

Blazor bileşeni(Component) oluşturmak…

Açıkcası şimdi, Blazor’da bir bileşen geliştirmenin çok fazla ayrıntısına girmeyeceğim. Daha önceki Blazor yazılarında biraz daha ayrıntılı bir şekilde bulabilirsiniz. Burada bileşenlerin farklı bir özelliğinden bahsedeceğim.

Senaryomuzda, Offline-Online durumumuzu göstereceğiz demiştim. Bunun için basit bir bileşen yapacağız, Status.razor. Blazor’da bileşenler içeriklerini belli bir şablona göre gösterebilmekte. Ne demek olduğunu ve nasıl yapıldığını anlamak için bileşenimize bakalım.

@using Blazor.PWA.Components

@if (IsOnline)
{
    @Online
}
else
{
    @Offline
}

“Bu mu bileşen dediğin ????? 🤬😡” Çok bir şey yok gördüğünüz gibi. Çok basit bir if..else ifadesi ile @Online ve @Offline şeklinde, bileşenin 2 özelliğini işleme alıyoruz. Şimdi bileşenin arka tarafına bakalım, Status.razor.cs.

namespace Blazor.PWA.Components
{
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Components;
    using Microsoft.JSInterop;

    public partial class Status
    {

        [Inject]
        protected NetworkStateInterop Interop { get; set; }
        
        public bool IsOnline { get; protected set; } = true;

        [Parameter]
        public RenderFragment Online { get; set; }

        [Parameter]
        public RenderFragment Offline { get; set; }


        protected override async Task OnInitializedAsync()
        {
            await Interop.InitializeAsync(OnStatusChanged);
        }

        private void OnStatusChanged(bool isOnline)
        {
            if (IsOnline != isOnline)
            {
                IsOnline = isOnline;
                StateHasChanged();
            }
        }
    }
}

Blazor bileşenlerini için tek bir dosyada, *.razor, hem html tag’leri hem de C# kodları olacak şekilde yapmak mümkün. Karmaşıklığı yönetebilmek adına o tarz geliştirmeye çok sıcak bakmıyorum.

Burada önemli olan kısımlar, [Parameter] public RenderFragment Online { get; set; } ve [Parameter] public RenderFragment Offline { get; set; } özellikleri. Bunlar bileşen görselinin belli kısımlarının UI açısından nasıl olabileceğini parametrik olarak tanımlanmasını sağlıyor.

Daha da basitçe, Status isimli bileşen, parametrik şekilde UI parçaları alabiliyor. Daha iyi anlamak için Status isimli bu bileşenin kullanımına bakalım.

<Blazor.PWA.Components.Status>
    <Online>
        <p class="text-success">You are online</p>
        <Blazor.PWA.Components.ExchangeRate></Blazor.PWA.Components.ExchangeRate>
    </Online>
    <Offline>
        <p class="text-danger">You are offline</p>
    </Offline>
</Blazor.PWA.Components.Status>

Dikkat ederseniz, html tag’lerine benzer bir şekilde, bir şablon gibi tanımlıyoruz. <Online> tag’i içinde, RenderFragment tipindeki parametremizi, <Offline> tag’ı içinde de diğer RenderFragment parametremizi belirtiyoruz. Bu sayede Status bileşeni, parametre olarak birer HTML içeriği almış gibi olabiliyor.
Bu örnekte Online parametremiz, <p> tag’i ile nasıl bir ifade göstereceğimizi ve başka bir Blazor bileşenimizi(ExchangeRage.razor) alıyor.

Bu şekilde, uygulamalarda görsel açıdan değişmesi çok farklı durumlara bağlı olan bileşenleri yönetmek çok daha kolay olabiliyor. Bu arada altını çizmek isterim ki, bu yaklaşım tüm Blazor uygulamaları için geçerli. Sadece PWA ya da Blazor WebAssembly tipindeki uygulamalar için değil.

Blazor ile C# ve JavaScript ilişkisi…

Yukarıda Status.razor bileşeninin kodlarında fark etmişinizdir;

        [Inject]
        protected NetworkStateInterop Interop { get; set; }


şeklinde bir ifade vardı. Bileşenin bu özelliği, tarayıcının network erişimini yani tarayıcının online/offline event’lerini, C# ve JavaScript’in bir arada çalışmasıyla takip edebiliyor. Status.razor bileşenimize, ASP.NET Core’un Dependency Injection(DI) yapısı ile kendi geliştirdiğimiz NetworkStateInterop tipindeki nesnemizi bileşenimize yerleştirebiliyoruz.([Inject])

NetworkStateInterop sınıfına bakıp, JavaScript ve C#’ın birlikte nasıl çalışıyor anlamaya çalışalım.

namespace Blazor.PWA.Components
{
    using System;
    using System.Threading.Tasks;
    using Microsoft.JSInterop;

    public class NetworkStateInterop
    {
        private readonly IJSRuntime jsRuntime;
        private Action<bool> handler;

        public NetworkStateInterop(IJSRuntime jsRuntime)
        {
            this.jsRuntime = jsRuntime;
        }

        public ValueTask InitializeAsync(Action<bool> handler)
        {
            this.handler = handler;

            return jsRuntime.InvokeVoidAsync("Network.Initialize", DotNetObjectReference.Create(this));
        }

        [JSInvokable("Network.StatusChanged")]
        public void OnStatusChanged(bool isOnline) => handler?.Invoke(isOnline);
    }
}

Yukarıdaki kodun biraz daha anlamlı hale gelmesi için JavaScript kodumuza da göz atalım.

window.Network = {
    Initialize: function (interop) {

        function handler() {
            interop.invokeMethodAsync("Network.StatusChanged", navigator.onLine);
        }

        window.addEventListener('online', handler);
        window.addEventListener('offline', handler);

        if (!navigator.onLine) {
            handler();
        }
    }
};

Adım adım ilerleyerek neler oluyor bakalım. Öncelikle Blazor’ın IJSRuntime yapısı ile JavaScript methodlarını C# ile çağırabilmekteyiz. IJSRuntime metot ve özelliklerine göz atıp yapıyı daha iyi anlamak mümkün. Şimdilik örneğimiz üzerinden devam edelim.

public ValueTask InitializeAsync(Action<bool> handler)
{
    this.handler = handler;

    return jsRuntime.InvokeVoidAsync("Network.Initialize", DotNetObjectReference.Create(this));
}


NetworkStateInterop tipindeki nesnemizin bu metotu ile, JavaScript kodundaki Network diye tanımladığımız nesnenin Initialize metotunu çalıştırabiliyor. JavaScript tarafında da, addEventListener‘lar ile online ve offline event’lerini dinlemeye başlıyoruz. Aynı zamanda bir tane Action<bool> tipindeki parametre ile NetworkStateInterop sınıfımızdaki handler değişkenimizi tanımlıyoruz.

[JSInvokable("Network.StatusChanged")]
public void OnStatusChanged(bool isOnline) => handler?.Invoke(isOnline);


JavaScript tarafından çağırılabilecek metodu [JSInvokable] özelliği ile yukarıdaki gibi tanımlıyoruz. Tanımladığımız isim ile de JavaScript tarafından bu metotu çağırabiliyoruz. Bu metot çalıştığı zaman, handler değişkenimizi JavaScript tarafından gelen parametre ile tetikleyip C# tarafında istediğimizi yapabiliyoruz.

Tarayıcılardaki Developer Tools kısmından Network segmesinden Offline duruma geçtiğimizde, JavaScript tarafında dinlediğimiz online/offline event’i tetiklenecektir. Bu da NetworkStateInterop nesnesi üzerinden bileşenimize yansayacak. Buradaki yapı ve benzeri yaklaşımlar ile JavaScript event’lerini, C# tarafında yorumlamak mümkün.

Blazor WebAssembly’den API çağrımı yapmak…

Yazının sonuna doğru gelirken örnek senaryomuzdaki döviz kurlarını almadan da kısaca bahsedip yazıyı sonlandırayım. Yoksa uzadıkça uzuyor yazılar. Döviz kurlarını da gösteren ayrı bir bileşenimiz olsun, ExchangeRate.razor. İçeriği de aşağıdaki basit bir şekilde gibi;

@using System.Net.Http
@using System.Globalization
@inject HttpClient Http


<p>Today <strong>1 TRY</strong> is;</p>
<ul>
@if(EUR.HasValue){
    <li> <p><strong>EUR:</strong> @string.Format("{0:N4}", EUR) </p></li>   
}
@if(USD.HasValue){
    <li><p><strong>USD:</strong> @string.Format("{0:N4}", USD)</p></li>
}
</ul>

<p><small><em>* Last update: @LastUpdate.ToString("T",CultureInfo.InvariantCulture)</em></small></p>
<p><small><em>* Rates are fetched via https://exchangeratesapi.io/</em></small></p>

<button class="btn btn-primary" @onclick="RefreshRates">Refresh</button>

Burada önemli nokta ilk satılardaki @inject HttpClient Http ifadesi, burada yine ASP.NET Core’daki Dependency Injection yöntemi ile bir HttpClient objesini bileşene ekliyoruz. Program.cs içerisinde de zaten uygulamamızdaki servislere eklediğimizi göreceksiniz.

namespace Blazor.PWA
{
    using System;
    using System.Net.Http;
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
    using Microsoft.Extensions.DependencyInjection;
    using Blazor.PWA.Components;

    public class Program
    {
        public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("#app");

            builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri("https://api.exchangeratesapi.io/") });
            builder.Services.AddTransient<NetworkStateInterop>();

            await builder.Build().RunAsync();
        }
    }
}

Bileşenimizin C# kodlarında da, eklediğimiz HttpClient tipindeki objemiz üzerinden API çağrılarını yapıyoruz.

namespace Blazor.PWA.Components
{
    using System.Threading.Tasks;
    using Microsoft.AspNetCore.Components;
    using System.Net.Http.Json;
    using Blazor.PWA.Components.Models;
    using Microsoft.AspNetCore.Components.Web;
    using System;

    public partial class ExchangeRate : System.IDisposable
    {
        public decimal? EUR { get; set; }
        public decimal? USD { get; set; }

        public DateTime LastUpdate { get; set; }
        protected override async Task OnInitializedAsync()
        {
            await GetRates();
            LastUpdate = DateTime.Now;

        }

        private async Task GetRates()
        {
            try
            {
                ExchangeRateDTO eur = await Http.GetFromJsonAsync<ExchangeRateDTO>("latest?base=EUR&amp;symbols=TRY");
                ExchangeRateDTO usd = await Http.GetFromJsonAsync<ExchangeRateDTO>("latest?base=USD&amp;symbols=TRY");

                EUR = eur?.Rates?.Try;
                USD = usd?.Rates?.Try;
            }
            catch (System.Exception ex)
            {
                System.Console.WriteLine($"ERROR - {ex.Message}");
            }
        }
        public void Dispose()
        {

        }
        private async Task RefreshRates(MouseEventArgs e)
        {
            await GetRates();
            LastUpdate = DateTime.Now;
            StateHasChanged();
        }

    }
}

Aslında çok komplike bir yapısı yok, tarayıcıdan standart bir HTTP sorgusunu JavaScript ile çok kolay bir şekilde yapabiliyorken, aynı zamanda artık C# ile de yapabiliyor olmak da ayrıca kolay.

Bu yazıda anlatmaya çalıştığım ve örnek olarak gerçekleştirdiğim uygulamanın tüm kodlarını GitHub’a ekledim. Buradan ulaşabilirsiniz.

Bir yazının daha sonuna geldik. Umarım faydalı olmuştur. Blazor WebAssembly uygulama modeli ve Progressive Web Apps. ile ilgili az biraz ilgili uyandırabildiysem ne mutlu. Açıkcası bu iki model de yeni ve gelişmekte olan kavramlar. Eksik yanları olduğu kadar, çeşitli ihtiyaçlar için çok güçlü getirileri de var. Hep beraber ilerleyen dönemlerde neler olacak göreceğiz…

Bir sonraki yazıda görüşmek üzere, mutlu kodlamalar.