Laden...

Events an/abmelden in WPF-Anwendungen

Erstellt von Lector vor 15 Jahren Letzter Beitrag vor 15 Jahren 7.113 Views
L
Lector Themenstarter:in
862 Beiträge seit 2006
vor 15 Jahren
Events an/abmelden in WPF-Anwendungen

Hallo,

Ich habe mal eine grundsätzliche Frage zum Eventhandling in WPF-Anwendungen:

Grundsätzlich gilt ja dass man Events die man anmeldet auch wieder abmelden sollte um den GarbageCollector die Möglichkeit zu geben alten Speicher zu beseitigen.

Wenn ein Window sich auf das Click-Event eines Buttons registriert entsteht ein delegate welcher eine Referenz auf das Window hat.
Folgerung: Das Window kann nach dem Schließen nicht aufgeräumt werden da der delegate noch eine Referenz darauf hat.

Das Problem das nun entsteht ist: Wo melde ich meine Events wieder ab???
WPF-Elemente haben keine Dispose-Methode (wenn ich selbst IDisposable implementiere wird es nicht aufgerufen) und Unloaded zeigt ein merkwürdiges Verhalten:siehe hier

Lösung bietet das WeakEventPattern von WPF erscheint mir aber ziemlich umständlich wenn ich bei jeden Event einen Listener erstellen muss.

Meine Frage ist nun was passiert eigendlich wenn ich per XAML einem Button einen ClickEventHandler gebe?
Werden dann die Resourcen noch aufgeräumt?
Funktionieren die RoutedEvents von WPF intern bereits per WeakEventPattern?
Wie räume ich normale Events auf?

Und wie geht ihr eigendlich mit dieser Problematik um?

1.044 Beiträge seit 2008
vor 15 Jahren

Hallo Lector,

so ganz habe ich deine Problemstellung nicht verstandn. Möchtest du einfach einen Event nicht mehr auslösen, so kannst du das so machen:

btn.Click -= new RoutedEventHandler(Button_Click); // btn ist der Name des Buttons; Button_Click der EventHandler.

zero_x

5.742 Beiträge seit 2007
vor 15 Jahren

Hallo zusammen,

Möchtest du einfach einen Event nicht mehr auslösen, so kannst du das so machen

Ja - aber wo macht man das ganze am geschicktesten?!?

Auf diese Frage habe ich bisher auch keine wirkliche Antwort gefunden.

Wenn ein Window sich auf das Click-Event eines Buttons registriert entsteht ein delegate welcher eine Referenz auf das Window hat.
Folgerung: Das Window kann nach dem Schließen nicht aufgeräumt werden da der delegate noch eine Referenz darauf hat.

Das stellt jedoch kein Problem dar - da der Button (normalerweise) nur vom Window referenziert wird, können beide entsorgt werden.

Kritisch wird es allerdings bei untereinander verknüpften ViewModel Klassen, sobald nur eine davon ein Window "überlebt".

Meine Frage ist nun was passiert eigendlich wenn ich per XAML einem Button einen ClickEventHandler gebe?
Werden dann die Resourcen noch aufgeräumt?

Bisher habe ich das einfach gehofft bzw. mir gewünscht (vor allem im Zusammenhang mit Bindings und NotifyPropertyChanged) - kA, ob das wirklich zutrifft.

Und wie geht ihr eigendlich mit dieser Problematik um?

Zu meinem eigenen Unbehagen muss ich sagen: Unter den Tisch kehren...

Aber jetzt mache ich mich wieder einmal auf die Suche - die beste Variante war bisher der FastSmartWeakEventManager aus Codeproject: Weak Events in C#.
Dieser funktioniert aber leider nicht mit

946 Beiträge seit 2008
vor 15 Jahren

Grundsätzlich gilt ja dass man Events die man anmeldet auch wieder abmelden sollte um den GarbageCollector die Möglichkeit zu geben alten Speicher zu beseitigen.

Nicht ganz. Schau mal da: MemoryLeaks / nicht abgehängte Events.
So gross scheint das Problem nicht zu sein.

Ich habe das mal getestet:

using System;
using System.Collections.Generic;
using System.Threading;
using System.Windows;
using System.Windows.Controls;

namespace EventTest
{
    public class TestWindow : Window
    {
        public TestWindow()
        {
            Height = 200;
            Width = 200;

            Button newWindowButton = new Button { Height = 25, Content = "Neues Fenster" };
            newWindowButton.Margin = new Thickness(5);
            newWindowButton.Click += (s, e) => openTestWindow();

            Button newWindowsButton = new Button { Height = 25, Content = "25 Fenster öffnen" };
            newWindowsButton.Margin = new Thickness(5);
            newWindowsButton.Click += (s, e) =>
            {
                for (int i = 0; i < 25; i++)
                    openTestWindow();
            };

            Button closeWindowsButton = new Button { Height = 25, Content = "Alle Fenster schliessen" };
            closeWindowsButton.Margin = new Thickness(5);
            closeWindowsButton.Click += (s, e) => closeWindows();

            Button allocRamButton = new Button { Height = 25, Content = "Speicher anfordern" };
            allocRamButton.Margin = new Thickness(5);
            allocRamButton.Click += (s, e) => allocRam();

            StackPanel stackPanel = new StackPanel();
            stackPanel.Children.Add(newWindowButton);
            stackPanel.Children.Add(newWindowsButton);
            stackPanel.Children.Add(closeWindowsButton);
            stackPanel.Children.Add(allocRamButton);

            Content = stackPanel;

            Closed += (s, e) => closeWindows();
        }
        List<Window> windows = new List<Window>();

        void openTestWindow()
        {
            Window window = new Window { Height = 120, Width = 180, Title = "Test" };

            window.Tag = windows.Count;
            window.Closed += (s, e) => windows.RemoveAt((int)window.Tag);
            window.ShowInTaskbar = false;

            ButtonControl button = new ButtonControl { Margin = new Thickness(10), Content = "Hallo" };
            button.Click += (s, e) => { MessageBox.Show("Event ist dran"); };
            window.Content = button;

            window.Show();
            windows.Add(window);
        }
        void closeWindows()
        {
            for (int i = windows.Count - 1; i >= 0; i--)
                windows[i].Close();
        }

        void allocRam()
        {
            ThreadPool.QueueUserWorkItem((WaitCallback)(o =>
            {
                for (int i = 0; i < 1000; i++)
                {
                    int[] array = new int[100000000];
                }
            }));
        }
    }

    public class ButtonControl : Button
    {
        public ButtonControl() { }

        ~ButtonControl()
        {
            MessageBox.Show("Destruktor aufgerufen!");
        }
    }

    static class Programm
    {
        [System.STAThreadAttribute()]
        public static void Main()
        {
            new Application().Run(new TestWindow());
        }
    }
}

Man verzeige mir den XAML-Verzicht und die kompakte Programmierung für dieses Beispiel.

Trotz den beiden Event-Registraturen

window.Closed += (s, e) => windows.RemoveAt((int)window.Tag);
// und
button.Click += (s, e) => { MessageBox.Show("Event ist dran"); };

wird der Speicher wieder freigegeben.

Du machst dir vermutlich unnötig Sorgen.

EDIT's: Code...

mfg
SeeQuark

5.742 Beiträge seit 2007
vor 15 Jahren

Du machst dir vermutlich unnötig Sorgen.

Na ja, wenn es tatsächlich nur um isolierte Windows geht, kann ich dir zustimmen.

Sobald aber, wie ja bereits geschrieben, ein paar ViewModels dazukommen, kann man ganz schnell Probleme bekommen.

Und zwar dann, wenn man ein paar ViewModel Klassen während der gesamten Lebenszeit einer Anwendung "am Leben" lässt.
Denn _Window_s (oder generell Controls), die sich von diesen nicht abmelden, werden ebenfalls nicht zerstört.

L
Lector Themenstarter:in
862 Beiträge seit 2006
vor 15 Jahren
  
        void allocRam()  
        {  
            ThreadPool.QueueUserWorkItem((WaitCallback)(o =>  
            {  
                for (int i = 0; i < 1000; i++)  
                {  
                    int[] array = new int[100000000];  
                }  
            }));  
        }  
    }  
  
  

Trotz den beiden Event-Registraturen

window.Closed += (s, e) => windows.RemoveAt((int)window.Tag);  
// und  
button.Click += (s, e) => { MessageBox.Show("Event ist dran"); };  

wird der Speicher wieder freigegeben.

Du machst dir vermutlich unnötig Sorgen.

Das liegt aber höchstwarscheinlich daran dass du nie wirklich Speicher reservierst. Das int-Array welches du erzeugst wird nach jedem Schleifendurchlauf sowieso weggeworfen da es sich um eine lokale Variable handelt. Wenn du sichergehen willst dass der GC das Int-Array nicht wegwirfst solltest du die Referenz darauf behalten und später irgendeine Aktion damit machen um den Compiler davon abzuhalten das Array wegzuoptimieren.
Was passiert wenn du das Int-Array im Window speicherst, dort Zufallszahlen reinschreibst und im Closed ein zufälliges Element daraus ausgibst?

L
Lector Themenstarter:in
862 Beiträge seit 2006
vor 15 Jahren

Ich habe mir jetzt nochmal Gedanken gemacht und bin zu folgenden gekommen:

Auf Events im eigenen Window/Control sollte man sich ohne Probleme einfach an- und nicht mehr abmelden können da die Referenz des EventHandlers ja sowieso auf das gleiche Objekt geht. (korrigiert mich wenn ich falsch liege)

Auf Events von anderen Klassen (auch wenns nur ein Button-Click-Handler ist) umbedingt abmelden! Jetzt stellt sich nur die Farge WO an und WO abmelden...

Einfach im Loaded und Unloaded wird nicht funktionieren da eines dieser Events öfters kommen kann als das andere.
Loaded eines Controls wird nämlich bei folgenden Aktionen aufgerufen:*Ein Fenster mit unseren Control als Innhalt wird geöffnet *Ein Tab (o.Ä) welcher unser Control beinnhaltet wird selektiert

oder um es kurz zu fassen: Loaded wird aufgerufen wenn das Control einem Element hinzugefügt wird (was ja beim wechseln eines Tabs auch eintritt).

Unloaded wird bei folgenden Aktionen aufgerufen:*Ein Tab (o.Ä.) der unser Control beinhaltet wird weggeklickt *Ein Fenster welcher unser Control beinhaltet wird geschlossen ABER NUR wenn dadurch NICHT das Programm beendet wird

Die Tatsache dass Unloaded beim Programmende nicht ausgeführt wird ist allerdings nicht störend da wir uns in diesem Fall eh keine Gedanken um Resourcenbereinigung mehr machen müssen.

Ich habe mir mal die Mühe gemacht ein kleines Testprogramm über das Verhalten von Loaded und Unloaded zu erstellen:

Wir haben ein Control welches nur einen Button enthält. Der ClickHandler dazu wird nur im Code zugewiesen


  public partial class MyControl : UserControl
  {
    public MyControl()
    {
      InitializeComponent();
    }
    private void UserControl_Loaded(object sender, RoutedEventArgs e)
    {
      btn.Click += btn_Click;
    }
    private void UserControl_Unloaded(object sender, RoutedEventArgs e)
    {
      btn.Click -= btn_Click;
    }
    private void btn_Click(object sender, RoutedEventArgs e)
    {
      MessageBox.Show("Test");
    }
  }

Nun einige Szenarien wie wir das Control verwenden:
Der einfachste Fall


<local:MyControl/>

=> Beim Klicken wir eine MessageBox ausgegeben
Das Verhalten ist so wie es sein soll. Wenn wir allerdings Tabs in Spiel bringen um Loaded 2 Mal auszulösen werden auf einmal 2 MessageBoxen ausgegeben:


           <TabControl>
            <TabItem Header="Hier ist nichts">
                <Button Content="Ich mache nichts"/>
            </TabItem>
            <TabItem Header="Hier ist das Control">
                <local:MyControl/>
            </TabItem>
        </TabControl>

Also scheint ein einfaches zuweisen und entfernen in Loaded/Unloaded nicht auszureichen.
Nun stellt sich immer noch die Frage WO registrieren und WO abmelden ?( ?( ?(

Ich kann doch nicht der einzige sein der sich darüber Gedanken macht...

U
1.578 Beiträge seit 2009
vor 15 Jahren

geht es nicht im ctor/dtor?


public partial class MyControl : UserControl
{
    public MyControl()
    {
        InitializeComponent();
        btn.Click += btn_Click;
    }
    ~MyControl()
    {
        btn.Click -= btn_Click;
    }
    private void btn_Click(object sender, RoutedEventArgs e)
    {
        MessageBox.Show("Test");
    }
}

L
Lector Themenstarter:in
862 Beiträge seit 2006
vor 15 Jahren

Ich habe nochmal erneute Tests gemacht und festgestellt dass der GC Kreuzabhängigkeiten bei Window/Button erkennt und wegräumt. Wenn allerdings das Objekt welches das Event definiert länger lebt als das Control wird es nicht weggeräumt (besonders schlimm bei statischen Events).

Das Event im Konstruktor anmelden funktioniert wunderbar.
Im Destruktor abmelden eher weniger.

Nehmen wir mal an wir haben ein Objekt welches ein Event zur verfügung stellt. Dieses Objekt ist ziemlich langlebig.
Nun haben wir ein Control welches sich auf das Objekt ranhängt. Dazu wird ein delegate erstellt. Der delegate zeigt auf das Control. Und das Objekt welches das Event zur verfügung stellt beinnhaltet den delegate.

Nun wird ein Fenster geschlossen und das Control nicht mehr angezeigt.
Der Destruktor wird nicht ausgeführt. Das Objekt bleibt im Speicher. Warum???
Weil der GarbageCollector nur Objekte aufräumt die nicht mehr referenziert werden. Innerhalb unseres langlebigen Objekts existiert immer noch der delegat welcher auf das Control zeigt. Und wenn das Event feuert wird im Control auch immer noch Code ausgeführt.

Wenn der Destruktor ausgeführt wird hat der GarbageCollector das Control bereits freigegeben (oder ist kurz davor). Events abzumelden hat in diesen Szenario nur den Sinn dass das Objekt vom GC weggeräumt wird.
Das Event im Destruktor abzumelden ist als ob man sagen würde: "Ich löse das Problem wenn es bereits gelöst ist" was dazu führt dass es nicht gelöst wird...

Hier habe ich ein Beispiel welches das Problem mit dem Speicherleck relativ einfach schildert:

Unser Control besteht nur aus einem Button


public partial class MyControl : UserControl
  {
    private const int SIZE = 2000;

    private byte[] m_bytes;
    private Window m_owner;

    public MyControl(Window owner)
    {
      InitializeComponent();
      generateBitmap();

      m_owner = owner;
      m_owner.KeyDown += owner_KeyDown;//wenn man diese Zeile auskommentiert wird der Speicher aufgeräumt
    }

    private void owner_KeyDown(object sender, KeyEventArgs e)
    {
      MessageBox.Show("Keydown eingetreten");
    }

    private void generateBitmap()
    {
      Random r = new Random();
      m_bytes = new byte[SIZE * SIZE * 4];
      r.NextBytes(m_bytes);
    }

    ~MyControl()
    {
      MessageBox.Show("Destruktor im Control");
    }

    private void Button_Click(object sender, RoutedEventArgs e)
    {
      MessageBox.Show("Click");
    }
  }

Im Konstruktor generieren wir ein byte-Array. Beim Click auf den Button wird ein zufälliges Element dieses Arrays ausgegeben. Somit verhindern wir dass dieses Array beim kompilieren wegoptimiert wird.

Ein TestWindow besteht aus 2 Buttons:


    private void Button_Click(object sender, RoutedEventArgs e)
    {
        Window wnd = new Window();
        wnd.Content = new MyControl(this);
        wnd.Show();
    }

    private void Button_Click_1(object sender, RoutedEventArgs e)
    {
      GC.Collect();
      GC.WaitForPendingFinalizers();
    }

Der erste Button öffnet ein Fenster mit unserem Control. Das Control registriert sich auf das KeyDown-Event des Windows. Wenn wir nun sagen wir 20 Fenster aufmachen und wieder schließen können wir im Taskmanager sehen dass wir eine enorme Menge Speicher verbraten. Ein Click auf den GC-Button bewirkt nichts.

Grund: Das Hauptfenster hat durch einen delegate im KeyDown-Event noch sämtliche Windows gespeichert. Dies können wir ganz leicht daran erkennen dass 20 MessageBoxen auftreten sobald wir auf die Tastatur drücken.

Beim Schließen des Hauptfensters tritt der Destruktor der Controls ein und der Speicher wird aufgeräumt.

Nun kommentieren wir folgende Zeile aus:

m_owner.KeyDown += owner_KeyDown;

Wir starten das Programm, offnen 20 Fenster mit Controls. Laut Tastmanager steigt die Speicherauslastung unseres Programms. Wir machen die Fenster wieder zu.

Um das ganze etwas zu beschleunigen klicken wir auf den GC-Button und siehe da: Wie werden mit MessageBoxen bombadiert. Die Destruktoren der Controls treffen ein. Wenn wir erneut auf den GC-Button klicken sehen wir auch dass im TaskManager der Speicher wieder freigegeben wurde.

FAZIT:
Sich auf Events der eigenen Klasse zu registrieren ist harmlos.
Sich auf Events von untergeordneten Controls zu registrieren ist auch harmlos.

Wer also in XAML seine Click- und Loaded-Handler zuweist läuft NICHT Gefahr ein Speicherleck zu produzieren.

VORSICHT mit statischen Events und Events von Objekten welche länger leben können als unser Control. Diese müssen UNBEDINGT abgemeldet werden.

Und erneut stellt sich die Frage: WO ABMELDEN ?( ?( ?(

U
1.578 Beiträge seit 2009
vor 15 Jahren

waere da nicht angebracht sich etwas mit Dispose zu ueberlegen (IDisposable fuer das UserControl) ? nur so ne idee

5.742 Beiträge seit 2007
vor 15 Jahren

Hallo zusammen,

ich habe mich auch noch einmal mit der Thematik beschäftigt.

Events von Objekten welche länger leben können als unser Control. Diese müssen UNBEDINGT abgemeldet werden.

Zum Glück verwendet die Bindingengine der WPF aber einen WeakEventManager, um PropertyChanged zu abbonieren.
Im Zusammenhang hiermit braucht man sich also IMHO daher keine Sorgen zu machen.

waere da nicht angebracht sich etwas mit Dispose zu ueberlegen (IDisposable fuer das UserControl) ? nur so ne idee

Na ja - aber wer ruft dann Dispose auf?
Die einzige Möglichkeit wäre, das im Destruktor bzw. Finalizer zu erledigen.
Dadurch müssen diese Objekte aber wiederum länger im Speicher verbleiben.

Daher mein Fazit aus dem ganzen: Wo möglich Bindings oder den WeakEventManager verwenden.

Und der Rest wird beim Neustart der Anwendung frei (was beim Durchschnittsuser auch nicht allzu selten vorkommen sollte).

L
Lector Themenstarter:in
862 Beiträge seit 2006
vor 15 Jahren

Dispose habe ich auch schon probiert. Es wird nicht vom System aufgerufen das muss man selbst machen.

Aber gut zu wissen dass die WPF intern WeakEvents benutzt.

Vermutlich ist das wohl auch der einzige Weg sich auf Events langlebiger Objekte zu registrieren. Das setzt aber vermutlich vorraus dass die delegates der Events ihre Parameter in der Form (object sender, EventArgs e) implementieren.

Angenommen man benutzt eine Fremdbibliothek welche ihre Events nicht in dieser Form aufbaut hat man wohl Pech gehabt?
Habe ich das richtig verstanden?