Premessa
E' cosa nota per chi programma in Windows Forms (ma non solo...) situazioni di inaspettata ricorsione in fase di gestione degli eventi.
Di solito capita quando un gestore di un evento, o indirettamente e più facilmente, un metodo comune chiamato da quest'ultimo, modifica
un controllo UI che a sua volta scatena un nuovo evento il cui gestore invoca lo stesso codice... ed ecco la situazione di ricorsione imprevista
che nella maggioranza dei casi porta ad avere una "fastidiosa" ;) StackOverflowException!
Soluzione
Ci sono varie soluzioni al problema, oltre a riprogettare il gestore per evitare queste situazioni, un metodo potrebbe essere quello di deregistrare
temporaneamente gli eventi sul controllo che dobbiamo modificare, anche se quest'ultimo approccio non mi piace e di solito non lo utilizzo perché troppo macchinoso. Io preferisco utilizzare una variabile di classe che mi dice se sto già eseguendo quella parte di codice.
Di seguito riporto un esempio.
Code Snippet
- bool m_inUseVariableForPreventingUnexpectedRecursion;
-
- [Test]
- public void UseVariableForPreventingUnexpectedRecursion()
- {
- if (m_inUseVariableForPreventingUnexpectedRecursion)
- return;
-
- try
- {
- m_inUseVariableForPreventingUnexpectedRecursion = true;
-
- //Some code ...
-
- //Generate recursion ..
- UseVariableForPreventingUnexpectedRecursion();
- }
- finally
- {
- m_inUseVariableForPreventingUnexpectedRecursion = false;
- }
- }
Ho pensato però di ridurre questo "overhead" progettando una classe, l'idea alla base è quella di utilizzare un dizionario per registare le
invocazioni di metodi legati ad istanze di classe, e l'interfaccia IDisposable per racchiudere il tutto in un elegante statment using.
Ecco il risultato:
Code Snippet
- [Test]
- public void UseEnterMonitorClassForPreventingUnexpectedRecursion()
- {
- using (var mon = new EnterMonitor(this, "UseEnterMonitorClassForPreventingUnexpectedRecursion"))
- {
- if (mon.IsAlredyEntered)
- return;
-
- //Some code ...
-
- //Generate recursion ..
- UseEnterMonitorClassForPreventingUnexpectedRecursion();
- }
- }
Di seguito vi riporto il codice della classe: (NOTA: Ci sono due miglioramenti che voglio introdurre successivamente: 1. Sincronizzazione per funzionare in ambito di concorrenza 2. Tipizzare il nome del metodo usando una Lambda Expression...
Code Snippet
- public class EnterMonitor : IDisposable
- {
- readonly object m_obj;
- readonly string m_name;
- bool m_thisExited;
-
- static readonly Dictionary<object, Dictionary<string, int>> Counters = new Dictionary<object, Dictionary<string, int>>();
-
- public EnterMonitor(object obj, string name)
- {
- m_obj = obj;
- m_name = name;
-
- RegisterEnter();
- }
-
- public void Dispose()
- {
- RegisterExit();
- }
-
- public bool IsAlredyEntered { get { return SafeGetCounter() > 1; } }
- public bool IsExited { get { return SafeGetCounter() == 0; } }
-
- void RegisterEnter()
- {
- Dictionary<string, int> counter;
-
- if (!Counters.TryGetValue(m_obj, out counter))
- {
- counter = new Dictionary<string, int>();
- Counters[m_obj] = counter;
- }
-
- int count;
-
- if (!counter.TryGetValue(m_name, out count))
- counter[m_name] = 0;
-
- counter[m_name]++;
- }
-
- void RegisterExit()
- {
- if (m_thisExited)
- return;
-
- Counters[m_obj][m_name]--;
-
- //Remove entry for object where all counters is zero for prevent Memory Leak
- if (Counters[m_obj].All(p => Counters[m_obj][p.Key] == 0))
- Counters.Remove(m_obj);
-
- m_thisExited = true;
- }
-
- int SafeGetCounter()
- {
- Dictionary<string, int> counter;
-
- if (!Counters.TryGetValue(m_obj, out counter))
- return 0;
-
- int count;
-
- if (!counter.TryGetValue(m_name, out count))
- return 0;
-
- return counter[m_name];
- }
- }
NOTA: TryGetValue(...) è il metodo più performante per verificare se un entry esiste in un dizionario in quanto evita situazioni di doppio matching ...