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);
}
Related
I'm using Blazor server-side (with pre-render disabled) with .NET 6. My page looks like:
<MyComponentA />
<MyComponentB />
and the components both look like:
#inject MyDbService db
...
#code {
protected override async Task OnParametersSetAsync()
{
var v = await db.SomeSelect();
// ... use v
}
}
MyDbService is a Scoped service that uses EFCore and has a DbContext member, so there is only one active database connection. It's an external requirement for this task to only have a single database connection -- I don't want to use a Transient service instead.
This code causes a runtime error because the DbContext throws an exception if two components both try to use it concurrently. (i.e. DbContext is not re-entrant).
aside If I understand correctly, the flow is that db.SomeSelect in MyComponentA initiates, and await means the execution of the Page continues and eventually reaches db.SomeSelect in MyComponentB, which makes a second request on the same thread that still has the first call in progress.
My question is: Is there a tidy way to make MyComponentB not make its database call until MyComponentA has finished initializing?
One option would be to make MyComponentB do nothing until a specific function is called; and pass that function as parameter to MyComponentA to call when it's finished loading. But that feels pretty clunky and spaghetti so I wonder if there is a better way.
Here's a lightweight scheduler that uses TaskCompletionSource objects to manually control Tasks passed back to the caller.
This is a simple demo, getting time with a configurable delay from a single source that throws an exception if you try and run it in parallel.
You should be able to apply this pattern to schedule sequential requests into your data pipeline.
The Scoped Queue Service:
public class GetTheTime : IAsyncDisposable
{
private bool _processing;
private Queue<TimeRequest> _timeQueue = new();
private Task _queueTask = Task.CompletedTask;
private bool _disposing;
// only way to get the time
public Task<string> GetTime(int delay)
{
var value = new TimeRequest(delay);
// Queues the request
_timeQueue.Enqueue(value);
// Checks if the queue service is running and if not run it
// lock it while we check and potentially start it to ensure thread safety
lock (_queueTask)
{
if (_queueTask.IsCompleted)
_queueTask = this.QueueService();
}
// returns the maunally controlled Task to the caller who can await it
return value.CompletionSource.Task;
}
private async Task QueueService()
{
// loop thro the queue and run the enqueued requests till it's empty
while (_timeQueue.Count > 0)
{
if (_disposing)
break;
var value = _timeQueue.Dequeue();
// do the work and wait for it to complete
var result = await _getTime(value.Delay);
value.CompletionSource.TrySetResult(result);
}
}
private async Task<string> _getTime(int delay)
{
// If more than one of me is running go BANG
if (_processing)
throw new Exception("Bang!");
_processing = true;
// Emulate an async database call
await Task.Delay(delay);
_processing = false;
return DateTime.Now.ToLongTimeString();
}
public async ValueTask DisposeAsync()
{
_disposing = true;
await _queueTask;
}
private readonly struct TimeRequest
{
public int Delay { get; }
public TaskCompletionSource<string> CompletionSource { get; } = new TaskCompletionSource<string>();
public TimeRequest(int delay)
=> Delay = delay;
}
}
A simple Component:
#inject GetTheTime service
<div class="alert alert-info">
#this.message
</div>
#code {
[Parameter] public int Delay { get; set; } = 1000;
private string message = "Not Started";
protected async override Task OnInitializedAsync()
{
message = "Processing";
message = await service.GetTime(this.Delay);
}
}
And a demo page:
#page "/"
<PageTitle>Index</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
<Component Delay="3000" />
<Component Delay="2000" />
<Component Delay="1000" />
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.
I am completely re-writing this question as nothing has made sense to anybody - I apologize for the issues.
To start, I have a Singleton service, called LocalSystemService, that handles RESTful communications between the Blazor Server App and a separate Web API system running on a separate server. I have added that service into my Blazor application using the following call:
services.AddScoped<ILocalSystemService, LocalSystemService>();
I have now moved away from a simple Timer and to a separate Service in response to other articles I have read. This service is called CheckLDC and is registered using the following:
services.AddSingleton<CheckLDC>();
That service is constructed as follows:
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using Telerik.Blazor;
using Telerik.Blazor.Components;
using Frontend.Data;
using Frontend.Services;
namespace Frontend.Services
{
public class LDCExecutedEventArgs : EventArgs { }
public class CheckLDC : IDisposable
{
public event EventHandler<LDCExecutedEventArgs> JobExecuted;
void OnJobExecuted()
{
JobExecuted?.Invoke(this, new LDCExecutedEventArgs());
}
#region Globals
static ReaderWriterLock locker = new ReaderWriterLock();
private System.Timers.Timer checkRemoteData;
private bool _Running;
[Inject]
public ILocalSystemService LocalSystemService { get; set; }
[Inject]
public ILogger<CheckLDC> _logger { get; set; }
[Inject]
public IMemoryCache _cache { get; set; }
private const string LocationCacheName = "LocalSystem";
#endregion
public void StartExecuting()
{
if (!_Running)
{
// Initiate a Timer
checkRemoteData = new System.Timers.Timer(1000);
checkRemoteData.Elapsed += HandleTimer;
checkRemoteData.AutoReset = true;
checkRemoteData.Enabled = true;
_Running = true;
}
}
private async void HandleTimer(object source, ElapsedEventArgs e)
{
**This call results in a NULL for the LocalSystemService!!!**
**if (LocalSystemService.IsThereAnUpdate().Result)**
{
await Task.Run(() =>
{
try
{
locker.AcquireWriterLock(int.MaxValue);
if (!_cache.TryGetValue(LocationCacheName, out transferSystem))
{
//We need to grab everything:
JsonSerializer serializer = new JsonSerializer();
#region Location
try
{
_cache.Set(LocationCacheName, LocalSystemService.GetLocalSystem().Result);
}
catch (Exception locWriteX)
{
_logger.LogError("Failed to restore location locally with the error: " + locWriteX.ToString());
}
#endregion
}
}
finally
{
locker.ReleaseWriterLock();
}
});
}
// Notify any subscribers to the event
OnJobExecuted();
}
public void Dispose()
{
if (_Running)
{
checkRemoteData?.Dispose();
}
}
}
}
I then placed this code in my main component behind
[Inject]
public ILocalSystemService LocalSystemService { get; set; }
[Inject]
public CheckLDC CheckTheBackend {get; set;}
Note that I am using a Memory Cache to store data across requests. Wit the injection in place, my OnInit method looks as follows:
protected override async Task OnInitializedAsync()
{
await Task.Run(() =>
{
//This call uses the LocalSystemService to grab and store the main data class into the cache
CurrentSystem = CreateCaches().Result;
});
//These are my event subscriptions
CheckTheBackend.JobExecuted += HandleJobExecuted;
CheckTheBackend.StartExecuting();
}
Finally, the method being called on JobExecute is:
public async void HandleJobExecuted(object sender, LDCExecutedEventArgs e)
{
await InvokeAsync(StateHasChanged);
}
Now I am getting a NULL exception when trying to call the LocalSystemService from the CheckLDC's HandleTimer event call - I bold typed the call that continues to fail. I have tried AddTransient and AddScope for the LocalSystemService but nothing works. In the Blazor app, I can call LocalSystemService without any issues - but is ALWAYS fails in the CheckLDC singleton.
Any ideas?
You can't use property injection here - that is for components only.
You will need to use constructor injection for CheckLDC
private readonly ILocalSystemService LocalSystemService;
private readonly ILogger<CheckLDC> _logger;
private readonly IMemoryCache _cache;
public CheckLDC(ILocalSystemService localSystemService,
ILogger<CheckLDC> logger,
IMemoryCache cache)
{
LocalSystemService = localSystemService;
_logger = logger;
_cache = cache;
}
Sorry, but it is hard to follow the flow of execution without having a complete reproducible code sample. However, the following code snippet, based on the FetchData page, and your code (I tried to be as true as possible to your code) works. The commented code is superfluous. Copy and test...then try to apply it to your program, and report of issues.
#page "/fetchdata"
#using WebApplication1.Data
#using System.Timers;
#inject WeatherForecastService ForecastService
#implements IDisposable
<h1>Weather forecast</h1>
<p>This component demonstrates fetching data from a service.</p>
#if (forecasts == null)
{
<p><em>Loading...</em></p>
}
else
{
<table class="table">
<thead>
<tr>
<th>Date</th>
<th>Temp. (C)</th>
<th>Temp. (F)</th>
<th>Summary</th>
</tr>
</thead>
<tbody>
#foreach (var forecast in forecasts)
{
<tr>
<td>#forecast.Date.ToShortDateString()</td>
<td>#forecast.TemperatureC</td>
<td>#forecast.TemperatureF</td>
<td>#forecast.Summary</td>
</tr>
}
</tbody>
</table>
}
#code {
private WeatherForecast[] forecasts;
private static System.Timers.Timer checkRemoteData;
protected override async Task OnInitializedAsync()
{
// forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
//await Task.Run(() =>
//{
forecasts = await ForecastService.GetForecastAsync(DateTime.Now); //.Result;
// What is ShowModal? Are you setting it to true somewhere ?
// Perhaps this is why you do not see the changes, if you design
// to show the changes in a window modal
// ShowModal = false;
//});
//This works and calls the main method every second
checkRemoteData = new System.Timers.Timer(4000);
checkRemoteData.Elapsed += OnTimedEvent;
checkRemoteData.AutoReset = true;
checkRemoteData.Enabled = true;
}
private async void OnTimedEvent(Object source, ElapsedEventArgs e)
{
//if (LocalSystemService.IsThereAnUpdate().Result)
//{
// await InvokeAsync(StateHasChanged);
// I have used the same call as in the OnInit - niether one works!
forecasts = await ForecastService.GetForecastAsync(DateTime.Now);
await InvokeAsync(() => StateHasChanged()); //.ConfigureAwait(false);
// What is this for ? Are you trying to emulate
// delay? Just increase the interval (to 4000...)
// await Task.Delay(500);
// Not needed
// await InvokeAsync(StateHasChanged);
//}
}
public void Dispose()
{
checkRemoteData.Elapsed -= OnTimedEvent;
}
}
Update:
I've added the following method to the WeatherForecastService in order to emulate LocalSystemService.IsThereAnUpdate().Result
public Task<bool> IsThereAnUpdate()
{
return Task.FromResult( true);
}
And added also this:
if (ForecastService.IsThereAnUpdate().Result)
{
}
And now when the IsThereAnUpdate returns true, the UI is updated, and when it returns false, it does not.
I am experiencing some weird behaviour with a windows service application I am working on. This is my 1st dip into Tasks so I am on a steep learning curve and in need of some assistance as I know my issue is probably down to something I have misunderstood.
I have the following setup:
public partial class MyService
{
protected override void OnStart(string[] args)
{
MasterTokenSource = new CancellationTokenSource();
MasterCancellationToken = MasterTokenSource.Token;
//Begin tasks.
StartAllTasks();
//This is the thread that is going to listen for updates in the database.
Task MasterService = Task.Factory.StartNew(() =>
{
while (!MasterCancellationToken.IsCancellationRequested)
{
//Sleep for the amount of time as determined in the DB
Thread.Sleep(ServiceInstance.PollInterval * 1000);
Console.WriteLine("Polled for changes");
//Check service modules for changes as per DB config
UpdateServiceModulePropertiesAndRunningTasks();
//MasterTokenSource.Cancel();
}
MasterCancellationToken.ThrowIfCancellationRequested();
}, MasterCancellationToken);
}
private void StartAllTasks()
{
//Index pages task
ServiceModule PageIndexersm = ServiceInstance.GetServiceModule("PageIndexer");
PageIndexer.StartNewInstance(PageIndexersm, ConfigInstance, MasterTokenSource);
//There are other calls to other methods to do different things here but they all follow the same logic
}
private void UpdateServiceModulePropertiesAndRunningTasks()
{
//Get a fresh copy of the service instance, and compare to current values
ServiceInstance compareServiceInstance = new ServiceInstance(ConfigInstance.OneConnectionString, ConfigInstance.TwoConnectionString, ConfigInstance.ServiceName);
foreach (ServiceModule NewServiceModuleItem in compareServiceInstance.AllServiceModules)
{
ServiceModule CurrentServiceModuleInstance = ServiceInstance.GetServiceModule(NewServiceModuleItem.ModuleName);
if (!NewServiceModuleItem.Equals(CurrentServiceModuleInstance))
{
//Trigger changed event and pass new instance
CurrentServiceModuleInstance.On_SomethingChanged(NewServiceModuleItem, MasterTokenSource);
}
}
}
}
public class PageIndexer
{
public ServiceConfig ServiceConfig { get; set; }
public ServiceModule ServiceModuleInstance { get; set; }
public Guid InstanceGUID { get; set; }
public CancellationTokenSource TokenSource { get; set; }
public CancellationToken Token { get; set; }
public PageIndexer(ServiceModule PageIndexerServiceModule, ServiceConfig _ServiceConfig)
{
ServiceModuleInstance = PageIndexerServiceModule;
ServiceModuleInstance.SomethingChanged += ServiceModuleInstance_SomethingChanged;
ServiceConfig = _ServiceConfig;
InstanceGUID = Guid.NewGuid();
}
//This is the method called within the PageIndexer instance
private void ServiceModuleInstance_SomethingChanged(ServiceModule sm, CancellationTokenSource MasterCancelToken)
{
Console.WriteLine(InstanceGUID + ": Something changed");
TokenSource.Cancel();
//Start new indexer instance
PageIndexer.StartNewInstance(sm, ServiceConfig, MasterCancelToken);
}
public void RunTask()
{
Console.WriteLine("Starting Page Indexing");
Task.Factory.StartNew(() =>
{
while (true)
{
if (TokenSource.Token.IsCancellationRequested)
{
Console.WriteLine(InstanceGUID + ": Page index CANCEL requested: " + TokenSource.IsCancellationRequested);
TokenSource.Token.ThrowIfCancellationRequested();
}
if (ServiceModuleInstance.ShouldTaskBeRun())
{
Console.WriteLine(InstanceGUID + ": RUNNING full index, Cancellation requested: " + TokenSource.IsCancellationRequested);
RunFullIndex();
}
else
{
Console.WriteLine(InstanceGUID + ": SLEEPING, module off, Cancellation requested: " + TokenSource.IsCancellationRequested);
//If the task should not be run then sleep for a bit to save resources
Thread.Sleep(5000);
}
}
}, TokenSource.Token);
}
public static void StartNewInstance(ServiceModule serviceModule, ServiceConfig eServiceConfig, CancellationTokenSource MasterCancellationToken)
{
PageIndexer pageIndexerInstance = new PageIndexer(serviceModule, eServiceConfig);
CancellationTokenSource NewInstanceCancellationTokenSource = new CancellationTokenSource();
NewInstanceCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(MasterCancellationToken.Token);
pageIndexerInstance.TokenSource = NewInstanceCancellationTokenSource;
pageIndexerInstance.Token = pageIndexerInstance.TokenSource.Token;
pageIndexerInstance.RunTask();
}
}
What I am seeing is that the cancel and start are working fine for me for the 1st change detected but subsequent cancels issued after other changes are not working. I can see the call to the event method happening, however, it appears to be calling on the original instance of the page indexer.
I am sure I have just got to a point where I have been going around so long I have made a complete mess, but I would be grateful for any guidance anyone can offer to get me back on the right track
Thank you in advance.
Regards
A CancellationTokenSource and CancellationToken can only be signaled once. They become cancelled forever. If you want multiple cancellation signals for multiple threads/tasks then you need one token for each such operation.
Often, it is a good pattern to group them in a class:
class MyOperation {
Task task; //use this for waiting
CancellationTokenSource cts; //use this for cancelling
}
That way there automatically is a 1:1 association of task and token. You are able to cancel a specific task this way.
Seeking some input on a behaviour I'm noticing in my code below. This is my first attempt at async/await using Xamarin Forms and I have perused hundreds of posts, blogs and articles on the subject including the writings from Stephen Cleary on async from constructors and best practices to avoid locking. Although I am using a MVVM framework I assume my issue is more generic than that so I'll ignore it for the moment here.
If I am still missing something or there are ways to improve what I'm trying to do ... happy to listen and learn.
At a high level the logic is as follows:
Application starts and initialises
During initialisation verify database exist and if not - create the SQLite DB. Currently I force this every time to simulate a new application and pre-populate it with some sample data for development purposes
After initialisation completed load results set and display
This works most of the time but I have noticed 2 infrequent occurrences due to the async handling of the database initialisation and pre-populating:
Occasionally not all sample records created are displayed once the app started up - I assume this is because the pre-population phase has not completed when the results are loaded
Occasionally I get an error that one of the tables have not been created - I assume this is because the database initialisation has not completed when the results are loaded
The code - simplified to show the flow during initialisation and startup:
----------- VIEW / PAGE MODEL ----------------
public class MyListItemsPageModel
{
private ObservableRangeCollection<MyListItem> _myListItems;
private Command loadItemsCommand;
public MyListItemsPageModel()
{
_myListItems = new ObservableRangeCollection<MyListItem>();
}
public override void Init(object initData)
{
if (LoadItemsCommand.CanExecute(null))
LoadItemsCommand.Execute(null);
}
public Command LoadItemsCommand
{
get
{
return loadItemsCommand ?? (loadItemsCommand = new Command(async () => await ExecuteLoadItemsAsyncCommand(), () => { return !IsBusy; }));
}
}
public ObservableRangeCollection<MyListItem> MyListItems {
get { return _myListItems ?? (_myListItems = new ObservableRangeCollection<MyListItem>()); }
private set {
_myListItems = value;
}
}
private async Task ExecuteLoadItemsAsyncCommand() {
if (IsBusy)
return;
IsBusy = true;
loadItemsCommand.ChangeCanExecute();
var _results = await MySpecificDBServiceClass.LoadAllItemsAsync;
MyListItems = new ObservableRangeCollection<MyListItem>(_results.OrderBy(x => x.ItemName).ToList());
IsBusy = false;
loadItemsCommand.ChangeCanExecute();
}
}
----------- DB Service Class ----------------
// THERE IS A SPECIFIC SERVICE LAYER BETWEEN THIS CLASS AND THE PAGE VIEW MODEL HANDLING THE CASTING OF TO THE SPECIFIC DATA TYPE
// public class MySpecificDBServiceClass : MyGenericDBServiceClass
public class MyGenericDBServiceClass<T>: IDataAccessService<T> where T : class, IDataModel, new()
{
public SQLiteAsyncConnection _connection = FreshIOC.Container.Resolve<ISQLiteFactory>().CreateConnection();
internal static readonly AsyncLock Mutex = new AsyncLock();
public DataServiceBase()
{
// removed this from the constructor
//if (_connection != null)
//{
// IsInitialized = DatabaseManager.CreateTableAsync(_connection);
//}
}
public Task<bool> IsInitialized { get; private set; }
public virtual async Task<List<T>> LoadAllItemsAsync()
{
// Temporary async/await initialisation code. This will be moved to the start up as per Stephen's suggestion
await DBInitialiser();
var itemList = new List<T>();
using (await Mutex.LockAsync().ConfigureAwait(false))
{
itemList = await _connection.Table<T>().ToListAsync().ConfigureAwait(false);
}
return itemList;
}
}
----------- DB Manager Class ----------------
public class DatabaseManager
{
static double CURRENT_DATABASE_VERSION = 0.0;
static readonly AsyncLock Mutex = new AsyncLock();
private static bool IsDBInitialised = false;
private DatabaseManager() { }
public static async Task<bool> CreateTableAsync(SQLiteAsyncConnection CurrentConnection)
{
if (CurrentConnection == null || IsDBInitialised)
return IsDBInitialised;
await ProcessDBScripts(CurrentConnection);
return IsDBInitialised;
}
private static async Task ProcessDBScripts(SQLiteAsyncConnection CurrentConnection)
{
using (await Mutex.LockAsync().ConfigureAwait(false))
{
var _tasks = new List<Task>();
if (CURRENT_DATABASE_VERSION <= 0.1) // Dev DB - recreate everytime
{
_tasks.Add(CurrentConnection.DropTableAsync<Table1>());
_tasks.Add(CurrentConnection.DropTableAsync<Table2>());
await Task.WhenAll(_tasks).ConfigureAwait(false);
}
_tasks.Clear();
_tasks.Add(CurrentConnection.CreateTableAsync<Table1>());
_tasks.Add(CurrentConnection.CreateTableAsync<Table2>());
await Task.WhenAll(_tasks).ConfigureAwait(false);
_tasks.Clear();
_tasks.Add(UpgradeDBIfRequired(CurrentConnection));
await Task.WhenAll(_tasks).ConfigureAwait(false);
}
IsDBInitialised = true;
}
private static async Task UpgradeDBIfRequired(SQLiteAsyncConnection _connection)
{
await CreateSampleData();
return;
// ... rest of code not relevant at the moment
}
private static async Task CreateSampleData()
{
IDataAccessService<MyListItem> _dataService = FreshIOC.Container.Resolve<IDataAccessService<MyListItem>>();
ObservableRangeCollection<MyListItem> _items = new ObservableRangeCollection<MyListItem>(); ;
_items.Add(new MyListItem() { ItemName = "Test 1", ItemCount = 14 });
_items.Add(new MyListItem() { ItemName = "Test 2", ItemCount = 9 });
_items.Add(new MyListItem() { ItemName = "Test 3", ItemCount = 5 });
await _dataService.SaveAllItemsAsync(_items).ConfigureAwait(false);
_items = null;
_dataService = null;
IDataAccessService<Sample> _dataService2 = FreshIOC.Container.Resolve<IDataAccessService<AnotherSampleTable>>();
ObservableRangeCollection<Sample> _sampleList = new ObservableRangeCollection<Sample>(); ;
_sampleList.Add(new GuestGroup() { SampleName = "ABC" });
_sampleList.Add(new GuestGroup() { SampleName = "DEF" });
await _dataService2.SaveAllItemsAsync(_sampleList).ConfigureAwait(false);
_sampleList = null;
_dataService2 = null;
}
}
In your DataServiceBase constructor, you're calling DatabaseManager.CreateTableAsync() but not awaiting it, so by the time your constructor exits, that method has not yet completed running, and given that it does very little before awaiting, it's probably barely started at that point. As you can't effectively use await in a constructor, you need to remodel things so you do that initialisation at some other point; e.g. perhaps lazily when needed.
Then you also want to not use .Result/.Wait() whenever possible, especially as you're in an async method anyway (e.g. ProcessDBScripts()), so instead of doing
var _test = CurrentConnection.DropTableAsync<MyListItem>().Result;
rather do
var _test = await CurrentConnection.DropTableAsync<MyListItem>();
You also don't need to use Task.Run() for methods that return Task types anyway. So instead of
_tasks.Add(Task.Run(() => CurrentConnection.CreateTableAsync<MyListItem>().ConfigureAwait(false)));
_tasks.Add(Task.Run(() => CurrentConnection.CreateTableAsync<AnotherSampleTable>().ConfigureAwait(false)));
just do
_tasks.Add(CurrentConnection.CreateTableAsync<MyListItem>()));
_tasks.Add(CurrentConnection.CreateTableAsync<AnotherSampleTable>()));
sellotape has correctly diagnosed the code problem: the constructor is starting an asynchronous method but nothing is (a)waiting for it to complete. A simple fix would be to add await IsInitialized; to the beginning of LoadAllItemsAsync.
However, there's also a design problem:
After initialisation completed load results set and display
That's not possible on Xamarin, or any other modern UI platform. You must load your UI immediately and synchronously. What you should do is immediately display a splash/loading page and start the asynchronous initialization work. Then, when the async init is completed, update your VM/UI with your "real" page. If you just have LoadAllItemsAsync await IsInitialized, then your app will sit there for some time showing the user zero data before it "fills in".
You may find my NotifyTask<T> type (available on NuGet) useful here if you want to show a splash/spinner instead of zero data.