Best practices for handling async code in Terminal.Gui apps #4377
-
|
Hi everyone, I’m wondering what’s the best way to deal with async code when building For example, how should I handle a case where, on the initial load of the app, I need to fetch some data from an HTTP endpoint using an async call and then display it on the screen using Any guidance or examples would be greatly appreciated! Thanks! |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 1 reply
-
|
See https://gui-cs.github.io/Terminal.Gui/docs/multitasking.html That's not sufficient please let us know. |
Beta Was this translation helpful? Give feedback.
-
|
If you want an example of how to integrate an HTTP call into #nullable enable
using System.Collections.ObjectModel;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace UICatalog.Scenarios;
[ScenarioMetadata ("HttpAsyncCall", "Http Async Call example")]
[ScenarioCategory ("Controls")]
public sealed class HttpAsyncCall : Scenario
{
public override void Main ()
{
Application.Init ();
Application.Run<HttpWindow> ().Dispose ();
Application.Shutdown ();
}
}
public class HttpWindow : Window
{
private readonly ListView _listView;
private readonly ObservableCollection<string> _items = [];
private object? _timeoutToken;
public HttpWindow ()
{
Title = $"Press {Application.QuitKey} to Quit";
BorderStyle = LineStyle.Single;
_listView = new ()
{
Width = Dim.Fill (),
Height = Dim.Fill (),
Source = new ListWrapper<string> (_items)
};
Add (_listView);
Loaded += HttpWindow_Loaded;
_timeoutToken = Application.AddTimeout (TimeSpan.FromHours (1), ProcessTimeout);
}
private void HttpWindow_Loaded (object? sender, EventArgs e)
{
Loaded -= HttpWindow_Loaded;
_ = LoadApiDataAsync ();
}
private bool ProcessTimeout ()
{
_ = LoadApiDataAsync ();
return true;
}
private async Task LoadApiDataAsync ()
{
AddAndMoveDown ("Fetching PM2.5 data from data.gov.sg...");
try
{
string url = "https://api.data.gov.sg/v1/environment/pm25";
var pm25Data = await FetchPm25DataAsync (url);
var item = pm25Data.Items [0];
AddAndMoveDown ($"Timestamp: {item.Timestamp}");
AddAndMoveDown ($"Update Timestamp: {item.UpdateTimestamp}");
AddAndMoveDown ($"API Status: {pm25Data.ApiInfo.Status}");
AddAndMoveDown (new ('-', 60));
AddAndMoveDown ($"{"Region",-10} {"Latitude",-10} {"Longitude",-10} {"PM2.5",-6} {"Quality",-10}");
AddAndMoveDown (new ('-', 60));
foreach (var region in pm25Data.RegionMetadata)
{
string name = region.Name;
double lat = region.LabelLocation.Latitude;
double lon = region.LabelLocation.Longitude;
// Find reading by region name
if (item.Readings.Pm25OneHourly.TryGetValue (name, out int value))
{
string quality = GetAirQuality (value);
StringBuilder sb = new ();
sb.Append ($"{name,-10} ");
sb.Append ($"{lat,-10:F5} {lon,-10:F5} ");
sb.Append ($"{value,-6}");
sb.Append ($"{quality,-10}");
AddAndMoveDown (sb.ToString ());
}
}
AddAndMoveDown (new ('-', 60));
}
catch (Exception ex)
{
AddAndMoveDown ($"Error fetching data: {ex.Message}");
}
}
private static string GetAirQuality (int value) => value switch
{
<= 12 => "Good",
<= 37 => "Moderate",
_ => "Poor"
};
private void AddAndMoveDown (string data)
{
_items.Add (data);
_listView.MoveDown ();
}
private static async Task<Pm25Response> FetchPm25DataAsync (string url)
{
using HttpClient client = new HttpClient ();
client.Timeout = TimeSpan.FromSeconds (10);
var response = await client.GetAsync (url);
response.EnsureSuccessStatusCode ();
var stream = await response.Content.ReadAsStreamAsync ();
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
var data = await JsonSerializer.DeserializeAsync<Pm25Response> (stream, options)
?? throw new InvalidOperationException ("Failed to parse JSON.");
return data;
}
/// <inheritdoc />
protected override void Dispose (bool disposing)
{
if (disposing)
{
if (_timeoutToken != null)
{
Application.RemoveTimeout (_timeoutToken);
_timeoutToken = null;
}
}
base.Dispose (disposing);
}
}
public class Pm25Response
{
[JsonPropertyName ("region_metadata")]
public List<RegionMetadata> RegionMetadata { get; set; } = new ();
[JsonPropertyName ("items")]
public List<Item> Items { get; set; } = new ();
[JsonPropertyName ("api_info")]
public ApiInfo ApiInfo { get; set; } = new ();
}
public class RegionMetadata
{
[JsonPropertyName ("name")]
public string Name { get; set; } = "";
[JsonPropertyName ("label_location")]
public LabelLocation LabelLocation { get; set; } = new ();
}
public class LabelLocation
{
[JsonPropertyName ("latitude")]
public double Latitude { get; set; }
[JsonPropertyName ("longitude")]
public double Longitude { get; set; }
}
public class Item
{
[JsonPropertyName ("timestamp")]
public DateTime Timestamp { get; set; }
[JsonPropertyName ("update_timestamp")]
public DateTime UpdateTimestamp { get; set; }
[JsonPropertyName ("readings")]
public Readings Readings { get; set; } = new ();
}
public class Readings
{
[JsonPropertyName ("pm25_one_hourly")]
public Dictionary<string, int> Pm25OneHourly { get; set; } = new ();
}
public class ApiInfo
{
[JsonPropertyName ("status")]
public string Status { get; set; } = "";
} |
Beta Was this translation helpful? Give feedback.
-
|
Thanks guys, that was exactly what I was looking for! |
Beta Was this translation helpful? Give feedback.
If you want an example of how to integrate an HTTP call into
Terminal.Gui, see the example below. This is a new scenario added to theUICatalogproject, but you can use it in your own project, with theMainmethod in theProgram.csfile. When the application loads, the data is immediately retrieved through theLoadedevent. Later, it can be retrieved via the timer added through theApplication.AddTimeoutcall. I used aListViewas the consumer of these calls, but you could use any other view or views, depending on your needs.