First Look in ASP.Net Core Blazor
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
Blazor Server App
after we build the demo app, run it.
demo blazor project
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
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
<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,
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.
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.
Above all, all that things can be explained easily, by looking that animation below,
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:
- Call JavaScript functions from .NET methods in ASP.NET Core Blazor
- Call .NET methods from JavaScript functions in ASP.NET Core Blazor
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.
How to build a good look Weather Prediction like that ?
Create data grabbing service
-
Choose data providers
-
before using these apis, we need to register as a member, to get api key
-
Build a service to grab data
-
a json data offering by cwb is quite complex, really hard to read
-
pick the right way to convert json data to classes, try NOT doing it by handmade, it's too harshly
-
grab data by HttpClitent
-
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">降雨 </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.
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
Microsoft.AspNetCore.Mvc.TagHelpers Namespace
ASP.NET Core Blazor hosting models