Прерывание выполняющийся задачи по нажатию кнопки. c#
Есть задача:
"Написать приложение просмотра погоды с UI. Требований к симпатичности UI нет. При нажатии кнопки "Узнать погоду" она меняется на "Отмена" и в это время приложение запрашивает результат у любого внешнего сервиса погоды. Затем на основании результатов запрашивает случайную картинку о полученной погоде из любого поисковика и отображает краткие сведения о погоде и картинку в окне. Кнопка "Отмена" меняется обратно. Пока идёт запрос, можно нажать "Отмена" чтобы его прервать."
Собственно проблема с прерыванием запроса, при нажатии кнопки создаётся поток, в котором выполняются запросы, и затем происходит заполнение контролов данными, из этих запросов. Но почему-то при повторном нажатии кнопки, до выполнении всего запроса, поток не прерывается. Совсем уже не понимаю в чем может быть дело.
using Newtonsoft.Json;
using System;
using System.IO;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using WeatherForecastApp.WeatherInfo;
namespace WeatherForecastApp
{
/// <summary>
/// Логика взаимодействия для MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
//Проверка статуса кнопки, пока кнопка не была нажата переменная хранит false
private bool btnPush = false;
private void btnMain_Click(object sender, RoutedEventArgs e)
{
try
{
//Фоновый поток в котором будет выполняться запрос, и заполнение ui элементов данными
Thread backThread = new Thread(new ThreadStart(loadData));
backThread.Name = "myThread";
backThread.IsBackground = true;
//Если false, значит запустить запрос
if (!btnPush)
{
btnPush = true;
btnMain.Content = "Cancel";
backThread.Start();
//loadData();
}
//Если true, прервать запрос
else
{
btnPush = false;
backThread.Abort();
backThread.Join();
MessageBox.Show("Прервано");
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
}
//Метод выполняющий запрос
private async void loadData()
{
//url api сайта с погодой
string urlWeather = "http://api.openweathermap.org/data/2.5/weather?q=Saint Petersburg&appid=8c5212cec55de3c0ae64db31343f805a&units=metric";
WebRequest webRequest = WebRequest.Create(urlWeather);
WebResponse webResponse = await webRequest.GetResponseAsync();
//Перменная для хранение результата запроса
string response;
//Читаем и сохраняем полученные данные
using (StreamReader strReader = new StreamReader(webResponse.GetResponseStream()))
{
response = await strReader.ReadToEndAsync();
}
//Конвертируем данные в специально созданную сущность для удобство чтения
WeatherResponse weatherResponse = JsonConvert.DeserializeObject<WeatherResponse>(response);
//Переменная хранящая описание текущих погодных условий, для поиска нужной картинки
string weatherName = weatherResponse.Weather[0].description;
//searchImage(weatherName);
//Url поисковика для передачи веб браузеру, в строку url передается название погоды, например overcast clouds
string urlImage = "https://www.google.com/search?q=" + weatherName + "&sxsrf=AOaemvLy4R0Xmx2A_aaD7zlWfr5cDOyWbA:1639733422777&source=lnms&tbm=isch&sa=X&sqi=2&ved=2ahUKEwj5_MKBw-r0AhVupZUCHdaQAtMQ_AUoAXoECAEQAw&biw=2560&bih=1297&dpr=1";
// string urlImage = "https://www.shutterstock.com/search/" + weatherName;
//Имитация продолжительног выполнения запроса для проверки функционала отмены запроса
await Task.Delay(3000);
//Вызов UI потока для заполняния UI элементов полученными данными
this.Dispatcher.Invoke(() => {
//Ищем нужную картинку
myWebBrowser.Navigate(urlImage);
lblWeatherName.Content = "Weather: " + weatherName;
//Если в контроле ListBox сохранились прошлые данные, чистим коллекцию
if (listTemp.Items != null)
listTemp.Items.Clear();
listTemp.Items.Add($"{weatherResponse.Main.temp} C");
listTemp.Items.Add($"Feels like: {weatherResponse.Main.feels_like} C");
listTemp.Items.Add($"Humidity: {weatherResponse.Main.humidity}%");
//Тоже самое с второй коллекцией
if (listWind.Items != null)
listWind.Items.Clear();
listWind.Items.Add($"Speed m/s: {weatherResponse.wind.speed}");
listWind.Items.Add($"Direction: {weatherResponse.wind.deg}");
//По завершеннии добавки данных меняем текст на кнопке
btnMain.Content = "Find weather";
});
//Кнопка снова готова к старту запроса
btnPush = false;
}
}
}
Ответы (1 шт):
Отмена операции - это последнее, о чем здесь следует переживать, напишу об этом в самом низу. Первое, что нужно сделать, это бросить устаревшие технологии. Второе - разобраться с многопоточностью.
По порядку:
WebRequest устарел
Об этом написано в его документации. Используйте HttpClient.
Особенность HttpClient в том, что его экземпляр не нужно создавать на каждый запрос. И самый простой вариант - использовать статическое поле.
private static readonly HttpClient _client = new HttpClient();
А сам запрос выполнить еще легче, чем с WebRequest. Мы же на .NET 6, давайте использовать его на полную катушку.
Но для начала создадим модель данных. Берем Json из запроса погоды по ссылке, которую вы показали, выполняем в браузере, далее открываем Visual Studio, Edit -> Paste Special -> Paste JSON as classes. Осталось переименовать RootObject и получится вот так.
public class WeatherResponse
{
public Coord coord { get; set; }
public Weather[] weather { get; set; }
public string _base { get; set; }
public Main main { get; set; }
public int visibility { get; set; }
public Wind wind { get; set; }
public Snow snow { get; set; }
public Clouds clouds { get; set; }
public int dt { get; set; }
public Sys sys { get; set; }
public int timezone { get; set; }
public int id { get; set; }
public string name { get; set; }
public int cod { get; set; }
}
public class Coord
{
public float lon { get; set; }
public float lat { get; set; }
}
public class Main
{
public float temp { get; set; }
public float feels_like { get; set; }
public float temp_min { get; set; }
public float temp_max { get; set; }
public int pressure { get; set; }
public int humidity { get; set; }
}
public class Wind
{
public int speed { get; set; }
public int deg { get; set; }
}
public class Snow
{
public float _1h { get; set; }
}
public class Clouds
{
public int all { get; set; }
}
public class Sys
{
public int type { get; set; }
public int id { get; set; }
public string country { get; set; }
public int sunrise { get; set; }
public int sunset { get; set; }
}
public class Weather
{
public int id { get; set; }
public string main { get; set; }
public string description { get; set; }
public string icon { get; set; }
}
3,5 клика мышкой. Если здесь модель выглядит богаче, чем нужно, можно просто поудалять лишнее.
Ну и сам запрос
string url = "http://api.openweathermap.org/data/2.5/weather?q=Saint%20Petersburg&appid=8c5212cec55de3c0ae64db31343f805a&units=metric";
WeatherResponse weather = await _client.GetFromJsonAsync<WeatherResponse>(url);
Согласитесь, так проще. В .NET Framework 4.x нет метода GetFromJsonAsync.
Асинхронность и многопоточность
Никогда не путайте эти 2 понятия, это 2 большие разницы. Углубляться не буду, можете почитать здесь и в документации.
В документации обратите внимание на разницу между CPU-интенсивными операциями
await Task.Run(() => СуперТяжелыеВычисления());
и I/O-зависимыми операциями.
await АсинхронныйЗапросВСеть();
Запрос в сеть или в базу или к диску, не требует явного запуска потока. Асинхронная операция вообще может не требовать выделенных потоков для работы.
WPF привязка данных
Чтобы реализовать привязку данных, надо сделать 3 вещи:
Иметь класс с публичным свойством
public class MainViewModel // Привет, MVVM!
{
public string КакоеТоСвойство { get; set; }
}
Назначить DataContext. Это можно сделать разными способами, самый простой вот:
public MainWindow()
{
InitializeComponent();
DataContext = new MainViewModel();
}
И просто привязаться к нему в XAML
<TextBlock Text="{Binding КакоеТоСвойство}"/>
Правда, чтобы привязки работали в реальном времени, а не только из конструктора вьюмодели, придется реализовать интерфейс INotifyPropertyChanged для вьюмодели, но это уже другая история, окунуться в нее можно например здесь, здесь на StackOverflow просто сотни примеров MVVM для WPF. Это важно, потому что вменяемые приложения под WPF без MVVM не пишутся. Сделайте паузу и разберитесь с этим. В код-бихайнде, то есть в классе MainWindow должно стать пусто, не должно быть кода кроме того что я выше показал.
Браузер
Не секрет, что гугл ненавидит ботов, и просто стукнуться туда с помощью HttpClient не получится. Пусть будет браузер, ваша идея вполне себе имеет право на жизнь, но с осторожностью. Браузер - штука тяжелая.
Есть непростой, но вполне вменяемый контрол WebView2, он не имеет ничего общего с устаревшим WebBrowser. Я не буду нагружать вас примерами, начните с чтения документации. Предупрежу только, что придется написать несколько очень маленьких javascript скриптов, чтобы распарсить ответ гугла и забрать из него картинку. HTML можно парсить и в C#, для этого рекомендую одну из 2 библиотек, на выбор: HtmlAgilityPack и AngleSharp.
Картинка для WPF - это просто ссылка, string. Берете свойство типа string, прямо как я выше показал, берете <Image Source="{Binding ТоСамоеСвойство}"/>, и готово, WPF сам вам загрузит эту картинку из сети в интерфейс. То есть самого WebView2 может и не быть в интерфейсе вовсе, всё для чего вам нужен браузер - это добыть нужную ссылку на картинку.
Отмена асинхронной операции
Эта тема весьма простая, все что нужно знать - это то что существует класс CancellationToken и то что практически все асинхронные методы в дотнете его принимают в качестве аргумента. Пример есть здесь.
Пока достаточно. Хотя нет, самый главный совет - никогда не используйте async void и обязательно разберитесь, почему.