In my Blazor app I am making an API call to a back end server that could take some time. I need to display feedback to the user, a wait cursor or a "spinner" image. How is this done in Blazor?
I have tried using CSS and turning the CSS on and off but the page is not refreshed until the call is completed. Any suggestions would be greatly appreciated.
#functions {
UserModel userModel = new UserModel();
Response response = new Response();
string errorCss = "errorOff";
string cursorCSS = "cursorSpinOff";
protected void Submit()
{
//Show Sending...
cursorCSS = "";
this.StateHasChanged();
response = Service.Post(userModel);
if (response.Errors.Any())
{
errorCss = "errorOn";
}
//turn sending off
cursorCSS = "cursorSpinOff";
this.StateHasChanged();
}
}
Option 1: Using Task.Delay(1)
Use an async method.
Use await Task.Delay(1) or await Task.Yield(); to flush changes
private async Task AsyncLongFunc() // this is an async task
{
spinning=true;
await Task.Delay(1); // flushing changes. The trick!!
LongFunc(); // non-async code
currentCount++;
spinning=false;
await Task.Delay(1); // changes are flushed again
}
Option 1 is a simple solution that runs ok but looks like a trick.
Option 2: Using Task.Run() (not for WebAssembly)
On January'2020. #Ed Charbeneau published BlazorPro.Spinkit project enclosing long processes into task to don't block the thread:
Ensure your LongOperation() is a Task, if it is not, enclose it into a Task and await for it:
async Task AsyncLongOperation() // this is an async task
{
spinning=true;
await Task.Run(()=> LongOperation()); //<--here!
currentCount++;
spinning=false;
}
Effect
Spinner and server side prerendering
Because Blazor Server apps use pre-rendering the spinner will not appear, to show the spinner the long operation must be done in OnAfterRender.
Use OnAfterRenderAsync over OnInitializeAsync to avoid a delayed server-side rendering
// Don't do this
//protected override async Task OnInitializedAsync()
//{
// await LongOperation();
//}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await Task.Run(()=> LongOperation());//<--or Task.Delay(0) without Task.Run
StateHasChanged();
}
}
More samples
Learn more about how to write nice spinner you can learn from open source project BlazorPro.Spinkit, it contains clever samples.
More Info
See Henk Holterman's answer with blazor internals explanation.
Lot's of great discussion surrounding StateHasChanged(), but to answer OP's question, here's another approach for implementing a spinner, universally, for HttpClient calls to a backend API.
This code is from a Blazor Webassembly app...
Program.cs
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(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<SpinnerService>();
builder.Services.AddScoped<SpinnerHandler>();
builder.Services.AddScoped(s =>
{
SpinnerHandler spinHandler = s.GetRequiredService<SpinnerHandler>();
spinHandler.InnerHandler = new HttpClientHandler();
NavigationManager navManager = s.GetRequiredService<NavigationManager>();
return new HttpClient(spinHandler)
{
BaseAddress = new Uri(navManager.BaseUri)
};
});
await builder.Build().RunAsync();
}
SpinnerHandler.cs
Note: Remember to uncomment the artificial delay. If you use the out-of-the-box Webassembly template in Visual Studio, click the Weather Forecast to see a demo of the spinner in action.
public class SpinnerHandler : DelegatingHandler
{
private readonly SpinnerService _spinnerService;
public SpinnerHandler(SpinnerService spinnerService)
{
_spinnerService = spinnerService;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
_spinnerService.Show();
//await Task.Delay(3000); // artificial delay for testing
var response = await base.SendAsync(request, cancellationToken);
_spinnerService.Hide();
return response;
}
}
SpinnerService.cs
public class SpinnerService
{
public event Action OnShow;
public event Action OnHide;
public void Show()
{
OnShow?.Invoke();
}
public void Hide()
{
OnHide?.Invoke();
}
}
MainLayout.razor
#inherits LayoutComponentBase
<div class="page">
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<div class="top-row px-4">
About
</div>
<div class="content px-4">
#Body
<Spinner />
</div>
</div>
</div>
Spinner.razor
Note: To add some variety, you could generate a random number in the OnIntialized() method, and use a switch statement inside the div to pick a random spinner type. In this method, with each HttpClient request, the end user would observe a random spinner type. This example has been trimmed to just one type of spinner, in the interest of brevity.
#inject SpinnerService SpinnerService
#if (isVisible)
{
<div class="spinner-container">
<Spinner_Wave />
</div>
}
#code
{
protected bool isVisible { get; set; }
protected override void OnInitialized()
{
SpinnerService.OnShow += ShowSpinner;
SpinnerService.OnHide += HideSpinner;
}
public void ShowSpinner()
{
isVisible = true;
StateHasChanged();
}
public void HideSpinner()
{
isVisible = false;
StateHasChanged();
}
}
Spinner-Wave.razor
Credit to: https://tobiasahlin.com/spinkit/
Note: There is a Nuget package for this spin kit. The drawback to the Nuget package is that you don't have direct access to the CSS to make tweaks. Here I've tweaked thee size of the spinner, and set the background color to match the site's primary color, which is helpful if you are using a CSS theme throughout your site (or perhaps multiple CSS themes)
#* Credit: https://tobiasahlin.com/spinkit/ *#
<div class="spin-wave">
<div class="spin-rect spin-rect1"></div>
<div class="spin-rect spin-rect2"></div>
<div class="spin-rect spin-rect3"></div>
<div class="spin-rect spin-rect4"></div>
<div class="spin-rect spin-rect5"></div>
</div>
<div class="h3 text-center">
<strong>Loading...</strong>
</div>
<style>
.spin-wave {
margin: 10px auto;
width: 200px;
height: 160px;
text-align: center;
font-size: 10px;
}
.spin-wave .spin-rect {
background-color: var(--primary);
height: 100%;
width: 20px;
display: inline-block;
-webkit-animation: spin-waveStretchDelay 1.2s infinite ease-in-out;
animation: spin-waveStretchDelay 1.2s infinite ease-in-out;
}
.spin-wave .spin-rect1 {
-webkit-animation-delay: -1.2s;
animation-delay: -1.2s;
}
.spin-wave .spin-rect2 {
-webkit-animation-delay: -1.1s;
animation-delay: -1.1s;
}
.spin-wave .spin-rect3 {
-webkit-animation-delay: -1s;
animation-delay: -1s;
}
.spin-wave .spin-rect4 {
-webkit-animation-delay: -0.9s;
animation-delay: -0.9s;
}
.spin-wave .spin-rect5 {
-webkit-animation-delay: -0.8s;
animation-delay: -0.8s;
}
##-webkit-keyframes spin-waveStretchDelay {
0%, 40%, 100% {
-webkit-transform: scaleY(0.4);
transform: scaleY(0.4);
}
20% {
-webkit-transform: scaleY(1);
transform: scaleY(1);
}
}
##keyframes spin-waveStretchDelay {
0%, 40%, 100% {
-webkit-transform: scaleY(0.4);
transform: scaleY(0.4);
}
20% {
-webkit-transform: scaleY(1);
transform: scaleY(1);
}
}
</style>
It's beautiful
To answer the notice in #daniherrera's solution, there is three more elegant solution proposed here.
In short :
Implement INotifyPropertyChanged to the Model and invoke StateHasChanged() on a PropertyChangedEventHandler event property from the Model.
Use delegates to invoke StateHasChanged() on the Model.
Add a EventCallBack<T> parameter to the component or page of the View and assign it to the function that should change the render of the component and their parents. (StateHasChanged() isn't necessary in this one`)
The last option is the most simple, flexible and high level, but choose at your convenience.
Overall, I'll advise to use one of those solutions presented more than the await Task.Delay(1); one if security of your app is a concern.
Edit : After more reading, this link provide a strong explanation on how to handle events in C#, mostly with EventCallBack.
Don't do the same mistake as I did by testing wait spinner using Thread.Sleep(n).
protected override async Task OnInitializedAsync()
{
// Thread.Sleep(3000); // By suspending current thread the browser will freeze.
await Task.Delay(3000); // This is your friend as dani herrera pointed out.
// It creates a new task that completes
// after a specified number of milliseconds.
forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
}
Not just for API call, but for every service call:
SpinnerService:
public class SpinnerService
{
public static event Action OnShow;
public static event Action OnHide;
public void Show()
{
OnShow?.Invoke();
}
public void Hide()
{
OnHide?.Invoke();
}
}
Spinner component:
Your spinner hier, in my case I have MudProgressCircular
#if (IsVisible)
{
<MudProgressCircular Color="Color.Primary"
Style="position: absolute;top: 50%;left: 50%;"
Indeterminate="true" />
}
#code{
protected bool IsVisible { get; set; }
protected override void OnInitialized()
{
SpinnerService.OnShow += ShowSpinner;
SpinnerService.OnHide += HideSpinner;
}
public void ShowSpinner()
{
IsVisible = true;
StateHasChanged();
}
public void HideSpinner()
{
IsVisible = false;
StateHasChanged();
}
}
ServiceCaller:
public class ServiceCaller
{
private readonly IServiceProvider services;
private readonly SpinnerService spinnerService;
public ServiceCaller(IServiceProvider services, SpinnerService spinnerService)
{
this.services = services;
this.spinnerService = spinnerService;
}
public async Task<TResult> CallAsync<TService, Task<TResult>>(Func<TService, TResult> method)
where TService : class
{
var service = this.services.GetRequiredService<TService>();
try
{
spinnerService.Show();
await Task.Delay(500); // ToDo: this line is not necessary
TResult? serviceCallResult = await Task.Run(() => method(service));
return serviceCallResult;
}
finally
{
spinnerService.Hide();
}
}
public async Task CallAsync<TService, TAction>(Func<TService, Action> method)
where TService : class
{
var service = this.services.GetRequiredService<TService>();
try
{
spinnerService.Show();
await Task.Delay(500); // ToDo: this line is not necessary
await Task.Run(() => method(service).Invoke());
}
finally
{
spinnerService.Hide();
}
}
}
How to use it?
#page "/temp"
#inject ServiceCaller serviceCaller;
<h3>Temp Page</h3>
<MudButton OnClick="CallMethodReturnsString">CallMethodReturnsString</MudButton>
<MudButton OnClick="CallVoidMethodAsync">CallVoidMethodAsync</MudButton>
<MudButton OnClick="CallTaskMethodAsync">CallTaskMethodAsync</MudButton>
<MudButton OnClick="CallMany">CallMany</MudButton>
#if (!string.IsNullOrEmpty(tempMessage)){
#tempMessage
}
#code{
string tempMessage = string.Empty;
// call method returns string
private async Task CallMethodReturnsString()
{
await serviceCaller.CallAsync<ITempService, string>(async x => this.tempMessage = await x.RetrieveStringAsync());
}
// call void method
private async Task CallVoidMethodAsync()
{
await serviceCaller.CallAsync<ITempService, Task>(x => () => x.MyVoidMethod());
}
// call task method
private async Task CallTaskMethodAsync()
{
await serviceCaller.CallAsync<ITempService, Task>(x => () => x.TaskMethod());
}
// call many methods
private async Task CallMany()
{
await serviceCaller.CallAsync<ITempService, Action>(x => async () =>
{
this.tempMessage = await x.RetrieveStringAsync();
x.MyVoidMethod();
x.TaskMethod();
});
}
}
use InvokeAsync(StateHasChanged), hopefully it will work.
protected async void Submit()
{
//Show Sending...
cursorCSS = "";
this.StateHasChanged();
response = Service.Post(userModel);
if (response.Errors.Any())
{
errorCss = "errorOn";
}
//turn sending off
cursorCSS = "cursorSpinOff";
await InvokeAsync(StateHasChanged);
}
Blazor Serverside - I needed to call StateHasChanged() to force the frontend to update so the spinner would show before the code moves onto the ajax call.
/* Show spinner */
carForm.ShowSpinner = true;
/* Force update of front end */
StateHasChanged();
/* Start long running API/Db call */
await _carRepository.Update(item);
Related
I'm working on a Blazor Server project using the default Microsoft Identity Platform.
My goal is to get/create a user in my db and save it to local storage after microsoft login is completed.
In the startup I'm sucessfully able to use the OnTokenValidated event to do some action after login. However it's not possible to write to local storage in this stage since the page isn't rendered yet.
I'd like to do something like this which is possible with Webassembly.
<RemoteAuthenticatorView Action="#Action" OnLogInSucceeded="SomeCode" />
Does anyone know a way to do this without using a solution like adding OnAfterRenderAsync in the MainLayout, which will fire on each page reload. I'd like to call a method after the Identity login redirects back to my site in a state where LocalStorage is accessible.
You do need to put some code in OnAfterRenderAsync, but in this demo I've put it in App and check if the component has already rendered.
Here's a demo and some code to interact with LocalStorage. You should be able to adapt it to fit your needs.
First a service to encapsulate getting and setting to Local Storage
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
public class LocalStorageService
{
private readonly ProtectedLocalStorage _storage;
public LocalStorageService(ProtectedLocalStorage storage)
=> _storage = storage;
public async ValueTask<CommandResult> SaveAsync<TRecord>(CommandRequest<TRecord> request)
{
if (request.Record is not null)
await _storage.SetAsync(request.StorageName, request.Record);
// No return so we return success!
return CommandResult.Success();
}
public async ValueTask<RecordQueryResult<TRecord>> ReadAsync<TRecord>(RecordQueryRequest<TRecord> request)
{
// We need to cover the situation were the component calling this is in the initial page
// and Blazor server is trying to statically render the page
try
{
var result = await _storage.GetAsync<TRecord>(request.StorageName);
return new RecordQueryResult<TRecord> { Successful = result.Success, Record = result.Value, Message = $"Failed to retrieve a value for {request.StorageName}" };
}
catch
{
return new RecordQueryResult<TRecord> { Successful = false, Message = $"Failed to retrieve a value for {request.StorageName}" };
}
}
}
The CQS Request and Result objects:
public record CommandRequest<TRecord>(string StorageName, TRecord Record);
public record RecordQueryRequest<TRecord>(string StorageName);
public record CommandResult
{
public bool Successful { get; init; }
public string Message { get; init; } = string.Empty;
public static CommandResult Success()
=> new CommandResult { Successful = true };
public static CommandResult Failure(string message)
=> new CommandResult { Successful = false };
}
public record RecordQueryResult<TRecord>
{
public TRecord? Record { get; init; }
public bool Successful { get; init; }
public string Message { get; init; } = string.Empty;
public static RecordQueryResult<TRecord> Success(TRecord record)
=> new RecordQueryResult<TRecord> { Record = record, Successful = true };
public static RecordQueryResult<TRecord> Failure(string message)
=> new RecordQueryResult<TRecord> { Successful = false };
}
Registered like this:
builder.Services.AddScoped<LocalStorageService>();
My simple Data:
public record TestData( string LastSaved);
Add code to App to set as if you are getting data after login. This implements a custom after render handler.
#inject LocalStorageService Service
#implements IHandleAfterRender
<CascadingAuthenticationState>
<Router AppAssembly="#typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="#routeData" DefaultLayout="#typeof(MainLayout)" />
<FocusOnNavigate RouteData="#routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="#typeof(MainLayout)">
<p role="alert">Sorry, theres nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
#code {
private bool _hasCalledOnAfterRender;
// implements a custom IHandleAfterRender handler
async Task IHandleAfterRender.OnAfterRenderAsync()
{
// Only do if first render and the data in local storage is empty
if (!_hasCalledOnAfterRender && !await GetData())
{
var newData = new TestData($"Saved at {DateTime.Now.ToLongTimeString()}");
var result = await this.Service.SaveAsync<TestData>(new CommandRequest<TestData>("TestData", newData));
_hasCalledOnAfterRender = true;
}
}
private async Task<bool> GetData()
{
var result = await this.Service.ReadAsync<TestData>(new RecordQueryRequest<TestData>("TestData"));
return result?.Successful ?? false;
}
}
And my test route/page to display the data.
#page "/"
#inject LocalStorageService Service
#implements IDisposable
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<SurveyPrompt Title="How is Blazor working for you?" />
<div class="bg-black text-white m-3">
Test Data Last Saved at : #this.data.LastSaved
</div>
<div class="m-3">
<button class="btn btn-primary" #onclick=SaveToLocal>Save Data to Local</button>
</div>
#code {
private TestData data = new TestData(string.Empty);
protected override async Task OnInitializedAsync()
{
await this.GetData();
this.Service.StorageChanged += this.DataChanged;
}
private async void DataChanged(object? sender, EventArgs e)
{
await this.GetData();
await this.InvokeAsync(StateHasChanged);
}
private async Task<bool> GetData()
{
var result = await this.Service.ReadAsync<TestData>(new RecordQueryRequest<TestData>("TestData"));
data = result?.Record ?? new TestData(string.Empty);
return result?.Successful ?? false;
}
private async Task SaveToLocal()
{
var newData = new TestData($"Saved at {DateTime.Now.ToLongTimeString()}");
var result = await this.Service.SaveAsync<TestData>(new CommandRequest<TestData>("TestData", newData));
await this.GetData();
}
public void Dispose()
=> this.Service.StorageChanged -= this.DataChanged;
}
Let's say I have the following code in MainLayout.razor
<CascadingValue Value="#(async () => await someclass.GetValueAsnyc())" Name="asyncValue">
<article class="content px-4">
#Body
</article>
</CascadingValue>
And in Index.razor:
<h1>Hello!</h1>
#code{
[CascadingParameter(Name = "asyncValue")] public string AsyncValue{ get; set; }
protected override async Task OnInitializedAsync()
{
//do something with AsyncValue- it is always null
}
}
My assumption is that the declaration of the variable in Index.razor isn't awaiting the cascading value in Mainlayout.razor. It does work is I call the method synchronsly with .Result at the end, but that's not what I would like to do if possible.
Does anyone have any guidance or can point me in the right direction so that the cascading value loads asynchronously?
I think this should help.
First a consumer component. The cascaded value is a Task which may or may not have completed. To consume it you await it. If it's already completed the there's no awaiting to do. It provides the result immediately.
<h3>#this.value</h3>
<button class="btn btn-primary" #onclick=this.OnClick>Get Value Again</button>
#code {
private string value = string.Empty;
[CascadingParameter] private Task<string>? DataTask { get; set; }
protected override async Task OnInitializedAsync()
{
if (this.DataTask is not null)
this.value = await DataTask;
}
private async Task OnClick()
{
if (this.DataTask is not null)
this.value = await DataTask;
}
}
And a test page. I haven't put it in a layout or App as it's easier to see what's happening in a test page. The cascaded value is a Task<string> field that you assign a method to.
#page "/Test"
<PageTitle>Test</PageTitle>
<CascadingValue Value="this.DataTask">
<MyComponent />
</CascadingValue>
#code {
private Task<string>? DataTask;
protected override Task OnInitializedAsync()
{
DataTask = GetSomeDateAsync();
return Task.CompletedTask;
}
private async Task<string> GetSomeDateAsync()
{
await Task.Delay(5000);
return DateTime.Now.ToLongTimeString();
}
}
This is basically how the AuthenticationState cascade works.
The #onpaste is a blazor DOM event executed when content is pasted in an input.
now, I want to get this text content from typescript (blazor doesn't support it).
razor:
#inject IJSRuntime _Js
<input class="otp-input"
#ref="myInputRef"
#onpaste="HandleOnPaste" />
#code {
private ElementReference myInputRef { get; set; }
private async Task HandleOnPaste()
{
var pastedData = await _js.InvokeAsync<string?>("OtpInput.getPastedData")
// DO Something
}
}
ts:
class OtpInput {
static getPastedData(e: ClipboardEvent) {
let pastedData = e.clipboardData!.getData('text');
return pastedData;
}
}
this does not work and return error: TypeError: Cannot read properties of undefined (reading 'clipboardData')
You can use a different approach. Add event listener to the input element using javascript then call dotnet method and pass the event data. I use javascript but you can modify accordingly for typescript.
pasteInteropHelper.js:
export function addOnPasteEventListener(elem, componentInstance) {
elem.onpaste = (e) => {
var text = e.clipboardData.getData('text');
componentInstance.invokeMethod('HandlePaste', text);
}
}
razor component:
#inject IJSRuntime JS
<input #ref="_inputRef" />
#code {
private ElementReference _inputRef;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./pasteInteropHelper.js");
var selfReference = DotNetObjectReference.Create(this);
await module.InvokeVoidAsync("addOnPasteEventListener", _inputRef, selfReference);
}
}
[JSInvokable]
public void HandlePaste(string text)
{
Console.WriteLine(text);
}
}
I have made a website with Blazor Server side. I am using Twilio Voice Api to enable the user to make an emergency call. I have been able to make the call successfully. When I leave the page and then go back to the page to make the call again I am receiving this error:
Error: Microsoft.JSInterop.JSException: Cannot read property 'setToken' of undefined
TypeError: Cannot read property 'setToken' of undefined
at a.register (https://media.twiliocdn.com/sdk/js/client/v1.3/twilio.min.js:39:420)
at Function.setup (https://media.twiliocdn.com/sdk/js/client/v1.3/twilio.min.js:47:126)
at Object.setup (https://localhost:44320/js/tw.js:6:23)
at https://localhost:44320/_framework/blazor.server.js:8:31619
at new Promise (<anonymous>)
at e.beginInvokeJSFromDotNet (https://localhost:44320/_framework/blazor.server.js:8:31587)
at https://localhost:44320/_framework/blazor.server.js:1:20052
at Array.forEach (<anonymous>)
at e.invokeClientMethod (https://localhost:44320/_framework/blazor.server.js:1:20022)
at e.processIncomingData (https://localhost:44320/_framework/blazor.server.js:1:18006)
at Microsoft.JSInterop.JSRuntime.InvokeWithDefaultCancellation[T](String identifier, Object[] args)
at Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(IJSRuntime jsRuntime, String identifier, Object[] args)
at QUBeMyGuest.Pages.GuestArrivals.EmergencyContact.OnAfterRenderAsync(Boolean firstRender) in C:Pages\GuestArrivals\EmergencyContact.razor:line 75
at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle)
My api is
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Twilio.Jwt;
using Twilio.Jwt.AccessToken;
using Twilio.Jwt.Client;
using Twilio.TwiML;
using Twilio.Types;
using System.Net.Http;
namespace api.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TwilioBackEndController : ControllerBase
{
public readonly string AccountSid = "xxxxxx";
public readonly string AuthToken = "xxxx";
public readonly string AppSid = "xxxxx";
public readonly string PhoneNumber = "xxxxx";
[HttpGet("token")]
public async Task<IActionResult> GetToken()
{
var scopes = new HashSet<IScope>
{
new OutgoingClientScope(AppSid),
new IncomingClientScope("tester")
};
var capability = new ClientCapability(AccountSid, AuthToken, scopes: scopes);
return await Task.FromResult(Content(capability.ToJwt(), "application/jwt"));
}
[HttpPost("voice")]
public async Task<IActionResult> PostVoiceRequest([FromForm] string phone)
{
var destination = !phone.StartsWith('+') ? $"+{phone}" : phone;
var response = new VoiceResponse();
var dial = new Twilio.TwiML.Voice.Dial
{
CallerId = PhoneNumber
};
dial.Number(new PhoneNumber(destination));
response.Append(dial);
return await Task.FromResult(Content(response.ToString(), "application/xml"));
}
}
}
My emergency contact page is:
#page "/guest/emergencycall"
#using System.ComponentModel.DataAnnotations
#inject HttpClient httpClient
#using Microsoft.Extensions.DependencyInjection
#using System.Net.Http
<EditForm Model="Input" OnValidSubmit="InitiatePhoneCall">
<DataAnnotationsValidator />
<ValidationSummary />
<p>
<label for="phoneNumber">Enter Phone Number:</label>
<InputText id="phoneNumber" #bind-Value="Input.PhoneNumber"></InputText>
<button type="submit" class="btn btn-primary" disabled="#IsDialDisabled">DIAL</button>
<button type="button" id="endBtn" class="btn btn-primary" disabled="#IsEndDisabled" #onclick="EndPhoneCall">END</button>
<button type="button" id="clearBtn" class="btn btn-primary" disabled="#IsClearDisabled" #onclick="ClearPhoneNumber">CLEAR</button>
</p>
</EditForm>
<hr />
#if (Logs.Count == 0)
{
<p>No Logs available yet</p>
}
else
{
<ul>
#foreach (var log in Logs)
{
<li>#log</li>
}
</ul>
}
#code {
private string _tokenUrl = "https://4b4cd1derdsb.ngrok.io/api/twiliobackend";
private bool appSetupRun = false;
protected bool IsDialDisabled { get; set; } = false;
protected bool IsEndDisabled { get { return !IsDialDisabled; } }
protected bool IsClearDisabled { get { return string.IsNullOrEmpty(Input.PhoneNumber); } }
protected List<string> Logs { get; set; } = new List<string>();
protected InputModel Input { get; set; } = new InputModel();
[Inject]
protected IJSRuntime JSRuntime { get; set; }
[Inject]
protected IHttpClientFactory HttpClientFactory { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender && !appSetupRun)
{
var token = await GetClientToken();
await JSRuntime.InvokeVoidAsync("appFunctions.setup", token);
appSetupRun = true;
}
}
protected async Task InitiatePhoneCall()
{
IsDialDisabled = true;
await LogMessage($"Calling the number {Input.PhoneNumber}");
await JSRuntime.InvokeVoidAsync("appFunctions.placeCall", Input.PhoneNumber);
await LogMessage($"Called the number {Input.PhoneNumber}");
StateHasChanged();
}
protected async Task EndPhoneCall()
{
IsDialDisabled = false;
await LogMessage($"Ending the call to {Input.PhoneNumber}");
await JSRuntime.InvokeVoidAsync("appFunctions.endCall");
await LogMessage($"Ended the call to {Input.PhoneNumber}");
StateHasChanged();
}
protected async Task ClearPhoneNumber()
{
await LogMessage("Clearing the phone number entry");
Input.PhoneNumber = string.Empty;
await LogMessage("Cleared the phone number entry");
StateHasChanged();
}
private async Task<string> GetClientToken()
{
var uri = new Uri(_tokenUrl);
using var client = HttpClientFactory.CreateClient();
var response = await client.GetAsync(uri);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
[JSInvokable]
public async Task LogMessage(string message)
{
Logs.Add($"{DateTimeOffset.Now} - {message}");
await Task.CompletedTask;
}
public class InputModel
{
[Required]
[Phone(ErrorMessage = "Please enter your phone number in a proper format")]
public string PhoneNumber { get; set; }
}
}
and my javascript:
window.appFunctions = {
setup: function (token) {
console.log('Getting connected');
// Setup Twilio Device
Twilio.Device.setup(token);
Twilio.Device.ready(() => {
console.log('We are connected and ready to do the thing');
});
Twilio.Device.error((err) => {
console.error('This should not have been reached. We need to do something here');
console.error(err);
});
},
placeCall: function (destination) {
console.log(`Calling ${destination}`);
Twilio.Device.connect({ phone: destination });
console.log(`Successfully called ${destination}`);
},
endCall: function () {
console.log('Ending the call');
Twilio.Device.disconnectAll();
console.log('Successfully ended the call');
}
};
Any suggestions would be great
From my little experience
when Microsoft.JSInterop.JSException occur , it's often from when you call javascript
In your code please validate javascript functions which called from JSRuntime
[Inject]
protected IJSRuntime JSRuntime { get; set; }
...
await JSRuntime.InvokeVoidAsync("appFunctions.setup", token);
await JSRuntime.InvokeVoidAsync("appFunctions.placeCall", Input.PhoneNumber);
await JSRuntime.InvokeVoidAsync("appFunctions.endCall");
and becuase setToken keyword is always used for communication setup 's function name between web and api. I guess in javascript function appFunctions.setup , some object became null after you go back to the page so It can't initialize.
as you can see in
TypeError: Cannot read property 'setToken' of undefined
at a.register (https://media.twiliocdn.com/sdk/js/client/v1.3/twilio.min.js:39:420)
at Function.setup (https://media.twiliocdn.com/sdk/js/client/v1.3/twilio.min.js:47:126)
at Object.setup (https://localhost:44320/js/tw.js:6:23)
your javascript function in file tw.js(may be line 23?) is going to use some null object for set up communicate?
When a property of my ViewModel is updated, other property is updated asynchronous.
Todo.cshtml:
#page "/todo"
<h1>Todo (#todos.Count(todo => !todo.IsDone))</h1>
<ul>
#foreach (var todo in todos)
{
<li>
<input type="checkbox" bind="#todo.IsDone" />
<input bind="#todo.Title" />
</li>
}
</ul>
<input placeholder="Something todo" bind="#newTodo"/>
<button onclick="#AddTodo">Add todo</button>
#functions {
IList<TodoItem> todos = new List<TodoItem>();
string newTodo;
void AddTodo()
{
if (!string.IsNullOrWhiteSpace(newTodo))
{
todos.Add(new TodoItem { Title = newTodo });
newTodo = string.Empty;
}
}
}
TodoItem.cs:
public class TodoItem
{
private bool _isDone;
public string Title { get; set; }
public bool IsDone
{
get => _isDone;
set
{
_isDone = value;
Task.Run(() =>
{
//Simulate work
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2));
//Update property
Title = Title + " - Done";
});
}
}
}
In synchronous (without Task.Run) this work fine, but in asynchronous the UI isn't updated.
I need explain the UI to update with StateHasChanged():
https://github.com/aspnet/Blazor/issues/1413
But I can't call this method in TodoItem (and I don't want TodoItem know Blazor component).
Have you a solution to update the UI?
You should do the following:
Define an action delegate in your class:
public event Action OnChange;
In this very class define a method NotifyStateChanged() as follows:
private void NotifyStateChanged() => OnChange?.Invoke();
This method triggers the OnChange event. You should call this method from your logic after fulfilling whatever task it does.
In your todo Component, add the StateHasChanged method to the event delegate used in your TodoItem class thus:
#functions
{
protected override void OnInit()
{
state.OnChange += StateHasChanged;
}
}
Easy answer is "just fire StateHasChanged(); after modify your var":
Task.Run(() =>
{
//Simulate work
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2));
//Update property
Title = Title + " - Done";
StateHasChanged();
});
Because your method is async, rewrite as:
Task.Run(async () => //<--here async
{
//Simulate async work
Task.Run( async () => {
await Task.Run( () => {} ); //<--- await
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2));
});
//Update property
Title = Title + " - Done";
StateHasChanged();
});
To avoid anti-pattern and write clean code, your ModelView may have a public event to let know UI it has changed, just connect this event on UI to StateHasChanged();.
I write here the Blazor Counter sample modified to do this:
#page "/counter"
<h1>Counter</h1>
<p>Current count: #currentCount</p>
<button class="btn btn-primary" onclick="#IncrementCount">
Click me #s <!-- here var -->
</button>
#functions {
int currentCount = 0;
string s = "";
void IncrementCount()
{
currentCount++;
Task.Run(() =>
{
//Simulate work
Task.Run( async () => {
await Task.Run( () => {} );
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2));}
);
//Update property
s = s + " - Done";
StateHasChanged();
});
}
}
}
Edited
Call StateHasChanged from a thread is not more supported. Just change:
Task.Run(() =>
{
//Simulate work
System.Threading.Thread.Sleep(TimeSpan.FromSeconds(2));
//Update property
Title = Title + " - Done";
StateHasChanged();
});
By
Invoke( () =>
{
...