Laden...

WPF Oberfläche friert ein - TimerEvent < 100ms

Erstellt von Nymos37 vor 2 Jahren Letzter Beitrag vor 2 Jahren 500 Views
N
Nymos37 Themenstarter:in
3 Beiträge seit 2022
vor 2 Jahren
WPF Oberfläche friert ein - TimerEvent < 100ms

Hallo,

Ich bin noch recht frisch und habe wenig Vorkenntnisse mit C#, bzw. möchte mich aktuell einarbeiten.
Dazu dachte ich wäre es super wenn ich kleines Programm schreibe, das mir Zyklisch Daten eines Arduinos über die Serielle Schnittstelle abfragt, bzw. auffordert diese zu senden.
Das Senden / Empfangen klappt auch soweit. Da ich die Daten gerne möglichst schnell "abtasten" würde, dachte ich mir: ich baue mir einen Timer mit den "Stopwatch" Befehlen, da die Standard Timer ja keine Zeiten kleiner 15ms hinbekommen (soweit ich das bisher gelesen habe).
Sogar das klappt mittlerweile so wie es mir vorgestellt hatte, zumindest der Teil mit dem Timer, wenn ich eine Liste Fülle mit den aktuellen Systemzeiten + Millisekunden schafft er auch 1ms Zeitabstand.
Ich löse aktuell jede Abtastung ein Event aus, das ich im Hauptprogramm abfrage und dann verarbeite.

Allerdings jetzt zu meinem Problem an dem ich nicht weiter komme.
Mein "Timer Event" soll zum testen erst mal nur einen Text in meine WPF Oberfläche schreiben, bzw. an eine Rich Text Box anhängen.
Mit 1ms am Timer friert die Oberfläche direkt ein.
Mit 10ms dauert es ein paar Sekunden bis die Oberfläche einfriert. 2s danach geht die Speicherauslastung nach oben.

Ich hab alles zur Übersicht in ein eigenes Projekt gespeichert. -> im Anhang.

Wenn ich etwas vereinfachen kann oder etwas umständlich gelöst habe bin ich um jede Anmerkung dankbar.
Wie gesagt versuche ich mich einzuarbeiten und stehe noch ziemlich am Anfang.

Ich hoffe ihr könnt mir helfen.

Ansonsten hier das Programm als Code:

Die Oberfläche:


<Window x:Class="Timer_Dummy.MainWindow"
        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:Timer_Dummy"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="262">
    <Grid>
        <Button Name="B_Timer" Content="Timer Start - Stop" HorizontalAlignment="Center" Margin="0,10,0,0" VerticalAlignment="Top" Width="242" Click="B_Timer_Click"/>
        <RichTextBox Name="RTB_Sent" Margin="0,35,0,10" HorizontalAlignment="Center" Width="242">
            <RichTextBox.Resources>
                <Style TargetType="{x:Type Paragraph}">
                    <Setter Property="Margin" Value="0"/>
                </Style>
            </RichTextBox.Resources>
            <FlowDocument>
                <Paragraph>
                    <Run Text=""/>
                </Paragraph>
            </FlowDocument>
        </RichTextBox>

    </Grid>
</Window>


Das eigentliche Programm:


using System;
using System.Threading;
using System.Windows;


namespace Timer_Dummy
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void B_Timer_Click(object sender, RoutedEventArgs e)
        {
            var MyTimer = new MyTimer(); //Referenz auf die Klasse (Event publisher)

            if (GlobalVars.MyTimerActive == false)
            {
                GlobalVars.MyTimerActive = true;
                MyTimer.MyTimerEvent += OnMyTimerEvent; //Methode die das Event Abboniert - bzw. => Abfragt ob es stattfindet -> Eintragen mit +=
                new Thread(() => { MyTimer.Run(10); }).Start();//Zyklischen Request in neuem Thread verarbeiten. (Damit GUI Thread direkt weiter geht) -> Parameter = xdel - time in ms
            }
            else
            {
                GlobalVars.MyTimerActive = false;
                MyTimer.MyTimerEvent -= OnMyTimerEvent; //Methode die das Event Abboniert - bzw. => Abfragt ob es stattfindet > Austragen mit -=

            }
        }

        private void OnMyTimerEvent(object source, EventArgs e)
        {
            Dispatcher.BeginInvoke(() => { TriggerEvent("MyTimerEvent ausgelöst"); });

        }

        private void TriggerEvent(string txt)
        {
            DateTime localDate = DateTime.Now;
            RTB_Sent.AppendText(localDate.ToLongTimeString() + ":" + localDate.Millisecond.ToString() + ":\t" + txt + "\r\n");//Text in RTB Box schreiben
            RTB_Sent.ScrollToEnd();
        }

    }
    public class GlobalVars
    {
        public static bool MyTimerActive = false;

    }
}


Die Timer Klasse:


using System;

namespace Timer_Dummy
{
    public class MyTimer
    {
        //create a Event:
        // 1. Define a delegate
        // 2. Define an event based on that delegate
        // 3. Raise the event
        public delegate void MyTimerEventHandler(object source, EventArgs args); //Event 1. Step - define a delegate
        public event MyTimerEventHandler MyTimerEvent; //Event 2. Step - define an event on the delegate
        
        System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();

        public void Run(int xdel)
        {
            int Sleep = 2;//to reduce cpu load for larger timesteps
            long OldElapsedMilliseconds = 0;
            sw.Start();
            while (sw.IsRunning)
            {
                long ElapsedMilliseconds = sw.ElapsedMilliseconds;
                long mod = (ElapsedMilliseconds % xdel);

                if (OldElapsedMilliseconds != ElapsedMilliseconds && (mod == 0 || ElapsedMilliseconds > xdel))
                {
                    //-----------------Do here whatever you want to do--------------Start
                    OnMyTimerEvent(); //Event 3. Step - Raise the event
                    //-----------------Do here whatever you want to do--------------End
                    //-----------------Restart----------------Start
                    OldElapsedMilliseconds = 0;
                    sw.Reset();
                    sw.Start();
                    System.Threading.Thread.Sleep(Sleep); //to reduce cpu load for larger timesteps
                    //-----------------Restart----------------End
                }
                
                //------------Must define some condition to break the loop here-----------Start
                if (GlobalVars.MyTimerActive == false)
                {
                    break;
                }
                //-------------Must define some condition to break the loop here-----------End
            }
        }

        protected virtual void OnMyTimerEvent()
        {
            if (MyTimerEvent != null)
            {
                MyTimerEvent(this, EventArgs.Empty);
            }
        }


    }
}


16.807 Beiträge seit 2008
vor 2 Jahren

Das kann man alles viel einfacher und stabiler machen, wenn man gewisse Pattern verwendet - hier Reactive Extensions.
GitHub - dotnet/reactive: The Reactive Extensions for .NET

Du erstellt einfach eine Subscription, die periodisch Deine Daten holt.
Reactive Extensions (Rx) - Part 4 - Replacing Timers - Muhammad Rehan Saeed

Die abgeholten Daten wirfst Du dann in eine Collection, die an Deine UI gebunden ist.
[Artikel] MVVM und DataBinding

MVVM ist der Mechanismus, den man in WPF verwenden sollte, weil das so konzeptionell vorgesehen ist.
Hier werden Dir auch alle Dinge wie Thread-Synchronität etc abgenommen - und Du musst das nicht von Hand zusammenwursten, wie Du es jetzt gemacht hast.

Dass die UI hier friert ist klar, weil Du Dich dauernd auf den UI-Thread für die Aktualisierung schalten musst.

PS: in .NET verwendet man für sowas keine Threads mehr, sondern Tasks.
Diese lassen sich viel einfacher und sicherer verwenden.

N
Nymos37 Themenstarter:in
3 Beiträge seit 2022
vor 2 Jahren

Hi Abt,

Vielen Dank für deine Antwort.
Ich hab versucht mal alles umzusetzen, und mir auch die Doku zum MVVM und DataBinding zu überfliegen. (Noch nicht dazu gekommen alles im Detail zu lesen).

Ich hab noch 2/3 Probleme bei der Umsetzung.

Die Sache mit Reactive Extensions scheint sinnig zu sein, um den Code übersichtlicher zu machen, aber offenbar kein muss (wenn ich es richtig verstanden habe).
Da das Thema mit .Net 6.0 noch nicht umgesetzt zu scheint sein, habe ich das erst mal außer acht gelassen.

Das mit dem DataBinding habe ich denke ich kapiert. Wenn WPF das schon bereit stellt, dann sollte man es auch nutzen.
Allerdings habe ich dabei jetzt ein Problem.

Meine Daten die mein Timer aktuell erzeugt schreibe ich nun in eine "StringCollection" - hoffe das ist soweit ok.
Beim auftreten des Timer Events schreibe ich jeden Eintrag hinten dran. Das scheint laut Debugger auch zu funktionieren. (Siehe Screenshot im Anhang)

Nur wie sage ich jetzt meiner "RichtTextBox" das diese die Daten aus dieser Collection herausziehen soll?
Finde auch bisher beim googlen nicht so richtig ein Beispiel, immer nur mit Textboxen, wo das im XAML recht einfach aussieht.
Mache ich das in XAML oder im Programm selbst? Wie ist da das normale, bzw. eleganteste Vorgehensweise ?

16.807 Beiträge seit 2008
vor 2 Jahren

Da das Thema mit .Net 6.0 noch nicht umgesetzt zu scheint sein, habe ich das erst mal außer acht gelassen.

Rx.NET unterstützt problemlos .NET 6. In .NET sind spezifische Releases nur bei direkten Abhängigkeiten notwendig, was es hier zuletzt bei .NET 5 (durch Windows SDK Changes) gab.
Ansonsten sind Updates hier aufwärtskompatibel, wie es in .NET konzeptionell gedacht ist.

Meine Daten die mein Timer aktuell erzeugt schreibe ich nun in eine "StringCollection" - hoffe das ist soweit ok.

Eine StringCollection verfügbar nicht über ein Binding-Benachrichtigungsmechanismus, wie zB. im Gegensatz zur ObservableCollection.

Nur wie sage ich jetzt meiner "RichtTextBox" das diese die Daten aus dieser Collection herausziehen soll?

Bei einem Binding geht das nur über eben diesen Benachrichtigungsmechanismus, bei WPF bzw. MVVM hier eben INotifyPropertyChanged.
Du musst also in solch eine Liste schreiben.

Die RichTextBox ist aber kein Element, der eine solche IEnumerable Bindung direkt anbietet, Du musst daher eine Datentransformation von einer Liste in einen String vornehmen.
RichTextBox Bindings sind etwas komplexer (weil halt auch andere/mehr Features als ne normale Textbox oder Liste), werden meist über FlowDocuments erzeugt.

Wie ich das mit Rx machen würde:

  • Eine Subscription erzeugen, die periodisch Daten abfragt
  • Ein Listener auf die Subscription setzen und die die Daten in eine ObservableCollection pumpen
  • RichTextBox Binding aktualisiert sich dann zB über das gebundene FlowDocument

Damit die RTB nicht unnötig lang wird (kein Mensch wird 1 Mio Einträge lesen), kannst zwischen Schritt 2 und 3 noch einen Range-Filter setzen.

N
Nymos37 Themenstarter:in
3 Beiträge seit 2022
vor 2 Jahren

Hi Abt,

danke nochmal für die weiteren Infos.
Mit den Extension werde ich mir anschauen. Sollte dann ja trotzdem gehen, wie du es beschrieben hast.

ObservableCollection fülle ich jetzt schon mit Daten. Allerdings bekomme ich das mit dem Anbinden an die RichtTextBox nicht hin.

Heute morgen dachte ich mir dann, probiere ich es eben mit einer TextBox, ich will ja erst mal nur Einträge darstellen - aber selbst auch da scheitere ich...
Gibt es irgendwo ein leicht verständliches Beispiel, damit ich das nachvollziehen kann, wie genau ich das aufbauen muss?

Tue mir damit echt schwer, sorry.

16.807 Beiträge seit 2008
vor 2 Jahren

Denke das einfachste Binding von ObservableCollection ist einfach eine ListBox.
Dazu kannst 1:1 das Beispiel hier nehmen, musst nur die Namen anpassen.
https://docs.microsoft.com/de-de/dotnet/desktop/wpf/data/how-to-create-and-bind-to-an-observablecollection?view=netframeworkdesktop-4.8