I have a Blazor WASM application that needs to call an API every second without blocking the UI. This codes demonstrates how I tried to do that:
List<int> testList = new();
testList.Add(1);
testList.Add(2);
testList.Add(3);
testList.Add(4);
List<int> emptyTestlist = new();
CancellationTokenSource cts;
Test();
void Test()
{
Parallel.Invoke(async () =>
{
do
{
Console.WriteLine("Start");
await Task.Delay(1000);
await Test2();
Console.WriteLine("END");
} while (true);
});
}
Console.ReadLine();
async ValueTask Test2()
{
emptyTestlist.Clear();
cts = new();
await Parallel.ForEachAsync(testList, cts.Token, async (test, token) =>
{
await Test4(test);
});
foreach (var test in emptyTestlist)
{
await Test3(test);
}
}
async Task Test4(int i)
{
await Task.Delay(300);
//Console.WriteLine("if I Add this console.WriteLine It's added perfectly");
emptyTestlist.Add(i);
Console.WriteLine($"from TEST4: {i}");
}
async Task Test3(int i)
{
Console.WriteLine($"TEST3 {i}.");
await Task.Delay(1000);
Console.WriteLine($"TEST3 {i}, after 1sec");
}
If I comment the line Console.WriteLine("if I Add this console.WriteLine It's added perfectly");
, it's not adding perfectly. (emptyTestlist.Count
is not always 4). But if I add Console.WriteLine
before emptyTestlist.Add(i)
it works correctly (emptyTestlist.Count
is always 4).
I don't know how to solve it. What's the problem?
CodePudding user response:
The easiest way to poll an API is to use a timer:
@code {
private List<Customer> custs=new List<Customer>();
private System.Threading.Timer timer;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
custs = await Http.GetFromJsonAsync<List<Customer>>(url);
timer = new System.Threading.Timer(async _ =>
{
custs = await Http.GetFromJsonAsync<List<Customer>>("/api/customers");
InvokeAsync(StateHasChanged);
}, null, 1000, 1000);
}
In this case InvokeAsync(StateHasChanged);
is needed because the state was modified from a timer thread and Blazor has no idea the data changed.
If we wanted to add the results to a list though, we'd either have to use a lock or a thread-safe collection, like a ConcurrentQueue
.
@code {
private ConcurrentQueue<Customer> custs=new ConcurrentQueue<Customer>();
private System.Threading.Timer timer;
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
custs = await Http.GetFromJsonAsync<List<Customer>>(url);
timer = new System.Threading.Timer(async _ =>
{
var results = await Http.GetFromJsonAsync<List<Customer>>("/api/customers");
foreach(var c in results)
{
custs.Enqueue(c);
}
InvokeAsync(StateHasChanged);
}, null, 1000, 1000);
}
Polling an API every second just in case there's any new data isn't very efficient though. It would be better to have the API notify clients of any new data using eg SignalR or Push Notifications
Borrowing from the documentation example this would be enough to receive messages from the server:
@code {
private HubConnection hubConnection;
private List<string> messages = new List<string>();
private string userInput;
private string messageInput;
protected override async Task OnInitializedAsync()
{
hubConnection = new HubConnectionBuilder()
.WithUrl(NavigationManager.ToAbsoluteUri("/chathub"))
.Build();
hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
{
var encodedMsg = $"{user}: {message}";
messages.Add(encodedMsg);
StateHasChanged();
});
await hubConnection.StartAsync();
}