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
  1.         bool m_inUseVariableForPreventingUnexpectedRecursion;
  2.  
  3.         [Test]
  4.         public void UseVariableForPreventingUnexpectedRecursion()
  5.         {
  6.             if (m_inUseVariableForPreventingUnexpectedRecursion)
  7.                 return;
  8.  
  9.             try
  10.             {
  11.                 m_inUseVariableForPreventingUnexpectedRecursion = true;
  12.  
  13.                 //Some code ...
  14.  
  15.                 //Generate recursion ..
  16.                 UseVariableForPreventingUnexpectedRecursion();
  17.             }
  18.             finally
  19.             {
  20.                 m_inUseVariableForPreventingUnexpectedRecursion = false;
  21.             }
  22.         }

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
  1.         [Test]
  2.         public void UseEnterMonitorClassForPreventingUnexpectedRecursion()
  3.         {
  4.             using (var mon = new EnterMonitor(this, "UseEnterMonitorClassForPreventingUnexpectedRecursion"))
  5.             {
  6.                 if (mon.IsAlredyEntered)
  7.                     return;
  8.  
  9.                 //Some code ...
  10.  
  11.                 //Generate recursion ..
  12.                 UseEnterMonitorClassForPreventingUnexpectedRecursion();
  13.             }
  14.         }

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
  1.     public class EnterMonitor : IDisposable
  2.     {
  3.         readonly object m_obj;
  4.         readonly string m_name;
  5.         bool m_thisExited;
  6.  
  7.         static readonly Dictionary<object, Dictionary<string, int>> Counters = new Dictionary<object, Dictionary<string, int>>();
  8.  
  9.         public EnterMonitor(object obj, string name)
  10.         {
  11.             m_obj = obj;
  12.             m_name = name;
  13.  
  14.             RegisterEnter();
  15.         }
  16.  
  17.         public void Dispose()
  18.         {
  19.             RegisterExit();
  20.         }
  21.  
  22.         public bool IsAlredyEntered { get { return SafeGetCounter() > 1; } }
  23.         public bool IsExited { get { return SafeGetCounter() == 0; } }
  24.  
  25.         void RegisterEnter()
  26.         {
  27.             Dictionary<string, int> counter;
  28.  
  29.             if (!Counters.TryGetValue(m_obj, out counter))
  30.             {
  31.                 counter = new Dictionary<string, int>();
  32.                 Counters[m_obj] = counter;
  33.             }
  34.  
  35.             int count;
  36.  
  37.             if (!counter.TryGetValue(m_name, out count))
  38.                 counter[m_name] = 0;
  39.  
  40.             counter[m_name]++;
  41.         }
  42.  
  43.         void RegisterExit()
  44.         {
  45.             if (m_thisExited)
  46.                 return;
  47.  
  48.             Counters[m_obj][m_name]--;
  49.  
  50.             //Remove entry for object where all counters is zero for prevent Memory Leak
  51.             if (Counters[m_obj].All(p => Counters[m_obj][p.Key] == 0))
  52.                 Counters.Remove(m_obj);
  53.  
  54.             m_thisExited = true;
  55.         }
  56.  
  57.         int SafeGetCounter()
  58.         {
  59.             Dictionary<string, int> counter;
  60.  
  61.             if (!Counters.TryGetValue(m_obj, out counter))
  62.                 return 0;
  63.  
  64.             int count;
  65.  
  66.             if (!counter.TryGetValue(m_name, out count))
  67.                 return 0;
  68.  
  69.             return counter[m_name];
  70.         }
  71.     }

NOTA: TryGetValue(...) è il metodo più performante per verificare se un entry esiste in un dizionario in quanto evita situazioni di doppio matching ...