I have a button and a message that should be shown if condition is true.
<button #onclick="Helper.CallFunc">Call async func</button>
#if(Helper.Loading)
{
#("Loading...")
}
LoadingHelper class look like this
public class LoadingHelper
{
public bool Loading { get; private set; } = false;
public Func<Task> PassedFunc { private get; set; } = async () => await Task.Delay(2000);
public Func<Task> CallFunc => async () =>
{
Loading = true;
await PassedFunc();
Loading = false;
};
}
When I define Helper object like this the message is indeed shown for 2000 miliseconds
LoadingHelper Helper { get; set; } = new()
{
PassedFunc = async () => await Task.Delay(2000)
};
However when it's defined like this the message is never shown
LoadingHelper Helper => new()
{
PassedFunc = async () => await Task.Delay(2000)
};
I'm a little bit confused here as to why the Loading change is not shown in second example. Shouldn't the change be visible regardless if the Helper object is set with getter and setter or only getter since I'm not modifying the Loading property directly?
Edit
When it's defined like this for some reason it works
LoadingHelper Helper { get; } = new()
{
PassedFunc = async () => await Task.Delay(2000)
};
Building an anonymous function every time you invoke the Func is expensive.
public Func<Task> CallFunc => async () =>
{
Loading = true;
await PassedFunc();
Loading = false;
};
You can cache the function like this:
public Func<Task> CallFunc;
public LoadingHelper()
{
CallFunc = async () =>
{
Loading = true;
await PassedFunc();
Loading = false;
};
}
If you want to apply the "loader" to all UI events in a component you can create a custom IHandleEvent.HandleEventAsync that overloads the standard ComponentBase implementation. Here's a page that demonstrates how to implement one. This only updates loading if it's a MouseEventArgs event.
#page "/"
#implements IHandleEvent
<h3>Loader Demo</h3>
<div class="m-2">
<button class="btn btn-primary" #onclick=HandleClickAsync>Call async func</button>
<button class="btn btn-dark" #onclick=HandleClickAsync>Call async func</button>
</div>
<div class="m-2">
<input type="checkbox" #onchange=HandleCheckAsync />
</div>
#if (this.Loading)
{
<div class="alert alert-warning">Loading... </div>
}
#code {
protected bool Loading;
private async Task HandleClickAsync()
=> await Task.Delay(2000);
private async Task HandleCheckAsync()
=> await Task.Delay(2000);
async Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
{
if (arg is MouseEventArgs)
Loading = true;
var task = callback.InvokeAsync(arg);
var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
task.Status != TaskStatus.Canceled;
StateHasChanged();
await task;
if (arg is MouseEventArgs)
Loading = false;
StateHasChanged();
}
}
I should've had researched a bit more before asking a question. The answer is pretty simple as explained here.
Basically LoadingHelper Helper => new() returned a new object which obviously had Loading set to false.
LoadingHelper Helper { get; } = new() on the other hand returned the same object every time.
Related
I'm trying Blazor Server + SignalR streaming. Take a look at the following example. WebSocketService is a websocket client which is registered as a singleton. The received messages from that websocket client are pushed to the SignalR stream.
The question is do I have to initialize the websocket client in a separate IHostedService/BackgroundService like I usually do?
It works the way it is right now because it's a singleton. What are the best practices? I believe it's more appropriate to call it in a background service as the websocket client needs to run continuously in the background. This way, it can continue running and performing tasks even if the Hub is not in use.
Please keep in mind I made it that way, so I have the flexibility to send messages whenever I want, just by DI'ing it.
public sealed class WebSocketStreamHub : Hub
{
private readonly WebSocketService _wsService;
public WebSocketStreamHub(WebSocketService wsService)
{
_wsService = wsService;
}
public ChannelReader<string> GetChannelStream(CancellationToken cancellationToken)
{
return _wsService.ChatObservable.AsChannelReader();
}
}
public sealed class WebSocketService : IDisposable
{
private readonly IWebsocketClient _ws;
public WebSocketService()
{
_ws = new WebsocketClient(uri);
_ws.ReconnectTimeout = null;
_ws.ErrorReconnectTimeout = TimeSpan.FromSeconds(15);
_ws.ReconnectionHappened.Subscribe(info =>
_logger.LogInformation("Reconnection happened, type: {Type}", info.Type));
_ws.DisconnectionHappened.Subscribe(info =>
_logger.LogInformation("Disconnection happened, type: {Type}", info.Type));
_ = _ws.Start();
}
public IObservable<ChatMessage> ChatObservable => _ws.MessageReceived;
public void Send(string message)
{
_ws.Send(message);
}
public void Dispose()
{
_ws.Dispose();
}
}
#page "/"
#inject NavigationManager NavigationManager
#using Microsoft.AspNetCore.SignalR.Client
#using Blazor.Data
#implements IAsyncDisposable
<div class="sticky-top w-50 mb-3 mt-3">
<form #onsubmit="#(async () => await Send())">
<div class="input-group input-group-lg">
<select #bind="#_room" class="form-select">
<option value="Trade">Trade</option>
<option value="New Players">New Players</option>
</select>
<input #bind="#_message" type="text" class="form-control w-auto">
<button type="submit" class="btn btn-outline-secondary" disabled="#(!IsConnected)">Send</button>
</div>
</form>
</div>
<pre>
#foreach (var message in _messages)
{
<div class="text-secondary">[#message.RoomId] #message.SenderName says: #message.MessageText</div>
}
</pre>
#code
{
private readonly Queue<ChatMessage> _messages = new();
private HubConnection? _hubConnection;
private CancellationTokenSource? _channelCancellationTokenSource;
private string _room = "Trade";
private string _message = "";
private async Task GetChannelStream()
{
if (_hubConnection is null)
{
return;
}
_channelCancellationTokenSource = new CancellationTokenSource();
var channel = await _hubConnection.StreamAsChannelAsync<ChatMessage>("GetChannelStream",
_channelCancellationTokenSource.Token);
while (await channel.WaitToReadAsync())
{
while (channel.TryRead(out var message))
{
if (_messages.Count >= 20)
{
_messages.Dequeue();
}
_messages.Enqueue(message);
await InvokeAsync(StateHasChanged);
}
}
_channelCancellationTokenSource.Cancel();
}
protected override async Task OnInitializedAsync()
{
_hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/chathub"))
.Build();
await _hubConnection.StartAsync();
_ = GetChannelStream();;
}
private async Task Send()
{
if (_hubConnection is not null)
{
await _hubConnection.SendAsync("SendMessage", _room, _message);
_message = "";
}
}
public bool IsConnected => _hubConnection?.State == HubConnectionState.Connected;
public async ValueTask DisposeAsync()
{
if (_hubConnection is not null)
{
await _hubConnection.DisposeAsync();
}
}
}
Context
I'm following a pattern that's something like https://chrissainty.com/3-ways-to-communicate-between-components-in-blazor/ or https://jonhilton.net/blazor-state-management/
So I have two razor components Hen.razor and Basket.razor as child components inside index.razor. A button inside Hen adds to the number of eggs displayed inside Basket.
The button calls a service I've injected called EggService that handles the number of eggs in Basket by storing it in local storage using Blazored.LocalStorage.
Problem
Clicking the button increases the number of eggs in local storage but doesn't update the Basket component unless I refresh.
Code
Repository for convenience: https://github.com/EducatedStrikeCart/EggTest/
EggService:
using Blazored.LocalStorage;
namespace BlazorSandbox.Services
{
public class EggService
{
public event Action OnChange;
private readonly ILocalStorageService _localStorageService;
public EggService(ILocalStorageService localStorageService)
{
this._localStorageService = localStorageService;
}
public async Task<int> GetEggs()
{
int currentEggs = await _localStorageService.GetItemAsync<int>("Eggs");
return currentEggs;
}
public async Task AddEgg()
{
int newEggs = await GetEggs();
if (newEggs == null)
{
newEggs = 0;
} else
{
newEggs += 1;
}
await _localStorageService.SetItemAsync("Eggs", newEggs);
OnChange?.Invoke();
}
}
}
Hen:
#using BlazorSandbox.Services
#inject EggService EggService
<div>
<h3>Hen</h3>
<button #onclick="TakeAnEgg">Take an egg</button>
</div>
#code {
public async Task TakeAnEgg()
{
await EggService.AddEgg();
}
}
Egg:
#using BlazorSandbox.Services
#inject EggService EggService
#implements IDisposable
<div>
<h3>Basket</h3>
Eggs: #Eggs
</div>
#code {
public int Eggs { get; set; }
protected override async Task OnInitializedAsync()
{
Eggs = await EggService.GetEggs();
EggService.OnChange += StateHasChanged;
}
public void Dispose()
{
EggService.OnChange -= StateHasChanged;
}
}
Index:
#page "/"
#using BlazorSandbox.Services
#inject EggService EggService
<h1>
Eggs!
</h1>
<div class="d-flex flex-row justify-content-around">
<Hen />
<Basket />
</div>
#code {
}
Program.cs:
using BlazorSandbox;
using BlazorSandbox.Services;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Blazored.LocalStorage;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddScoped<EggService>();
builder.Services.AddBlazoredLocalStorage();
await builder.Build().RunAsync();
Solution
Special thank you to person who deleted their comment. I'm kind of new to asking questions on StackOverflow so I'm sorry if I should've selected your answer as the Answer!
#code {
public int Eggs { get; set; }
protected override void OnInitialized()
{
//Subscribe
EggService.OnChange += UpdateEgg;
//Set value of Eggs on load
UpdateEgg();
}
public void UpdateEgg()
{
// Set value of Eggs to new value and trigger component rerender
InvokeAsync(async () => {Eggs = await EggService.GetEggs(); StateHasChanged(); });
}
public void Dispose()
{
// Unsubscribe
EggService.OnChange -= UpdateEgg;
}
}
There are a few oddities in your code.
if (newEggs == null)
This is an int, so it can never be null. The default value for int is 0. You should be seeing a warning for this.
Eggs = await EggService.GetEggs();
After you set Eggs here, you never update it anywhere in your code! So even if you call StateHasChanged, there is nothing to update.
What you will want to do is keep track of the egg count inside of your EggService and then inside of your Basket component you will need a way to know that the egg count has increased so you can update your Egg property and then call StateHasChanged. Let me know if you need help with this.
How can I add a delay to an event (OnInput) in Blazor ?For example, if a user is typing in the text field and you want to wait until the user has finished typing.
Blazor.Templates::3.0.0-preview8.19405.7
Code:
#page "/"
<input type="text" #bind="Data" #oninput="OnInputHandler"/>
<p>#Data</p>
#code {
public string Data { get; set; }
public void OnInputHandler(UIChangeEventArgs e)
{
Data = e.Value.ToString();
}
}
Solution:
There is no single solution to your question. The following code is just one approach. Take a look and adapt it to your requirements. The code resets a timer on each keyup, only last timer raises the OnUserFinish event.
Remember to dispose timer by implementing IDisposable
#using System.Timers;
#implements IDisposable;
<input type="text" #bind="Data" #bind:event="oninput"
#onkeyup="#ResetTimer"/>
<p >UI Data: #Data
<br>Backend Data: #DataFromBackend</p>
#code {
public string Data { get; set; } = string.Empty;
public string DataFromBackend { get; set; } = string.Empty;
private Timer aTimer = default!;
protected override void OnInitialized()
{
aTimer = new Timer(1000);
aTimer.Elapsed += OnUserFinish;
aTimer.AutoReset = false;
}
void ResetTimer(KeyboardEventArgs e)
{
aTimer.Stop();
aTimer.Start();
}
private async void OnUserFinish(Object? source, ElapsedEventArgs e)
{
// https://stackoverflow.com/a/19415703/842935
// Call backend
DataFromBackend = await Task.FromResult( Data + " from backend");
await InvokeAsync( StateHasChanged );
}
void IDisposable.Dispose()
=>
aTimer?.Dispose();
}
Use case:
One example of use case of this code is avoiding backend requests, because the request is not sent until user stops typing.
Running:
This answer is the middle ground between the previous answers, i.e. between DIY and using a full-blown reactive UI framework.
It utilizes the powerful Reactive.Extensions library (a.k.a. Rx), which in my opinion is the only reasonable way to solve such problems in normal scenarios.
The solution
After installing the NuGet package System.Reactive you can import the needed namespaces in your component:
#using System.Reactive.Subjects
#using System.Reactive.Linq
Create a Subject field on your component that will act as the glue between the input event and your Observable pipeline:
#code {
private Subject<ChangeEventArgs> searchTerm = new();
// ...
}
Connect the Subject with your input:
<input type="text" class="form-control" #oninput=#searchTerm.OnNext>
Finally, define the Observable pipeline:
#code {
// ...
private Thing[]? things;
protected override async Task OnInitializedAsync() {
searchTerm
.Throttle(TimeSpan.FromMilliseconds(200))
.Select(e => (string?)e.Value)
.Select(v => v?.Trim())
.DistinctUntilChanged()
.SelectMany(SearchThings)
.Subscribe(ts => {
things = ts;
StateHasChanged();
});
}
private Task<Thing[]> SearchThings(string? searchTerm = null)
=> HttpClient.GetFromJsonAsync<Thing[]>($"api/things?search={searchTerm}")
}
The example pipeline above will...
give the user 200 milliseconds to finish typing (a.k.a. debouncing or throttling the input),
select the typed value from the ChangeEventArgs,
trim it,
skip any value that is the same as the last one,
use all values that got this far to issue an HTTP GET request,
store the response data on the field things,
and finally tell the component that it needs to be re-rendered.
If you have something like the below in your markup, you will see it being updated when you type:
#foreach (var thing in things) {
<ThingDisplay Item=#thing #key=#thing.Id />
}
Additional notes
Don't forget to clean up
You should properly dispose the event subscription like so:
#implements IDisposable // top of your component
// markup
#code {
// ...
private IDisposable? subscription;
public void Dispose() => subscription?.Dispose();
protected override async Task OnInitializedAsync() {
subscription = searchTerm
.Throttle(TimeSpan.FromMilliseconds(200))
// ...
.Subscribe(/* ... */);
}
}
Subscribe() actually returns an IDisposable that you should store and dispose along with your component. But do not use using on it, because this would destroy the subscription prematurely.
Open questions
There are some things I haven't figured out yet:
Is it possible to avoid calling StateHasChanged()?
Is it possible to avoid calling Subscribe() and bind directly to the Observable inside the markup like you would do in Angular using the async pipe?
Is it possible to avoid creating a Subject? Rx supports creating Observables from C# Events, but how do I get the C# object for the oninput event?
I have created a set of Blazor components. One of which is Debounced inputs with multiple input types and much more features. Blazor.Components.Debounce.Input is available on NuGet.
You can try it out with the demo app.
Note: currently it is in Preview. Final version is coming with .NET 5. release
I think this is the better solution for me, I used it for searching.
Here's the code that I used.
private DateTime timer {
get;
set;
} = DateTime.MinValue;
private async Task SearchFire(ChangeEventArgs Args) {
if (timer == DateTime.MinValue) {
timer = DateTime.UtcNow;
} else {
_ = StartSearch(Args);
timer = DateTime.UtcNow;
}
}
private async Task StartSearch(ChangeEventArgs Args) { //2000 = 2 seconeds you can change it
await Task.Delay(2000);
var tot = TimeSpan.FromTicks((DateTime.UtcNow - timer).Ticks).TotalSeconds;
if (tot > 2) {
if (!string.IsNullOrEmpty(Args.Value.ToString())) { //Do anything after 2 seconds.
//reset timer after finished writhing
timer = DateTime.MinValue;
} else {}
} else {}
}
You can avoid bind the input. Just set #oninput
<Input id="theinput" #oninput="OnTextInput" />
#code {
public string SomeField { get; set; }
public void OnTextInput(ChangeEventArgs e)
{
SomeField = e.Value.ToString();
}
}
and set initial value in javascript (if there is).
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await JSRuntime.InvokeVoidAsync("setInitialValueById", "theinput", SomeField);
}
}
The setInitialValueById method:
window.setInitialValueById = (elementId, value) => {
document.getElementById(elementId).value = value;}
This will resolve the known input lag issue in blazor. You can set a label value with delay if it's the case:
public async Task OnTextInput(ChangeEventArgs e)
{
var value = e.Value.ToString();
SomeField = value;
await JSRuntime.InvokeVoidAsync("setLabelValue", value);
}
The setLabelValue method:
let lastInput;
window.setLabelValue = (value) => {
lastInput = value;
setTimeout(() => {
let inputValue = value;
if (inputValue === lastInput) {
document.getElementById("theLabelId").innerHTML = inputValue;
}
}, 2000);
}
I have a task in my viewmodel that looks like this:
ICommand getWeather;
public ICommand GetWeatherCommand =>
getWeather ??
(getWeather = new Command(async () => await ExecuteGetWeatherCommand()));
public async Task ExecuteGetWeatherCommand()
{
if (IsBusy)
return;
IsBusy = true;
try
{
WeatherRoot weatherRoot = null;
var units = IsImperial ? Units.Imperial : Units.Metric;
if (UseGPS)
{
//Get weather by GPS
var local = await CrossGeolocator.Current.GetPositionAsync(10000);
weatherRoot = await WeatherService.GetWeather(local.Latitude, local.Longitude, units);
}
else
{
//Get weather by city
weatherRoot = await WeatherService.GetWeather(Location.Trim(), units);
}
//Get forecast based on cityId
Forecast = await WeatherService.GetForecast(weatherRoot.CityId, units);
var unit = IsImperial ? "F" : "C";
Temp = $"Temp: {weatherRoot?.MainWeather?.Temperature ?? 0}°{unit}";
Condition = $"{weatherRoot.Name}: {weatherRoot?.Weather?[0]?.Description ?? string.Empty}";
}
catch (Exception ex)
{
Temp = "Unable to get Weather";
//Xamarin.Insights.Report(ex);
}
finally
{
IsBusy = false;
}
}
How can I reach that Task and make it execute the function properly?
My goal is for it to execute right when the user enters the contentpage (StartPage). Right now I use this code below but the Command does not execute.
public StartPage ()
{
InitializeComponent ();
loadCommand ();
}
async Task loadCommand ()
{
var thep = new WeatherViewModel ();
await thep.ExecuteGetWeatherCommand ();
}
I bind the command into my listview:
RefreshCommand="{Binding GetWeatherCommand}"
With my current code the Task does not execute. What am I missing?
Firstly, your naming convention is odd, theTask is not a Task, so you probably should not call it one. Secondly, because you are calling loadCommand in your constructor and not awaiting it, its possible for the constructor to complete before the function is completed. In general, you want to avoid async in your constructor.
Stephen Cleary has a great article on async in constructors here: http://blog.stephencleary.com/2013/01/async-oop-2-constructors.html
What may be appropriate is attaching a handler to the Appearing event of your page, and do the async work there. Without more context it is a little hard to say if this is the best approach for your use case.
For instance:
public StartPage ()
{
InitializeComponent ();
Appearing += async (sender, args) => await loadCommand();
}
async Task loadCommand ()
{
var viewModel = new StartPageViewModel();
await viewModel.ExecuteCommand();
}
I have a graphic method CancelChanges() used and called by a ViewModel.
I want to test this method but we have a Task inside.
We use a Task to not freeze the UI.
My test method needs to wait the result of this Task to check the result.
The code is:
public override void CancelChanges()
{
Task.Run(
async () =>
{
this.SelectedWorkflow = null;
AsyncResult<IncidentTypeModel> asyncResult = await this.Dataprovider.GetIncidentTypeByIdAsync(this.Incident.Id);
Utils.GetDispatcher().Invoke(
() =>
{
if (asyncResult.IsError)
{
WorkflowMessageBox.ShowException(
MessageHelper.ManageException(asyncResult.Exception));
}
else
{
this.Incident = asyncResult.Result;
this.Refreshdesigner();
this.HaveChanges = false;
}
});
});
}
And my test method:
/// <summary>
/// A test for CancelChanges
/// </summary>
[TestMethod]
[TestCategory("ConfigTool")]
public void CancelChangesTest()
{
string storexaml = this._target.Incident.WorkflowXamlString;
this._target.Incident.WorkflowXamlString = "dsdfsdgfdsgdfgfd";
this._target.CancelChanges();
Assert.IsTrue(storexaml == this._target.Incident.WorkflowXamlString);
Assert.IsFalse(this._target.HaveChanges);
}
How can we do to have my test that is waiting the result of the Task?
Thanks.
Make the CancelChanges method return a Task, and then await this or set up a continuation in the test method. Some this like
public override Task CancelChanges()
{
return Task.Factory.StartNew(() =>
{
// Do stuff...
});
}
notice the change from Task.Run to Task.Factory.StartNew. This is a better way of starting tasks in such cases. Then in the test method
[TestMethod]
[TestCategory("ConfigTool")]
public void CancelChangesTest()
{
string storexaml = this._target.Incident.WorkflowXamlString;
this._target.Incident.WorkflowXamlString = "dsdfsdgfdsgdfgfd";
this._target.CancelChanges().ContinueWith(ant =>
{
Assert.IsTrue(storexaml == this._target.Incident.WorkflowXamlString);
Assert.IsFalse(this._target.HaveChanges);
});
}
You could also mark the test method as async and use await in the test method to do the same thing.
I hope this helps.
I would take the refactoring one step further, and if possible avoid using Task.Run. Since all you do is await and then invoke work on the UI Thread, I would do the following:
public override Task CancelChanges()
{
this.SelectedWorkflow = null;
AsyncResult<IncidentTypeModel> asyncResult = await this.Dataprovider.GetIncidentTypeByIdAsync(this.Incident.Id);
if (asyncResult.IsError)
{ WorkflowMessageBox.ShowException(MessageHelper.ManageException(asyncResult.Exception));
}
else
{
this.Incident = asyncResult.Result;
this.Refreshdesigner();
this.HaveChanges = false;
}
});
});
}
And the test method:
[TestMethod]
[TestCategory("ConfigTool")]
public async Task CancelChangesTest()
{
string storexaml = this._target.Incident.WorkflowXamlString;
this._target.Incident.WorkflowXamlString = "dsdfsdgfdsgdfgfd";
var cancelChanges = await this._target.CancelChanges();
Assert.IsTrue(storexaml == this._target.Incident.WorkflowXamlString);
Assert.IsFalse(this._target.HaveChanges);
}