Showing posts with label C#. Show all posts
Showing posts with label C#. Show all posts

05/10/2009

Raportowanie

Home
Ostatnio poznałem nieznany mi wcześniej, a prosty sposób tworzenia raportów i zapisywania ich do formatu PDF czy Excel. Działa on zarówno w kontekście aplikacji ASP.NET jak i w aplikacjach Windows Forms i innych. Mam tutaj na myśli klasy z przestrzeni nazw Microsoft.Reporting.WebForm (w przypadku aplikacji stacjonarnych chodzi o przestrzeń Microsoft.Reporting.WinForms).

W przestrzeni Microsoft.Reporting.WebForm znajdziemy wiele rzeczy, najważniejsze to po pierwsze kontrolka ReportViewer do prezentowania raportów. Po drugie klasy LocalReport oraz ServerReport służące odpowiednio wykonywaniu raportów lokalnie oraz zdalnie na serwerze. Źródłem danych dla raportów może być oczywiście relacyjna baza danych ale również obiekty biznesowe. Definicja raportu to dokument XML stworzony przy pomocy języka RDL (ang. Report Definition Language).

Zanim przeję dalej podam założenia/wymagania jakimi się kierowałem:
  • Raporty chcę zapisywać do formatu PDF i Excel.
  • Proces generowania raportu ma odbywać się lokalnie bez połączenia z bazą danych.
  • Dane mam zapisane w obiekcie klasy DataTable.
Po pierwsze musiałem znaleźć sposób dynamicznego generowania definicji raportów. Język RDL nie wygląda na trudny ale nie zmienia to faktu, że go nie znam. Po krótkich poszukiwaniach napotkałem na ten artykuł Lesson 4: Creating Code to Generate the Report Definition File. Zaprezentowany tam kod tworzy plik z definicją raportu jako dane wejściowe przyjmując: listę pól/kolumn, zapytanie do bazy danych oraz connection string. Jak widać kod jest przydatny ale nie do końca ponieważ nie odpowiada postawionym wymaganiom. Jak sie jednak okazało konieczne modyfikacje były bardzo proste. Pomijając zmianę argumentów wejściowych metody, delikatną modyfikację logiki wystarczyło, że jako zapytanie do bazy danych oraz connection string przekazałem pusty ciąg znaków String.Empty. Cała metoda jest dość długa ale w gruncie rzeczy nie zawiera niczego skomplikowanego. Dopowiem tylko, że liczba kolumn generowanego raportu zależy od liczby kolumn w przekezanym do metody obiekcie klasy DataTable. Pełny kod został pokazany poniżej.

Pokaż/Ukryj kod

Proces generowania raportu wynikowego w żądanym formacie również jest prosty. Poniższy fragment kody generuje raport w formacie PDF. Żeby wytworzyć arkusz programu Excel to metody Render należy przekazać ciąg znaków "Excel". Zmienna reportPath powinna zawierać ścieżkę do pliku z definicją raportu stworzonego przez pokazaną wcześniej metodę. Zmienna dataTable to z kolei tabela z danymi do zapisania w żądanym formacie. Powinna to być ta sama tabela, która została przekazana do metody GenerateRdl.
...
FileStream fs = File.OpenWrite( reportPath);
GenerateRdl(fs, dataTable);

LocalReport lr = new LocalReport();
lr.ReportPath = reportPath;
lr.DataSources.Add(new ReportDataSource("DummyDataSet", dataTable));

string deviceInfo = "<DeviceInfo><SimplePageHeaders>True</SimplePageHeaders></DeviceInfo>";
string mimeType = null;
string encoding = null;
string ext = null;
string[] streamids = null;
Warning[] warnings = null;
byte[] bytes = lr.Render("PDF", deviceInfo, out mimeType, out encoding, out ext, out streamids, out warnings);
...

15/09/2009

GridView oraz puste źródło danych

Home

Programistom używającym kontrolki GridView na co dzień znany jest zapewne fakt, że w przypadku pustego źródła danych kontrolka nie generuje żadnego widocznego markup'u. W szczególności nie będą widoczne nagłówki kolumn czy wiersz dodający.

Kiedy w wyszukiwarce wpiszemy hasło Show GridView if datasource is empty otrzymamy oczywiście mnóstwo rozwiązań tego problemu. Niestety pośród nich nie znajdziemy, a przynajmniej ja nie znalazłem, satysfakcjonującej odpowiedzi dotyczącej źródła danych typu ObjectDataSource. Nie będziemy natomiast osamotnienie jeśli używamy SqlDataSource. Ale co jeśli nie chcemy, nie możemy lub najzwyczajniej w świecie nie chce nam się zmieniać używanego typu źródła danych. Ja zastosowałem rozwiązanie, które opisałem poniżej.

Dla ustalenia uwagi załóżmy, że metoda dostarczająca danych wygląda następująco:
public static IEnumerable GetData()
{
  return _data;
}
Zacznijmy od zmodyfikowania tej metody w ten sposób aby zawsze zwróciła niepustą kolekcję:
public static IEnumerable GetData()
{
  if(_data.Count == 0)
  {
    _data.Add(new TestClass());
  }
  
  return _data;
}
Oczywiście teraz wszystko zadziała z wyjątkiem tego, że na kontrolce pojawi się jakiś "dziwny", sztuczny obiekt. Można temu jednak zaradzić zmieniając lekko definicję TestClass:
public class TestClass
{
  ...
  public bool IsFake
  {
    get; set;
  }
  ...
}
Przy okazji zmodyfikujemy ponownie metodę GetData:
public static IEnumerable GetData()
{
  if(_data.Count == 0)
  {
    _data.Add(new TestClass() { IsFake = true; });
  }
  
  return _data;
}
Ostatni element rozwiązania do podczepienie się pod zdarzenie OnRowDataBound kontrolki GridView w celu sterowania widocznością wierszy:
protected void GridView_RowDataBound(object sender, GridViewRowEventArgs e)
{
  TestClass ts = e.Row.DataItem as TestClass;
  if(ts != null && ts.IsFake)
  {
    e.Row.Visible = false;
  }
}
Rozwiązanie można jeszcze rozszerzyć o usunięcie sztucznego obiektu z kolekcji w momencie kiedy pojawią się "prawdziwe" dane ale moim zdaniem nie jest to konieczne.

22/07/2009

Czemu należy używać właściwości SyncRoot?

Home

SyncRoot to właściwość zdefiniowana na poziomie interfejsu ICollection służąca do synchronizowania operacji wykonywanych na kolekcjach przy pomocy słowa kluczowego lock lub jawnie przy pomocy monitora. Czemu jednak należy używać tej właściwości zamiast instancji kolekcji, czyli czemu zalecany jest taki kod:
lock(list.SyncRoot)
{
   ...
}
A nie taki?
lock(list)
{
   ...
}
Kiedy postawiłem sobie to pytanie okazało się, że odpowiedź nie jest dla mnie oczywista. Wizyta w dokumentacji MSDN nic nie pomogła bo udzielone wyjaśnienie należy do grupy tych, które stają się jasne i oczywiste jak już znasz prawidłową odpowiedź ;).

Spędziłem trochę czasu zastanawiając się nad tą kwestią oraz szperając po sieci i doszedłem do następujących wniosków. Należy używać właściwości SyncRoot ponieważ zapewnia ona centralny, dobrze określony, bezpieczny punkt synchronizacji dla kodu zewnętrznego używającego kolekcję jak i dla kodu wewnętrznego kolekcji. Inaczej mówiąc zapewnia to, że każdy kod używa tego samego obiektu do synchronizacji. Bezpieczny w tym sensie, że bardzo łatwo można stwierdzić kto i kiedy uzyskuje dostęp do obiektu synchronizacyjnego - wystarczy ustawić pułapkę w getter-ze właściwości. Synchronizacja bezpośrednio przy użyciu instancji kolekcji czy też na obiekcie this może natomiast doprowadzić to bardzo trudnych do wykrycia zakleszczeń ponieważ nie będzie można łatwo określić kto i w jakim momencie blokuje obiekt. Z drugiej strony jeśli taka semantyka SyncRoot jest z jakiegoś powodu niepożądana kolekcje wydziedziczone z kolekcji bazowej mogą dostarczyć własnej implementacji tej właściwości.

04/06/2009

Bardzo użyteczne narzędzie do pracy z WMI

Home

Przeglądając dzisiaj fora internetowa, dotyczące platformy .NET, w odpowiedzi na jedno z zadanych pytań znalazłem wzmiankę o bardzo użytecznym narzędziu WMI Code Creator v1.0. Narzędzie to pozwala na wygenerowanie kodu używającego WMI (ang. Windows Management Instrumentation) do wykonywania różnego rodzaju zadań zarządzania: odczytywanie danych, oczekiwanie na zdarzenia WMI czy wywoływanie metod z klas WMI.

WMI nie jest za pewne narzędziem używanym w codziennej pracy ale mogącym się czasem przydać. Przy pomocy tej technologii możemy dobrać się do szczegółowych inforamacji na temat BIOS'u, procesora, dysków i innych urządzeń, procesów, usług oraz najróżniejszych ustawień systemu operacyjnego. WMI posługuje się dobrze znanymi pojęciami takimi jak: przestrzenie nazw, klasy, metody, zdarzenia i właściwości. Przestrzenie nazw zawierają klasy dotyczące poszczególnych obszarów zarządzania. Klasy modelują różne byty w tych obszarach. Właściwości tych klas to różne parametry konfiguracyjne. Na przykład dla klasy modelującej procesor możemy odczytać jego rodzinę, producenta czy liczbę rdzeni. Dla klasy modelującej proces została zdefiniowana metoda pozwalająca go zamknąć. Przykładem zdarzenia na jakie można oczekiwać jest natomiast zmiana statusu usługi.

Z poziomu platformy .NET do komunikacji z WMI służą klasy z przestrzeni nazw System.Management. Ich użycie nie jest bardzo skomplikowane, główna trudność polega na mnogości przestrzeni nazw i klas WMI. Nie sposób tego spamiętać. Tutaj z pomocą przychodzi wspomniane narzędzie. Przy jego pomocy możemy wybrać interesującą nas przestrzeń, potem klasę i jej właściwości, a następnie wygenerować kod w jednym z trzech języków: C#, Visual Basic .NET, Visual Basic Script, który odczyta parametry jakie wybraliśmy, wywoła metodę lub będzie oczekiwał na zdarzenia WMI. Co bardzo przydatne WMI Code Creator v1.0 pozwala podejrzeć wartości wybranych właściwości.

02/06/2009

Data Binding i dobre praktyki programistyczne

Home

The data source for GridView with id 'GridView1' did not have any properties or attributes from which to generate columns. Ensure that your data source has content.

Czy spotkaliście się z takim błędem pomimo, że byliście pewni, że poprawnie zasilacie kontrolkę lub powiązane z nią źródło danych? Jeśli tak to problem był związany z użytymi strukturami danych. Przykładowy kod, który spowoduje powyższy błąd został przedstawiony poniżej. Zacznijmy od prostej klasy, którą chcemy zaprezentować na kontrolce GridView.
public class Data
{
  public int Id;
  public string Name;
}
Fragment kodu strony:
...
<asp:GridView ID="GridView1" runat="server">
</asp:GridView>
...
Kod zasilający kontrolkę jest równie prosty, dla uproszczenie nie korzystam ze źródła danych:
...
this.GridView1.DataSource = new Data[] { new Data { Id = 1, Name = "1" }, new Data { Id = 2, Name = "2" } };
this.GridView1.DataBind();
...
Uważny czytelnik mógł zauważyć, że w klasie Data użyłem publicznych pól składowych zamiast właściwości. I tu leży pies pogrzebany. Pola składowe klasy nie są automatycznie wykrywane i obsługiwane przy dowiązywaniu kontrolki (ang. binding). Innymi słowy pola trzeba opakować we właściwości. Nie wiem czy to przeoczenie w implementacji czy celowe działanie (biorąc pod uwagę treść komunikatu to drugie) ale w każdym razie ograniczenie to wspiera dobre praktyki programistyczne. Poprawny kod klasy Data powinien wyglądać tak:
public class Data
{
  public int Id { get; set; };
  public string Name { get; set; };
}

06/05/2009

Dopowiedzenie

Home

Mam niewielką uwagę w kontekście postów Czemu zdarzenia nie działają??? oraz Czemu zdarzenia nie działają??? (część 2). Główne ich przesłanie:

W przypadku dynamicznego tworzenia kontrolek, czy to bezpośrednio czy to przy okazji użycia kontrolek data bound zawsze należy pamiętać aby dynamiczne kontrolki były tworzone nie tylko przy inicjalnym odwołaniu do strony ale również przy kolejnych.

Jest cały czas aktualne przy czym chciałbym dodać jedną rzecz. Otóż zapomniałem napisać, że w przypadku dowiązywania kontrolek do źródeł danych (na przykład do ObjectDataSource) nie trzeba się tym przejmować. Jest tak ponieważ źródła danych same dbają o to aby kontrolka została zasilona danymi w odpowiednim momencie. W szczególności nie trzeba jawnie wołać metody DataBind.

04/05/2009

Skrzydła furii

Home

Wiele osób, w tym i ja, spędziło długie godziny swoich młodzieńczych lat grając w takie gry jak Wings of fury. Kilka kolorów na krzyż, idea prosta ja budowa cepa, a grywalności tyle, że można by podzielić pomiędzy kilkadziesiąt współczesnych tytułów i jeszcze by zostało. Kiedy, więc przed weekendem majowym natknąłem się na sequel tej gry stworzony przez polskich programistów - Wings of fury 2 moje serce przyspieszyło.

Na szczęście, po za poprawioną grafiką, dużo się nie zmieniło. Znowu latamy w lewo i w prawo, zrzucam bomby, odpalamy rakiety, startujemy i lądujemy na lotniskowcu itd. Jednym słowem to co tygryski lubią najbardziej.

Jest trochę bugów, które utrudniają grę (problemy ze sterowaniem samolotu, menu gry słabo działa) ale da się z nimi grać. Najgorsze, że pomimo niewielkich wymagań występują problemy z uruchomieniem gry. W moim przypadku gra nie chciała działać na dwóch z trzech testowanych komputerów.

Tytuł jest w pełni darmowy i co dla mnie najciekawsze został napisany w C# na silniku MOGRE. Jest to zarządzana nakładka na silnik OGRE, a jego możliwości wydają się bardzo duże. Zaciekawiło mnie to tyle, że przymierzam się do zrobienia czegoś przy jego pomocy. Co z tego wyjdzie czas pokaże.

Jako podsumowanie mogę powiedzieć, pomimo pewnych zastrzeżeń, brawo dla autorów.

30/03/2009

.NET Remoting - podsumowanie

Home

W ostatnim czasie kilkukrotnie rozmawiałem, odpowiadałem na pytanie dotyczące przekazywania danych z serwera do klienta i w drugą stronę przy użyciu .NET Remoting. W poście tym chciałbym zebrać wszystkie informacje i przedstawić w spotaci klarownej tabelki. Dalej będę używał skrótu MBVO (ang. Marshal by value object) dla określenia obiektów klas przekazywanych przez wartość oraz MBRO (ang Marshal by reference object) dla określenia obiektów klas przekazywanych przez referencję. Klasy obiektów MBVO oznaczone są atrybutem SerializableAttribute, a klasy obiektów MBRO dziedziczą po MarshalByRefObject i ewentualnie mogą być oznaczone atrybutem SerializableAttribute ale jest to zbyteczne.



Metody obiektów MBVO wykonują się zawsze lokalnie dla wywołującego. Metody obiektów MBRO wywołują się albo lokalnie albo zdalnie dla wywołującego w zależności od tego czy pracuje od z oryginalnym obiektem czy z proxy do niego.

W przypadku obiektów MBVO zarówno klient jak i serwer muszą znać pełną definicję klasy obiektu. Serwer po to aby utworzyć obiekt i przekazać go do klienta, a klient aby odtworzyć go po swojej stronie. W przypadku obiektów MBRO nie jest to konieczne ponieważ obiekty te szeregowane są przez proxy. Aby utworzyć proxy wystarczy znajomość interfejsu. Dokładną definicję klasy musi więc znać tylko strona udostępniająca obiekt tej klasy.

25/03/2009

Synchronizacja - wydajność

Home

Niedawno opublikowałem post dotyczący prymitywów synchronizacyjnych. Przy tej okazji zostałem zapytany czemu użycie prymitywów takich jak semafor czyli implementowanych po stronie systemu operacyjnego jest wolniejsze. Odpowiedź zamieściłem w odpowiedzi do komentarz. W tym poście chciałem przedstawić jeszcze wyniki krótkiego testu jaki przeprowadziłem. Dotyczył on zbadaniu wydajności rozpatrywanych wcześniej prymitywów i polegał na wielokrotnym wchodzeniu i wychodzeniu do/z sekcji krytycznej przez dwa watki. Sekcja krytyczna była oczywiście zabezpieczonej przez te prymitywy. Do pomiaru czasu użyłem klasy Stopwatch. Wyniki są zgodne z tym co napisałem. Zamieściłem je poniżej.


08/03/2009

Prymitywy synchronizacyjne - jak dobrze je znamy?

Home

Ostatnio zadałem kilku osobom następujące pytanie. Spośród wymienionych poniżej prymitywów synchronizacyjnych wybierz, te które mogą zostać użyte do synchornizacji między-procesowej:
  • Klasa Monitor
  • Klasa Mutex
  • Klasa Semaphore
  • Słowo kluczowe lock
  • Klasa AutoResetEvent
  • Klasa ManualResetEvent
Sądziłem, że pytanie należy raczej do tych z kategorii łatwiejszych. Odpowiedź na nie przysporzyła jednak niestety sporo kłopotów. Na poniższej liście zostawiłem tylko elementy stanowiące poprawną odpowiedź:
  • Klasa Mutex
  • Klasa Semaphore
  • Klasa AutoResetEvent
  • Klasa ManualResetEvent
Czym różnią się wybrane prymitywy synchronizacyjne od pozostałych? Moim zdaniem można wymienić cztery zasadnicze różnice:
  • Po pierwsze klasy te dziedziczą pośrednio po klasie MarshalByRefObject i w związku z tym instancje tych klas mogą przekraczać granice po między dziedzinami aplikacyjnymi (ang. application domain).
  • Po drugie klasy te są wywiedzione z klasy WaitHandle i w związku można ich używać w podobny sposób chociaż mają zupełnie inną semantykę. W szczególności można sobie wyobrazić scenariusz, w którym wątek oczekuje równocześnie na zwolnienie semafora, mutexa i zapalenie się sygnału/zdarzenia (należy użyć metody statycznej WaitHandle.WaitAll())
  • Po trzecie klasy te stanowią w rzeczywistości opakowanie na natywne/systemowe prymitywy synchronizacyjne udostępnione w systemie operacyjny co generalnie powoduje, że są wolniejsze.
  • Po czwarte i najważniejsze z perspektywy pytania te cztery prymitywy synchronizacyjne mogą zostać nazwane. Inaczej mówiąc, w jednym procesie możemy utworzyć semafor o nazwie "MY SEMAPHORE", a w drugim procesie uzyskać dostęp do tego samego semafora posługując się podaną nazwą. W tym celu można użyć metody statycznej Semaphore.OpenExisting() lub z odpowiedniego konstruktora.
Moim zdaniem wiedza o sposobie działania, semantyce poszczególnych prymitywów synchronizacyjnych jest niezwykle ważna dla każdego programisty bez względu z jaką technologią pracuje. Błędy związane z synchronizacją są jednymi z najtrudniejszych do wykrycia i naprawienia, a wiele z nich można by unikać gdyby wszyscy programiści znali dokładnie narzędzie swojej pracy.

22/02/2009

Zakamarki Visual Studio (cz. 4)

Home

W czwartym już poście dotyczącym różnych ciekawych funkcji środowiska programistycznego Visual Studio chciałbym przyjrzeć się dokładniej oknu Call Stack, a przy okazji poruszyć kilka innych tematów.

Okno Call Stack, breakpoint'y...

Czym jest breakpoint zapewne każdy programista wie, a jeśli nie to wstyd. Z drugiej strony nie jestem już taki pewny czy każdy programista zna wszystkie możliwości breakpoint'ów w Visual Studio: filtrowanie, warunki, zliczanie, uruchamiania makr czy wstawienie breakpoint'a do metody, której kodu nie znamy (o tym pisałem tutaj). Opisanie tych rzeczy to temat na oddzielny post. W tym miejscu skupmy się na użyciu pułapek w oknie Call Stack.

Po pierwsze umożliwia ono wstawianie breakpoint'ów. W tym celu wybieramy interesującą nas ramkę (metodę), zaznaczamy ją i naciskamy przycisk F9 (albo wywołujemy menu kontekstowe i wybieramy Breakpoint -> Insert Breakpoint). Breakpoint pojawi się zarówno w oknie Call Stack jak i na marginesie edytora kodu. W odwrotnym scenariuszu breakpoint postawiony w kodzie pojawi się w oknie Call Stack dopiero kiedy spowoduje zatrzymanie aplikacji. W każdej metodzie może zostać umieszczony wiele pułapek i trudno by je wszystkie umiescić w oknie Call Stack.

Przy pomocy okna Call Stack można również zarządzać pułapkami czyli określić warunek zatrzymania, filtr, wyłączyć pułapkę itd. W tym celu klikamy prawym przyciskiem wybraną pułapkę, z menu kontekstowego wybieramy Breakpoint, dalej interesującą nas komendę np.: Disable Breakpoint w celu wyłączenia pułapki lub Condition... w celu określenia warunku zatrzymania się pułapki.



Okno Call Stack umożliwia również ustawienie punktu śledzenia (ang. tracepoint). Tracepoint to w gruncie rzeczy specjalny breakpoint, który nie powoduje zatrzymania wykonania ale wypisanie komunikatu do okna Output. W celu ustawienie tracepoint z poziomu okna Call Stack wywołujemy menu kontekstowe i wybieramy Breakpoint -> Insert Tracepoint. Ustawienie punktu śledzienia z poziomu edytora kodu rozpoczyna się od ustawienia zwykłego brakpoint'a. Następnie należy wywołać menu kontekstowe, dalej wybrać When Hit... (sposób ten działa również w przypadku okna Call Stack). W oknie, które pojawi się zaznaczamy pola wyboru jak na rysunku:

Jeśli nie zaznaczymy pola wyboru Continue execution utworzymy zwykły breakpoint, który dodatkowo będzie powodował wypisanie komunikatu do okna Output. Tracepoint'y reprezentowane są przez romby:

W polu tekstowym okna When Breakpoint Is Hit możemy wpisać dowolny tekst, wyświetlić dowolną zmienną lub użyć jednego ze specjalny słów kluczowych. Ale po kolei. Aby wyświetlić zmienną należy wpisać { nazwa }. Słowa kluczowe wpisujemy natomiast po znaku $. Na przykład $PID służy do wypisania identyfikatora procesu. Nie będę omawiał wszystkich słów kluczowych ponieważ, jak widać na powyższym rysunku, ich opis można znaleźć w oknie When Breakpoint Is Hit. Zwrócę tylko jeszcze uwagę na bardzo ciekawą możliwość wyświetlenia stosu przy pomocy słowa kluczowego $CALLSTACK oraz na to, że przy tworzeniu komunikatu jesteśmy ograniczeni tylko do jednej linii.

Run To Cursor

Run To Cursor to bardzo znane polecenie, które umożliwia rozpoczęcie debugowania i wskazanie miejsca w kodzie, w którym debugger ma się zatrzymać (pod warunkiem, że nie zatrzyma się wcześniej z powodu breakpoint'a). Niewiele osób jednak wie, że polecenie to jest dostępna z poziomu okna Call Stack. Pozwala wskazać, do którego miejsca chcemy zwinąć stos. Wystarczy wybrać interesującą nas ramkę/metodę i z menu kontekstowego wybrać tę komendę.

Wartość argumentów

Inną bardzo fajną cechą okna Call Stack jest to, że podaje ono wartości przekazanych do funckji argumentów. Jeśli wartości argumentów nie są widoczne powinniśmy w onie Call Stack wywołać menu kontekstowe, a następnie zaznaczyć Show Parameter Values. No dobrze, w przypadku typów prostych jest to z pewnością przydatne ale co z typami złożonymi. Domyślnie zostanie wyświetlona po prostu nazwa typu ale można to zmienić korzystając z atrybutu DebuggerDisplayAttribute. Dla tych co nie wiedzą jest to jeden z wielu atrybutów, który pozwala na dostosowanie debuggera do naszych potrzeb (kolejny dobry temat na oddzielny post, a nawet kilka :)). Najłatwiej wyjaśnić to na przykładzie. Posłużmy się kodem jak poniżej:
public class Test
{
   private int value;

   public int Value
   {
      get { return this.value; }
   }

   public Test(int value)
   {
      this.value = value;
   }
}
Wartości typu Test zostaną przedstawione w taki o to mało interesujący sposób:

ConsoleApplication.Program.Equals(a = {ConsoleApplication.Program.Test}, b = {ConsoleApplication.Program.Test})

Wystarczy jednak zmodyfikować definicję klasy Test jak poniżej:
[DebuggerDisplay("Value = {value}")]
public class Test
{
...
}
Aby osiągnąć taki efekt:

ConsoleApplication.Program.Equals(a = Value = 1, b = Value = 1

Technika ta działa nie tylko w przypadku okna Call Stack ale również w przypadku innych okien debuggera czyli np.: Quick Watch.

Unwind To This Frame

Unwind To This Frame to jedna z komend dostępna w menu kontekstowym okna Call Stack. W większości przypadków kiedy wywołamy menu jest wyszarzona. Dostępna staje się tylko w przypadku kiedy zostanie rzucony wyjątek. W momencie rzucenia wyjątki środowisko najczęściej zatrzymuje wskazując linię w kodzie, w której wyjątek został wygenerowany (w szczególności może się nie zatrzymać jeśli wyjątek został obsłużony, a my nie mamy zaznaczonej opcji zatrzymywania na wszystkich wyjątkach). W oknie Call Stack ramka, w której został rzucony wyjątek wskazywana jest przy pomocy żółtej strzałki. Jeśli wyjątek został rzucony poza naszym kodem ostatnia ramka wskazująca nasz kod zostanie oznaczona strzałką zieloną .

Kiedy środowiska zatrzyma się już z powodu wyjątku możemy w oknie Call Stack wybrać interesującą nas ramkę, wywołać menu i wybrać komendę Unwind To This Frame. Spowoduje to zwinięcie stosu do wybranej ramki (działanie podobne do Run To Cursor) i umożliwi np.: zdiagnozowanie przyczyny wyjątku, modyfikację zmiennych lokalnych tak aby wyjątek nie został rzucony itd.

Polecenie Unwind To This Frame nie jest dostępne dla wszystkich ramek. Stos można zwinąć do każdej ramki powyżej ostatniej ramki, która wskazuje miejsce w naszym kodzie (pomiędzy żółtą i zieloną strzałką :)). Czyli jeśli wyjątek został rzucony bezpośrednio w naszym kodzie polecenie będzie dostępne tylko dla jednej ramki. Z kolei dla przypadku pokazanego niżej:

System.Xml.Serialization.XmlSerializer.Deserialize(xmlReader, encodingStyle, events)
System.Xml.Serialization.XmlSerializer.Deserialize(stream)
ConsoleTest.Program.Main(args)
...


Stos możemy zwinąć do jednej z metod Deserialize() albo do metody Main() w naszym kodzie.

Omawiając tą funkcjonalność należy koniecznie napisać o exception assistant. Całkiem możliwe, że większość użytkowników Visual Studio nigdy o nim nie słyszała. W sumie nie dziwne ponieważ jest domyślnie włączony (Tools -> Options -> Debugging -> General -> Enable the exception assistant). A do czego służy? Poniżej zamieszczam okienka wyświetlone przez środowisk w momencie rzucenia wyjątku kiedy asystent jest wyłączony (pierwszy obrazek) i kiedy jest włączony (drugi obrazek):



Mam nadzieję, że zalety asystenta są oczywiste :) ale asystent to nie tylko ładne okienko. Jeśli jest włączony w momencie rzucenia wyjątki środowisko wskaże zawsze linię w naszym kodzie nawet jeżeli prawdziwe źródło wyjątku jest gdzie indziej. Dzięki temu możemy po prostu "złapać" żółtą strzałkę, wskazującą w głównym oknie ostatnio wykonaną instrukcję, i przeciągnąć ją kilka linijek wcześniej lub kilka linijek dalej. Innymi słowy możemy ominąć wyjątek lub prześledzić jak został spowodowany. Przy włączonym asystencie polecenie Unwind To This Frame staje się trochę mniej ponieważ przydatne ale czasami możemy chcieć wyłączyć asystenta. W każdym razie dobrze zdawać sobie sprawę z jego istnienia.

Opisane techniki testowałem w środowiskach Visual Studio 2005 oraz Visual Studio 2008.

19/02/2009

Wycieki obiektów GDI

Home

Ostatnio zajmowałem się w pracy nieprzyjemnym problem z wyciekiem obiektów GDI. Sytuacja wydawała się dziwna ponieważ wyglądało na to, że metoda Dispose() była wołana wszędzie gdzie było to potrzebne. Po mówiąc szczerze dłuższym czasie dowiedziałem się o dwóch istotnych rzeczach:
  • Kontrolka TreeView zawiera bug polegający na tym, że jeżeli włączymy pokazywanie pól wyborów (ang. CheckBox) dla węzłów drzewa to zostaną zaalokowane cztery obiekty GDI. Nie zostaną one jednak zwolnione przy wywołaniu Dispose(). Ciekawą dyskusję na ten temat można znaleźć tutaj. Co gorsza jeśli poszperamy na sieci okaże się, że błąd jest znany od co najmniej kilku lat!
  • Drugi bug, o którym chciałem powiedzieć dotyczy klasy ImageList. Otóż klasa ta ma problemy ze zwalnianiem zasobów dla umieszczonych w niej obrazków/ikon, co doprowadza oczywiście do wzrostu liczby obiektów GDI. O błędzie tym możemy przeczytać na stronach firmy Microsft. Rozwiązanie, a raczej obejście problemu polega na wywołaniu metod GC.Collect() oraz GC.WaitForPendingFinalizers() po wywołaniu Dispose() dla instancji ImageList. Rozwiązanie działa ale korzystanie z metody GC.Collect() może mieć negatywny wpływ na wydajność aplikacji. Co gorsza może wystąpić jeszcze mniej pożądany efekt. Opieram się tutaj na doświadczeniach kolegi. Zgodnie z nimi używanie GC.Collect() w aplikacjach korzystających z kontrolek ActiveX może doprowadzić do ich zawieszenia w losowym momencie.
    ...
    imageList.Dispose();
    
    GC.Collect();
    GC.WaitForPendingFinalizers();
    ...
    
Z opisanych przeze mnie rzeczy trzeba zdawać sobie sprawę i być uważnym przy stosowania klasy TreeView, a w szczególności klasy ImageList. W przypadku posługiwania się TreeView można pokusić się jeszcze o użycie, zaproponowanej we wspomnianej przeze mnie dyskusji, klasy LeaklessTreeView (testowałem i wygląda, że działa).

17/02/2009

operator==

Home

Pomysł na napisanie tego posta podsunął mi kolega z pracy. Na początek fragment klasy, która we wzorcowy sposób dostarcza własnej implementacji operatora równości:
public class Test
{
  private int i;
  
  public Test(int i)
  {
    this.i = i;
  }

  public static bool operator==(Test a, Test b)
  {
    if (System.Object.ReferenceEquals(a, b))
      return true;

    if (((object)a == null) || ((object)b == null))
      return false;

    return a.i == b.i;
  }
  
  ...
}
Oczywiście potrzebna jest jeszcze implementacja operatora nierówności !=. Chciałbym też zwrócić uwagę, ponieważ sam zapomniałem o tym pisząc ten przykład :), że implementując operator równości (czy też nierówności) kiedy wykonujemy porównanie takie jak a == b, a == null czy b == null musimy zrzutować testowane obiekty do typu object. W innym wypadku nastąpi rekurencyjne wywołanie naszej implementacji operatora. Rzutowanie zapewni, że zostanie wywołana implementacja bazowa - operatory nie działają jak metody wirtualne. Inne rozwiązanie to użycie metody System.Object.ReferenceEquals. Teraz popatrzmy na kod poniżej i odpowiedzmy sobie na pytanie jaką wartość przyjmą zmienne lokalne res oraz res2:
public class Program
{
  public static bool MyEquals(T a, T b) where T : class
  {
    return a == b;
  }
  
  public static void Main(string[] args)
  {
    Test a = new Test(1);
    Test b = new Test(1);

    bool res = a == b;
    bool res2 = MyEquals(a, b);
  }
}
Odpowiedź brzmi oczywiście true oraz false. Piszę o tym również dla siebie ponieważ sam musiałem się zastanowić jaki będzie wynik. Pierwszy rezultat jest chyba oczywisty i nie wymaga żadnych tłumaczeń. Po prostu wywołany zostanie zaimplementowany przez nas operator. A co z wywołaniem metody generycznej MyEquals. Może się wydawać, że skoro jest to metoda generyczna to zostanie wywołany operator równości dla typu przekazanego jako argument typu generycznego (ang. generic type parameters). Argument typu generycznego to to co podstawiamy pod T czyli w tym przypadku Test.

Jest jednak inaczej. W momencie kompilacji kompilator nie wie co zostanie podstawione pod T. W związku z tym generowany jest kod sprawdzający równość referencyjną. Na poziomie IL oznacza to użycie instrukcji ceq. Jeśli zmienimy kod metody MyEquals tak jak poniżej kompilator będzie już mógł wybrać operator równości dostarczony przez nas zamiast bazowego.
public static bool MyEquals(T a, T b) where T : Test
{
  return a == b;
}

03/02/2009

Zakamarki Visual Studio (cz. 3)

Home

W trzecim już poście dotyczącym zakamarków Visual Studio, czyli mało znanych funkcji tego środowiska programistycznego, chciałbym napisać o oknie Object Test Bench. Okno to pozwala na szybkie testowanie wywołań metod statycznych i instancyjnych bez potrzeby pisania małych programików testowych. Jeśli po uruchomieniu środowiska nie jest widoczne możemy je wyświetlić wybierając View->Other Widnows->Object Test Bench. Aby wygodnie pracować z tym oknem przyda się również okno Class View (View->Class View), które służy do przeglądania typów zdefiniowanych w naszych projektach oraz w innych pakietach/podzespołach (ang. assembly), do których się odwołujemy. Ważna uwaga! Widok Object Test Bench może zostać użyty tylko i wyłącznie do testowania klas w projekcie oznaczonym jako StartUp oraz w pakietach, do których ten projekt się odwołuje. Co prawda zgodnie z dokumentacją powinniśmy móc testować wszystkie klasy w bieżącym rozwiązaniu (ang. solution) ale niestety nie jest to możliwe.

Dalej będę posługiwał się prościutką, a wręcz naiwną klasą zdefiniowaną poniżej, w celu omówienia zastosowania okna Object Test Bench.
public class Person
{
 private static int count = 0;
 
 private string name;
 private DateTime dateOfBirth;

 public string Name
 {
  get { return name; }
  set { name = value; }
 }

 public DateTime DateOfBirth
 {
  get { return dateOfBirth; }
  set { dateOfBirth = value; }
 }

 public Person()
 {
  Person.count++;
 }

 public Person(string name, DateTime dateOfBirth)
 : this()
 {
  this.name = name;
  this.dateOfBirth = dateOfBirth;
 }

 public int GetAge(bool inDays)
 {
  if(!inDays)
   return DateTime.Now.Year - dateOfBirth.Year;
  else
   return (DateTime.Now - dateOfBirth).Days ;
 }

 public static int GetCount()
 {
  return Person.count;
 }
}
Aby rozpocząć testowanie jakiejś klasy musimy zlokalizować ją w oknie Class View i wywołać dla niej menu kontekstowe. W menu kontekstowym powinny być widoczne dwie komendy:
  • Create Instance - Pozwala wywołać dowolny konstruktor dla danego typu i przypisać mu identyfikator. Stworzony obiekt pojawi się w oknie Object Test Bench. Dla stworzonych obiektów możemy następnie wywoływać metody instancyjne. W ten sposób nie powołamy do życia instancji klasy abstrakcyjnej czy też delegata.
  • Invoke Static Mehtod - Pozwala wywołać wybraną metodę statyczną danej klasy (również klasy abstrakcyjnej).
Polecenia te będą widoczne tylko dla klas z projektu głównego. Oczywiście przed wywołaniem metody statycznej, instancyjnej czy też konstruktora powinniśmy w interesujących nas miejscach w kodzie ustawić breakpointy.

Zacznijmy od wywołania metody statycznej. Po najechaniu w menu kontekstowym na polecenie Invoke Static Mehtod wyświetlone zostaną wszystkie metody statyczne dostępne w tej klasie. Należy wybrać jedną z nich. W przypadku naszej klasy testowej Person możemy wybierać pomiędzy GetCount() oraz dwie metodami odziedziczonymi po klasie Object: Equals(object, object) oraz ReferenceEquals(object, object). Po wybraniu GetCount() pojawi się okno potwierdzające chęć wywołania tej metody. Natomiast w przypadku metod, które posiadają parametry pojawi się okno pozwalające na wyspecyfikowanie wartości tych parametrów. Kiedy klikniemy Ok metoda zostanie wywołana. Na zakończenie pojawi się okno z wynikiem, które powinno wyglądać jak poniżej: Istnieje również możliwość zapisania wyniku wywołania metody. W tym celu powinniśmy zaznaczyć opcję Save return value (w oknie pokazanym powyżej) i nadać identyfikator dla zwracanego obiektu/struktury. Jeśli zdecydujemy się na zapisanie wyniku pojawi się on w oknie Object Test Bench: Stworzone obiekty reprezentowane są w oknie Object Test Bench przez prostokąty. Dla każdego obiektu możemy wywołać menu kontekstowe, które pozwoli nam wybrać metodę instancyjną do wywołania. Spróbujmy wykonać metodę posiadającą parametry np.: CompareTo(int). Po jej wybraniu powinniśmy zobaczyć okno jak poniżej: Warto zauważyć, że jeżeli metoda opatrzona była komentarze to zostanie on przytoczony. Po podaniu wartości parametru aktualnego i wybraniu przycisku Ok, metoda zostanie wywołana, a wynik zostanie przedstawiony w takim samym oknie jak przy wywoływaniu metody statycznej (jego też możemy zapisać).

Przyjrzyjmy się teraz poleceniu Create Instance i stwórzmy instancję klasy Person korzystając z konstruktora dwuargumentowego. Po wybraniu konstruktora powinno pojawić się okno przedstawionej dalej: Po pierwsze powinniśmy podać nazwę dla tworzonego obiektu. W tym miejscu trzeba uważać bo jeśli podanym nazwę, której już użyliśmy zostaniemy o tym poinformowani dopiero po naciśnięciu przycisku Ok, a nie ma możliwości cofnięcia się i naprawienia błędu.

Po drugie, podobnie jak przy wywoływaniu metod, należy podać wartości poszczególnych argumentów. Napiszę o tym troszeczkę więcej. Jeśli chodzi o typy proste nie ma co się rozpisywać. Po prostu wpisujemy liczbę lub ciąg znaków (zamknięty w cudzysłowy) i koniec. Troszeczkę trudniej jest z typami złożonymi. Na rysunku widać, że dla parametru dateOfBirth wstawiłem dziwną wartość dateTime1. Niestety ale w przypadku typów złożonych musimy najpierw stworzyć instancję danego typu, a dopiero potem powołując się na przypisaną danej instancji nazwę, użyć jej jako argumentu wywołania. Nazwa stworzonej instancji pojawi się na rozwijanej liście dla argumentów o odpowiednim typie.

W tym przypadku stworzyłem uprzednio strukturę typu DateTime i nadałem jej nazwę dateTime1. Zacząłem od zlokalizowania typu struktury w drzewie (najwygodniej posłużyć się przyciskiem Search u góry okna Class View). Następnie wybrałem polecenie Create Instance, dalej zdecydowałem się na konstruktor trójargumentowy, jako argumenty wywołania podając rok, miesiąc i dzień mojego urodzenia (wszystkie wartości to typy proste). Na koniec powróciłem do klasy Person i ponownie wybrałem interesujący mnie konstruktor. Jak widać tworzenie obiektów wymagających skomplikowanej inicjalizacji jest trudne i kłopotliwe ale te okno nie do tego służy.

Po stworzeniu instancji klasy Person możemy przystąpić do testowania jej metod, a właściwie jednej metody. Odbywa się to dokładnie w ten sam sposób jak opisałem na przykładzie metody CompareTo(int). Polecam spróbować wywołać metodę GetAge(bool) z parametrem true oraz false oraz sprawdzić jaki wynik zwraca metoda statyczna GetCount() po utworzeniu instancji klasy Person.

Jeszcze dwie uwagi na koniec. Jeśli wprowadzimy jakieś zmiany do kodu to okno Object Test Bench wymusi na nas ponowną kompilację źródeł. Równoznaczne jest to z utratą wszystkich obiektów, struktur, które umieściliśmy wcześniej w tym oknie.

W drugiej części tej serii postów pisałem o oknie Immediate. W ciekawy sposób współpracuje ono z oknem Object Test Bench. Dla przypomnienia w oknie Immediate również możemy wywoływać metody i tworzyć obiekty. Istotne jest to, że obiekty, które stworzymy w tym oknie pojawią się również w oknie Object Test Bench. Tutaj też obowiązuje warunek, żeby użyty typ pochodził z projektu oznaczonego jako StartUp lub z pakietu/podzespołu, do którego tej projekt się odwołuje. Kolejny warunek jaki musimy spełnić (wygląda to na błąd w Visual Studio) jest taki, że okno Object Test Bench nie może być puste w momencie tworzenia obiektu w oknie Immediate. Jeśli będzie tworzone obiekty nie zostaną do niego dodane. Co ciekawe jeśli w pewnym momencie dodamy jakiś obiekt do okna Object Test Bench (z poziomu okna Class View) to nagle pojawią się w nim również obiekty utworzone wcześniej w oknie Immediate.

Warto jeszcze dodać, że obiekty z okna Immediate w oknie Object Test Bench będą miały takie same identyfikatory jak zmienne użyta w oknie Immediate. Czyli wpisanie takiej komendy:
Person person = new Person();
spowoduje dodanie do okna Object Test Bench obiektu o nazwie person

Opisane techniki testowałem w środowiskach Visual Studio 2005 oraz Visual Studio 2008.

26/01/2009

Environment.CurrentDirectory

Home

Właściwość Environment.CurrentDirectory jest zapewne znana każdemu programiście .Net (przy jej pomocy odczytujemy ścieżkę do bieżącego katalogu roboczego). Co już może nie jest tak bardzo oczywiste właściwość ta posiada setter i może być dowolnie modyfikowana. Jest to ważne jeżeli używamy w naszych projektach ścieżek względnych np.:
./WorkingDir/Temp/log.txt
W takim wypadku aby określić ścieżkę bezwzględną platforma odczytuje właściwość Environment.CurrentDirectory. I tutaj mogą pojawić się problemy. Po pierwsze jeśli to my zmodyfikujemy tą właściwość możemy nieświadomie doprowadzić do wystąpienia błędu gdzieś w innej części projektu. Na przykład plik, do którego została podana ścieżka względna nie zostanie odnaleziony. Sytuacja może być oczywiście odwrotna. Żeby nie szukać daleko, użycie klasy OpenFileDialog spowoduje zmianę katalogu roboczego jeśli po wybraniu pliku użytkownik zatwierdzi swój wybór.
OpenFileDialog ofd = new OpenFileDialog();
ofd.ShowDialog();
Jeśli teraz, po wybraniu pliku przy pomocy klasy OpenFileDialog, spróbujemy uzyskać dostęp do innego pliku, do którego znamy tylko ścieżkę względna operacja zakończy się niepowodzeniem (chyba, że katalog wybrany przez użytkownika okaże się taki sam jak pierwotny katalog roboczy). Używanie ścieżek bezwzględnych jest dobrym rozwiązaniem ale nie zawsze zadziała. Co jeśli ścieżka wczytywana jest z pliku konfiguracyjnego, który może zmienić użytkownik? Wytłumaczenie klientowi żeby nie stosował ścieżek względnych nie jest dobrym pomysłem bo co złego może być w ścieżkach względnych z perspektywy klienta :).

Moim zdaniem należy więc: używać ścieżek bezwzględnych wszędzie tam gdzie jest to możliwe lub też nie bazować na wartości Environment.CurrentDirectory. Mam tu na myśli sytuację, w której używamy ścieżek względnych ale zamieniamy je na bezwzględne korzystając z jakiegoś parametru konfiguracyjnego. Można też pomyśleć o wykrywaniu ścieżek względnych podawanych przez użytkownika i zamianie ich na ścieżki bezwzględne.

Po drugie należy unikać modyfikowania właściwości Environment.CurrentDirectory, a jeśli właściwość ta zostanie zmieniona to przywrócić jej pierwotną wartość chyba, że jej modyfikacja była w 100% zamierzona.

Przyda się też wiedza o tym, że katalog roboczy może zostać zmieniony niejako za naszymi plecami oraz, że właściwość Environment.CurrentDirectory jest używana przez platformę .Net w celu określenia ścieżek bezwzględnych.

12/01/2009

Czemu zdarzenia nie działają ??? (część 2)

Home

W poście Czemu zdarzenia nie działają??? opisałem błąd, który doprowadza do sytuacji, w której nie otrzymujemy zdarzeń od dynamicznie tworzonych kontrolek. Jego rozwiązanie nie jest specjalnie skomplikowane i dla przypomnienia przytoczę je tutaj:

W przypadku dynamicznego tworzenia kontrolek, czy to bezpośrednio czy to przy okazji użycia kontrolek data bound zawsze należy pamiętać aby dynamiczne kontrolki były tworzone nie tylko przy inicjalnym odwołaniu do strony ale również przy kolejnych.

W tym poście chciałbym jeszcze zwrócić uwagę gdzie powinny być tworzone dynamiczne kontrolki. Najlepszym miejsce jest zdarzenie PreInit strony. Powodów jest kilka, a najważniejsze to:
  • Zdarzenie PreInit jest odpalane najwcześniej w cyklu życia strony. Jeśli stworzymy kontrolki w tym miejscu będą one dostępne w każdym kolejnym zdarzeniu - kontrolki dynamiczne będą zachowywały się tak samo jak kontrolki statycznie osadzone na stronie. To jest pierwszy i najważniejszy powód, a wszystkie wymienione dalej stanowią jego rozwinięcie.
  • Zdarzenie PreInit to jedyne miejsce, w którym możemy dynamicznie ustawić właściwość Theme. Jeśli stworzymy nasze kontrolki później to wartość tej właściwości nie będzie miała wpływu na sposób ich prezentacji.
  • Będziemy mieli zagwarantowane, że właściwości kontrolek zostaną odtworzone na podstawie ViewState.
  • Będziemy mieli pewność, że jeżeli dla dynamicznie stworzonej kontrolki ma zostać wygenerowane zdarzenie to zostanie. Innymi słowy będziemy mieli pewność, że w punkcie, w którym maszyneria ASP.NET określa dla jakiej kontrolki wywołać jakie zdarzenie, ta kontrolka będzie istnieć.
  • Takie są zalecenia :)

06/01/2009

ASP.NET Caching

Home

W poście tym chciałbym napisać kilka słów o unieważnianiu zawartości cache'a w aplikacjach ASP.NET na podstawie aktualizacji danych przechowywanych w bazie danych. Post ten nie jest jednak przewodnikiem na temat cache'owania w aplikacjach ASP.NET, opisuję w nim kilka moim zdaniem ważnych elementów. Post ten można traktować jako punkt wyjścia do dalszej nauki.

Idea jest bardzo prosta. W celu przyspieszenia aplikacji umieszczamy w cache'u jakieś dane np.: wynik zapytania czy też wyrenderowaną stronę (przy pomocy dyrektywy @OutputCache). Chcielibyśmy jednak aby w przypadku jeśli dane, na podstawie których zostało wykonane zapytanie, ulegną zmianie, ponownie wykonać zapytanie zamiast wczytywać jego wynik z cache'a. W aplikacjach ASP.NET możemy wybierać z dwóch możliwości. Jedna to obiekt klasy Cache dostępny przez właściwość strony o takiej samej nazwie. Dane umieszczone w tym pojemniku mogą zostać usunięte po upływie określonego czasu, kiedy zmianie uległ wskazy plik czy też (co interesuje nas w tej chwili) kiedy dane w bazie danych zostały zmienione. Po drugie mamy dostęp do cache'a przechowującego wyrenderowane strony czy też ich fragmenty. Z tego cache'a nie korzystamy bezpośrednio ale przy pomocy dyrektywy @OutputCache. W tym przypadku również możemy wybierać pomiędzy kilkoma wariantami unieważniania zawartości cache'a.

Przejdźmy do konkretów. W obu przypadkach aby powiązać dane w cache'u z danymi w bazie danych używamy klasy SqlCacheDependency. W przypadku cache'a dostępnego z poziomu strony musimy jawnie utworzyć instancję tej klasy. Przykład użycia znajdziemy tutaj. Natomiast w przypadku dyrektywy @OutputCache musimy użyć atrybutu SqlDependency.

Istotne jest co dzieje się pod spodem. SqlCacheDependency używa klasy SqlDependency (nazwa klasy jest taka sama jak nazwa wspomnianego wcześniej atrybutu). SqlDependency to z kolei wysoko poziomowa nakładka na inną klasę SqlNotificationRequest. Ważne jest to, że SqlDependency i SqlNotificationRequest współpracują tylko z bazą danych Sql Server 2005 i nowszymi, które dostarczają usługi powiadomień na poziomie wierszy - query notifications. To znaczy, że możemy wskazać wierszy, w przypadku zmiany których dane z cache'a zostaną usunięte. W przypadku starszych baz danych (lub nowych jeśli nie odpowiada nam query notifications) SqlCacheDependency wykorzystuje mechanizm monitorowania całych tabel zamiast wskazanej grupy wierszy. Jest to ogólnie rozwiązanie mniej wydajne.

Jeszcze parę słów o @OutputCache. Jak napisałem wcześniej aby użyć unieważniania na podstawie bazy danych należy wysterować atrybut SqlDependency. Składnia tego atrybutu jest następująca:
<%@OutputCache
...
SqlDependency="database/table name pair | CommandNotification"
...
%>
W przypadku podania pary nazwa bazy danych i nazwa tabeli zostanie użyte monitorowanie na poziomie całych tabel. W przypadku podania wartości CommandNotification zostanie użyte powiadamianie na poziomie wierszy, a dokładniej ASP.NET użyje wszystkich komend użytych w kontekście danej strony do utworzenia zależności pomiędzy danymi w cache'u, a danymi w bazie danych.

W tym momencie należy jeszcze zaznaczyć, że o ile klasa SqlCacheDependency jest właściwa dla aplikacji ASP.NET to SqlDependency i SqlNotificationRequest mogą być użyte również w innych przypadkach. W ogólności użycie SqlNotificationRequest jest raczej trudne i pracochłonne. W większości przypadków używa się SqlDependency. Przykład użycia tej klasy można znaleźć tutaj tutaj.

15/12/2008

Domknięcie - Jak to działa?

Home

Domknięcie (ang. Closure) to cecha języków programowania, która pozwala funkcji odwołać się to zmiennych zdefiniowanych poza jej ciałem. Domknięciem nazywa się również obiekt (byt) wiążący funkcję z środowiskiem jej wykonania. Oczywiście chodzi o zmienne inne niż globalne. Takie powiązane zmienne w języku angielskim określane są mianem bound variables. Przykład domknięcia został przedstawiony poniżej. Proponuję przyjrzeć się temu kodowi i odpowiedzieć co zostanie wypisane na ekran. W dalszej części posta będę używał terminu metoda zamiast funkcja, który bardziej pasuje do obiektowego języka jakim jest C#.
delegate void Fun();
...
List array = new List();
for (int i = 0; i < 5; ++i)
{
  array.Add(delegate() { Console.WriteLine(i); });
}

for (int i = 0; i < 5; ++i)
{
  array[i]();
}
Implementacja domknięcia zależy od konkretnego języka programowania. Mnie oczywiście zaciekawiło jak to zostało zrealizowane w C# i tym zajmę sie w tym poście. W rozwiązaniu zagadki pomógł oczywiście reflektor Lutz Roeder’s Reflector, o którym wspominałem już wcześniej.

Implementacja domknięcia w języku C# bazuje na automatycznie generowanej przez kompilator klasie. Klasa ta zawiera kod metody oraz powiązane z nią zmienne. Dla naszego przykładu klasa ta będzie wyglądać jak poniżej:
[CompilerGenerated]
private sealed class DisplayClass
{
  public int i;

  public void b_0()
  {
    Console.WriteLine(this.i);
  }
}
Kod zmieniłem tak aby był bardziej czytelny. Jak widać wygenerowana klasa jest trywialna. Zawiera jedną metodę, której zawartości odpowiada zawartości zdefiniowanej przez nas anonimowej metody oraz jedno publiczne pole, które stanowi nic innego jak powiązaną zmienną. No dobrze, ale kto korzysta z wygenerowanej klasy i kto ustawia wartość publicznego pola i. Odpowiedź nasuwa się sama - oczywiście kompilator. A jak? Zostało to schematycznie przedstawione poniżej:
DisplayClass d = new DisplayClass();

List array = new List();
for (d.i = 0; d.i < 5; ++d.i)
{
  array.Add(d.b_0);
}

for (int i = 0; i < 5; ++i)
{
  array[i]();
}
Kod ten może trochę dziwić. Od razu nasuwa się pytanie, czemu utworzono tylko jedną instancję automatycznie wygenerowanej klasy? Wszystko się jednak zgadza. Domknięcie działa w ten sposób, że anonimowa metoda ma dostęp do wartości zmiennej powiązanej z czasu jej wykonywania, a nie z czasu kiedy została utworzona. Nie trudno zauważyć, że w momencie wywoływania metody wartość zmiennej i wynosi 5. Odpowiedź na postawione przeze mnie wcześniej pytanie brzmi, więc: Na ekran zostanie wypisany ciąg: 5 5 5 5 5. Poniżej podaję sposób w jaki osiągnąć bardziej intuicyjny efekt wypisania na ekran ciągu: 0 1 2 3 4. Należy tylko zmodyfikować pierwszą pętlę:
for (int i = 0; i < 5; ++i)
{
  int j = i;
  array.Add(delegate() { Console.WriteLine(j); });
}
Dla takiego przypadku kod wygenerowany przez kompilator będzie już inny. W szczególności zostanie utworzonych 5 instancji klasy DisplayClass, a nie jedna jak w poprzednim wypadku.

Przykład omówiony przeze mnie jest bardzo prosty. W bardziej skomplikowanych przypadkach sprawa nie jest już tak prosta ale koncepcja implementacji domknięcia pozostaje taka sama.

10/12/2008

Czemu zdarzenia nie działają???

Home

Ostatnio pomogłem rozwiązać dwa problemy z "nie działającymi" zdarzeniami. Jak to najczęściej bywa, znając rozwiązanie, problem wydaje się banalnie prosty. Ponieważ jednak dojście do rozwiązania nie zawsze jest już tak proste postanowiłem napisać ten post.

Ogólnie problem został mi przedstawiony mniej więcej w taki sposób (luźny cytat): Podczepiłem się pod zdarzenia kilku kontrolek ale po wykonaniu post back'a do strony, metody obsługi zdarzeń nie są wołane.

W obu wspomnianych sytuacjach obserwowany efekt był taki sam (metody obsługi zdarzeń nie były wołane), natomiast przyczyna błędu troszeczkę inna. Błędu, ponieważ to oczywiście nie bug w maszynerii ASP.Net tylko najzwyklejszy w świecie błąd programisty.

Problem numer 1

W pierwszym przypadku programista dynamicznie tworzył kontrolkę, subskrybował jej zdarzenia, a następnie dodawał ją do strony. Dla ustalenia uwagi przyjmijmy, że była to kontrolka ListBox. Opisywany kod mógł więc wyglądać następująco:
...
ListBox lb = new ListBox();
lb.AutoPostBack = true;
lb.SelectedIndexChanged += new EventHandler(lb_SelectedIndexChanged);
lb.Items.Add("a");
lb.Items.Add("b");
lb.Items.Add("c");

this.Panel.Controls.Add(lb);
...
Sam w sobie, kod ten jest jak najbardziej poprawny. Błąd polegał na tym, że kod ten był wykonywany tylko w przypadku inicjalnego odwołania do strony. W przypadku post back'a czyli kiedy właściwość IsPostBack była równa true już nie. A skoro kontrolka nie została utworzona to zdarzenie nie mogło zostać wygenerowane.

Problem numer 2

W drugim przypadku programista używał statycznie osadzonej na stronie kontrolki DataList, zasilał ją danymi i wołał metodę DataBind. Kod strony przypominał koncepcyjnie kod poniżej:
...
this.DataList.DataSource = new string[] { "a", "b", "c" };
this.DataList.DataBind();
...
...
<asp:DataList ID="DataList" runat="server" EnableViewState="false">
   <ItemTemplate>
      <asp:Button ID="Button" runat="server" Text="<%# Container.DataItem %>" OnClick="OnClick" />
   </ItemTemplate>
</asp:DataList>
...
Podobnie jak wcześniej, strona nie działała tak jak powinna, ponieważ w przypadku post back'a nie była wołana metoda DataBind kontrolki DataList i przyciski nie były tworzone i w związku z tym zdarzenia nie mogły zostać wygenerowane. W tym jednak przypadku, pośrednią przyczyną błędu był fakt, że View State został wyłączony. Gdyby był włączony, przyciski zostałyby odtworzone na jego podstawie.

Wnioski

Konkluzja jest bardzo prosta. W przypadku dynamicznego tworzenia kontrolek, czy to bezpośrednio czy to przy okazji użycia kontrolek data bound zawsze należy pamiętać aby dynamiczne kontrolki były tworzone nie tylko przy inicjalnym odwołaniu do strony ale również przy kolejnych.

08/12/2008

Włączanie i wyłączanie kaskadowych arkuszy stylów

Home

Jeśli potrzebujemy szybki i łatwo zaimplementować włączanie i wyłączanie kaskadowych arkuszy stylów na stronie możemy zastosować taki kod:
...
<head runat="server">
    <link id="link" 
        type="text/css" 
        rel="Stylesheet"
        href="~/style.css" 
        runat="server" />
</head>
...
Teraz wystarczy sterować widocznością serwerowej kontrolki Html. Jeśli kontrolka nie będzie widoczna:
this.link.Visible = false;
to nie zostanie wzięta pod uwagę podczas renderowania strony i styl nie zostanie zastosowany do strony. Odwrotnie, jeśli kontrolka będzie widoczna:
this.link.Visible = true;
to podczas renderowania strony zostanie uwzględniona i styl zostanie zastosowany do strony. Bardzo proste ale skuteczne.