Laden...

async Task mit HttpClient friert UI ein

Erstellt von littles vor 2 Jahren Letzter Beitrag vor 2 Jahren 339 Views
L
littles Themenstarter:in
2 Beiträge seit 2019
vor 2 Jahren
async Task mit HttpClient friert UI ein

Hi,
ich habe ein (verständnis-) Problem. Ich möchte einen JSON string durch Buttonclick via HTTPClient erhalten und dann in einem Textblock ausgeben . Dies funktioniert soweit ohne Probleme , jedoch friert die UI während der Abfrage für ca.90 sek. ein. Ich wäre euch dankbar, wenn mir jemand erklären könnte, wie es richtig funktioniert. asyncTask versucht, ebenso BackgroundWorker. Ich denke ich habe da etwas fundamentales nicht verstanden, finde den Fehzler einfach nicht.
Hier mal etwas Code:


class DataViewModel : Helper.ObservableObject
    {
        private string _dataString;
        private Models.DataModel _dataModel;
        private ICommand _getDataStringCommand;
        //internal async Task Worker_DoWork()
        //{
        //    ServicePointManager.Expect100Continue = true;
        //    ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
        //    ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;

        //    var request = (HttpWebRequest)WebRequest.Create("https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_COVID19/FeatureServer/0/query?where=1%3D1&outFields=*&outSR=4326&f=json");
        //    var response = (HttpWebResponse)request.GetResponse();

        //    var str = new StreamReader(response.GetResponseStream()).ReadToEnd();
        //    response.Close();
        //    DataString = str;

        //}
        private static readonly HttpClient _client = new HttpClient();
        public async Task<string> GetWebApiInfo()
        {
            ServicePointManager.Expect100Continue = true;
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;
            ServicePointManager.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
            var url = "https://services7.arcgis.com/mOBPykOjAyBO2ZKk/arcgis/rest/services/RKI_COVID19/FeatureServer/0/query?where=1%3D1&outFields=*&outSR=4326&f=json";
            string responseFromServer;
            using (var dataStream = await _client.GetStreamAsync(url))
            using (var reader = new StreamReader(dataStream, Encoding.UTF8))
            responseFromServer = await reader.ReadToEndAsync();

            return responseFromServer;
        }
       
        public string DataString
        {
            get { return _dataString; }
            set
            {
                if (value != _dataString)
                {
                    _dataString = value;
                    OnPropertyChanged("DataString");
                }
            }
        }
        public Models.DataModel DataModel
        {
            get { return _dataModel; }
            set
            {
                if (value != _dataModel)
                {
                    _dataModel = value;
                    OnPropertyChanged("DataModel");
                }
            }
        }
        public ICommand GetDataStringCommand
        {
            get
            {
                if (_getDataStringCommand == null)
                {
                    _getDataStringCommand = new Helper.RelayCommand(
                        param => RunCommand()
                    );
                }
                return _getDataStringCommand;
            }

        }
        private async void RunCommand()
        {
           string s = await GetWebApiInfo();
            DataString = s;
        }
       
    }


public abstract class ObservableObject : INotifyPropertyChanged
    {
        #region INotifyPropertyChanged Members

        /// <summary>
        /// Raised when a property on this object has a new value.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Raises this object's PropertyChanged event.
        /// </summary>
        /// <param name="propertyName">The property that has a new value.</param>
        protected virtual void OnPropertyChanged(string propertyName)
        {
            this.VerifyPropertyName(propertyName);

            if (this.PropertyChanged != null)
            {
                Debug.WriteLine("PropertyVhanged inner");
                var e = new PropertyChangedEventArgs(propertyName);
                this.PropertyChanged(this, e);
            }
        }

        #endregion // INotifyPropertyChanged Members

        #region Debugging Aides

        /// <summary>
        /// Warns the developer if this object does not have
        /// a public property with the specified name. This
        /// method does not exist in a Release build.
        /// </summary>
        [Conditional("DEBUG")]
        [DebuggerStepThrough]
        public virtual void VerifyPropertyName(string propertyName)
        {
            // Verify that the property name matches a real,
            // public, instance property on this object.
            if (TypeDescriptor.GetProperties(this)[propertyName] == null)
            {
                string msg = "Invalid property name: " + propertyName;

                if (this.ThrowOnInvalidPropertyName)
                    throw new Exception(msg);
                else
                    Debug.Fail(msg);
            }
        }

        /// <summary>
        /// Returns whether an exception is thrown, or if a Debug.Fail() is used
        /// when an invalid property name is passed to the VerifyPropertyName method.
        /// The default value is false, but subclasses used by unit tests might
        /// override this property's getter to return true.
        /// </summary>
        protected virtual bool ThrowOnInvalidPropertyName { get; private set; }

        #endregion // Debugging Aides
    }


public class RelayCommand : ICommand
    {
        #region Fields

        readonly Action<object> _execute;
        readonly Predicate<object> _canExecute;

        #endregion // Fields

        #region Constructors

        /// <summary>
        /// Creates a new command that can always execute.
        /// </summary>
        /// <param name="execute">The execution logic.</param>
        public RelayCommand(Action<object> execute)
            : this(execute, null)
        {
        }

        /// <summary>
        /// Creates a new command.
        /// </summary>
        /// <param name="execute">The execution logic.</param>
        /// <param name="canExecute">The execution status logic.</param>
        public RelayCommand(Action<object> execute, Predicate<object> canExecute)
        {
            if (execute == null)
                throw new ArgumentNullException("execute");

            _execute = execute;
            _canExecute = canExecute;
        }

        #endregion // Constructors

        #region ICommand Members

        [DebuggerStepThrough]
        public bool CanExecute(object parameters)
        {
            return _canExecute == null ? true : _canExecute(parameters);
        }

        public event EventHandler CanExecuteChanged
        {
            add { CommandManager.RequerySuggested += value; }
            remove { CommandManager.RequerySuggested -= value; }
        }

        public void Execute(object parameters)
        {
            _execute(parameters);
        }

        #endregion // ICommand Members
    }


<Window x:Class="WpfApp7.Views.Home"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfApp7.Views"
        mc:Ignorable="d"
        xmlns:ll="clr-namespace:WpfApp7.ViewModels"
        Title="Home" Height="450" Width="800">
    <Window.DataContext>
        <ll:DataViewModel/>
    </Window.DataContext>

    <Grid>
        <TextBlock x:Name="textBlock" HorizontalAlignment="Left" TextWrapping="Wrap" Text="{Binding DataString}" VerticalAlignment="Top"/>
        <Button x:Name="button" Content="Button" HorizontalAlignment="Left" Margin="421,225,0,0" VerticalAlignment="Top" Width="75" Command="{Binding Path=GetDataStringCommand}"/>

    </Grid>
</Window>

Ich wäre euch für jeden Ratsachlag dankbar!
Lg littles

16.842 Beiträge seit 2008
vor 2 Jahren

Du führst Deinen Task mit ConfigureAwait(true) aus, weil Du nicht ConfigureAwait(false) verwendest. True ist das Standardverhalten.
Im UI Thread sorgt das dafür, dass der Task synchronisiert mit der UI ausgeführt wird, was zum einfrieren der UI führen kann, aber dafür sorgt, dass zB Values im richtigen Thread gesetzt werden und es keine Thread-Verletzung gibt.
Siehe FAQ: ConfigureAwait FAQ
Kommt also auf die Aufgabe an, ob man True oder False benötigt. Für Nicht-UI Code sollte man immer ConfigureAwait(false) verwenden - überall!

Das Abrufen der HTTP Abfrage muss mit ConfigureAwait(false) ausgeführt werden; UI Aktualisierungen dann wieder mit ConfigureAwait(true)

PS: API Abfragen kannst Du super einfach mit Refit umsetzen.
Das nimmt Dir all die Konfiguration und das Modelling ab (nicht aber async/await).
https://github.com/reactiveui/refit

PPS:
Dein Code ist voller Magic Strings
Schreibt statt throw new ArgumentNullException("execute"); lieber throw new ArgumentNullException(nameof(execute));

4.942 Beiträge seit 2008
vor 2 Jahren

Deine OnPropertyChanged-Aufrufe kannst du auch entweder per nameof(...) von den Magic-Strings bereinigen oder noch besser per CallerMemberName-Attribut in deiner OnPropertyChanhed-Methode auf die Angabe ganz verzichten, s. Beispielcode in INotifyPropertyChanged.PropertyChanged Ereignis (dort heißt die Methode NotifyPropertyChanged).

L
littles Themenstarter:in
2 Beiträge seit 2019
vor 2 Jahren

Danke für die schnellen Antworten. Ich habe es mal getestet, jedoch friert die UI immernoch ein. Mir ist aufgefallen, dass es "hängt", wen ich den String in de Textbox schreibe (mehrere 1000 Zeilen lang in einem Rutsch). Wie kann ich das lösen? Die Daten Zeile für Zeile in einem eigenen task an die Textbox senden ,oder observableobject als async umschreiben? Als absoluter Anfänger stehe ih gerade etwas auf dem Schlauch. Das Ergebnis vom httprequest und vom streamreader stehen innerhalb von kürzester Zeit bereit, danach friert die ui ein und hängt, bis die Textbox gefüllt ist. Dauert dann ca. 90 Sekunden.

16.842 Beiträge seit 2008
vor 2 Jahren

Stell Dir die Frage wie sinnvoll es ist, wenn Du mehrere tausend Zeilen in eine Textbox schreibst, wem hilft das?
Das kannst Du an der Stelle auch nicht verhindern, weil das ein UI Zugriff ist - und die Textbox bei viel Text auch nicht performant ist; das ist nicht der Sinn einer Textbox.
Willst Du mehrere MB an Text sehr schnell anzeigen, dann musst Du das selbst zeichnen - ohne Control.