Прерывание выполняющийся задачи по нажатию кнопки. 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 шт):

Автор решения: aepot

Отмена операции - это последнее, о чем здесь следует переживать, напишу об этом в самом низу. Первое, что нужно сделать, это бросить устаревшие технологии. Второе - разобраться с многопоточностью.

По порядку:

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 и обязательно разберитесь, почему.

→ Ссылка