Blazor and WebAssembly examples (part of a Blazor presentation)
Get your Free Azure Account
This repository contains samples for a presentation about using C# and .NET in the browser using WebAssembly with Blazor.
👋🏻 Introduction/Overview of Blazor
This repository is continuously built and deployed using free Azure Pipelines. If you're interested in how it was setup and configured to build automatically and deploy to low cost Azure Storage Static Websites, read Deploy WebAssembly from GitHub to Azure Storage Static Websites with Azure Pipelines.
🎦 You can download the related PowerPoint presentation here.
To see how Blazor compares to other SPA frameworks like Angular, read: Angular vs. Blazor.
This section contains step-by-step instructions to execute each of the demos.
The following should be installed for the demos to work:
asm.js
and WebAssembly demosThe current version used in this repo is 3.2.0-preview1.20073.1
.
Navigate into the primes directory. First, show the speed of the JavaScript version.
cat primes-js.js
node primes-js.js
Next, show the C code.
cat primes.c
Then, compile the C code to asm.js:
emcc primes.asm.c -s WASM=0 -Os -o primes.asm.js
Show the expanded code for reference, then run the asm.js version:
cat primes-asm.js
node primes-asm.js
Show the C code:
cat primes.wasm.c
Compile the C code to WebAssembly:
emcc primes.wasm.c -s WASM=1 -Os -o primes.wasm.js
Use a simple server like http-server
to serve files in the directory. Navigate to primes.html
:
http://localhost:8080/primes.html
Open the console and show the time with JavaScript vs. WebAssembly.
Create a new Blazor project with .NET Core hosting. Run the application and step through the tabs.
Shared
project defines a WeatherForecast
class that is shared between the client and the serverStartup
Startup
on the client for similar servicesCounter.razor
FetchData.razor
uses the HttpClient
but it is injected for the correct configurationCreate a new Blazor project (no hosting, client only).
Under Shared
create a Razor view component and name it LabelSlider.razor
Paste the following html:
<input type="range" min="@Min" max="@Max" @bind="@CurrentValue" />
<span>@CurrentValue</span>
In a @code
block add:
[Parameter]
public int Min { get; set; }
[Parameter]
public int Max { get; set; }
[Parameter]
public int CurrentValue { get; set; }
Drop into the Counter
page:
<LabelSlider Min="0" Max="99" CurrentValue="@currentCount"/>
Show how the clicks update the slider, but sliding doesn't update the host page. Also note that the value only updates when you stop sliding, and it only updates on the slider and not on the "current count" (although clicking the button will update the slider, the converse isn't true.)
Inside LabelSlider
change the binding to @bind-value="@CurrentValue"
then add an additional @bind-value:event="oninput"
to refresh as it is sliding
Add an event for the current value changing and implement it (this will replace the existing CurrentValue
property)
private int _currentValue;
[Parameter]
public int CurrentValue
{
get => _currentValue;
set
{
if (value != _currentValue)
{
_currentValue = value;
CurrentValueChanged?.Invoke(value);
}
}
}
[Parameter]
public Action<int> CurrentValueChanged { get; set; }
Update the binding to @bind-CurrentValue
in Counter.razor
Run and show it is picking up the value, but not refreshing. Explain we'll cover manual UI refresh later.
Create a new client-only project.
In NuGet packages, search for and install Markdown
here
In Index.razor
add the following HTML (remove the SurveyPrompt
):
<textarea style="width: 100%" rows="5" @bind="@SourceText"></textarea>
<button @onclick="@Convert">Convert</button>
<p>@TargetText</p>
Add a @using HeyRed.MarkdownSharp
to the top
Add a @code
block:
string SourceText { get; set; }
string TargetText { get; set; }
Markdown markdown = new Markdown();
void Convert()
{
TargetText = markdown.Transform(SourceText);
}
Run and show the conversion. Explain that the bindings are "safe" and don't expand the HTML.
Create a file under wwwroot
called markupExtensions.js
and populate it with:
window.markupExtensions = {
toHtml: (txt, target) => {
const area = document.createElement("textarea");
area.innerHTML = txt;
target.innerHTML = area.value;
}
}
Reference it from index.html
under wwwroot
with <script src="./markupExtensions.js"></script>
In index.razor
remove the TargetText
references and inject the JavaScript interop: @inject IJSRuntime JsRuntime
Change the paragraph element to a reference: <p @ref="Target"/>
Update the @code
to call the JavaScript via interop
string SourceText { get; set; }
ElementReference Target;
Markdown markdown = new Markdown();
void Convert()
{
var html = markdown.Transform(SourceText);
JsRuntime.InvokeAsync<object>("markupExtensions.toHtml", html, Target);
}
Run and show the goodness. Explain Convert
could be async
and await a response if necessary
Add a class named MarkdownHost
under Shared
:
using HeyRed.MarkdownSharp;
using Microsoft.JSInterop;
namespace LibrariesInterop.Shared
{
public static class MarkdownHost
{
[JSInvokable]
public static string Convert(string src)
{
return new Markdown().Transform(src);
}
}
}
Re-run the app and from the console type. Be sure to change LibrariesInterop
to the name of your project:
alert(DotNet.invokeMethod("LibrariesInterop", "Convert", "# one\n## two \n* a \n* b"))
Explain this can also use Task
to make it asynchronous
Create a new client-only project.
Create a class under Pages
named FetchDataBase
(not to be confused with a database)
public class FetchDataBase : ComponentBase
{
[Inject]
public HttpClient Http { get; set; }
public WeatherForecast[] forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await Http.GetJsonAsync<WeatherForecast[]>
("sample-data/weather.json");
}
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF { get; set; }
public string Summary { get; set; }
}
}
These are the using statements:
using Microsoft.AspNetCore.Components;
using System;
using System.Net.Http;
using System.Threading.Tasks;
Open FetchData.razor
and remove the @Inject
line and entire @code
block
Add @inherits FetchDataBase
after the @page
directive
Run it and show it working
Create a new client-only project.
Add a class named MainModel
to the root
public class MainModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int _age = 30;
public int Age
{
get => _age;
set
{
if (value != _age)
{
_age = value;
PropertyChanged?.Invoke(value, new PropertyChangedEventArgs(nameof(Age)));
}
}
}
public int MaximumHeartRate
{
get
{
return 220 - _age;
}
}
public int TargetHeartRate
{
get
{
return (int)(0.85*MaximumHeartRate);
}
}
}
Add a using for System.ComponentModel
Register the class in Program
:
builder.Services.AddSingleton<MainModel>();
Under Shared
add Age.razor
@inject MainModel Model
Age: <span style="cursor: pointer" @onclick="@(()=>Decrement(true))">
<strong> < </strong>
</span>
<input type="range" min="13" max="120" @bind-value="Model.Age"
@bind-value:event="oninput" />
<span style="cursor: pointer" @onclick="@(()=>Decrement(false))">
<strong> > </strong>
</span>
<span>@Model.Age</span>
Add the code block:
void Decrement(bool decrement)
{
if (decrement && Model.Age > 13)
{
Model.Age -= 1;
}
if (!decrement && Model.Age < 120)
{
Model.Age += 1;
}
}
Then add HeartRate.razor
under Shared
:
@inject MainModel Model
<div>
<p>Your target heart rate is: @Model.TargetHeartRate</p>
<p>Your maximum heart rate is: @Model.MaximumHeartRate</p>
</div>
Add the new controls to Index.razor
(remove SurveyPrompt
):
<Age/>
<HeartRate/>
Run the app and show that the heart rates aren't updating
Add this @code
code to the bottom of HeartRate.razor
protected override void OnInitialized()
{
base.OnInitialized();
Model.PropertyChanged += (o, e) => StateHasChanged();
}
Re-run the app and show it working
Explain that this can be done at a higher level to automatically propagate across controls
Learn more about: MVVM support in Blazor.
SHIFT+ALT+D
key press