c# .net core blazor

First Look in ASP.Net Core Blazor

Matt 2020/07/27 17:09:24
1476

What Is Blazor ?

A brain new SPA Framework, using C#.

Blazor is a framework for building interactive client-side web UI with .NET Core:

  • Create rich interactive UIs using C# instead of JavaScript.

  • Share server-side and client-side app logic written in .NET Core.

  • Render the UI as HTML and CSS for wide browser support, including mobile browsers.

  • Integrate with modern hosting platforms, such as Docker.

  • Using .NET Core for client-side web development offers the following advantages:

  • Write code in C# instead of JavaScript.

  • Leverage the existing .NET Core ecosystem of .NET Core libraries.

  • Share app logic across server and client.

  • Benefit from .NET Core's performance, reliability, and security.

  • Stay productive with Visual Studio on Windows, Linux, and macOS.

  • Build on a common set of languages, frameworks, and tools that are stable, feature-rich, and easy to use.

We can now easily create an example of blazor app to taste it,

Add a new Project

img "Add a new Project"

Blazor Server App

img "Blazor Server App"

after we build the demo app, run it.

demo blazor project

img "run a demo"

Components

Blazor apps are based on components. A component in Blazor is an element of UI, such as a page, dialog, or data entry form.

In many modern javascript SPA frameworks, such as Angular, Vue, React, etc., the're all using Components technique a lot.

Components are .NET Core classes built into .NET Core assemblies that:

  • Define flexible UI rendering logic.
  • Handle user events.
  • Can be nested and reused.
  • Can be shared and distributed as Razor class libraries or NuGet packages.

The component class is usually written in the form of a Razor markup page with a .razor file extension. Components in Blazor are formally referred to as Razor components. Razor is a syntax for combining HTML markup with C# code designed for developer productivity. Razor allows you to switch between HTML markup and C# in the same file with IntelliSense support. Razor Pages and MVC also use Razor. Unlike Razor Pages and MVC, which are built around a request/response model, components are used specifically for client-side UI logic and composition.

!Important thing: A Component must be named by Pascal style, just like ForecastBlock.razor. With this powerful weapon, parent component could reduce its logic into child-components. Yeah, it's an implementation of "Separation of Concerns".

If you were familiar with Angular framework (or else), you should recognize what they offering.

Let's have a quick look about blazor file structure,

blazor file structure

img "blazor file structure"

basically, the structure is still very close to MVC or Razor Page projects.

And there're some strange files spotted.

- App.razor

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

App.razor in project root, it's a app entry. Usually, we don't need to change that.

It also contains a MainLayout component defined in Shared folder, it's a very first (default) layout defined under _Host.razor.

- _Imports.razor

@using System.Net.Http
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.JSInterop
@using BlazorWeather // base on your project
@using BlazorWeather.Shared  // base on your project

_Imports.razor in project root, it defines (imports) many useful namespaces. It can help us to use them quickly.

- _Host.razor

_Host.razor in Pages folder. As we can see, it's a basic layout for our app.

- MainLayout.razor

@inherits LayoutComponentBase

<div class="sidebar">
    <NavMenu />
</div>
<div class="main">
    <div class="top-row px-4">
        <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
    </div>
    <div class="content px-4">
        @Body
    </div>
</div>

MainLayout.razor in Shared folder, a default layout for our app. Look at that, it inherits LayoutComponentBase, to be recognized as a default layout by system.

Let's quickly add a new component named SurveyPrompt.razor in Shared folder.

add Razor Component

img "add Razor Component"

<div class="alert alert-secondary mt-4" role="alert">
    <span class="oi oi-pencil mr-2" aria-hidden="true"></span>
    <strong>@Title</strong>
    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold" href="https://go.microsoft.com/fwlink/?linkid=2112271" @onclick="ClickMe">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    // Demonstrates how a parent component can supply parameters, and events (functions)
    [Parameter]
    public string Title { get; set; }

    private void ClickMe()
    {
        Console.WriteLine($"Clicked!");
    }
}

The SurveyPrompt's Title are provided by the component that uses this component in its UI. ClickMe is a C# method triggered by the anchor's onclick event. All of these are supported by IntelliSense, that's really very useful, and powerful for developers.

Blazor uses natural HTML tags for UI composition. HTML elements specify components, and a tag's attributes pass values to a component's properties.

How to use that component we created?

It's also a very easy thing, in our another new component or page, just add

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

and SurveyPrompt component will be injected in it.

The SurveyPrompt is rendered when the parent (Pages/Index.razor) is accessed in a browser,

img "run a demo"

Components render into an in-memory representation of the browser's DOM called a render tree, which is used to update the UI in a flexible and efficient way.

Blazor WebAssembly

Blazor WebAssembly is a single-page app (aka SPA) framework for building interactive client-side web apps with .NET. Blazor WebAssembly uses open web standards without plugins or code transpilation and works in all modern web browsers, including mobile browsers.

Running .NET code inside web browsers is made possible by WebAssembly (abbreviated wasm). WebAssembly is a compact bytecode format optimized for fast download and maximum execution speed. WebAssembly is an open web standard and supported in web browsers without plugins.

WebAssembly code can access the full functionality of the browser via JavaScript, called JavaScript interoperability (or JavaScript interop). .NET code executed via WebAssembly in the browser runs in the browser's JavaScript sandbox with the protections that the sandbox provides against malicious actions on the client machine.

img "blazor webassembly"

When a Blazor WebAssembly app is built and run in a browser:

  • C# code files and Razor files are compiled into .NET assemblies.
  • The assemblies and the .NET runtime are downloaded to the browser.
  • Blazor WebAssembly bootstraps the .NET runtime and configures the runtime to load the assemblies for the app. The Blazor WebAssembly runtime uses JavaScript interop to handle DOM manipulation and browser API calls.

The size of the published app, its payload size, is a critical performance factor for an app's useability. A large app takes a relatively long time to download to a browser, which diminishes the user experience. Blazor WebAssembly optimizes payload size to reduce download times:

  • Unused code is stripped out of the app when it's published by the Intermediate Language (IL) Linker.
  • HTTP responses are compressed.
  • The .NET runtime and assemblies are cached in the browser.

Blazor Server

Blazor decouples component rendering logic from how UI updates are applied. Blazor Server provides support for hosting Razor components on the server in an ASP.NET Core app. UI updates are handled over a SignalR connection.

The runtime handles sending UI events from the browser to the server and applies UI updates sent by the server back to the browser after running the components.

The connection used by Blazor Server to communicate with the browser is also used to handle JavaScript interop calls.

img "blazor server"

Above all, all that things can be explained easily, by looking that animation below,

img "looped demo"

as the loop animation shown us, when the app first loaded, change to new route won't cause page load, even a event triggered. That's amazing.

The power of Blazor: when model changed, it will be triggered by SignalR from server, and update to browser's DOM tree.

The Angular might do the similar thing, but it will be triggered by detecting many things, called change detection. About that, Angular app might cause a heavy performance.

JavaScript interop

For apps that require third-party JavaScript libraries and access to browser APIs, components interoperate with JavaScript. Components are capable of using any library or API that JavaScript is able to use. C# code can call into JavaScript code, and JavaScript code can call into C# code. For more information, see the following articles:

Code sharing and .NET Standard

Blazor implements .NET Standard 2.1, which enables Blazor projects to reference libraries that conform to .NET Standard 2.1 or earlier specifications. .NET Standard is a formal specification of .NET APIs that are common across .NET implementations. .NET Standard class libraries can be shared across different .NET platforms, such as Blazor, .NET Framework, .NET Core, Xamarin, Mono, and Unity.

APIs that aren't applicable inside of a web browser (for example, accessing the file system, opening a socket, and threading) throw a PlatformNotSupportedException.

Weather Prediction

It's a great opportunity to learn a new Blazor technique for building a sample project, we called it Weather Prediction.

img "weather prediction"

How to build a good look Weather Prediction like that ?

Create data grabbing service


public class WeatherService
{
    private readonly HttpClient _httpClient;
    private readonly Cwb _cwb;

    public WeatherService(IHttpClientFactory httpClientFactory, IOptionsSnapshot<AppSettings> options)
    {
        _httpClient = httpClientFactory.CreateClient(nameof(WeatherData));
        _cwb = options?.Value?.Cwb;
    }

    public WeatherPrediction GeePrediction()
    {
        // 36 hrs
        var url = $"{_cwb?.BaseUrl}/F-C0032-001?Authorization={_cwb?.ApiKey}&locationName=%E9%AB%98%E9%9B%84%E5%B8%82";
        var jsonStr = _httpClient.GetStringAsync(url).GetAwaiter().GetResult();
        return System.Text.Json.JsonSerializer.Deserialize<WeatherPrediction>(jsonStr);
    }

    public Task<Forecast.Forecast> GetForecastAsync()
    {
        // 7 days forcast
        var url = $"{_cwb?.BaseUrl}/F-D0047-067?Authorization={_cwb?.ApiKey}&locationName=%E8%8B%93%E9%9B%85%E5%8D%80";
        return _httpClient.GetFromJsonAsync<Forecast.Forecast>(url);
    }
}

Again, we will always use HttpClient. please use it in a right way, try reading 在 .NET Core 與 .NET Framework 上使用 HttpClientFactory.

After service created, don't forget to inject it into Startup to enable it.

public void ConfigureServices(IServiceCollection services)
{
    // ... other settings

    services.AddHttpClient(nameof(WeatherData));
    services.AddScoped<WeatherService>();
}

Create a Weather.razor

I picked a Weather Widgets from here randomly, for using it as the theme.

- CurrentWeatherDetail.razor component

<div class="col-lg-4 col-md-5 pl-30 py-30">
    <div class="row">
        <div class="col-4">
            <div class="vertical-align" aria-hidden="@WxName, @WxValue">
                <i class="vertical-align-middle @WxClassName font-size-60 mt-10"></i>
            </div>
        </div>
        <div class="col-8">
            <span class="font-size-24">
                @CI
            </span>
            <span class="font-size-24 black">
                <span class="font-size-12">降雨&nbsp;</span>
                @PoP %
            </span>
            <br />
            <span class="blue-600 font-size-40">
                @MaxT °
                <span class="font-size-30">C</span>
            </span>
            <span class="font-size-18 blue-grey-700">/</span>
            <span class="font-size-18 blue-grey-700">
                @MinT °
                <span class="font-size-16">C</span>
            </span>
            <p class="font-size-14 mb-0">@DateTime.Today.ToString("yyyy/MM/dd ddd")</p>
        </div>
    </div>
</div>

@code {
    [Parameter]
    public string WxName { private get; set; }
    [Parameter]
    public string WxValue { private get; set; }
    [Parameter]
    public string WxClassName { private get; set; }
    [Parameter]
    public string CI { private get; set; }
    [Parameter]
    public string PoP { private get; set; }
    [Parameter]
    public string MaxT { private get; set; }
    [Parameter]
    public string MinT { private get; set; }
}

-ForecastBlock.razor component

<div class="col-2">
    <div class="weather-day vertical-align">
        <div class="vertical-align-middle font-size-16">
            <div class="mb-10">@WeekDay</div>
            <i class="@WxClass font-size-24 mb-10"></i>
            <div>
                @Degree °
                <span class="font-size-12">@MeasureUnit</span>
            </div>
        </div>
    </div>
</div>

@code {
    [Parameter]
    public string WeekDay { private get; set; }
    [Parameter]
    public string WxClass { private get; set; }
    [Parameter]
    public string Degree { private get; set; }
    [Parameter]
    public string MeasureUnit { private get; set; }
}

- Weather.razor itself

@page "/weather"
@inject BlazorWeather.Data.cwb.WeatherService WeatherService
@using BlazorWeather.Data.cwb
@using forecast = BlazorWeather.Data.cwb.Forecast

<h3>Weather Prediction</h3>

@if (prediction is null || forecast is null)
{
    <h3>Loading</h3>
    <img src="/images/spin.gif" />
}
else
{
    <div class="col-xxl-7">
        <!-- Card -->
        <div class="card card-shadow weather">
            <div class="card-header card-header-transparent cover overlay">
                <img class="cover-image h-xl-450" src="images/city-7-960x480.jpg" loading="lazy" alt="city-7-960x480.jpg">
                <div class="overlay-panel">
                    <span class="font-size-40 white">@prediction?.Records?.Locations?.FirstOrDefault()?.LocationName</span>
                    <div class="weather-location input-search float-right mt-10">
                        @*<input type="text" class="form-control form-control-sm grey-200" placeholder="Location">*@
                        <button type="button" class="btn btn-sm btn-default" @onclick="Reload">
                            <i class="icon wb-search grey-400" aria-hidden="true"></i>Reload
                        </button>
                    </div>
                    <p />
                    <span class="font-size-14 black">@oneDayWeatherDescription</span>
                </div>
            </div>
            <div class="card-footer bg-white">
                <div class="row no-space">
                    <CurrentWeatherDetail WxName="@currentWx?.Parameter?.ParameterName"
                                          WxValue="@currentWx?.Parameter?.ParameterValue"
                                          WxClassName="@wxClassName"
                                          CI="@currentCI?.Parameter?.ParameterName"
                                          PoP="@currentPoP?.Parameter?.ParameterName"
                                          MaxT="@currentMaxT?.Parameter?.ParameterName"
                                          MinT="@currentMinT?.Parameter?.ParameterName"></CurrentWeatherDetail>
                    <div class="col-lg-8 col-md-7">
                        <div class="row no-space text-center">
                            @{
                                var now = DateTime.Today;
                                var t = FindForecastWeatherElements(now, "T");
                                var wx = FindForecastWeatherElements(now, "Wx");
                            }
                            @for (var d = 1; d < 7; d++)
                            {
                                <ForecastBlock WeekDay="@DateTime.Today.AddDays(d).ToString("ddd")"
                                               WxClass="@WxClassMapper(FindElementValuesFromWeatherElements(wx, now.AddDays(d))?.FirstOrDefault(o => o.Measures == "自定義 Wx 單位")?.Value)"
                                               Degree="@FindElementValuesFromWeatherElements(t, now.AddDays(d))?.FirstOrDefault()?.Value"
                                               MeasureUnit="@(FindElementValuesFromWeatherElements(t, now.AddDays(d))?.FirstOrDefault()?.Measures == "攝氏度" ? "C" : "F")"></ForecastBlock>
                            }
                        </div>
                    </div>
                </div>
            </div>
        </div>
        <!-- End Card -->
    </div>
}

@code {
    private WeatherPrediction prediction;

    private Time currentWx;
    private Time currentCI;
    private Time currentPoP;
    private Time currentMaxT;
    private Time currentMinT;
    private string wxClassName;

    private forecast.Forecast forecast;

    private string oneDayWeatherDescription;

    protected override void OnInitialized()
    {
        LoadPrediction();
    }

    protected override Task OnInitializedAsync()
    {
        return Task.Run(() => LoadForecast());
    }

    private void Reload()
    {
        LoadPrediction();
        LoadForecast();
    }

    private void LoadPrediction()
    {
        // current day prediction
        prediction = WeatherService.GeePrediction();

        var now = DateTime.Now;
        currentWx = FindWeatherElementTimePart(now, "Wx"); // 天氣現象
        currentCI = FindWeatherElementTimePart(now, "CI"); // 舒適度 體感 feel like
        currentPoP = FindWeatherElementTimePart(now, "PoP"); // 降雨機率
        currentMaxT = FindWeatherElementTimePart(now, "MaxT"); // 最高溫
        currentMinT = FindWeatherElementTimePart(now, "MinT"); // 最低溫

        wxClassName = WxClassMapper(currentWx?.Parameter?.ParameterValue);
    }

    private void LoadForecast()
    {
        forecast = WeatherService.GetForecastAsync().Result;
        oneDayWeatherDescription = FindForecastWeatherElements(DateTime.Now, "WeatherDescription")?.FirstOrDefault()?.Times?.FirstOrDefault()?.ElementValues?.FirstOrDefault()?.Value;
    }
	
    private Time FindWeatherElementTimePart(DateTime time, string elementName)
    {
        var data = prediction?.Records?.Locations?.FirstOrDefault()?.WeatherElements?.FirstOrDefault(o => o.ElementName == elementName)?.Times?.FirstOrDefault(o => TimeParse(o.StartTime) <= time && time <= TimeParse(o.EndTime));
        if (data is null)
        {
            const int hoursShift = 2;
            time = time.AddHours(hoursShift);
            //retry data by 2 hours shifting
            data = prediction?.Records?.Locations?.FirstOrDefault()?.WeatherElements?.FirstOrDefault(o => o.ElementName == elementName)?.Times?.FirstOrDefault(o => TimeParse(o.StartTime) <= time && time <= TimeParse(o.EndTime));
        }
        return data;
    }

    private IEnumerable<forecast.WeatherElement> FindForecastWeatherElements(DateTime time, string elementName)
    {
        return forecast?.Records?.Locations?.FirstOrDefault()?.Deails?.FirstOrDefault().WeatherElements.Where(o => o.ElementName == elementName);
    }

    private IEnumerable<forecast.Elementvalue> FindElementValuesFromWeatherElements(IEnumerable<forecast.WeatherElement> source, DateTime time)
    {
        return source?.FirstOrDefault()?.Times?.FirstOrDefault(o => TimeParse(o.StartTime) <= time && time <= TimeParse(o.EndTime))?.ElementValues;
    }

    private static DateTime? TimeParse(string s)
    {
        // treat 12:00:00 like a day begin 00:00:00
        if (DateTime.TryParse(s.Replace("12:00:00", ""), out var _s))
            return _s;

        return default;
    }

    private static string WxClassMapper(string val)
    {
        if (string.IsNullOrEmpty(val))
        {
            return string.Empty;
        }
        else
        {
            if (val.Length <= 1)
            {
                val = $"0{val}";
            }
            switch (val)
            {
                case "01":
                    return "wi-day-sunny";

                case "02":
                    return "wi-day-cloudy";

                // ... many of them

                case "08":
                    return "wi-day-rain";

                // ... many of them

                case "21":
                    return "wi-day-rain-mix";

                case "22":
                    return "wi-day-rain-mix";

                default:
                    return "";
            }
        }
    }
}

Because of the complicated data, we created many helper functions to help us getting information needed.

Combining everything above, we will get a fresh look about the Weather Widgets.

img "weather prediction"

Of course, the blazor app still can work with Razor Pages, MVC, and APIs.

And so, add them to Startup.cs to enable them.

public void ConfigureServices(IServiceCollection services)
{
    // ... other settings

    services.AddRazorPages();
    services.AddMvcCore();
    services.AddServerSideBlazor();
    services.AddHttpClient(nameof(WeatherData));
    services.AddScoped<WeatherService>();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    //... other settings

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapRazorPages();
        endpoints.MapBlazorHub();
        endpoints.MapFallbackToPage("/_Host");
    });
}

!Important thing: When client route to a page created by Razor Pages, or MVC, it will cause page loading, because they're NOT running under Blazor's managing.

Your turn now, try it :)


References:

Introduction to ASP.NET Core Blazor

LayoutComponentBase

Tag Helpers in ASP.NET Core

Microsoft.AspNetCore.Mvc.TagHelpers Namespace

WebAssembly

ASP.NET Core Blazor hosting models

Open Weather Map

中央氣象局開放資料平臺之資料擷取API

在 .NET Core 與 .NET Framework 上使用 HttpClientFactory

HttpClient,該 using 還是 static?

System.Net.Http.Json

Matt