C# Power ponownie - Control.AllChildControls

Podczas przygotowywania kolejnego posta z serii "Samples" zaimplementowało mi się coś bardzo fajnego, co chyba zasługuje na osobną notkę. Oto zatem kolejna krótka demonstracja potęgi połączonych mechanizmów C# (v. 2 i 3).
Scenariusz: mamy formatkę wypełnioną panelami, groupboxami, layoutami i wszystkimi innymi kontenerami jakie tam jeszcze Bozia z Redmond na toolbox wrzuciła. Nachodzi nas chętka na wykonanie pewnej operacji na WSZYSTKICH kontrolkach zawartych w oknie, niezależnie od tego gdzie są zagnieżdżone. Jak się do nich dobrać? Here comes the beauty of YIELD:

  1:  public static IEnumerable<Control> AllChildControls(this Control instance)
  2:  {
  3:  	foreach (Control control in instance.Controls)
  4:  	{
  5:  		yield return control;
  6:  		foreach (Control childControl in control.AllChildControls())
  7:  			yield return childControl;
  8:  	}
  9:  }

Wykorzystajmy to w jakimś interesującym przypadku... na przykład pod każdy TextBox podepnijmy ToolTipa pokazującego aktualną wartość właściwości Tag. Warunki z tego wynikające są dwa: kontrolka-dziecko musi być TextBoxem i jej Tag nie może być null. Na potęgę posępnego czerepu, LINQ przybywaj!!!

  1:  this.AllChildControls().OfType<TextBox>()
  2:  	.Where(tb => tb.Tag != null).ToList()
  3:  	.ForEach(tb => toolTip.SetToolTip(tb, tb.Tag.ToString()));

Może to jakieś zaćmienie, może za dużo kodowania bez przerwy, może nie widzę innego równie wyśmienitego rozwiązania, ale... jestem pod wrażeniem. Jak wyglądałby kod robiący to samo jeszcze kilkanaście miesięcy temu? Pewnie jakoś tak. Nie uzależniajmy się od technologii, ale wykorzystujmy w pełni to co nam oferuje!

Opublikowane 30 sierpnia 08 10:22 przez Procent
Filed under: ,

Powiadamianie o komentarzach

Jeżeli chciałbyś otrzymywać email gdy ta wypowiedź zostanie zaktualizowana, to zarejestruj się tutaj

Subskrybuj komentarze za pomocą RSS

Komentarze:

# szogun1987 said on sierpnia 31, 2008 22:03:

Byłaby ładna rekurencja i chyba nawet czytelniejsza :P

# Procent said on września 1, 2008 08:19:

"AllChildControls" znajdziemy i w linii 1 (deklaracja) i w linii 6 (wywołanie samej siebie), więc rekurencję jak najbardziej mamy:)

# ucel said on września 1, 2008 12:51:

Da sie to zrobic oszczedniej, ale wymaga implementacji ForEach dla IEnumerable. Zauwaz ze wywolujesz metode ToList(), co w efekcie podwoi wymagania pamieciowe kolekcji. Implementacja extension method ForEach() dla IEnumerable zlikwidowalaby ten problem . Z drugiej strony nie rozumiem dlaczego nie ma tego natywnie w System.Core :/.

# Procent said on września 1, 2008 13:09:

Tak jest, też się nad tym zastanawiałem. "Przeszukałem nawet pół internetu" ale odpowiedzi nie znalazłem.

A co do podwojenia wymagań pamięciowych... czy na pewno? W obecnej postaci utworzona zostanie lista ze wszystkimi textboxami a dopiero potem niepotrzebne się odfiltrują. Z ForEach w IEnumerable filtrowanie odbędzie się na etapie tworzenia listy. Tak czy siak - wszystkie kontrolki już istnieją, więc gdzie może wystąpić dodatkowe zuzycie pamięci? ToList() wymusza po prostu wykonanie instrukcji o jeden krok wcześniej niż gdyby nastąpiło to po Where(). W efekcie od razu uzyskalibyśmy mniejszą listę.

Chyba że się mylę, wtedy z radością powitam wszelkie wyjaśnienia:)

# apl said on września 1, 2008 16:13:

@ucel: Z koncepcyjnego punktu widzenia zapytanie LINQ nie powinno wywoływać efektów ubocznych na elementach kolekcji, stąd brak metod takich jak ForEach, które ośmielałyby programistów do adopcji takiego modelu programowania. Jeśli spojrzeć z tej strony, to podejście Procenta jest słuszne: zapytanie jest formułowane i ewaluowane, a następnie elementy, które znalazły się w zbiorze wynikowym są przetwarzane. Inna sprawa, że możnaby zakończyć zapytanie zaraz po Where, a następnie użyć zwykłej pętli foreach do iteracji po wynikach.

Rolę takiego deklaratywnego ForEacha w świecie LINQ może z powodzeniem pełnić operator Select:

this.AllChildControls()

  .OfType<TextBox>()

  .Where(tb => tb.Tag != null)

  .Select(tb => { toolTip.SetToolTip(tb, tb.Tag.ToString()); return tb; });

Problemem jest tylko to, że musimy jeszcze takie wyrażenie jakoś ewaluować, żeby miało sens.

@Procent: ucel może trochę przeszacował zużycie pamięci, jednak słusznie zwrócił uwagę, że etapu, na którym tworzymy nowy obiekt listy można uniknąć. Do szczęścia wystarczy nam referencja do iteratora zwracana przez metodę Where:

public static void ForEach<T>(this IEnumerable<T> items, Action<T> action)

{

  foreach (T item in items) {

     action(item);

  }

}

Teraz możemy zapisać wyrażenie w ten sposób:

this.AllChildControls()

  .OfType<TextBox>()

  .Where(tb => tb.Tag != null)

  .ForEach(tb => toolTip.SetToolTip(tb, tb.Tag.ToString()));

Na żadnym etapie nie są tworzone tymczasowe kolekcje - wszystko oparte jest o iteratory.

# Procent said on września 1, 2008 16:26:

Dzięki Olek za wyjaśnienia, szczególnie kwestii "dlaczego w System.Core nie zaimplementowano IEnumerable<T>.ForEach". "Koncepcyjny punkt widzenia" ma sens, nigdy w ten sposób na to nie spojrzałem.

# ucel said on września 1, 2008 23:31:

No przeciez wlasnie cos takiego mialem na mysli ;)

Co o tym myślisz?

(wymagane) 
(opcjonalne)
(wymagane) 

  
Wprowadź kod: (wymagane)

About Procent