Author Archive

.NET i memory leak?! czyli dzień z życia programisty

Author: Jarek Kożdoń (jarek.kozdon) | kwiecień 1st, 2011
avatar

Dzisiaj historia z życia wzięta – czyli ponad dzień pracy programisty. Akcja będzie wartka, a potencjalnemu czytelnikowi zalecam wczytanie do pamięci RAM wewnątrz głów dll’ek z zasobami o WPF’ie.

Zaczęło się niewinnie, przyszło zgłoszenie od testerów, że aplikacja zajmuje strasznie dużo pamięci, a przy wykonywaniu pewnej operacji ta zajętość jeszcze rośnie i wielce nazywać to chcieli memory leakiem. Jako programista raczej nie dowierzałem, przecież w dot-necie tak być nie może, zgodnie z teorią pamięć sama się zwalnia, Pan Garbage Collector robi wszystko za nas… pomijając fakt że programista generalnie nie wierzy testerom, którzy zawsze szukają dziury w całym…

ot… teoretycznie…

Godzina 1

kawa i te sprawy… partyjka piłkarzyków, przecież zadanie na pewno zostanie odbite!

Godzina 2

Sprawdzam – i już widzę, że jednak coś jest na rzeczy. Coś się nie zwalnia. Dziwne…

Godzina 3

No tak – testerzy jak to w ich zwyczaju coś znaleźli, ale sami nie wiedzieli co. Nie trzeba nawet wykonywać zgłoszonej akcji… wystarczy wejść do okienka i wyjść z niego. I tak kilka razy i task manager poda nam jakieś fantastyczne, zawrotne sumy pożeranej przez naszą aplikację pamięci.

Godzina 4

Kod przeanalizowany, wszystko co trzeba zdaje się być zwalniane. Wszystko gra, a jednak… jednak nie… Powoli pojawiają się jednak promyki nadziei na znalezienie przyczyny.

Godzina 5

Nadzieja rośnie, podejrzany jest namierzony – udało się zablokować przeciek. Ale coś jednak tutaj nie pasuje, coś jest nie-tak. Bug niby zwalczony, ale wewnątrz płonie ogień buntu, że przecież to nie była faktyczna przyczyna. Trzeba sięgnąć po cięższą artylerię. Trzeba skombinować jakiegoś profilera z opcją analizy pamięci. Na pierwszy ogień idzie ANTS Memory Profiler firmy Red Gate („tej od .NET Reflectora”). Piłkarzyki, kawa… w końcu musi się pobrać i zainstalować (tak trochę tej kawy dzisiaj… to piątek, zmęczenie tygodnia się kumuluje, a rano wstałem o 5:15… pobiegać…)

Godzina 6

Profiler odpalony, bardzo pozytywne zaskoczenie, intuicyjność pierwsza klasa, bez czytania tutoriali, bez instrukcji w ciągu kilku minut znajduję – prawdziwą przyczynę błędu. Otóż okienko WPF zostaje w pamięci, a jest trzymane przez… przez jego własną kontrolkę! Przecież to niemożliwe… przecież „GC” wykrywa i cykliczne referencje i w ogóle te wszystkie szmery bajery…

Mija jeszcze kilka chwil zanim ogarnę cały temat…

Sprawdzam co to za kontrolka, Google, dokumentacja MSDN i robi mi się smutno, dopisuję 1 (słownie jedną) linię kodu i „fixuję buga w JIRA’ze”.

Po przydługim wstępie wytłumaczenie na przykładzie, dla uproszczenia malutka aplikacja WPF – 2 formatki i jedna klasa oczywiście:

Okienko główne z buttonem i akcją otwierającą nowe okienko:

<Window x:Class=”MemoryLeaking.Window1″
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
Title=”Window1″ Height=”170″ Width=”306″>
<Grid>
<Button Margin=”50,50,50,50″ Click=”Button_Click”>Take the RAM!</Button>
</Grid>
</Window>

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

private
void Button_Click(object sender, RoutedEventArgs e)
{
new MemoryTakingWindow().ShowDialog();
GC.Collect();
}
}
}

Klasa „zjadająca RAM” (banalnie prosta – lista stringów, do każdego przypisujemy GUID):

namespace MemoryLeaking
{
class MemoryTakingClass
{
private List<string> MemoryTaker;

public MemoryTakingClass()
{
MemoryTaker = new List<string>(100000);

for
(int i = 0; i < 100000; i++)
{
MemoryTaker.Add(Guid.NewGuid().ToString());
}
}
}
}

Okienko „zjadające RAM”:

<Window x:Class=”MemoryLeaking.MemoryTakingWindow”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:winForms=”clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms”
Title=”MemoryTakingWindow” Height=”134″ Width=”306″>
<Grid>
<WindowsFormsHost Name=”host”></WindowsFormsHost>
</Grid>
</Window>

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

this.DataContext = new MemoryTakingClass();
}
}
}

Wszystkich estetów wyczulonych na GC.Collect() proszę o wybaczenie. Użycie tego wywołania w naszym przykładzie ma bardzo głęboki sens – chodzi o to, by mieć pewność, że w pamięci zajmowanej przez proces będą zajęte wyłącznie bajty rzeczywiście zajęte. Zresztą czasem takie wywołanie ma głębszy sens, chociaż oczywiście lepiej go unikać i projektować aplikację tak, by nie było ono potrzebne. To jednak temat na osobne dyskusje.

Dla wyjaśnienia – oprócz przekopiowania powyższego kodu do własnej aplikacji należy jeszcze dodać do projektu referencję do biblioteki WindowsFormsIntegration (z niej pochodzi ta dziwna kontrolka <WindowsFormsHost>. Kontrolka służy do hostowania kontrolek pochodzących z tradycyjnych “winformsów” w kontrolkach / oknach WPF.

I teraz – do dzieła. Najpierw wykres zużycia pamięci dla takiej aplikacji, po kilku wywołaniach okna MemoryTakingWindow:


Gdybyśmy byli budowniczymi moglibyśmy się cieszyć z (prawie) równych schodów, jednak jako programiści wpadamy właśnie w konsternację… przecież to niemożliwe, żeby takie cyrki działy w aplikacji .NET!

Dokładnie to samo czułem. Pierwsza myśl skierowała mnie na DataContext (tutaj oczywiście uproszczony), gdyż w rzeczywistej aplikacji klasa do niego podpinana zabierała najwięcej pamięci i przechowywała olbrzymie ilości danych. Dokonałem małej modyfikacji w celu weryfikacji podejrzeń – mianowicie do klasy DataContext dodałem finalizer (u nas byłby to ~MemoryTakingClass(){ }) i postawiłem tam breakpointa. Breakpoint po kilku minutach miał wciąż tyle trafień, co nasz reprezentacja w ostatnim meczu z Litwą (dla mniej zorientowanych – 0).

Pierwsza myśl była banalnie prosta, skoro tam gdzieś jakieś zależności się trzymają, to przecież wystarczy odpiąć DataContext od kontrolki! Czyli dodać taki kod:

Closed += ((sender, e) =>
{
this.DataContext = null;
});

na przykład gdzieś w konstruktorze naszego okienka MemoryTakingWindow. Postęp jest natychmiastowy – po powtórzeniu doświadczenia otrzymujemy dużo ładniejszy wynik w postaci ząbków:

Pięknieje także liczba trafień w breakpointa – staje się dokładnie równa liczbie wywołań okna.

Na tym etapie można by zamknąć zadanie i przejść dalej. Jednak bardziej dociekliwym jednostkom jest wciąż mało i wciąż coś nie gra. Dlaczego przecież wcześniej obiekt klasy MemoryTakingClass nie był zwalniany? Dlaczego ciągle coś go trzymało, fakt uwolnienia go po odpięciu od DataContext jest podejrzany – oznacza przecież, że… coś ciągle trzyma okienko! Tylko co i dlaczego, no i czy na pewno.

Tutaj właśnie sięgnąłem po grubą altyrerię w postaci ANTS Memory Profilera. Po uruchomieniu aplikacji w Profilerze wykonujemy “doświadczenie” (otwarcie kilkukrotne okienka) i używamy funkcji “Take memory snapshot”, po czym w szukamy naszych klas na liście:

Tutaj wybieramy naszego “podejrzanego” i wykorzystujemy opcję “Instance list”, która zwraca nam listę wszystkich instancji klasy istniejących w momencie stworzenia snapshota.


I cóż to?! W pamięci zalega nam 5 obiektów okna! Wiemy już gdzie leży przyczyna całej tej udręki – a odpinanie DataContext nie było fixem, a tylko ograniczeniem uciążliwości błędu – pozbywamy się z pamięci najcięższych obiektów, ale niestety nie wszystkich. Pozostaje ostatni krok – odpowiedź na pytanie – co powstrzymuje nasze okno przed zwolnieniem? Musimy prześledzić wszelkie referencje do niego. Gdyby przyszło nam w tym momencie analizować kod…

Całe szczęście możemy wykorzystać ostatnią “prezentowaną” dziś opcję ANTS Profilera – “Instance Retention Graph”. Jest to graf przedstawiający łańcuchy obiektów wstrzymujących Garbage Collectora przed “skolekcjonowaniem” naszych obiektów. Analizując ten graf wypatrujemy wszystkich niepewnych połączeń, w naszym przypadku wygląda to tak:

Już pierwszy stopień od okna pokazuje kontrolkę WindowsFormsHost, co rodzi pytanie – dlaczego kontrolka będąca “dzieckiem” naszego okna miałaby niby blokować jego sprzątnięcie?

Patrzymy “stopień wyżej” na klasę “WinFormsAdapter”, której pole _host zawiera referencję do naszego hosta – klasa ta nam zapewne za wiele nie mówi (co nie dziwi, gdyż jest to wewnętrzna klasa w WindowsFormsIntegration).

Stopień wyżej widzimy klasę ProcessInputEventHandler, po nazwie i polu (_target) sądzić można, że metoda klasy WinFormsAdapter obsługuje zdarzenie PostProcessInput pochodzące z klasy InputManager.
Gdyby bardziej zgłębić kalsę InputManagera – okazuje się, ze ma ona statyczne pole Current i do niego właśnie podpina się WindowsFormsHost za posrednictwem klasy WinFormsAdapter, z tego powodu referencja do kontrolki jest trzymana “na zawsze”, a co za tym idzie blokuje całe nasze okno. Co gorsza okno blokowało obiekt podpięty do DataContext, a obiekt ten mały zdecydowanie nie był!

Gdyby za każdym razem zanim użyje się kontrolki przestudiować MSDN można by zauważyć, że klasa dziedziczy po HwndHost, który jak wszyscy wiemy implementuje interfejs IDisposable… zaraz nie wszyscy? i wcale mnie to nie dziwi… w każdym razie –  jeżeli nawet pośrednio nasza kontrolka dziedziczy po IDisposable, to  po skończeniu pracy z naszą kontrolką wystarczy wywołać metodę Dispose() i wszystko będzie działać jak należy.

Zmieniamy zatem naszą obsługę zdarzenia Closed na następującą:

Closed += ((sender, e) =>
{
this.host.Dispose();
});

i wszystko gra – wykres zajmowanej pamięci właściwie się nie zmieni w porównaniu do poprzedniego (niepełnego) fixa, gdyż nasze okienko samo w sobie pamięci za wiele nie pochłania (brak zauważalnych efektów), ale już wynik działania profilera – owszem. Na liście klas nie znajdziemy już pozycji MemoryTakingWindow (pod warunkiem, że okienko nie jest otwarte podczas pobierania snapshota, oczywiście).


Kilka wniosków na koniec:
- Panowie z Red Gate mają głowy na karku. Wiedzą co programiści lubią – ich narzędzia są świetnie wykonane, bardzo funkcjonalne, a zarazem intuicyjne i estetyczne. W celu znalezienia mojego błędu nie potrzebowałem korzystać z żadnej dokumentacji, czy instrukcji. Wszystko wydawało się super-naturalne!

- kontrolka WindowsFormsHost lubi zrobić psikusa. Należy pamiętać, że przy zamykaniu okna należy wywołać metodę Dispose (wg. informacji znalezionych „na Google’u” bywa tak, że trzeba ją najpierw jeszcze ręcznie usunąć z nadrzędnego grida, czy innego kontenera, jednak nie ręczę za prawdziwość tej informacji, tylko przestrzegam przed ewentualnością). Nie jest to wiedza “oczywista” i raczej wymaga przekazania informacji przez kogoś, bądź też kilku godzin męki – jak w moim przypadku.

- WPF jest “fajny” i daje sporo możliwości, ale czyha tu na programistę wiele przykrych niespodzianek, które mogą napsuć krwi. Nie jest to jedyny tego przykład

- teoretycznie winą programisty są konsekwencje nie zwolnienia zasobów i nie wywołania metody Dispose w klasie implementującej interfejs IDisposable.


I… kilka usprawiedliwień:

- kontrolka WindowsFormsHost, jak to przystało na kontrolkę WPF dodawana jest w XAML‘u – a z tego poziomu raczej ciężko stwierdzić, że akurat TA kontrolka rozwija jakiś tam interfejs. Metod również nie zobaczymy w intelisensie – nawet przypadkiem…

- oczywiście wypadało zajrzeć na dokumentację kontrolki przed jej użyciem – szczególnie takiej „wyjątkowej” – ale jak to już było wyżej zaznaczone, WindowsFormsHost nie implementuje IDisposable bezpośrednio, a poprzez klasę bazową, więc i w dokumentacji na pierwszy rzut oka nie widzimy nic nakierowującego nas.

- podpinanie się pod zdarzenia statycznych pól jest generalnie mechanizmem „trochę dziwnym” i ryzykownym i dość często odradzanym. Jeśli o mnie chodzi – mam wrażenie, że kod pod WindowsFormsHost mógłby być napisany troszkę lepiej – w celu zniwelowania tego ryzyka.

- kontrolki implementujące interfejs IDisposable to rzadkość i raczej niewielu programistów spodziewa się takiego psikusa.

- Czytelnikowi wydać się może dziwne – przecież w przykładzie na pierwszy rzut oka widać, że to WidnowsFormsHost coś tu miesza, jednak w naszej prawdziwej aplikacji – jak to w życiu rzeczywistym – kontrolka nie rzucała się w oczy… była dodana do formatki pośrednio (stanowiła element innej kontrolki dodanej do okienka), ponadto stanowiła jedną z kilkudziesięciu kontrolek, a przy tym zdecydowanie nie istotna, pokazywała się tylko na kilka chwil i miała rozmiar kilkunastu px…)

- to nie ja dodałem kontrolkę do tamtego formularza ;)

- nadmierne pseudo-poczucie humoru we wpisie jest implikacją pory oraz dnia, w którym powstawał. Czyli „jakiś weekendowy wieczór” ;)

Dynamiczne tworzenie interfejsu użytkownika przy pomocy refleksji.

Author: Jarek Kożdoń (jarek.kozdon) | luty 19th, 2011
avatar

Niedawno, przy okazji rozmyślania nad konstrukcją interfejsu użytkownika dla narzędzia tworzonego dla własnych potrzeb, przypomniał mi się pewien projekt, nad którym miałem okazję pracować. Projekt nie był wielki, ale naszpikowany ciekawymi rozwiązaniami, m.in. LINQ (zarówno dla bazy danych jak i LINQ to XML), SQL Server Compact Edition, przetwarzanie bardzo sporych XMLi. Było również jedno rozwiązanie, które szczególnie zapadło mi w pamięci – kontrolka do edycji danych obiektów, co ciekawe – dla wszystkich obiektów bazodanowych (a było ich trochę, nie setki – ale powiedzmy kilkanaście) była tylko i wyłącznie jedna kontrolka. Nasuwa się tutaj pytanie „ale jak?!, przecież to musiało mieć milion linii kodu!”. Ale odpowiedź brzmi „wcale nie”. Z pomocą przyszła refleksja…

Zamieszczam więc niniejszym skromny instruktarz, który być może kogoś zainspiruje, do kodu kontrolki obecnie dostępu nie posiadam, przedstawię więc bardziej ideę jako propozycje alternatywy dla typowego myślenia o edycji właściwości naszych obiektów.

1. Co to jest refleksja?

Dla tych co wiedzą – przypomnienie (bądź akapit do przeskrolowania), dla pozostałych – kilka podstawowych informacji.

Refleksja to mechanizm pozwalający na pozyskiwanie informacji o klasach, czy kodzie skompilowanych już klas (na przykład właśnie wykonywanego programu, czy też odczytanych przez niego binarek).

W celu użycia refleksji w projekcie .NETowym należy użyć przestrzeń nazw System.Reflection. Dzięki temu będziemy mieli dostęp do podstawowych klas refleksji czyli: Type, MethodInfo, ProprtyInfo, EventInfo, FieldInfo, ConstructorInfo… Każda odpowiada jednemu z elementów tworzących klasy.

Żeby pobrać informacje dotyczące obiektu danej klasy wywołać należy na nim metodę .GetType() dziedziczoną z bazowej klasy Object.

2. Skąd wiemy co wyświetlać?

Metod na pozyskanie interesującej nas informacji jest co najmniej kilka. Możemy pozyskać informacje o klasie wczytując plik binarny i w nim szukając, możemy szukać po nazwie klasy i jej przestrzeni nazw, ale zdecydowanie najłatwiejszym sposobem jest wywołanie wspomnianej powyżej metody GetType() na obiekcie, który nas interesuje.

Co wyświetlać? O tym decydujemy już wyłącznie my! Powinniśmy jednak coś założyć, w tym przykładzie wyświetlać chciałbym wszystkie publiczne Property obiektów.

Skoro już wiemy jak pobrać typ obiektu oraz wiemy co będziemy wyświetlać pozostaje wyłącznie pobrać listę, nie jest to nic trudnego, wystarczy wywołać metodę GetProperties() klasy Type, która zwraca tablicę obiektów PropertyInfo danego typu. Stworzymy więc taki kod:

var type = ob.GetType();
var properties = type.GetProperties();

kod ten nie spełnia jednak naszego założenia, bowiem zwrócone zostaną wszystkie publiczne properties (domyślne działanie nie zwraca prywatnych właściwości!) łącznie z tymi statycznymi (chcemy tylko właściwości obiektu). W celu pozyskania odpowiedniej listy użyć należy przeładowania metody GetProperties wraz z argumentem w postaci wartości enuma BindingFlags (dopuszczając bitowe dodawanie wielu wartości)

var type = ob.GetType();
var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);

Lista gotowa!

3. A co jeżeli jakiegoś pola NIE chcę wyświetlać?

Nie, nie trzeba w takim przypadku robić dziwnych warunków czy aby dane property jest jednym z wykluczonych, czy nie, dużo bardziej uniwersalnym sposobem jest użycie atrybutu. Można działać w 2 strony – stworzyć atrybut informujący o tym, że dane pole powinno być wyświetlane, bądź atrybut informujący o tym, że jego edycja nie jest potrzebna. Pokażę drugie rozwiązanie, jako bardziej ogólne i bardziej do mnie przemawiające, poza tym pozwalające w dalszej drodze edytować wartości obiektów nie należących do danego projektu.

Najpierw klasa atrybutu:

public class HiddenValueAttribute : Attribute
{
}

Po czym umieszczamy atrybut przy Property, którego nie chcemy widzieć:

[HiddenValue]
public
string HiddenName { get set }

a teraz modyfikacja naszej metody pobierającej listę do wyświetlenia (dodajemy również jej nazwę):

public IEnumerable<PropertyInfo> GetPropertiesToDisplay(object ob)

{
var type = ob.GetType();
var
properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
var
returnValue = from PropertyInfo p in properties where p.GetCustomAttributes(typeof(HiddenValueAttribute), true).Length == 0 select p;
}

Jak widać dodana została linijka wyszukująca tylko te PropertyInfo, które wśród swoich atrybutów nie mają tego o typie HiddenValueAttribute, w celu tym użyta zostało użyte zapytanie LINQ wywołujące metodę GetCustomAttributes, która zwraca tablicę atrybutów spełniających warunki. Warunki podawane w argumentach to typ atrybutu (opcjonalny parametr, w drugim przeładowaniu metody nie występuje) oraz czy brać pod uwagę dziedziczone atrybuty.

4. Jak wyświetlać?

Przechodzimy teraz do najistotniejszego punktu – wyświetlanie. Założymy, że wyświetlamy kontrolki na obiekcie klasy Panel o nazwie… panel!

Metoda wyświetlająca kosztować będzie nas najwięcej pracy – tutaj generalnie przewidzieć będziemy musieli wszystkie potencjalnie wyświetlane typy oraz obsługę każdego z nich.

Ale powoli.

Zacznijmy od organizacji naszego panelu, proponuję wyświetlać wartości w wierszach, w każdym etykieta z nazwą i odpowiednia kontrolka dla wartości – taka para dla każdej Property naszego obiektu. Czyli na przykład:

private void CreateControlsForProps(IEnumerable<PropertyInfo> properties, object ob)
{

foreach
(var prop in properties)
{

Panel
row = new Panel();
row.Dock =
DockStyle.Top;
row.Height = 36;

this.panel.Controls.Add(row);
row.Controls.Add(
this.GetLabelForProperty(prop));

if (prop.PropertyType == typeof(String))
{

row.Controls.Add(
this.GetInputForString(prop));
}

else
if (prop.PropertyType == typeof(Int32))
{

row.Controls.Add(
this.GetInputForNumber(prop));
}

else
if (prop.PropertyType == typeof(Color))
{

row.Controls.Add(
this.GetInputForColor(prop));
}

else
if (prop.PropertyType.IsEnum)
{

row.Controls.Add(
this.GetInputForEnum(prop));
}

else

{

row.Controls.Add(
new Label()
{

Text =
„Niestety, typu „ + prop.PropertyType.Name + ” jeszcze nie obsługujemy.”,
Left = row.Controls[0].Right + 15,

AutoSize =
true,
Anchor = Anchor |
AnchorStyles.Right
});
}
}
}

Tak przygotowany kod wystarczy „tylko” uzupełnić o metody tworzące odpowiednie kontrolki dla wprowadzania wartości i etykiety. Podam przykład dla klasy String, dla pozostałych typów odsyłam to pokładów własnej inwencji (nie jest to nic trudnego!):

private Control GetInputForString(PropertyInfo prop)
{

TextBox tb = new TextBox();tb.Left = 75;
tb.Anchor = tb.Anchor | AnchorStyles.Right;

return
tb;
}

i dla etykiet:

private Control GetLabelForProperty(PropertyInfo prop)
{

Label l = new Label();
l.Text = prop.Name;

return
l;
}

5. A co z wartościami?!

Bardzo dobre pytanie… jak widać… nic. Ale przecież nie po to tworzymy naszą kontrolkę do wyświetlania wartości, żeby wartości nie wyświetlać…

Dlatego konieczna będzie mała modyfikacja powyższego kodu. Zacznijmy od podstaw – jak w ogóle pobrać wartość Property obiektu, którego typu nie znamy z początku, zresztą sami przecież nie wiemy co to za Property (więc rzutowanie, czy wpisywanie nazwy Property po kropce nie jest możliwe)? Nic trudnego, w naszym kodzie mamy już obiekt opisujący Property, potrzebujemy jeszcze obiekt typu, którego dane Property dotyczy – a potem już tylko wywołanie metody GetValue:

prop.GetValue(ob, null);

W naszym kodzie więc potrzebujemy obiektu, którego wartość chcemy pobierać, modyfikacje będą więc następujące:

Metoda tworząca kontrolkę do edycji/wyświetlania wartości:

private Control GetInputForString(PropertyInfo prop, object ob)
{

TextBox
tb = new TextBox();
tb.Text = (
String)prop.GetValue(ob, null);tb.Left = 75;
tb.Anchor = tb.Anchor | AnchorStyles.Right;

return
tb;
}

I co za tym idzie uzupełnienie wywołań w głównej pętli wyświetlającej nasze wartości:

row.Controls.Add(this.GetInputForString(prop, ob));

i podobnie dla wszystkich pozostałych metod dotyczących Property.

6. Ale opis jest brzydki…

Fakt, opisy w tym momencie stanowią nazwy pól w klasie – brak spacji, często niewłaściwy język i skrótowe nazwy niewiele mówią potencjalnemu użytkownikowi. Tutaj z pomocą znowu przyjść nam mogą atrybuty.

Zacznijmy więc od zadeklarowania odpowiedniego (powinien zawierać przeładowany konstruktor oraz musi zawierać Property dla wyświetlanej wartości):

public class VisualDisplayAttribute : Attribute
{

public
string VisualDisplay { get; set; }

public VisualDisplayAttribute(string VisualDisplay)
:
base()
{

this
.VisualDisplay = VisualDisplay;
}
}

Następnie niewygodne Property „ozdabiamy” takim atrybutem:

[VisualDisplay("Tło")]
public
Color Background { get; set; }

I modyfikujemy tworzenie etykiety:

private Control GetLabelForProperty(PropertyInfo prop)
{

Label
l = new Label();
var
attributes = prop.GetCustomAttributes(typeof(VisualDisplayAttribute), true);
if
(attributes.Length > 0)
{

l.Text = ((
VisualDisplayAttribute)attributes[0]).VisualDisplay;
}

else

{
l.Text = prop.Name;
}

return
l;
}

7. Czy można zrobić pola tylko do odczytu? Albo obowiązkowe?

Jak najbardziej – proponowane rozwiązanie to odpowiedni zestaw atrybutów. Np. nowy atrybut ReadOnlyValueAttribute, którego posiadanie przez Property badamy w identyczny sposób jak dla wcześniejszych przykładów i w przypadku znalezienia ustawiamy np. wartość enabled tworzonej kontrolki na false.

Dzięki odpowiedniemu zestawowi atrybutów możemy praktycznie dowolnie sterować całą naszą kontrolką – od możliwości edycji danej wartości, po kolor tła kontrolki, czy jej kontenera – granicą jest chyba tylko nasza wyobraźnia. Faktem jest, że pewne rzeczy dużo łatwiej było by zaprojektować w „tradycyjny” sposób, ustawiając kontrolki w designerze, niż odczytywać wartości z atrybutów pól klasy, po czym przeliczać je na wartości, które „ręcznie” trzeba ustawić w kontrolkach, granicę opłacalności wyznaczyć musimy sobie sami.

8. Zaraz, zaraz… miała być edycja!

I nikt o niej nie zapomniał. Jak łatwo zauważyć – wartości co prawda odczytujemy… ale ich już nie zmieniamy… metod jest kilka, ale wszystkie wymagają jednego – zapamiętania lub pozyskania obiektu PropertyInfo oraz obiektu, który wyświetlamy / edytujemy. Kiedy już owe obiekty mamy, wystarczy nam… jak zwykle jedna linijka… a dokładniej wywołanie metody SetValue klasy PropertyInfo

Zmodyfikujmy raz jeszcze nasz kod – do pól Tag paneli przypiszemy odpowiednie informacje (dla obiektu panel naturalny będzie obiekt na panelu wyświetlany, dla panelu wiersza – PropertyInfo, którego wiersz dotyczy):

private void CreateControlsForProps(IEnumerable<PropertyInfo> properties, object ob)
{

this
.panel.Tag = ob;
foreach
(var prop in properties)
{

Panel
row = new Panel();
row.Tag = prop;

Dodajmy obsługę zdarzenia zmiany wartości kontrolki edytującej Property, na przykład:

private Control GetInputForString(PropertyInfo prop, object ob)
{

TextBox
tb = new TextBox();
tb.Text = (
String)prop.GetValue(ob, null);
tb.Left = 75;

tb.TextChanged +=
new EventHandler(tb_TextChanged);
return
tb;
}

void tb_TextChanged(object sender, EventArgs e)
{

TextBox
tb = sender as TextBox;
PropertyInfo
prop = tb.Parent.Tag as PropertyInfo;
object
ob = tb.Parent.Parent.Tag;

prop.SetValue(ob, tb.Text, null);
}

Gotowe!

Zdaję sobie sprawę, że posługiwanie się polem Tag bywa mętne i trochę utrudnia analizę kodu następcom, ale zostało użyte raczej jako przykład niż wzór – osobiście proponowałbym np. podziedziczyć po panelu i stworzyć klasę panelu wiersza z odpowiednim, dedykowanym Property dla PropertyIfno itp. Takie rozwiązanie będzie dużo bardziej czytelne no i nikt nam wtedy wartości nie nadpisze, gdy stwierdzi, że fajnie by było dla niego przechować coś w Tagu

9.Przecież refleksja jest wolna!

Tak, refleksja działa wolniej niż pobierania i ustawianie wartości „normalnymi” odwołaniami do właściwości obiektów, jednak stworzenie i wyświetlenie jednej kontrolki zajmuje nieporównanie więcej czasu… więc nie stanowi to w tym przypadku żadnego argumentu, ani nie powinno nas to napełniać obawami.

10. Plus i minus, to jedyne co słyszę – czyli kiedy stosować?

Zdecydowanym plusem takiego rozwiązania jest jego elastyczność, można wyświetlić w ten sposób wszystko. Dosłownie. Wystarczy spojrzeć na Visualowe komponenty do edycji obiektów – PropertyGrid’y, które mają bardzo szerokie możliwości, a działają na dokładnie takiej samej zasadzie. Będzie to rozwiązanie szczególnie cenne, kiedy klasy wyświetlane zmieniają się co chwila, albo interfejs powstaje w celu edycji danych, których jeszcze nie znamy.

Kolejnym plusem jest modna „reusability”, jeżeli stworzymy dobrze taką kontrolkę oraz zestaw atrybutów użycie ich w kolejnym projekcie będzie dziecinnie proste – w końcu nie mamy żadnych odwołań do klas encji biznesowych, czy jakichkolwiek innych klas, które chcemy wyświetlać.

Oczywiście są i minusy…
M.in. dużo cięższe debugowanie wymuszone przez użytą refleksję.

Brak możliwości podejrzenia jak wygląda kontrolka w trybie design, jedyna możliwość to uruchomienie aplikacji na przykładowych danych.

Monotonność interfejsu i dużo trudniejsze modyfikacje wyglądu kontrolek pod konkretne Propery. Bez napracowania się nad atrybutami i ich interpretacją każde użycie kontrolki do wyświetlania danych wyglądać będzie identycznie.

Można zadać sobie pytanie po co tak się męczyć? przecież są wspomniane już PropertyGrid’y, są dobre do edycji danych Gridy. Ciężko temu argumentowi zaprzeczyć, to prawda. Tym niemniej tych gridów nie zmodyfikujemy, nie powiemy im co mają wyświetlać, a co nie w tak łatwy sposób itp. To rozwiązanie da nam pełną dowolność, której być może będziemy potrzebować.

public IEnumerable<PropertyInfo> GetPropertiesToDisplay(object ob)

{

var type = ob.GetType();

var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);

var returnValue = from PropertyInfo p in properties where p.GetCustomAttributes(