Pobierz - Zine.net online

Transkrypt

Pobierz - Zine.net online
01/2006
asp.net
Edycja struktury organizacyjnej za pomocą
TreeView w ASP.NET
UMBRACO. Wprowadzenie i opis instalacji
winforms
Gra w kości a usługi kompilatora
Obsługa kontrolki NotifyIcon w Windows
Forms na przykładzie aplikacji wyłączającej
monitor
tools.net Wprowadzenie
do śledzenia aplikacji
przy pomocy biblioteki
rozmowa.net z Jarkiem Kowalskim
tips&tricks
books.net Wyszukiwanie
i usuwanie
błędów
z aplikacji
ASP,NET
Edycja struktury organizacyjnej za pomocą
TreeView w ASP.NET
Aplikacje www przeżywają burzliwy
rozkwit, znajdując zastosowanie w coraz
to większym zakresie dziedzin. Tworząc
aplikację ASP.NET dla dużych klientów
korporacyjnych, z dużym prawdopodobieństwem zetkniemy się z problemem
edycji i prezentacji struktury firmy. W
przypadku, kiedy struktura ta jest mało
skomplikowana i „płaska” problemu nie
napotkamy i możemy z powodzeniem
stosować dowolny z ulubionych mechanizmów edycji danych jak listy, tabele, czy co jeszcze lubimy. W przypadku
dużych struktur drzewiastych sprawa
się już komplikuje i ich prezentacja lub,
co gorsza edycja nie jest już taka prosta.
Niezmiernie pomocna okazuje kontrolka
TreeView, która w ASP.NET 2.0 wylądowała, nie wiedzieć czemu, w grupie kontrolek odpowiedzialnych za nawigację,
służąc do prezentowania mapy serwisu.
Sprawdza się ona w tej roli znakomicie,
lecz nie jest to bynajmniej jej jedyne zastosowanie.
Wymagania klienta
Problem jako taki, wyniknął przy okazji jednego z projektów, kiedy to klient
zażyczył sobie właśnie edycji struktury firmy. Pech chciał, że firma ta posiada
kilkanaście oddziałów, z czego każdy oddział kilkanaście regionalnych przedstawicielstw a do tego jest kilka grup
biznesowych w ramach korporacji. W
rezultacie okazało się, że liczba jednostek organizacyjnych będzie bliska czterystu, a możliwe, że więcej. Szczęśliwie,
wymaganie edycji ograniczone zostało,
przy niemałym wkładzie autora, do edycji tylko nazwy jednostki i dodawania
nowych, możliwości zmiany statusu informującego czy dana jednostka istnieje
czy nie (zgodnie z założeniem, że żadne
dane z systemu nie mogą być bezpowrotnie kasowane) oraz przenoszenia jednostek wraz z całymi gałęziami drzewa w
dowolne jego miejsce (klasyczne wytnijwklej). Struktura danych (Rysunek 1) do
przechowywania tychże informacji jest
bardzo prosta i składa się z jednej tabeli
zawierającej unikalny identyfikator, identyfikator elementu nadrzędnego, nazwę
oraz pole informujące o statusie Enabled
/ Disabled. Referencja pomiędzy ID a ParentID zapewnia integralność danych.
Przykład w pełni funkcjonalnego drzewa zamieszczono na rysunku 2. PrzedRysunek 1. Tabela do przechowywania danych o strukturze
organizacyjnej.
2
Rysunek 2. Finalna postać drzewa
stawia on w górnej części wygląd węzła
w trybie edycji oraz dodawanie nowego
elementu w dolnej. Dodatkowo zamieszczono wygląd gałęzi, w której węzły są
nieaktywne (Disabled). Każdy węzeł opatrzony jest ikonami poleceń, odpowiednio edycja, dodanie nowego elementu,
przenoszenie gałęzi oraz zmiana statusu.
Dostępność ostatniego polecenia jest zależne od stanu węzła rodzica. Jak widać
na przykładzie jednostki „5 Northampton”, opcja ta jest niedostępna ponieważ
węzeł nadrzędny posiada status Disabled,
co oznacza ten sam status dla wszystkich węzłów potomnych. Zabezpiecza to
strukturę przed sytuacją, w której jednostka aktywna znajdowałaby się w hierarchii pod jednostką nieaktywną.
Ładowanie danych
W pierwszym podejściu do zadania,
aby zminimalizować liczbę odwołań do
bazy danych, zastosowano obsługę zdarzenia TreeView.TreeNodePopulate()
wraz z opcją PopulateOnDemand. Zgodnie z teorią, rozwiązanie takie powinno ładować tylko dane do węzłów, które
są wyświetlane. Rzeczywistość ujawniła jednakże kilka problemów. Najistotniejszym z nich, jest fakt, iż metoda ta
powoduje postback przy każdym rozwinięciu lub zwinięciu elementu drzewa.
Samo w sobie problemem to nie jest, pod
warunkiem jednak, że operujemy na małych strukturach. Kod HTML generowany
przez TreeView jest daleki od optymalnego. W efekcie, mimo iż spodziewano się skomplikowanej i dużej strony w
miarę rozwijania struktury, to rzeczywistość przeszła oczekiwania. Okazało
się, że w przypadku tak dużej struktury
kod HTML szybko przekroczył 1 MB objętości. Kilkukrotne odświeżenie takiej
strony, aby dotrzeć do wybranego węzła
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
w gałęzi staje się mocno uciążliwe, czyni
starannie wykonaną stronę bezużyteczną
a użytkownika przyprawia o mordercze
myśli pod adresem programisty. Dzięki
zastosowaniu serii zabiegów odchudzających stronę udało się ograniczyć jej rozmiar do około 500KB, jednakże nie spowodowało to znaczącej poprawy sytuacji.
Niezbędnym okazało się jednorazowe załadowanie całej struktury drzewa ponosząc koszt ładowania danych i pozostawić
obsługę rozwijania i zwijania gałęzi po
stronie klienta.
Ładowanie danych do kontrolki TreeView można rozwiązać poprzez użycie
hierarchicznego źródła danych (np XmlDataSource), obiektu DataSet z relacjami
lub poprzez ręczne dodawanie węzłów do
kontrolki. Opisana w dalszej części metoda implementacji edycji danych w drzewie wymaga modyfikacji własności TreeNode.Text, między innymi bazując na
stanie elementów nadrzędnych, dlatego
też najprostszym jest ręczne, rekurencyjne budowanie drzewa. Należy pamiętać,
że ViewState dla TreeView zawierającego
kilkaset węzłów potrafi zająć nawet kilkaset kilobajtów i powinna być to pierwsza czynność na drodze odchudzenia
strony. W pierwszym kroku konieczne
jest wyczyszczenie kolekcji węzłów drzewa a następnie utworzenie pierwszego
węzła będącego węzłem głównym (root).
Mając tak przygotowane drzewo możemy
rekurencyjnie wywoływać metodę ładującą LoadSubtree() z parametrem null, co
spowoduje załadowanie węzła głównego. Kolejne rekurencyjne wywołania tej
samej metody spowodują załadowanie
kolejnych elementów i dodanie ich do już
istniejących. (Rysunek 3) Utworzone w
ten sposób drzewo zawiera jedynie gołe
dane, bez możliwości ich edycji oraz bez
wskazania statusu węzła.
Edycja
Prezentacja struktury w postaci drzewa jest zaledwie pierwszym krokiem ku
uzyskaniu w pełni funkcjonalnego rozwiązania. Do realizacji zamierzonych
funkcji trzeba dodać do każdego węzła
przyciski służące do uruchomienia trybu
edycji, dodania nowego elementu, zmiany
statusu oraz przeniesienia. Trzeba także
umieścić pole tekstowe do edycji nazwy
wraz z odpowiednimi przyciskami. Nieszczęśliwie się składa, że węzeł drzewa,
TreeNode, nie posiada kolekcji Controls,
zatem bezpośrednie dodawanie kontrolek
jest niemożliwe. Zwróćmy jednak uwagę
na kod HTML, jaki jest generowany przez
TreeView. W celu umożliwienia obsługi zaznaczania węzłów, właściwość TreeNode.Text jest opakowywana odnośnikiem wywołującym postback po stronie
serwera. W przypadku przedstawionym
na rysunku 4, właściwość TreeNode.Text
zawiera tekst „701 Kent”, pozostałe elementy zostały dodane w momencie ren-
derowania węzła. Wybrana wcześniej
metoda ładowania danych, pozwala na
łatwą manipulację tekstem, który trafi
do TreeNode.Text, zatem nic nie stoi na
przeszkodzie, aby umieścić tam fragment
HTML, który zamknie generowany automatycznie link oraz dołączy ikony poleceń. Zwrócić należy uwagę, że ponieważ
do naszego tekstu dodany zostanie automatycznie znacznik końca odnośnika
</A>, aby zachować spójność należy jako
ostatni element również umieścić odnośnik, aby dopełnić generowaną końcówkę. Rysunek 5 przedstawia ten sam węzeł,
ale z TreeNode.Text zawierającym dodatkowo przycisk i dopełnienie odnośnika,
przy czym węzeł zachowuje nadal obsługę zdarzenia SelectedNodeChanged(). W
analogiczny sposób można dowolnie modyfikować zawartość węzła.
Poszukiwanie rozwiązania
Skoro możemy dodać przyciski do węzła to pozostało już je tylko obsłużyć.
Pula zdarzeń, jakie oferuje TreeView jest
stosunkowo uboga i potencjalne zastosowanie ma jedynie SelectedNodeChanged,
które wywoływane jest w momencie
zaznaczenia jednego z węzłów. Ze
względu na niemożność ingerencji
w parametry tego zdarzenia oraz
jego niedoskonałość nie nadaje się
ono do oprogramowania dodanych
przycisków. Można pokusić się
o zastosowanie którejś z metod
ClientScriptManager, przykładowo, GetPostbackEventReference(), jednakże rozwiązania
te mają pewne ograniczenia, z których
głównym jest możliwość przekazania tylko jednego parametru, co jest niewystarczające dla naszych celów. Należy, zatem
znaleźć metodę, która pozwoli wykonać
postback oraz przekazać, co najmniej trzy
parametry – identyfikator węzła wywołującego polecenie, nazwę tego polecenia
oraz jego argument.
Zanim zajmiemy się samym rozwiązaniem problemu parę słów o parametrach
wspomnianych powyżej. Jako identyfikator węzła najlepiej użyć własności Tre-
eNode.ValuePath, która określa ścieżkę
prowadzącą do wybranego węzła zbudowaną z wartości TreeNode.Value wszystkich węzłów aż do węzła głównego. Pozwala to jednoznacznie zlokalizować dany węzeł w strukturze. Parametr ten może być porównany do parametru Sender,
tree.Nodes.Add(newNode);
Rysunek 3. Rekurencyjne ładowanie danych do TreeView
/// <summary>
else
/// </summary>
//wywolujemy ponownie metode z nowo utworzonym wezlem ja-
/// Ladowanie drzewa
parentNode.ChildNodes.Add(newNode);
private void LoadTree()
{
}
tree.Nodes.Clear();
LoadSubtree(null);
ko parametr
//wyczyszczenie zawartosci drzewa
//rozpoczecie ladowania
}
/// <summary>
/// Ladowanie listi wezlow dla podanego wezla nadrzednego
}
}
LoadSubtree(newNode);
Rysunek 4. Wygenerowany przez TreeView kod HTML węzła.
/// </summary>
<td style=”white-space:nowrap;”>
private void LoadSubtree(TreeNode parentNode)
href=”javascript: _ _ doPostBack(‘ctl00$mc$tree’,’s1\\57\\60\\408’)”
/// <param name=”parentNode”>Wezel nadrzedny</param>
{
<a class=”ctl00 _ mc _ tree _ 0”
//dla parentNode = null ladujemy dane wezla glownego,
//dla pozostalych uzywamy Value jako ID wezla nadrzednego
string sql = „SELECT * FROM OrgUnits WHERE ParentID IS NULL”;
if (parentNode != null)
{
onclick=”TreeView _ SelectNode(ctl00 _ mc _ tree _ Data, this,’ctl00 _ mc _ treet6’);”
id=”ctl00 _ mc _ treet6”>
701 Kent
</a>
</td>
int parentId = Convert.ToInt32(parentNode.Value);
sql = string.Format(„SELECT * FROM OrgUnits o WHERE ParentID =
{0}”, parentId);
}
Rysunek 5. Węzeł z rysunku 4 z rozbudowanym TreeNode.Text
<td style=”white-space:nowrap;”>
<a class=”ctl00 _ mc _ tree _ 0”
string connStr = ConfigurationManager.ConnectionStrings[„MyCon-
href=”javascript: _ _ doPostBack(‘ctl00$mc$tree’,’s1\\57\\60\\408’)”
using (SqlConnection conn = new SqlConnection(connStr))
s,’ctl00 _ mc _ treet6’);”
nectionString”].ConnectionString;
{
SqlCommand cmd = new SqlCommand(sql, conn);
onclick=”TreeView _ SelectNode(ctl00 _ mc _ tree _ Data, thiid=”A1”>
701 Kent</a><input type=”button” onclick=”alert(‘hello’” valu-
conn.Open();
e=”Hello” /><a>
while (rd.Read())
</td>
SqlDataReader rd = cmd.ExecuteReader();
{
</a>
Rysunek 6. Blok zawierający kontrolki pośredniczące w wywołaniach
postback
string name = rd[„Name”].ToString();
int id = Convert.ToInt32(rd[„ID”]);
TreeNode newNode = new TreeNode(name, id.ToString());
//utworzenie nowego wezla
//jesli ladujemy wezel glowny to dodajemy go bezposrednio
do tree,
//jesli nie to do kolekcji wezlow przekazanego wezla na-
drzednego parentNode
if (parentNode == null)
<div style=”display: none;”>
<input type=”hidden” id=”cmdName” runat=”server” />
<input type=”hidden” id=”cmdArg” runat=”server” />
<input type=”hidden” id=”cmdSender” runat=”server” />
<asp:LinkButton ID =”cmdBtn” runat=”server”
OnClick=”cmdBtn _ Click”></asp:LinkButton>
</div>
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
3
ASP,NET
zwyczajowo przekazywanego do obsługi zdarzeń. Nazwę polecenia zachować
można w postaci tekstu, identyfikującego
jednoznacznie (z punktu widzenia logiki
programu) polecenie do wykonania, analogicznie jak CommandEventArgs.CommandName znane z obsługi zdarzeń
Command. Analogicznie argument polecenia jest odpowiednikiem CommandEventArgs.CommandArgument i służy do
przekazania dowolnej wartości. Wszystkie przedstawione parametry mogą być
przekazywane w postaci ciągów znakowych i nie wymagają żadnych dodatkowych mechanizmów serializacji lub podobnych, które utrudniałyby implementację. (Rysunek 6)
Akcja i reakcja
Wracając do zasadniczego problemu.
Najprostszym sposobem wywołania postback jest stworzenie go. Standardowo, na
postback, składa się szereg różnych funkcji JavaScript i ich wywołań. W zależności od tego, jaki rodzaj kontrolki powoduje postback, akcje mogą zmieniać się od
standardowego ustawienia typu przycisku jako Submit do całkiem skomplikowanych kawałków kodu zawierających
dodatkowo obsługę walidatorów. Konstruowanie ręcznie zawartości atrybutu OnClick dla przycisków nie przyniesie spodziewanego efektu, i w
najlepszym przypadku nie spowoduje żadnego działania, a w
najgorszym będzie źródłem
błędu walidacji zdarzeń.
Aby możliwe było bezpieczne wywołanie postback
przydatne jest użycie dodatkowej kontrolki, przycisku cmdBtn,
który ukryty przed użytkownikiem pełnić będzie rolę pośrednika pomiędzy przyciskami dodanymi do drzewa
a kodem server-side aplikacji. Dodatkowo, analogiczne kontrolki serwerowe,
cmdArg, cmdName i cmdSender, można
zastosować do przekazywania opisanych
wcześniej trzech parametrów zdarzenia. Za pomocą prostej funkcji JavaScript,
możliwe jest ustawienie wartości dla każdej z tych trzech kontrolek danych zdarzenia, a następnie wywołanie polecenia powodującego postback dla przycisku
cmdBtn.
Niestety nie jest to jeszcze koniec problemów. Jak można zauważyć na rysunku 7, w funkcji callCmd() JavaScript
kilkukrotnie używane są identyfikatory
kontrolek, nadawane są automatycznie w
momencie renderowania przez ASP.NET
strony. Nie można z góry założyć, iż identyfikatory te będą znane, bowiem ich postać zależy od położenia kontrolek, i tak,
dla zwykłej strony może być to po prostu
cmdBtn, natomiast dla strony zawierającej maski może to już być ctl00_ContentPlaceHolder_cmdButton. Można, posługując się właściwością Control.ClientID,
4
która zwraca identyfikator kontrolki taki,
jaki będzie on po wygenerowaniu strony wygenerować funkcję w momencie
ładowania strony. W trakcie generowania tej funkcji, należy jeszcze nieznacznie zmodyfikować identyfikator cmdBtn.
Poszczególne jego segmenty oddzielane są znakiem ‘_’ podczas gdy funkcja
__doPostBack() wymaga identyfikatora z segmentami oddielanymi znakiem
‘$’. Po utworzeniu ciała funkcji pozostaje
zarejestrować ją na stronie używając metody ClientScriptManager.RegisterClientScriptBlock(). (Rysunek 8)
Możemy teraz wrócić do tworzenia tekstu i przycisków dla węzłów. Znając już
wszystkie elementy układanki, możemy
utworzyć metodę BuildNodeText(), która
utworzy na postawie podanych parametrów odpowiednio spreparowany tekst
węzła. Metoda ta wymaga wiedzy o statusie jednostki nadrzędnej, trzeba więc
zmodyfikować także odpowiednio opisaną wcześniej metodę LoadSubtree().
(Rysunek 9) Należy zwrócić uwagę, że
definiowanie wywołań funkcji CallCmd()
wymaga, jak wcześniej opisano, znajomości TreeNode.ValuePath. Wartość ta
jest ustalana dopiero po dodaniu węzła
do struktury TreeView, należy zatem najpierw utworzyć węzeł a następnie poddać
go modyfikacji.
Majac tak spreparowane węzły, po naciśnięciu któregokolwiek z dodanych
do drzewa przycisków, odpowiednie wartości zostaną wpisane do
stosownych pól a następnie zostanie wywołany postback wraz
z obsługą zdarzenia Click() dla
przycisku cmdBtn. W ramach
procedury obsługi tego zdarzenia mamy dostęp do wartości wysłanych w kontrolkach cmdArg, cmdName i cmdSender. Pozostaje już tylko
odnaleźć węzeł, który wywołał polecenie
oraz odczytać jego argumenty. (Rysunek 10)
Przykładowo, jak pokazuje rysunek
11, włączenie trybu edycji sprowadza się
dodania do węzła, który jest adresatem
zdarzenia, nowego węzła z odpowiednio
przygotowanym TreeNode.Text. Należy
zwrócić uwagę, że przyciski powiązane z
edycją również obsługiwane są za pomocą callCmd(). W analogiczny sposób należy włączyć tryb edycji nazwy.
Podsumowanie
Przedstawiona metoda opiera się na
wykorzystaniu ukrytych kontrolek jako pośredników w wywołaniu postback.
Rozwiązanie to sprawdziło się doskonale
w opisanym drzewie do edycji struktury
organizacyjnej. Może ono być łatwo rozszerzane o dodatkowe parametry.
Co ciekawe, opisana metoda została
później zastosowana jeszcze wielokrotnie w zupełnie innych sytuacjach, jako
pomoc w wywoływaniu poleceń z pozioZ I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
my generowanych przez kod server-side
tabel, list czy nawet Repeater. Przykładowo, tabela z interaktywnym raportem,
generowana dynamicznie, zawierająca
odnośniki pozwalające na przejście na
kolejny zakres szczegółowości i pozbawiona ViewState. W normalnych warunkach, tabela ta zawierałaby odnośniki
typu LinkButton i przed obsługą zdarzenia dla odnośników należałoby ponownie wygenerować całą tabelę. Wykonanie
obsługi zdarzenia dla takich odnośników
jest warunkowane odtworzeniem tabeli, która te odnośniki zawiera. Bez tego,
obsługa zdarzenia nie zostanie wykonana. W przypadku, kiedy generowanie tabeli z raportem trwa kilka minut, jest to
rozwiązanie niedopuszczalne. Zastosowanie opisanego mechanizmu pozwala
się uwolnić od struktury, która generuje
impuls do postback. Wszelkie dane przekazane zostały po stronie klienta do stosownych kontrolek, które nie są zależne
od żadnej dodatkowej struktury. Umożliwia to proste i szybkie wykonanie obsługi
zdarzenia, bez konieczności generowania
struktur zawierających kontrolki wywołujące akcje.
Opisana metoda jest prostym sposobem
rozszerzenia funkcjonalności kontrolek
bez konieczności tworzenia ich nowych
wersji. Przedstawione TreeView może
uzyskać dzięki temu nowy wymiar funkcjonalności. Nie należy jednakże zapominać, iż jest to rozwiązanie nastawione na
prostotę. Zanim zdecydujesz się je zastosować, należy rozważyć, czy nie będzie
bardziej wskazane utworzyć nową kontrolkę, która zastąpi standardową.
Pozostaje jeszcze opcja zastosowania
AJAXa. W opisanym rozwiązaniu postback następuje po wykonaniu którejkolwiek z akcji pochodzących z przycisków
dodanych do drzewa. Oznacza to konieczność przeładowania całego drzewa. Zastosowanie AJAXa umożliwia pominiecie
konieczności przeładowywania całego
drzewa i pozwala na ograniczenie się tylko do wybranych jego fragmentów. Niemniej jest to zagadnienie, które wykracza
poza ramy niniejszego tekstu.
Ziemek Skowroński
Rysunek 7. Funkcja wywołująca postback dla przycisków dodanych do
drzewa
tEnabled);
function callCmd(name, arg, sender) {
ko parametr
document.getElementById(“ctl00 _ mc _ cmdName”).value = name;
//wywolujemy ponownie metode z nowo utworzonym wezlem ja-
document.getElementById(“ctl00 _ mc _ cmdArg”).value = arg;
document.getElementById(“ctl00 _ mc _ cmdSender”).value = sender;
}
_ _ doPostBack(“ctl00$mc$cmdBtn”,””);
}
}
}
LoadSubtree(newNode);
/// <summary>
/// Metoda budujca tekst wezla
Rysunek 8. Generowanie funkcji wywołującej postback
string scr = “function callCmd(name, arg, sender) {\n” +
“
document.getElementById(\”” + cmdName.ClientID + “\”).value
“
document.getElementById(\”” + cmdArg.ClientID + “\”).value =
“
document.getElementById(\”” + cmdSender.ClientID + “\”).value
“
_ _ doPostBack(\”” + cmdBtn.ClientID.Replace(“ _ ”, “$”) + “\
= name;\n” +
arg;\n” +
= sender;\n” +
”,\”\”);\n” +
“}\n”;
ClientScript.RegisterClientScriptBlock(this.GetType(), “cmd”, scr,
/// </summary>
/// <param name=”newNode”>Nowo utworzony wezel, przed dodaniem
do struktury</param>
/// <param name=”isEnabled”>Flaga okreslajaca status wezla</
param>
/// <param name=”isParentEnabled”>Flaga okreslajaca status wezla
nadrzednego</param>
/// <returns>Nowa postac tekstu wezla</returns>
private string BuildNodeText(TreeNode newNode, bool isEnabled,
bool isParentEnabled)
{
true);
string currentNodeText = newNode.Text;
StringBuilder nodeText = new StringBuilder();
//jesli wezel nie ma statusu Enabled (isEnabled = false) to
Rysunek 9. Zmodyfikowana metoda ładująca oraz metoda budująca tekst
dla węzła
tekst obejmowany jest
/// <summary>
stu na szary
/// Ladowanie listy wezlow dla podanego wezla nadrzednego
/// </summary>
/// <param name=”parentNode”>Wezel nadrzedny</param>
if (!isEnabled)
nodeText.AppendFormat(“<span class=\”nodeDisabled\”>{0}</
span>”, currentNodeText);
private void LoadSubtree(TreeNode parentNode)
{
//elementem span z odpowiednim stylem zmieniajacym kolor tek-
else
nodeText.Append(currentNodeText);
//dla wezla glownego przyjmujemy ParentEnabled = 1
string sql = “SELECT *, 1 AS ParentEnabled FROM OrgUnits WHERE
ParentID IS NULL”;
if (parentNode != null)
{
//kazdy wezel ma opcje edycji
nodeText.AppendFormat(“</a><input class=\”icon\” type=\”image\
” “ +
“src=\”../images/edit.gif\” “ +
“onClick=\”callCmd(‘edit’, ‘’, ‘{0}’); return false;\” “ +
int parentId = Convert.ToInt32(parentNode.Value);
“title=\”Edit\”>”, newNode.ValuePath);
sql = string.Format(“SELECT o.*, “ +
//tylko wezly posiadajace rodzica maja pozostale opcji
“(SELECT Enabled FROM OrgUnits p WHERE p.ID = o.ParentID) AS
if (newNode.Parent != null)
ParentEnabled “ +
{
“FROM OrgUnits o WHERE o.ParentID = {0}”, parentId);
}
string connStr = ConfigurationManager.ConnectionStrings[“MyCon-
nectionString”].ConnectionString;
//polecenie edycji
nodeText.AppendFormat(“</a><input class=\”icon\” type=\
”image\” “ +
“src=\”../images/add.gif\” “ +
using (SqlConnection conn = new SqlConnection(connStr))
{
“onClick=\”callCmd(‘add’, ‘’, ‘{0}’); return false;\” “ +
“title=\”Add\”>”, newNode.ValuePath);
SqlCommand cmd = new SqlCommand(sql, conn);
//polecenie dodania nowego elementu
conn.Open();
nodeText.AppendFormat(“<input class=\”icon\” type=\”image\
SqlDataReader rd = cmd.ExecuteReader();
” “ +
while (rd.Read())
{
“src=\”../images/move.gif\” “ +
“onClick=\”callCmd(‘cut’, ‘’, ‘{0}’); return false;\” “ +
string name = rd[“Name”].ToString();
“title=\”Move\”>”, newNode.ValuePath);
int id = Convert.ToInt32(rd[“ID”]);
if (isParentEnabled)
bool isEnabled = Convert.ToBoolean(rd[“Enabled”]);
bool isParentEnabled = Convert.ToBoolean(rd[“ParentEna-
bled”]);
//utworzenie nowego wezla
{
string toolTip = isEnabled ? “Disable” : “Enable”;
TreeNode newNode = new TreeNode(name, id.ToString());
//jesli ladujemy wezel glowny to dodajemy go bezposrednio
string comandArg = isEnabled ? “0” : “1”;
do tree,
“ +
drzednego parentNode
“ +
//jesli nie to do kolekcji wezlow przekazanego wezla naif (parentNode == null)
//ustawienie zmiennych pomocniczych i dodanie polecenia
zmiany statusu
nodeText.AppendFormat(“<input class=\”icon\” type=\”image\”
“src=\”../images/enable.gif\” “ +
“onClick=\”callCmd(‘enable’, ‘{0}’, ‘{1}’); return false;\”
}
“title=\”{2}\”>”, comandArg, newNode.ValuePath, toolTip);
tree.Nodes.Add(newNode);
}
parentNode.ChildNodes.Add(newNode);
nodeText.Append(“<a>”);
else
//dodanie dopelnienia tekstu renderowanego przez TreeView
//modyfikacja tekstu wezla musi nastapic po dodaniu wezla do
struktury
newNode.Text = BuildNodeText(newNode, isEnabled, isParen-
}
return nodeText.ToString();
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
5
ASP,NET
Rysunek 10. Szablon dla obsługi zdarzenia cmdBtn.Click()
//budowa tekstu wezla
/// <summary>
StringBuilder newNodeText = new StringBuilder();
/// Obsluga zdarzenia przycisku posredniczacego
newNodeText.Append(“</a>”);
/// </summary>
protected void cmdBtn _ Click(object sender, EventArgs e)
{
string senderPath = cmdSender.Value;
eNode.ValuePath
string commandName = cmdName.Value;
snika, pozostaje pusty
//dodanie pola Text Box, dodatkowo dodawana jest obsluga Enter
newNodeText.Append(“<input id=\”treeEdit\” “ +
//cmdSender zawiera Tre//nazwa polecenia
string commandArgument = cmdArg.Value; //argument polecenia
//lokalizujemy wezel, ktory wywolal akcje
“type=\”text\” value=\”{0}\” “ +
“onkeydown=\”if (event.keyCode == 13) {{ “ +
“callCmd(‘save’, document.getElementById(‘treeEdit’).value,
‘{1}’); “+
“event.returnValue = false; }}\”/>”);
TreeNode senderNode = tree.FindNode(senderPath);
//dodanie przyciskow
switch (cmdName.Value)
{
}
}
newNodeText.Append(“<input type=\”button\” “ +
“onClick=\”callCmd(‘save’, document.getElementById(‘treeEdi-
//obsluga poszczegolnych polecen
t’).value, ‘{1}’)\” “ +
“value=\”Save\”>”);
newNodeText.AppendFormat(“<input type=\”button\” “ +
Rysunek 11. Metoda dodająca nowy element do drzewa w trybie edycji
“onClick=\”callCmd(‘cancel’, ‘’, ‘{1}’)\” value=\”Cancel\”>”,
/// <summary>
newNode.Text, newNode.Parent.ValuePath);
/// Metoda dodajaca nowy wezel w trybie edycji
senderNode.Text = newNodeText.ToString();
/// <param name=”senderNode”>Wezel, ktory wygenerowal zdarzenie,
ClientScript.RegisterStartupScript(this.GetType(),
/// </summary>
do niego dodajemy nowy</param>
private void AddNewNode(TreeNode senderNode)
{
//utworzenie i dodanie nowego wezla
TreeNode newNode = new TreeNode(“New Item”, “-1”);
senderNode.ChildNodes.Add(newNode);
6
//zamkniecie generowanego odno-
//ustawienie focus na polu Text Box
“selectfocus”, “document.getElementById(‘treeEdit’).focus();”,
true);
senderNode.Expand();
}
return;
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
UMBRACO.
Wprowadzenie
i opis instalacji
Słowem wstępu
Dlaczego – umbraco, a nie na przykład
Community Server, DotNetNuke lub inny system CMS dla .NET? Dlatego, że to
prosty w obsłudze, a zarazem zaawansowany system CMS o potężnych możliwościach (zobacz ramka). Piszę tak z pełną
odpowiedzialnością, gdyż miałem okazję
pracować z takimi systemami CMS jak
EpiServer (.NET), NXSystem (php), Mambo(php), oraz wieloma innymi systemami z pogranicza systemów CMS. Oczywiście Community Server czy DotNetNuke
doskonale się sprawują w określonych
zastosowaniach i od razu „z pudełka” dostarczają wymaganej funkcjonalności,
problemy zaczynają się, gdy ta funkcjonalność nie jest dokładnie taka jak tego
wymagamy.
Jak można połączyć tak przeciwstawne się sobie cechy jak prostota obsługi
i zaawansowane możliwości w jednym
produkcie? Najlepiej odgradzając je od
siebie, czyli prostota potrzebna redaktorom jest w ich dziale, natomiast zaawansowane funkcje programistyczne,
programiści widzą w swojej części systemu. Oczywiście pomiędzy programistami a redaktorami nie może zabraknąć
administratorów, którzy za pomocą tego
samego interfejsu otrzymują funkcje administracyjne.
Historia
Prace nad umbraco rozpoczął Niel
Hartvig w 2001. Umbraco jest internetowym systemem zarządzania treścią, rozpowszechnianym na zasadach otwartego kodu. Zostało napisane przy użyciu
języka C# w oparciu o .NET 1.x. Ponieważ system jest rozpowszechniany w
oparciu o licencję GPL jest on dostępny
za darmo do zastosowań niekomercyjnych. W licencji jest dodany ustęp o tym,
że w przypadku użycia umbraco do tworzenia rozwiązań komercyjnych należy
przeznaczyć 3% kwoty uzyskanej na dotację dla umbraco. Firmy, które zechcą
„CMS, Content Management System (ang. dosłownie „system zarządzania treścią”) jest to jedna lub zestaw aplikacji internetowych pozwalających na łatwą aktualizację i rozbudowę serwisu WWW przez personel nietechniczny. Modyfikacja
i dodawanie nowych materiałów do serwisu odbywa się za pomocą prostych w obsłudze interfejsów użytkownika, zazwyczaj w postaci stron WWW zawierających
rozbudowane formularze.” http://pl.wikipedia.org/wiki/Content _ Management _ System
Definicja
użyć umbraco do własnych rozwiązań
mogą też zakupić wersję komercyjną
za około €800 dla jednego serwera, lub
wynegocjować cenę w przypadku większych instalacji. Aktualnie umbraco jest
rozwijane przez Duńską firmę Umbraco
ApS, która zarabia na konsultacjach oraz
wdrażaniu konkretnych rozwiązań CMS.
Funkcjonalność umbraco może zostać
w bardzo prosty sposób rozszerzona poprzez użycie:
·XSLT
·Kontrolek ASP.NET
·Kodu .NET
·Makr umbraco
Umbraco zawiera potężny zestaw API,
który może zostać wykorzystany w powyższych rozwiązaniach.
Z najważniejszych funkcji, które dodatkowo wyróżniają umbraco można wyliczyć:
·Zarządzanie treścią w oparciu o bibliotekę wersjonowanych dokumentów.
Każdy dokument otrzymuje wersję, a prace nad dokumentami są zapisywane w
dzienniku zdarzeń,
·WYSIWYG, jest już standardem w systemach CMS. Umbraco wyróżnia wsparcie dla automatycznego poprawiania kodu HTML kopiowanego z Worda, wsparcie dla standardów HTML i CSS, sprawdzanie kodu HTML przy użyciu TIDY
HTML,
·Własnych rodzajów treści. Można zdefiniować własne rodzaje treści, które będzie obsługiwał system np. treść dla WAP,
·Wsparcie dla szablonów z własnymi
znacznikami, przy czym nie ma ograniczeń, co do szablonów do tego stopnia, iż
każda strona może opierać się o inny szablon,
·Interfejs użytkownika, który można łatwo poddać lokalizacji, wszystkie komunikaty znajdują się w jednym pliku XML.
Przykładowa strona pokazująca możliwości umbraco to Duńska Federacja Golfowa www.dgu.org. Oto parę faktów dotyczących tego serwisu:
- ponad 60 000 stron z informacją (daje to ponad 1,5 mln właściwości dla stron),
- pełna personalizacja dla 150 000 użytkowników portalu,
- dzienniki internetowe (blogi), praca grupowa, fora dyskusyjne, zarządzanie projektami, newsletter oraz współdzielenie plików – wszystko to dostępne dla
użytkowników portalu,
- listy turniejów pełne szczegółowych informacji podzielonych na sezony,
- ponad 120 makr i…
to wszystko działa dzięki umbraco, dane przechowywane w jednej bazie MSSQL
oraz jeden (sic!) serwer WWW
Przykład udanego wdrożenia
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
W pierwszym artykule z serii o umbraco, zostanie przedstawiony sposób instalacji.
Instalacja
W celu zainstalowania umbraco należy wypełnić formularz rejestracyjny na stronie http://
umbraco.org/frontpage/download/
lateststableversion.aspx (zobacz
rysunek 1)
UWAGA: Należy podać prawdziwy adres email, gdyż na ten adres zostanie wysłana informacja skąd należy pobrać wersję instalacyjna umbraco
Wymagania
System umbraco wymaga następujących składników:
Jeżeli instalacja odbywa się na lokalnym komputerze to:
a.Należy zainstalować i skonfigurować serwer WWW taki jak IIS 5.1 lub
nowszy, lub Cassini Personal Web
Server
b.Komputer powinien posiadać zainstalowany .Net Framework (wersja
1.0 lub nowsza)
c.Serwer bazy danych MS SQL jest kolejnym wymaganym narzędziem.
Zainstalować trzeba darmowe MSDE
2000, MSDE Express 2005 lub inne
wersje płatne MS SQL.
d.System zarządzania bazą danych
(może być darmowy SQL Server
Management Studio Express) w celu stworzenia nowej bazy danych.
Zaawansowani użytkownicy SQL
Servera mogą się posłużyć linią komend.
e.Ściągnięte pliki umbraco CMS (najnowsze)
Gdy instalacja systemu CMS odbywa się u usługodawcy internetowego
to dla przykładu w Amm Komputer
lub ASPNET.pl możemy całkowicie
za darmo i bez dodatkowych wydatków zainstalować ściągnięte pliki
umbraco.
Gdy interfejs użytkownika umbraco w
wersji angielskiej jest niewystarczający, można ściągnąć plik xml z polską
wersją językową
(http://
kierepka.top100.org.pl/
systemy-cms/instalacja-polskiego-tłumaczenia.aspx)
1
2
3
7
ASP,NET
Rysunek 1. Formularz rejestracyjny
pozwalający na otrzymanie plików
instalacyjnych dla umbraco
Rysunek 2. Zrzut ekranu dla konfiguracji
serwera Cassini Personal Web Server v2.0
Rysunek 7. Poprawnie wpisano
dane do pliku web.config
Rysunek 8. Informacja o
rozpoczęciu procesu tworzenia
właściwych wpisów w bazie
danych
Rysunek 6. Błedne wpisy w
pliku web.config
Rysunek 4. Nowa baza danych przeznaczona dla
umbraco
Rysunek 10. Ustawienie
nowego hasła
Rysunek 9. Informacja
o wyniku sprawdzania
możliwości instalacji
Instalacja systemu w serwerze
WWW
Rozpakowanie plików
Pierwszą czynnością jest rozpakowanie ściągniętych plików zip do wybranego
katalogu i rozpoczęcie konfiguracji serwera WWW.
Konfiguracja serwera WWW
W przypadku Cassini PWS wpisujemy
tylko katalog w którym znajdują się rozpakowane pliki umbraco. Podobnie też
postępujemy w przypadku serwera Internetowych usług informacyjnych (IIS) jak
pokazano na rysunku 3.
Konfiguracja bazy danych
Po tym jak zostanie skonfigurowany
poprawnie serwer WWW należy zainstalować pustą bazę danych, która posłuży
umbraco do przechowywania wszystkich
informacji.
Zmiany w pliku Web.config
Po stworzeniu bazy danych należy
8
Rysunek 5. Ekran powitalny
instalatora umbraco
Rysunek 3. Tworzenie nowej witryny w IIS
dla umbraco
Rysunek 11. Zakończona poprawnie instalacja
uzupełnić wpisy w pliku konfiguracyjnym Web.config, który znajduje się w
głównym katalogu z rozpakowanymi plikami umbraco.
Proces instalacji rozpoczyna się od poprawienia wpisu informującego o kodowaniu strony WWW:
p100.net.pl,
·Database – nazwę utworzonej bazy danych
·ID – nazwę użytkownika
·Password – hasło
Oto domyślny wpis, który należy zaktualizować
<globalization requestEncoding=”UTF-8”
<add key=”umbracoDbDSN” value=”Server=127
Na wpis, który dodatkowo pozwoli umbraco na jasne określenie jakiej użyć wersji językowej
OUR _ USER;Password=YOUR _ PASSWORD;Trusted _
responseEncoding=”UTF-8” />
<globalization requestEncoding=”UTF-8”
responseEncoding=”UTF-8” culture=”pl-PL”
uiCulture=”pl-PL”/>
Gdy istnieje potrzeba ustawienia języka polskiego jako języka głównego dla całej witryny należy wpis:
<add key=”umbracoDefaultUILanguage”
value=”en”/>
zmienić na:
<add key=”umbracoDefaultUILanguage”
value=”pl”/>
Następnie konieczna jest zmiana domyślnego wpisu informującego o połączeniu z bazą danych na właściwy. Czyli w
miejscu:
·Server podajemy adres naszego komputera lub w przypadku instalacji w
AMM-Komputer podajemy mssql2005.toZ I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
.0.0.1;Database=YOUR _ DATABASE;User ID =YConnection=False”/>
Oczywiście osoby bardziej zorientowane mogą zmienić ten wpis na dowolny
inny poprawny ciąg połączenia z bazą danych (np. z uwierzytelnianiem Windows
a nie SQL).
Instalacja za pomocą
pomocnika umbraco
Pomocnika umbraco uruchamia
się poprzez przez wpisanie w przeglądarce adresu url skonfigurowanego serwera WWW z aplikacją (w moim przypadku jest to adres http://
localhost/umbraco dla IIS lub http:
//localhost:801 dla Cassini PWS).
Umbraco rozpozna, że baza danych nie
została jeszcze skonfigurowana i automatycznie przeniesie nas do strony z po-
Rysunek 12. Okno logowania do panelu administracyjnego
umbraco
Rysunek 15. Okno panelu administracyjnego po poprawnym
skonfigurowaniu użytkownika
Rysunek 13. Panel administracyjny
Rysunek 14. Administracja użytkownikami
Rysunek 17. Instalacja pakietu
Rysunek 16. Panel programisty. Instalacja makra.
mocnika instalacji, tak jak pokazano na
rysunku 5.
Przechodzimy do następnego kroku
instalacji poprzez naciśnięcie przycisku
Next. Gdyby przez przypadek podano
błędne dane w pliku konfiguracyjnym
Web.config zostanie pokazany komunikat jak na rysunku 6. Należy wtedy poprawić wpisy połączenia z bazą danych i
ponowić przejście o następnego kroku poprzez naciśnięcie przycisku Retry.
Gdy informacje w pliku Web.config będą poprawne otrzymamy komunikat jak
na rysunku 7.
Naciśnięcie przycisku Install spowoduje rozpoczęcie procesu tworzenia struktury bazy danych oraz przejście do ekranu jak na rysunku 8.
Następnym krokiem jest sprawdzenie
poziomu uprawnień umbraco do poszcze-
gólnych plików i katalogów. Po wciśnięciu przycisku Next zostanie pokazany
wynik sprawdzania tak jak pokazano na
rysunku 9.
W następnym kroku zostanie założony główny użytkownik administrujący
serwisem. Naszym zadaniem jest podanie
nowego hasła dla domyślnego użytkownika. Proces ten pokazano na rysunku 10.
Po zmianie hasła zostanie wyświetlony
komunikat o tym czy udało się poprawnie zmienić hasło dla administratora.
Po naciśnięciu przycisku Next zostanie
przedstawiony ekran końcowy jak na rysunku 11.
Instalacja zostanie zakończona poprawnie po zmianie w pliku Web.config
wartości (value) dla wpisu umbracoConfigurationDone na warość 211 tak jak pokazano poniżej:
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
<add key=”umbracoConfigurationDone”
value=”211”/>
Ostatnim etapem jest usuniecie katalogu Install z głównego katalogu w którym został zainstalowany umbraco. Jest
to bardzo ważny etap, gdyż uniemożliwi
to postronnym osobom ponowną instalację umbraco, przez co moglibyśmy stracić
wszystkie ustawienia i zmiany w aktualnym serwisie.
Naciśnięcie przycisku Next w tym etapie, spowoduje przeniesienie do strony
umieszczonej na serwerze www.umbraco.org, informującej o tym jak należy
zacząć pracę z umbraco. Znajduje się tam
odnośnik do pakietów instalacyjnych z
dodatkami do umbraco oraz 72 stronicowa książka o umbraco. Warto z tej strony
ściągnąć plik http://umbraco.org/
assets/websiteWizard.umb i zapisać
9
ASP,NET
Rysunek 18. Zakończony import pakietu, który stworzył nową serwis z pełną strukturą
na dysku. Plik ten jest prostym instalatorem przykładowej strony. Na końcu artykułu znajduje się krótki opis jak uruchomić ten instalator.
Pierwsze kroki z umbraco
Po zainstalowaniu główna witryna jest
pusta, gdyż nie zawiera żadnych informacji. Należy, więc wprowadzić właściwe informacje poprzez przejście do części administracyjno/edycyjnej umbraco. Część edycyjna umbraco domyślnie
znajduje się w katalogu umbraco i można
się do niej dostać przez wpisanie http:
//localhost/umbraco/umbraco tak
jak w moim przypadku dla serwera IIS
lub http://localhost:801/umbraco
dla Cassini. Po prostu dodajemy na końcu adresu url do naszego serwera wpis:
/umbraco.
Po uruchomieniu tego adresu pokaże
się okno jak na rysunku 12.
Domyślna nazwa administratora to
oczywiście „umbraco” a hasło jest tym
które wprowadzaliśmy powyżej podczas
procesu instalacji.
Po poprawnym zalogowaniu się, zostanie pokazane okno panelu administracyjnego, które można zobaczyć na rysunku 13.
Jak widać wszystkie informacje są wyświetlane w języku angielskim. Wynika
to z faktu iż założony domyślny użytkownik administracyjny ma ustawiony język
jako język angielski. Jeżeli chcemy zmienić ten język to w dolnym prawym rogu
należy wybrać ikonę Users, która uruchomi część panelu administracyjnego po-
10
zwalającego na administrację użytkownikami pokazaną na rysunku 14.
Pierwszy zakładany użytkownik w
procesie instalacyjnym ma nazywa się
umbraco_system pokazany jest z lewej
strony w drzewku Users. Należy wybrać
użytkownika umbraco_system i w pustym do tej pory miejscu pokażą się informacje o użytkowniku. Ze względów
bezpieczeństwa należy zmienić nazwę
użytkownika Username oraz jego Login i
hasło - Password. Należy nie zapomnieć o
zmianie języka na polski poprzez zaznaczenie Polish(pl) w polu Language.
Po tych zmianach wylogowujemy się
naciskając przycisk Logout: umbraco_
system widoczny w prawym górnym rogu. Po wylogowaniu się pokaże się standardowe okno logowania jak pokazano
na rysunku 12. Wpisujemy tym razem aktualnie zmienioną nazwę konta (tą z pola
Login) oraz nowe hasło. Jeżeli wszystko
poszło poprawnie zostanie pokazany ten
sam panel administracyjny co poprzednio z tą różnicą, że komunikaty będą już
przedstawiane w języku polskim jak pokazano na rysunku 15.
Na tym etapie chciałbym zakończyć
prezentację. Ponieważ instalacja nie dodaje przykładowych stron i nie ma „czym
się bawić” polecam dwa dodatkowe odnośniki (w języku angielskim) opisujące jak
zrobić to samemu:
1)Instalacja umbraco w windows 2003
– screen cast: http://kasperb.dk/
video/umbraco-install-on-windows-2003.html
2)http://en.wikibooks.org/wiki/
Umbraco/ - Wiki poświęcone umbraco
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
Możemy też posłużyć się instalatorem
przykładowych stron jak opisano poniżej.
Uruchomienie instalatora
przykładowych stron
Jeżeli został ściągnięty plik websiteWizard.umb o którym wspominałem wcześniej to
należy w dolnym lewym rogu panelu
administracyjnego wybrać ikonę Developers (programiści). Zostanie wyświetlone
okno jak na rysunku 16. Ponieważ instalator jest makrem więc uruchamiamy go
poprzez instalację makra.
Instalację makra uruchamiamy naciskając prawym przyciskiem na polu
Makra (rysunek 16). Zostanie pokazane
menu podręczne. W menu podręcznym
wskazujemy Importuj pakiet, zostanie
pokazane okno jak na rysunku 17.
W oknie z rysunku 17 należy wskazać
plik poprzez wybranie Browse, a następnie nacisnąć przycisk Load Package, który wczyta a następnie uruchomi makro.
Tak uruchomione makro pokaże stronę
z informacją o licencji, którą należy po
przeczytaniu zaakceptować. Po akceptacji zostanie pokazane okno konfiguracji,
w którym wybieramy ustawienia wyglądu witryny. Zakończony etap konfiguracji potwierdzamy przyciskiem Finish.
Po zakończeniu w zakładce Content/
Treści pokazanej na rysunku 18 można
zauważyć nową strukturę serwisu. Po
przejściu do głównej witryny (lub po wylogowaniu się z serwisu) zobaczymy też
przykładową stronę.
Mateusz Kierepka
Gra w kości a usługi kompilatora
D
ążenie do tworzenia coraz bardziej
czytelnego kodu powoduje, że wymyślamy coraz to nowe rozwiązania różnych
problemów. Już kilkukrotnie zdarzyło mi
się implementować mechanizm rzutów
kośćmi do gry. Podchodząc do tworzenia nowej gry stanąłem przed podobnym
problemem - potrzebowałem rozwiązania, które będzie efektywne pod względem
szybkości działania oraz proste i wszechstronne w użyciu.
W grze opartej na systemie RPG d20 postać jest określona za pomocą 6 podstawowych współczynników: Strength, Intelligence, Wisdom, Dexterity, Constitution i Charisma. Formuły definiujące wartość każdego
współczynnika są określane za pomocą wyrażeń typu „d4” czy „3d6 + 3” gdzie wyrażenie „d4” oznacza rzut jedną kością czerościenną (tak, istnieją takie kostki do gry), a
„3d6 + 3” oznacza rzut trzema kośćmi sześciościennymi i dodanie do wyniku 3.
Kod pisanego programu powinien być
czytelny i przejrzysty dla osoby go czytającej. Ponieważ powyższe formuły są
czytelne, jasne i zwarte dla graczy RPG,
więc postanowiłem że taki sposób definiowania rzutów kośćmi będzie najwygodniejszy.
Proponuję rozwiązanie tego problemu,
które aktualnie mnie najbardziej satysfakcjonuje. Wyrażenie opisujące rzut kośćmi
zapisane zostanie w postaci łańcucha tekstowego, całość zostanie skompilowana aby
osiągnąć większą wydajność przy wielokrotnym generowaniu wartości z tego samego wyrażenia. Zysk będzie z tego taki, że
do optymalizacji wyrażeń użyjemy optymalizatora z kompilatora języka C#.
Krok 1. Pierwsza przymiarka
Najprostszym sposobem na zaimplementowanie generatora współczynników
postaci jest część kodu przedstawiony na
Rysunku 1.
Zaletą tego rozwiązania jest prostota. Dużą wadą jest mało czytelny zapis
samego wyrażenia rzutu kośćmi oraz
utrudnione przetważanie wyrażeń zapisanych w pliku tekstowym. Definicje
obiektów, potworów, ras, klas postaci zapisane są często w zewnętrznych plikach
i używają standardowej, rpg-owej notacji.
Bez specjalnego interpretatora takich wyrażeń wczytywanie tych danych jest mocno utrudnione.
Chciałbym, aby rozwiązanie problemu
generowania wyników rzutu kośćmi pozwalało na użycie w sposób przedstawiony na Rysunku 2.
Słowo wyjaśnienia do przypadku d3.
Czasami zdarza się, że określenie jakiejś
wartości (na przykład ilości zadawanych
obrażeń) zależy od jakiegoś czynnika,
takiego jak poziom postaci lub odległość.
Wygodne było by więc zawarcie takiego zmiennego czynnika w formule i przy
generowaniu rzutu kością podanie jego
wartości. Jeśli w wyrażeniu pojawi się jakaś nazwa otoczona nawiasami klamrowyni, to będzie określała nazwany parametr wyrażenia.
O ile prostsze jest wyrażenie “(3d6 + d4
/ 2) / d4 + 6” od kodu z Rysunku 3.
Jak to wykonac? Tworzenie
uproszczonego parsera do takich
wyrażeń to używanie armaty do
wystrzelenia na wiwat. Parsowanie
na piechotę może stać się zbyt
skomplikowane. Ponieważ wyrażenie
rzutu kością przypomina z grubsza
kawałek kodu języka C# oraz stosują się
do niego te same zasady interpretacji
(priorytet operatorów, itp), skorzystam
więc z kompilatora tego języka,
przerabiając wcześniej wyrażenie do
postaci akceptowalnej przez kompilator.
Pierwszym zadaniem jest zdefiniowanie interfejsu (kontraktu) klas typu Dice.
Potrzebujemy bezparametrowej metody Next oraz metody Next przyjmującej
zbiór wartości parametrów nazwanych
wyrażenia (nazw ujętych nawiasami
klamrowymi). Niestety C# to nie jest język typu VB gdzie można wybrać sobie
jakie argumenty przekażemy do metody,
a jakie nie. Więc decyduję się na zdefiniowanie metody przyjmującej zmienną liczbę parametrów i przy użyciu trzeba będzie pamiętać o kolejności argumentów.
Alternatywnym sposobem jest dodanie
indexiera do klasy, ktorego argumentem
była by nazwa zmiennej. Zmienne były
by ustawiane przed wywołaniem metody
Next. Niestety ten sposób nie byłby thread safe, ponieważ wartości ustawione w
jednym wątku mogły by być zamazane
przez inny wątek.
public interface IDice
{
string Expression { get; }
int Next();
}
int Next(params int[] args);
Podsumowując ten krok, mamy już jasno zdefiniowany cel oraz sposób jego
realizacji.
Krok 2.
Tworzenie szablonu klasy
Czas na sprawdzenie jak zadziała w
praktyce to rozwiązanie. Tworzę klasę
implementującą jakieś średnio skomplikowane wyrażenie, na przykład „3d6 +
{level}” (Rysunek 4).
Metoda InternalNext jest właściwą implementacją generatora. To tutaj trzeba
będzie wstawić cały kod przetworzonego
wyrażenia. Metoda NextDice z abstrakcyjnej klasy bazowej losuje tyle razy liczbę
Rysunek 1. Implementacja generatora współczynników postaci
Rysunek 2. Przykładowe użycie generatora
static Random rand = new Random();
int d1 = new Dice(“d4”);
{
for (int i = 0; i < 1000; i++)
static void Main()
int STR, INT, WIS, DEX, CON, CHA;
STR = NextDice(3, 6);
// 3d6
INT = NextDice(3, 6) - 2; // 3d6-2
WIS = NextDice(2, 6) + 3; // 2d6+3
DEX = NextDice(10);
CON = NextDice(3, 4);
// d10
// 3d4
CHA = NextDice(4, 4) + 1; // 4d4+1
}
static int NextDice(int sides)
v = d1.Next();
{ v += d1.Next(); }
int d2 = new Dice(“(3d6 + d4 / 2) / d4 + 6”);
v = d2.Next();
int d3 = new Dice(“100 + 2d100 + {level}d10 + {level} + 1”);
v = d3.Next();
Rysunek 3. Tego chcemy uniknąć
{ return rand.Next(sides) + 1; }
Random rand = new Random();
{
(rand.Next(6)+rand.Next(6)+rand.Next(6)+3)
static int NextDice(int count, int sides)
int result = 0;
for (int i = 0; i < count; i++)
{ result += rand.Next(sides) + 1; }
return result;
}
int d2a = (
+ (rand.Next(4)+1) / 2)
) / (rand.Next(4)+1)
+ 6;
// d4
// 3d6
// d4 / 2
int d2b = new Dice(“(3d6 + d4 / 2) / d4 + 6”).Next();
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
11
WINFORMS
Rysunek 4. Klasa DiceBase
Rysunek 5. Klasa Dice
public abstract class DiceBase: IDice
public sealed class Dice: IDice
{
protected static readonly Random rand = new Random();
{
protected DiceBase() { }
public Dice(string expression)
protected static int NextDice(int count, int sides)
{
{
int result = 0;
for (int i = 0; i < count; i++)
}
return result;
public int Next() { return _ impl.Next(); }
public abstract int Next();
}
public int Next(params int[] args) { return _ impl.Next(args); }
Rysunek 6. Tester klas
public abstract int Next(params int[] args);
class Program
{
/* expression 3d6 + {level} */
internal sealed class TestDice: DiceBase
{
static void Main(string[] args)
{
public TestDice() { }
new int[] { 44 }
vel}”;} }
);
public override int Next()
{ return InternalNext(0); }
}
{
{
public override int Next(params int[] args)
}
}
}
Z przykładowej implementacji można
już stworzyć szablon klasy. Zastępujemy
te elementy kodu, które zależą od wyra-
12
[ ] = {0}”, d.Next());
Console.WriteLine(„
[1] = {0}”, d.Next(args1));
Console.WriteLine(„
Console.WriteLine(„
Console.WriteLine(„
return NextDice(3, 6) + level;
Krok 3.
Tworzenie kompilatora
wyrażeń
Console.WriteLine(„
Console.WriteLine(„
else { throw new ArgumentException(„{level}”); }
odpowiadającą jednej kostce (dice) ile to
jest wymagane (count). Tylko wyrażenia
o stałej liczbie kości i ilości ścianek, mogą
zostać rozpisane jako suma wyrażeń. Ze
względu na to, że oba wspołczynniki mogą
być zastąpione przez parametr nazwany,
prostsze jest użycie metody pomocniczej.
Teraz kolej na stworzenie klasy Dice,
która zajmie się opakowywaniem całego
procesu przetworzenia i kompilacji wyrażenia (Rysunek 5).
Zakładam że ciało konstruktora zostanie zastapione wywołaniem procesu
kompilacji wyrażenia do obiektu go implementującego. Teraz odrobina kodu testujacego aplikację. Klasa wyrażenia wywoływana jest najpierw bez parametrów,
potem z pierwszą wartością parametru
a na końcu z drugą wartością parametru
(Rysunek 6).
Cały projekt na tym etapie znajduje sie
w pliku Ww.Texts.Dices.1.zip.
IDice d = new Dice(exp);
Console.WriteLine(„Dice: ‚{0}‘”, d.Expression);
return InternalNext(args[0]);
private int InternalNext(int level)
{
Console.ReadKey();
static void TestDice(string exp, int[] args1, int[] args2)
if (args != null && args.Length == 1)
}
TestDice(„3d6 + {level}”,
new int[] { 4 },
public override string Expression { get {return „3d6 + {le-
{
else { throw new NotImplementedException(); }
public string Expression { get { return _ impl.Expression; } }
public abstract string Expression { get; }
}
if (expression == „3d6 + {level}”)
{ _ impl = new TestDice(); }
{ result += rand.Next(sides) + 1; }
}
private IDice _ impl;
}
}
Console.WriteLine();
żenia wejściowego, stałym napisem. Cały
plik szablonu umieszczam w projekcie jako Embedded Resource pod nazwą DiceCompilerTemplate.cs (Rysunek 7).
Otwierając ten plik w Microsoft Visual C# 2005 Express Edition można zauważyć że VS pokazuje błędy składni.
Związane jest to z błędem w Visual Studio, który igoruje ustawienia właściwości „Build Action” i intepretuje wszystkie pliki o rozszerzeniu .cs jako pliki z
kodem. Te błędy można zignorować lub
użyć na przykład Notatnika z systemu
Windows.
Teraz trzeba stworzyć automat który
będzie generował kod klas podobnych
do TestDice. Potrzebnymi elementami do
wytworzenia kodu źródłowego klasy jest
lista użytych parametrów nazwanych
oraz wynik zamiany wyrażeń pojedyńczych kości na wywołania metody NextDice.
Najpierw wyszukiwane są wyrażenia
określające rzut kością. Służy do tego
wyrażenie regularne regexDices. Podczas wykonywania operacji replace na
wyrażeniu używana jest metoda ReplaceDices, której zadaniem jest zapamiętanie nazw parametrów oraz skonstruowanie wywołania metody NextDice. W
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
[ ] = {0}”, d.Next());
[1] = {0}”, d.Next(args1));
[2] = {0}”, d.Next(args2));
[2] = {0}”, d.Next(args2));
kolejnym kroku, za pomocą operacji replace na wyniku z poprzedniego etapu,
wyszukiwane są wystąpienia pozostałych nazwanych parametrów. Z obu etapów uzyskujemy listę wszystkich nazw
parametrów, która jest przechowywana
w polu klasy o nazwie _variables (Rysunek 8).
Po powyższych operacjach mamy gotowy kod metody InternalNext. Wartości
placeholderów DefArgs, Args, ExcArgs i
ArgDecls są uzyskiwane z kolekcji nazw
parametrów. Tak uzyskany kod klasy jest
kompilowany za pomoca metody CompileCode (Rysunek 9).
Tworzymy listę parametrów kompilatora za pomocą klasy CompilerParameters.
Ustawiamy tryb debugowania w zależności od stałej DebugMode. Dodajemy referencje to wszystkich modułów (assembly)
używanych przez kod klasy, włącznie z
assembly w której zdefiniowaliśmy nasze
interfejsy i klasę bazową. Kompilujemy
kod i zwracamy uzyskane w ten sposób
assembly.
Jeśli ustawimy w parametrach kompilatora TempFiles.KeepFiles na wartość
true, wtedy tymczasowe pliki użyte przez
kompilator nie zostaną usunięte. Jeśli
zajrzymy do katalogu %TEMP% (tak, wy-
starczy wpisac w Explorator Windows
„%TEMP%”) możemy zobaczyć pliki o losowej nazwie i rozszerzeniach takich jak:
.cs, .cmdline, .err, .out, .pdb, .tmp.
Ze skompilowanego assembly wyciagamy typ używając zapamiętanej nazwy
namespace i tworzymy instancję nowego
typu za pomocą klasy Activator. Uzycie
Activator jest prostsze od wyszukania
bezparametrowego konstruktora i jego
wywolania. Zmieniamy jeszcze w konstruktorze klasy Dice poprzedni kod zastępując go wywołaniem kompilatora.
Cały projekt na tym etapie znajduje sie
w pliku Ww.Texts.Dices.1.zip.
Podsumowanie
Ten prosty projekt można dalej rozbudowywać tworząc dodatkowe udoskonalenia ułatwiające prace programisty, czy
dając większe możliwości dla użytkownika programu.
Aby zwiększyć wydajność, można dodać klasę zarządzającą wszystkimi
obiektami kości i zapamiętywać typy
1
danych uzyskane z kompilacji za wypadek powtórnego tworzenia kości o
tym samym wyrażeniu.
W celu ograniczenia liczby kompilacji kodu, można zebrać kilka wyrażeń,
wygenerować dla nich kod i skompilować do jednego assembly.
Za pomocą Custom Tool z VisualStudio.NET można generować kod takich
klas i dołączać do projektu uzyskując
w ten sposób możliwość użycia IntalliSense do tak wygenerowanych klas.
Podobny mechanizm można użyć do
uzyskania większej wydajności niż
reflection. Zamiast w czasie dzialania
programu wyszukiwać właściwości i
ustawiać im wartość, można wygenerować klasę, która użyje bezpośrednio
właściwości. W podobny sposób działa
XmlSerializer z .NET Framework.
Wykorzystując funkcjonalność kompilatora należy uważać między innymi na:
Wzrost pamięci używanej przez aplikację, ponieważ każda skompilowana
biblioteka ładowana jest do AppDomain,
2
3
4
1
Rysunek 7. Szablon klasy
2)); }
// obsluga zmiennej jako liczby scian kosci
public override int Next()
if (sSides[0] == ’{’)
{ sSides = AddVariable(sSides.Substring(1, sSides.Length -
return InternalNext(#DefArgs#); }
2)); }
{
}
public override int Next(params int[] args)
if (args != null && args.Length == #ArgsCount#)
}
return InternalNext(#Args#); }
else { throw new ArgumentException(„#ExcArgs#”); }
private static Assembly CompileCode(string code)
{
CSharpCodeProvider csc = new CSharpCodeProvider();
CompilerParameters cscParams = new CompilerParameters();
cscParams.GenerateInMemory = true;
cscParams.ReferencedAssemblies.Add(typeof(DiceCompiler)
return #ParsedExp#; }
.Assembly.Location);
bool useDebug = DebugMode;
cscParams.IncludeDebugInformation = useDebug;
Rysunek 8. Prosty automat
cscParams.TempFiles.KeepFiles = useDebug;
private static readonly Regex regexDices = new Regex(
CompilerResults cscResults =
@”(?<d>
csc.CompileAssemblyFromSource(cscParams, code);
(?<c> ( \d+ ) | ( { [^}]+ } ) ) ?
if (cscResults.Errors.Count == 0)
d
{ return cscResults.CompiledAssembly; }
(?<s> ( \d+ ) | ( { [^}]+ } ) )
else
)”,
);
return String.Concat(„NextDice(„, sCount, „, „, sSides, „)”);
Rysunek 9. Metoda CompileCode
private int InternalNext(#ArgDecls#)
}
{
DefaultOptions
private static readonly Regex regexVariables = new Regex(
Environment.NewLine);
DefaultOptions
foreach (CompilerError error in cscResults.Errors)
{
private string ReplaceVariables(Match match) {
}
return AddVariable(match.Groups[„n”].Value);
}
private string ReplaceDices(Match match) {
string sCount = match.Groups[„c”].Value;
string sSides = match.Groups[„s”].Value;
StringBuilder buffer = new StringBuilder();
buffer.AppendFormat(„Invalid expression:{0}”,
@”(?<v> { (?<n> [^ } ]+ ) } )”,
);
3
if (sCount[0] == ’{’)
public override string Expression { get { return „#Exp#”; } }
{
2
{ sCount = AddVariable(sCount.Substring(1, sCount.Length -
public DiceImpl() { }
}
1
// obsluga zmiennej jako liczby kosci
public sealed class DiceImpl : DiceBase
{
3
{ sCount = „1”; }
namespace #Ns# {
{
my z ilością kodu kompilowanego w
locie, to cała aplikacja będzie działać
wolniej,
Stopień komplikacji generowanego kodu, ponieważ rozwiązanie w tej postaci
nie ma możliwosci użycia debuggera.
W podobny sposób działają niektóre komponenty .NET Framework, takie jak:
XmlSerializer – generuje kod klasy
która zapisuje i wczytuje dane z dokumenty XML do obiektów .NET,
Pliki .aspx z ASP.NET – treść pliku jest
przekształcana do klasy, która renderuje wygląd strony na podstawie definicji kontrolek i tekstu stałego,
Funkcje XSLT są kompilowane dynamicznie do assemblies.
Zaprezentowałem w jaki sposób można
użyć kompilatora do wykonania wyrażeń
wprowadzanych przez użytkownika w
czasie dzialania programu. Osiągnięto w
ten sposób większą wydajność oraz walidację wprowadzanych wyrażeń.
Wojtek Gębczyk
if (sCount == „”)
using System;
{
końcową aplikacji. Kompi2Wydajność
lacja trochę trwa, więc jeśli przesadzi-
}
}
buffer.Append(error.ToString());
buffer.Append(Environment.NewLine);
throw new Exception(buffer.ToString());
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
13
WINFORMS
Obsługa kontrolki NotifyIcon w Windows Forms
na przykładzie aplikacji wyłączającej monitor
T
ekst przedstawia, krok po kroku, proces tworzenia prostego programu narzędziowego służącego do programowego wyłączania monitora. Choć być może
mało użyteczna na komputerze stacjonarnym, aplikacja ta jest bardzo wygodna
w użytkowaniu na komputerach przenośnych – pamiętajmy, że monitor jest najbardziej „energożerną” częścią laptopa
i wyłączenie go, gdy nie jest używany,
potrafi znacznie zwiększyć czas pracy
na bateriach całego zestawu. Do obsługi
programu wykorzystana zostanie często
niezauważana i niedoceniana kontrolka NotifyIcon. Dzięki temu będzie można
wyłączyć monitor praktycznie jednym
kliknięciem myszki, nie czekając aż system sam go wyłączy po pewnym okresie
bezczynności.
Omawiana przykładowa aplikacja Windows Forms została stworzona za pomocą
Visual Studio .NET 2005 i z tego też edytora pochodzą wszystkie ilustracje użyte
w niniejszym tekście.
Podejście pierwsze
– prosta aplikacja
Jak wyłączyć monitor? Głupie pytanie,
odpowie czytelnik. Trzeba nacisnąć przycisk i monitor wyłączony. Proste, prawda?
Ale co zrobić, jeśli producent nie wyposażył monitora w taki przycisk? W monitorach laptopów przykładowo się takich
elementów raczej nie montuje, a jeśli już
to jest to funkcja dostępna bądź przez
kombinację klawiszy bądź aktywowana
poprzez „złożenie” monitora laptopa.…
Z pomocą przychodzi tutaj Win32 API.
Rysunek 2. Konfiguracja Menu
Otóż jeśli komputer potrafi zarządzać
pobieraną energią (a potrafi to dziś w zasadzie każdy komputer PC obsługujący
standard ACPI), możliwe jest programowe
wyłączenie monitora, za pomocą funkcji
SendMessage(). W ten sposób możemy
wysyłać komunikaty do systemu operacyjnego, a w tym konkretnym przypadku
komunikat WM _ SYSCOMMAND z parametrem SC _ MONITORPOWER.
W dokumentacji do polecenia SC _
MONITORPOWER czytamy:
Zarządza stanem monitora. Komenda
dostępna jest na komputerach potrafiących zarządzać energią, jak na przykład
komputery zasilane bateriami.
Parametr lParam może przyjmować
następujące wartości:
monitor przechodzi na niski pobór
energii
monitor zostaje wyłączony.
Posiadając takie informacje, napisanie odpowiedniego programu staje się
banalne:
1
2
#include <windows.h>
int main()
Rysunek 1.
P/Invoke do wygaszenia monitora.
{
using System;
MAND, SC _ MONITORPOWER, 2);
using System.Runtime.InteropServices;
}
// atrybut DllImport
class OFF
{
[DllImport(“user32.dll”)]
static extern int SendMessage(
int hWnd,
uint Msg,
ushort wParam,
uint lParam);
Do „przetłumaczenia” tego programu
na C# można wykorzystać mechanizm
.NET P/Invoke, pozwalający na wywołanie praktycznie dowolnej funkcji napisanej w kodzie niezarządzanym, o ile tylko
znana jest jej deklaracja. A deklarację
SendMessage() można znaleźć ją można w pliku winuser.h lub dokumentacji MSDN:
HWND hWnd,
static void Main(string[] args)
SendMessage(
0xFFFF,
0x0112,
0xF170,
}
14
}
2);
return 0;
LRESULT SendMessage(
[STAThread]
{
SendMessage(HWND _ BROADCAST, WM _ SYSCOM-
// HWND _ BROADCAST
// WM _ SYSCOMMAND
// SC _ MONITORPOWER
UINT Msg,
WPARAM wParam,
);
LPARAM lParam
Dodatkowo w dokumentacji do funkcji
znajduje się informacja, że jest ona zdefiniowana w pliku user32.dll. Dysponując tymi informacjami jesteśmy w stanie
przepisać nasz prosty program z C do C#
(Rysunek 1).
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
Parametry użyte w funkcji SendMessage można by (a w zasadzie nawet należało by) zdefiniować wcześniej jako
stałe i te użyć w kodzie. Jest to rozwiązanie zalecane, jeśli planujemy użwwanie większej ilości funkcji używających stałych zdefiniowanych w WinAPI.
Ponieważ tutaj poprzestajemy tylko na
pojedynczym wywołaniu SendMessage(), można było poprzestać na odpowiednio skomentowanych wartościach
numerycznych.
Podejście drugie
– aplikacja okienkowa
Podstawową wadą powyższej aplikacji jest to, że kończy się ona tuż po wygaszeniu monitora. Poza tym wygodne
byłoby mieć program, który raz uruchomiony działałby w tle i umożliwiał wyłączenie monitora jednym kliknięciem
myszki. Właściwym „kontenerem” dla
takiego programu wydaje się zasobnik
systemowy (System Tray) – standardowa aplikacja okienkowa zajmowała by
za dużo miejsca w pasku zadań. Do tego
celu wykorzystamy, obecną zresztą już
w poprzednich wersjach .NET, kontrolkę
NotifyIcon.
Rozpoczynamy od stworzenia nowego projektu typu Windows Application.
Próba uruchomienia wygenerowanego
programu kończy się pokazaniem pustego formularza, nie jest to więc efekt którego pożądamy. Formularz ten użyjemy
później do zarządzania ustawieniami
programu.
Czas zdefiniować interfejs naszego programu, czyli ikonę zasobnika. Jak napisano powyżej, w tym celu użyjemy obiektu
NotifyIcon. Zaznaczamy nasz projekt,
otwieramy menu kontekstowe i wybieramy opcję Add -> New Element, a następnie z listy elementów wybieramy Component Class. Jako nazwę pliku wprowadzamy na przykład OFF.cs. Do utworzonego
komponentu przeciągamy obiekty NotifyIcon i ContextMenuStrip. Ten drugi będzie odpowiadał za menu kontekstowe naszej ikony.
Choć nieco poszerzony w wersji 2.0,
obiekt NotifyIcon nie oferuje przytłaczającej liczby ustawień. Mamy więc
tekst pojawiający się na ikonę po najechaniu na nią myszką, mamy menu kontekstowe, ikonę pokazaną w zasobniku
(16x16) i to w zasadzie prawie wszystko.
Ostatni konfigurowalny element to dymek z komunikatem – dla niego można
zdefiniować tytuł, tekst i osobną ikonę.
Następnym krokiem będzie konfiguracja menu (Rysunek 2).
Dla wygody warto zmienić tutaj domyślne nazwy elementów menu, ponieważ będziemy zmuszeni na nich opero-
Rysunek 3. Dodawanie obsługi zdarzenia
wać. Nasze menu będzie więc mieć cztery
elementy: „Wyłącz monitor” – jak sama
nazwa mówi wyłączający monitor, „Ustawienia” pokazujący okienko z ustawieniami programu, „O programie” pokazujący okienko z informacjami o aplikacji i
„Zakończ” kończący program.
Teraz, gdy mamy już wszystkie elementy menu, należałoby skonfigurować
dla nich procedury obsługi zdarzeń.
Niestety cały czas nie ma możliwości
automatycznego utworzenia procedur
obsługi zdarzeń menu i należy je zdefiniować ręcznie, tak jak to miało miejsce
w poprzedniej wersji VS.NET
(Rysunek 3).
Ostatnim krokiem będzie uruchomienie programu tak, aby widoczna była tylko ikona w zasobniku,. Do tego celu należy użyć klasy Application, zapewniającej zbiór statycznych metod i właściwości służących do zarządznia bieżącym
programem. Klasa ta jest standardowo
używana do inicjacji programu typu Windows Application:
static void Main()
{
}
Application.EnableVisualStyles();
Application.Run(new Form1());
Uruchomienie metody Run() z parametrem powoduje start programu, który
będzie działał tak długo, jak długo istnieje obiekt podany jako parametr. Jeśli
natomiast metoda ta zostanie uruchomiona bez parametru, aplikacja zostanie zainicjowana, a następnie przejdzie
w tryb przetwarzania skierowanych do
siebie komunikatów i pozostanie w nim
tak długo aż nie zostanie zakończona.
Może to nastąpić albo poprzez zewnętrzne zakończenie procesu, albo z wewnątrz
programu poprzez wywołanie Application.Exit(). Aby więc uruchomić program musimy po zainicjowaniu obiektu
typu OFF (tak nazywa się klasa stworzona na początku dla kontenera), wykonać
Application.Run(), a aby go zakończyć dodać Application.Exit() do
kodu obsługującego opcję menu Zakończ
(Rysunek 4).
W ten sposób możemy cieszyć się
pierwszymi efektami działania naszego
programu (Rysunek 5)
Funkcjonalność
programu
Stosunkowo najłatwiej będzie napisać
funkcje wyłączającą monitor (jest na listingu powyżej) i kończącą program. W
tym drugim przypadku pozbyć musimy
się tylko jednego niezbyt eleganckiego
efektu: otóż po wybraniu opcji kończącej
program kończy się proces, ale nie znika ikona z zasobnika. Jest to związane
z zarządzaniem zasobami w systemie
Windows oraz w Microsoft.NET. Użyty
zasób powinien zostać zwolniony, kiedy nie jest już potrzebny. Jeśli zasób (w
naszym przypadku ikona) był używany z poziomu aplikacji .NET może zwolić go (ale nie musi i nie należy na to liczyć) Garbage Collector. Z drugiej strony
obiekty takie powinny w .NET implementować interfejs IDisposable i tak
się zresztą dzieje w przypadku obiektu
NotifyIcon. Wywołanie metody Dispose() dla tego obiektu powoduje
zwolnienie używanych zasobów, czyli w
tym wypadku uchwytu do ikony. Można więc funkcję tą wywowałać ręcznie,
tuż przed Application.Exit(), można także wykorzystać fakt, że obiekt typu OFF jest komponentem i użyć pewnej
eleganckiej, choć nie wszystkim znanej
konstrukcji:
static void Main()
{
Application.EnableVisualStyles();
using (OFF wylacznik = new OFF())
{
}
}
Application.Run();
Polecenie using w tym kontekscie
działa tylko dla obiektów implementujących interfejs IDisposable, a kostrukcja taka jak wyżej jest równoważna następującej:
OFF wylacznik = new OFF();
try
Rysunek 4. Obsługa menu - ‚Zakończ‘
{
static void Main()
}
{
Application.EnableVisualStyles();
OFF wylacznik = new OFF();
}
Application.Run();
...
private void MenuExitClick(object sender, EventArgs e)
{
Application.Exit();
};
Application.Run();
finally
{
}
wylacznik.Dispose();
Ponieważ OFF jest komponentem, wywołanie Dispose() spowoduje także
wywołanie tej metody dla wszystkich
komponentów przechowywanych w wewnętrznym kontenerze, czyli jest to efekt,
którego właśnie pożądamy.
Jeśli natomiast chodzi o wyłączenie
monitora, szybko da się zauważyć, że nie
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
Rysunek 5. Menu aplikacji
wystarczy po prostu wywołać funkcji wyłączającej jak ją przedstawiliśmy na początku artykułu. W przypadku wygaszonego monitora system reaguje ponownym
włączeniem go chociażby po minimalnym ruchem myszy, co jest praktycznie
nieuniknione po kliknięciu wybierającym opcję. Można to jednak w prosty
sposób obejść wprowadzając niewielkie
opóźnienie pomiędzy kliknięciem a wyłączeniem monitora:
private void MenuOFFClick(object sender,
EventArgs e)
{
System.Threading.Thread.Sleep(200);
WylaczMonitor();
};
Wartość parametru metody Sleep()
oznacza długość pauzy w milisekundach,
można ją oczywiście zmienić według
własnego uznania.
Jak można było zauważyć wyżej, opcja
Wyłącz monitor jest w menu pogrubiona
– oznacza to rzecz jasna, że jest to opcja
domyślna i program po prostu powinien
uruchomić odpowiedni fragment kodu po
zwykłym kliknięciu myszką – bez odwoływania się do menu kontekstowego. W
tym celu musimy zdefiniować dla obiektu
NotifyIcon procedurę obsługi odpowiedniego zdarzenia. Którego? Najbardziej oczywiste – Click – odpada, gdyż
nie oferuje możliwości sprawdzenia który
przycisk został naciśnięty. A to oznacza,
że ekran zostałby wygaszony bez względu na to czy został naciśnięty przycisk
lewy czy też prawy. Z innych zdarzeń
mamy do wyboru MouseUp (przycisk myszy zwolniony) i MouseDown (przycisk
myszy naciśnięty). Osobiście sugeruję to
pierwsze, gdyż w drugim wypadku, jeśli nie zdążymy zwolnić przycisku przed
upłynięciem zdefiniowanej pauzy system zareaguje na zwolnienie ponownym
włączeniem monitora. Nie będę podawać
w tym miejscu już kodu obsługi zdarzenia, gdyż nie różni się on zbytnio od tego
dla menu.
Do pokazania informacji o programie
można użyć jednego z nowych szablonów
kodu, które dostarcza Microsoft Visual
Studio .NET 2005, a mianowicie AboutBox. Wszystko co należy wykonać, to dodanie do projektu nowego elementu typu
15
WINFORMS
Rysunek 6. About Box
AboutBox, ewentualna zmiana domyślnej nazwy (tutaj OFFAboutBox) i modyfikacja kodu odpowiedzialnego za pokazanie informacji o programie:
private void MenuAboutClick(object sender,
EventArgs e)
{
(new OFFAboutBox()).ShowDialog();
};
Co ciekawe, już na etapie generowania pliku projektu środowisko zapisuje
domyślne informacje o właściwościach
programu w pliku AssemblyInfo.cs
znajdującym się w folderze Properties.
Sam formularz AboutBox odczytuje te
informacje za pomocą refleksji z bieżącego Assembly i pokazuje je odpowiednio
sformatowane (Rysunek 6).
Ustawienia programu
Pisząc w ten sposób aplikację same nasuwają się miejsca, które można by sparametryzować. Przede wszystkim można
określić, czy program ma wyłączać monitor zaraz po uruchomieniu, czy też ma
Rysunek 7. Edytor ustawień
czekać aż użytkownik kliknie ikonkę
w zasobniku. Warto zdefiniować opóźnienie wygaszenia monitora – osoby z
„ciężką reką” zmienią je np. chętnie na
1000ms. Skoro program może wyłączać
monitor przy starcie, to może go od razu
zakończyć – po co ma rezerwować cenne zasoby systemowe. Te wszystkie właściwości można zapisać jako ustawienia
programu.
Do edycji ustawień Visual Studio .NET
2005 oferuje wygodny edytor (Rysunek
7). Warte zauważenia jest także to, że
nie ustawienia nie są zapisywane tylko
16
w pliku app.config, jak to miało miejsce w poprzedniej wersji środowiska,
lecz także (a w zasadzie przede wszystkim) w pliku o nazwie Settings.settings. Jego zawartość zostanie w czasie
kompilacji przetłumaczona na format
zrozumiały dla menedżera konfiguracji
i zapisana w pliku OFF.exe.config. Co
więcej, do obsługi ustawień zostanie wygenerowana klasa, za pomocą której domyślne wartości będzie można odczytać
i ewentualnie zmienić we własnym programie. Dzięki temu już podczas kompilacji zostanie przeprowadzona kontrola zgodności typów, a co najważniejsze
bardzo ułatwiony zostanie dostęp do
ustawień.
Wracając do opisywanego programu,
zdefiniujemy cztery zmienne:
• Delay – określającą opóźnienie pomiędzy wybraniem opcji a wyłączeniem
monitora
• ExitOnStart – określającą czy program po uruchomieniu ma zakończyć
pracę
• StartOff – określającą czy po starcie
programu ma zostać wyłączony monitor
• SysTrayIcon – określającą czy ma zostać pokazana ikona programu w zasobniku systemowym
Zdefiniujemy je jako zmienne użtkownika, a nie aplikacji co pozwoli nam na
edycję domyślnych wartości. Zmieniać
je będziemy za pomocą formularza Ustawienia, stworzonego na samym początku
(Rysunek 8).
Pozostaje więc już tylko dopisać kod
synchronizujący zawartość formularza.
Dzięki wygenerowanej klasie wystarczy
zaledwie kilka linijek (Rysunek 9).
Pozostało więc już tylko powiązać istniejący formularz i ustawienia z resztą
programu. To zadanie pozostawię już
czytelnikom jako ćwiczenie.
W tym momencie warto zaznaczyć
gdzie zapisywane są ustawienia użytkownika. Przy pierwszym uruchomieniu zostaną one odczytane z pliku
OFF.exe.config, ale zapisane już w
innym pliku, mianowicie user.config. Plik ten znajdzie się w katalogu
%HOMEPATH%\Ustawienia lokalne\
Dane aplikacji\
<AssemblyCompany>\OFF.exe _
Url _ <key>\<AssemblyVersion>,
gdzie AssemblyCompany i AssemblyVersion zdefiniowane są w pliku AssemblyInfo.cs. Niestety nie udało mi
Rysunek 8. Ustawienia aplikacji
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
Rysunek 9. Zapisywanie ustawień.
private OFF.Properties.Settings settings;
private void Ustawienia _ Load(object
sender, EventArgs e)
{
settings = new OFF.Properties.Set-
tings();
Delay.Value = settings.Delay;
StartOff.Checked = settings.StartOff;
ExitOnStart.Checked = settings.Exi-
tOnStart;
SystrayIcon.Checked = settings.Sy-
sTrayIcon;
}
private void Ustawienia _ FormClosed(object sender, FormClosedEventArgs e)
{
settings.Delay = Delay.Value;
settings.StartOff = StartOff.Checked;
settings.ExitOnStart = ExitOn-
Start.Checked;
settings.SysTrayIcon = Systray-
Icon.Checked;
}
settings.Save();
się nigdzie znaleźć informacji na jakiej
podstawie generowana jest wartość klucza key.
Na koniec uwaga: niniejszy program
nie posiada zbyt dużej ilości ustawień.
Można wobec tego zamiast tworzyć formularz zawrzeć ustawienia programu
w podmenu stworzonym do tego celu.
Pewnego rodzaju problemem może być
edycja informacji o opóźnieniu wygaszenia monitora, ale obiekt ContextMenuStrip odpowiedzialny za menu kontekstowe może posiadać oprócz etykiet
także pola tekstowe lub listy rozwijane.
Wszystko zależy tutaj od inwencji twórcy programu.
Sławek Procelewski
Źródła
1. S.Matz, Delegieren 2.0. dot.net magazin,
04.2005
2. D. Dobric, Application Settings im .NET Framework
2.0. dot.net magazin, 05.2005
3. A. Kosch, Visual Studio XXL. dot.net magazin
07/08.2005
TOOLS.NET
Wprowadzenie do śledzenia aplikacji
przy pomocy bibliteki NLog
Dawno, dawno temu, kiedy na świecie
nie było jeszcze debugerów, a aplikacje
były w większości oparte o tekstową konsolę, programiści umieszczali w nich instrukcje printf() wypisujące komunikaty
diagnostyczne na ekran. Kilka lat później
świat poszedł znacznie do przodu i instrukcje printf() zostały zastąpione przez
Console.WriteLine()...
Zapewne każdy z nas napisał kiedyś
kod podobny do przedstawionego na Rysunku 1.
Instrukcje Console.WriteLine() pełnią
tu rolę instrukcji śledzących, które informują o działaniu programu. Na ich podstawie możemy się zorientować, czy program działa poprawnie lub też czy się nie
zawiesił. Zwykle po jako-takim przetestowaniu instrukcje Console.WriteLine() są
wyłączane, aby nie spowalniać działania programu. Często jednak, po pewnym
czasie przychodzi potrzeba ponownego
włączenia śledzenia w aplikacji, dlatego
też instrukcje śledzące w kodzie zwykle
zamieniamy na komentarze zamiast fizycznie je usuwać.
Po kilku iteracjach dochodzimy zwykle
do wniosku, że obsługa śledzenia naszej
aplikacji, byłaby dużo bardziej użyteczna, gdyby możliwe było:
·sterowanie poziomem szczegółowości
informacji diagnostycznych (np. wyświetlanie wyłącznie błędów i ostrzeżeń lub
bardzo dokładne informacje przydatne do
debugowania)
·włączanie i wyłączanie śledzenia dla
poszczególnych klas i bibliotek w trakcie
działania programu, bez jego zatrzymywania
·zapisywanie komunikatów do pliku,
systemowego dziennika zdarzeń, dołączonego debugera, itp.
·wysyłanie szczególnie ważnych komunikatów mailem lub zapisywanie ich w
bazie danych
·i wiele innych
Wydaje się, że w dobie graficznych narzędzi do debugowania użyteczność rozwiązań opartych na śledzeniu jest mała.
Często jednak są to jedyne dostępne na-
rzędzia, które muszą nam wystarczyć np.
do zlokalizowania przyczyny błędu w
krytycznym systemie działającym na serwerze, którego nie można wyłączyć nawet na chwilę.
Czym jest NLog?
NLog (http://www.nlog-project.org) jest biblioteką na platformie
.NET, która umożliwia łatwe wzbogacenie naszej aplikacji o obsługę śledzenia,
realizującą wymagania opisane powyżej,
a także dużo więcej.
Najprościej rzecz ujmując NLog umożliwia tworzenie reguł sterujących przepływem komunikatów diagnostycznych
od źródła do celu, którym może być:
· plik
·konsola tekstowa
·wiadomość email
·baza danych
·inny komputer w sieci (protokół TCP/
UDP)
·kolejka komunikatów MSMQ
·systemowy dziennik zdarzeń (Event
Log)
·i inne opisane na stronie http:
//www.nlog-project.org/
targets.html
Dodatkowo, każdy komunikat diagnostyczny może być wzbogacony o informacje kontekstowe, które razem z nim zostaną przesłane do celu. Mogą to być:
·bieżąca data i godzina (w różnych formatach)
·poziom ważności
·nazwa źródła
·informacje o metodzie, która wyemitowała komunikat diagnostyczny
·zawartości zmiennych środowiskowych
·informacje o wyjątku
·nazwa maszyny, procesu i wątku, który emituje komunikat
·i wiele innych opisanych na stronie
http://www.nlog-project.org/
layoutrenderers.html
Każdy komunikat diagnostyczny ma
swój poziom (log level). Wspierane są na-
stępujące poziomy
·Trace - Szczegółowe komunikaty diagnostyczne o potencjalnie dużej częstotliwości i objętości
·Debug -Komunikaty diagnostyczne o
niewielkiej częstotliwości i małej objętości
·Info - Komunikaty informacyjne, zwykle niezbyt częste
·Warn - Ostrzeżenia nie powodujące
błędnego działania programu
·Error - Błędy, które objawiają się widocznym dla użytkownika komunikatem
·Fatal - Błędy krytyczne, po których
aplikacja zwykle kończy swoje działanie
NLog jest rozwiązaniem open source
dostępnym za darmo na licencji BSD. Najnowszą wersję biblioteki można zawsze
pobrać pod adresem http://www.nlogproject.org/download.html. Dostępny jest graficzny instalator, dzięki któremu można szybko i łatwo zainstalować
NLoga w wybranym miejscu, jak również
(w najnowszej wersji), integracja z Visual
Studio 2005 udostępniająca:
·wybór biblioteki NLog.dll z okna Add
Reference...
·wsparcie dla kontekstowego podpowiadania składni - Intellisense podczas
edycji plików konfiguracyjnych
·szablony plików konfiguracyjnych
·code snippet
Pierwsza aplikacja
wykorzystująca NLoga
Na początek przyjrzyjmy się, jak wygląda tworzenie aplikacji wykorzystującej NLog przy użyciu Visual Studio 2005.
Przeanalizujemy 2 przykłady - jeden najprostszy, drugi bardziej skomplikowany, które pokazują jak łatwo jest sterować
konfiguracją logowania.
Zaczynamy od utworzenia projektu w
Visual Studio i dodania w nim pliku konfiguracyjnego NLoga. Będziemy się posługiwać językiem C#, ale NLog równie
dobrze obsługuje VB.NET i inne języki
na platformie .NET. Instalator NLoga dodaje do okna „Add New Item...” kilka nowych opcji pozwalających nam na „szybki start”. Dodajmy więc do projektu pusty
plik konfiguracyjny NLoga (Empty NLog
Configuration File).(Rysunek 2)
Rysunek 1
Rysunek 3
static void Main()
<nlog xmlns=”http://www.nlog-
{
Console.WriteLine(“Program
SuperApp został uruchomiony...”);
DoSomething(0);
Console.WriteLine(„Program
SuperApp kończy działanie”);
}
project.org/schemas/NLog.xsd”
xmlns:xsi=”http://
www.w3.org/2001/XMLSchema-instance”>
<targets>
</targets>
<rules>
</rules>
</nlog>
Rysunek 2. Szablony plików konfiguracyjnych NLoga
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
17
TOOLS.NET
layout=”${longdate}|${level}|${messa-
ge}”/>
</targets>
Zauważmy, że po Visual Studio podpowie nam dostępne nazwy elementów i ich
atrybutów, a po wpisaniu xsi: uzyskamy
listę dostępnych celów logowania (Rysunek 5).
W sekcji <rules /> dodajmy regułę
kierującą komunikaty na poziomie Debug
lub wyższym na konsolę.
<rules>
Rysunek 5. Intellisense w Visual Studio
2005
<logger name=”*” minlevel=”Debug” writeT
o=”console”/>
</rules>
Rysunek 4. Właściwości pliku
konfiguracyjnego
Zauważmy, że automatycznie została dodana referencja do biblioteki NLog
i w projekcie pojawił się plik o nazwie
NLog.config o treści z Rysunku 3.
W oknie właściwości dla tego pliku należy zaznaczyć opcję „Copy To Output Directory” (Rysunek 4):
W sekcji <targets /> dodajmy następujący wpis definiujący wyjście na konsolę i format tego wyjścia:
<targets>
<target name=”console” xsi:type=”Console”
Aby wysłać komunikat diagnostyczny posługujemy się obiektem klasy Logger, który udostępnia metody o nazwach
odpowiadających poziomom ważności
komunikatu. Obiekty Logger tworzymy
za pośrednictwem klasy LogManager,
najwygodniej jest w tym celu wywołać
metodę LogManager.GetCurrentClassLogger(), która tworzy źródło o takiej samej
nazwie jak bieżąca klasa.
Zmodyfikujmy wygenerowany plik
C# dodając na początku polecenie using
NLog, a w treści klasy instrukcję tworzenia obiektu Logger i wypisanie przykładowego komunikatu (Rysunek 6)
Wynikiem działania programu jest wy-
Rysunek 6
Rysunek 8.
using System;
static void C()
using System.Collections.Generic;
{
using System.Text;
using NLog;
}
namespace NLogExample
{
{
logger.Info(“Info BBB”);
C();
sLogger();
}
}
logger.Warn(“Warn BBB”);
logger.Error(“Error BBB”);
static void Main(string[] args)
}
}
logger.Debug(„Hello World!”);
logger.Fatal(“Fatal BBB”);
static void A()
{
logger.Trace(“Trace AAA”);
logger.Debug(“Debug AAA”);
logger.Info(“Info AAA”);
Rysunek 7
B();
<nlog xmlns=”http://www.nlog-project.org/schemas/NLog.xsd”
logger.Warn(“Warn AAA”);
xmlns:xsi=”http://www.w3.org/2001/XMLSchema-instance”>
<targets>
<target name=”console” xsi:type=”ColoredConsole”
layout=”${date:format=HH\:mm\:ss}|${level}|${stacktrac
e}|${message}”/>
<target name=”file” xsi:type=”File” fileName=”${basedir}/
file.txt”
logger.Error(“Error AAA”);
}
{
logger.Info(„Komunikat informacyjny na poziomie Info”);
A();
logger.Warn(„Komunikat-ostrzeżenie na poziomie Warn”);
<logger name=”*” minlevel=”Trace” writeTo=”console,file”/>
</nlog>
logger.Trace(„Komunikat techniczny na poziomie Trace”);
logger.Debug(„Komunikat techniczny na poziomie Debug”);
layout=”${stacktrace} ${message}”/>
<rules>
</rules>
logger.Fatal(“Fatal AAA”);
static void Main(string[] args)
</targets>
18
logger.Trace(“Trace BBB”);
logger.Debug(“Debug BBB”);
private static Logger logger = LogManager.GetCurrentClas-
{
logger.Info(“Info CCC”);
static void B()
class Program
{
pisana na konsoli tekstowej bieżąca data,
poziom logowania (Debug) i komunikat
Hello World .
Spróbujmy prześledzić teraz poszczególne etapy przetwarzania powyższego
komunikatu diagnostycznego:
1.Metoda LogManager.GetCurrentClassLogger(); tworzy obiekt
klasy Logger reprezentujący źródło logowania powiązane z bieżącą klasą.
2.Wywołanie metody Debug() na tym
obiekcie powoduje wysłanie zadanego komunikatu za pośrednictwem wskazanego
źródła z poziomem logowania Debug
3.Ponieważ poziom logowania i źródło
odpowiadają zdefiniowanej regule, komunikat jest formatowany zgodnie z war-
logger.Error(„Komunikat o błędzie na poziomie Error”);
logger.Fatal(„Komunikat o błędzie krytycznym na poziomie Fa-
tal”);
}
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
Rysunek 10. Konsola na kolorowo
tością parametru layout elementu <target /> i wysyłany na konsolę.
Bardziej skomplikowana konfiguracja
logowania
Załóżmy, że oprócz wypisywania na
konsolę, chcemy jednocześnie zapisywać
nasze komunikaty do pliku testowego i
rejestrować informacje o bieżącym stosie
wywołań, czyli nazwy metod. Zdefiniujmy drugi cel logowania typu plikowego i
skierujmy wszystkie komunikaty do niego. Włączmy też poziom Trace. Wymaga to jedynie dodania nowego elementu
<target /> typu File, modyfikacji reguły <logger /> a także zmodyfikowania
parametru layout (Rysunek 7).
Wypiszmy także nieco więcej komunikatów na różnych poziomach logowania. Wprowadźmy też kilka dodatkowych
metod, tak aby zaobserwować ślad stosu
(Rysunek 8)
Uruchomienie programu spowoduje
wypisanie do pliku file.txt informacji takich jak te z Rysunku 9.
W tym samym czasie na konsolę wypisana zostanie wiadomość w innym formacie (Rysunek 10)
Załóżmy, że chcemy do pliku zapisywać wszystkie informacje, a na konsolę
jedynie te najistotniejsze (na poziomie Info lub wyższym). Nic prostszego, wystarczy jedynie zmienić sekcję reguł:
<rules>
<logger name=”*” minlevel=”Info” writeTo
=”console”/>
<logger name=”*” minlevel=”Trace” writeT
o=”file”/>
</rules>
Po uruchomieniu programu okaże się,
że komunikaty na poziomie Trace i Debug
są zawarte wyłącznie w pliku, podczas
gdy nie widzimy ich na konsoli.
Konfiguracja logowania
Czas teraz, aby odpowiedzieć na pytanie: jak to się stało, że NLog automatycznie odczytał poprawną konfigurację?
Otóż, w odróżnieniu od innych narzędzi
tego typu, NLog stara się maksymalnie
ułatwić konfigurację, stosując rozmaite
zachowania domyślne. NLog szuka pliku
konfiguracyjnego w następujących standardowych lokalizacjach:
·standardowy plik konfiguracyjny programu (zwykle aplikacja.exe.config)
·plik aplikacja.exe.nlog w katalogu aplikacji
·plik NLog.config w katalogu aplikacji
·plik NLog.dll.nlog w katalogu, w
którym znajduje się plik NLog.dll
·jeśli istnieje zmienna środowiskowa NLOG _ GLOBAL _ CONFIG _ FILE,
sprawdzane jest miejsce wskazywane
przez tę zmienną
Rysunek 9
Program.Main Komunikat techniczny na poziomie Trace
Program.Main Komunikat techniczny na poziomie Debug
Program.Main Komunikat informacyjny na poziomie Info
Program.Main => Program.A Trace AAA
Program.Main => Program.A Debug AAA
Program.Main => Program.A Info AAA
Program.Main => Program.A => Program.B Trace BBB
Program.Main => Program.A => Program.B Debug BBB
Program.Main => Program.A => Program.B Info BBB
Program.A => Program.B => Program.C Info CCC
Program.Main => Program.A => Program.B Warn BBB
Program.Main => Program.A => Program.B Error BBB
Program.Main => Program.A => Program.B Fatal BBB
Program.Main => Program.A Warn AAA
W przypadku aplikacji ASP.NET przeszukiwane są:
·standardowy plik konfiguracyjny
web.config
·plik web.nlog w katalogu aplikacji
(obok web.config)
·plik NLog.config w katalogu aplikacji
·plik NLog.dll.nlog w katalogu,
w którym znajduje się plik NLog.dll
(zwykle „bin”)
·jeśli istnieje zmienna środowiskowa NLOG _ GLOBAL _ CONFIG _ FILE,
sprawdzane jest miejsce wskazywane
przez tę zmienną
W środowisku .NET Compact Framework nie występuje pojęcie pliku konfiguracyjnego aplikacji (*.exe.config) ani
zmienne środowiskowe, dlatego sprawdzane są tylko trzy lokalizacje:
·plik aplikacja.exe.nlog w katalogu aplikacji
·plik NLog.config w katalogu aplikacji
·plik NLog.dll.nlog w katalogu, w
którym znajduje się plik NLog.dll
Przeszukiwany zestaw plików został
tak dobrany, aby umożliwić automatyczną konfigurację we wszystkich typowych
trybach działania aplikacji bez konieczności wykonywania żadnych dodatkowych czynności.
Format pliku konfiguracyjnego
Dostępne są 2 formaty konfiguracji
NLoga:
·osadzona wewnątrz pliku konfiguracyjnego aplikacji
·uproszczona
W pierwszym przypadku posługujemy
się standardowym mechanizmem sekcji
konfiguracyjnych, dzięki którym nasz
plik wygląda tak jak na Rysunku 11.
Format uproszczony występuje w przypadku plików z rozszerzeniem *.nlog
oraz NLog.config i składa się bezpośrednio z pliku XML zawierającego element
<nlog /> jako korzeń. Użycie przestrzeni nazw jest opcjonalne, ale umożliwia
działanie mechanizmu Intellisense w Visual Studio.
<nlog xmlns=”http://www.nlog-project.org/
schemas/NLog.xsd”
xmlns:xsi=”http://www.w3.org/2001/
XMLSchema-instance”>
Program.Main => Program.A Error AAA
Program.Main => Program.A Fatal AAA
Program.Main Komunikat-ostrzeżenie na poziomie Warn
Program.Main Komunikat o błędzie na poziomie Error
Program.Main Komunikat o błędzie krytycznym na poziomie Fatal
Rysunek 11
<configuration>
<configSections>
<section name=”nlog” type=”NLog.Config.ConfigSectionHandler,
NLog”/>
</configSections>
<nlog>
</nlog>
</configuration>
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
19
TOOLS.NET
Wewnątrz sekcji <targets /> definiujemy cele logowania, które są reprezentowane przez elementy <target />
Każdy cel musi mieć określone 2 atrybuty:
·name - nazwa
·type - typ celu (w przypadku korzystania z przestrzeni nazw xsi:type)
Dodatkowo cele zwykle akceptują inne
parametry - wpływające na sposób zapisywania informacji diagnostycznej. Dla
każdego celu zestaw parametrów jest inny - są one szczegółowo opisane na stronie projektu jak również dostępne kontekstowo w trakcie edycji pliku konfiguracyjnego dzięki Intellisense.
Przykładowo - cel typu „File” (plik) akceptuje parametr „fileName” definiujący
nazwę pliku a cel typu „Console” akceptuje parametr „error” określający czy komunikaty diagnostyczne mają być wypisywane na standardowe wyjście (stdout)
czy na standardowe wyjście dla błędów
(stderr).
NLog udostępnia wiele predefiniowanych celów. Są one opisane na stronie projektu. Bardzo łatwo jest także utworzyć
swój własny cel - wymaga to zaledwie
kilkunastu wierszy kodu i jest opisane w
dokumentacji projektu.
wania wymagany do tego aby reguła zadziałała
·maxlevel - maksymalny poziom logowania wymagany do tego aby reguła
zadziałała
·level - poziom logowania (pojedynczy) wymagany do tego aby reguła zadziałała
·levels - lista poziomów logowania,
które aktywują regułę
·writeTo - lista nazw celów, do których zapisać komunikat
·final - czy reguła jest kończąca. Jeśli
tak, po jej zadziałaniu nie są przetwarzane żadne dalsze reguły
Poniżej kilka przykładów:
·<logger name=”Name.Space.Klasa1” minlevel=”Debug” writeTo=”f1”
/> - komunikaty z klasy Klasa1 w przestrzeni nazw Name.Space na poziomie
Debug lub wyższym są zapisywane do
celu „f1”
·<logger name=”Name.Space.Klasa1” levels=”Debug,Error” writeTo=”f1” /> - komunikaty z klasy Klasa1
w przestrzeni nazw Name.Space na poziomach Debug i Error zapisywane do celu „f2”
·<logger name=”Name.Space.*”
writeTo=”f3,f4” /> - komunikaty ze
wszystkich klas w przestrzeni nazw Name.Space niezależnie od ich poziomu są
zapisywane do celów „f3” i „f4”
·<logger name=”Name.Space.*”
minlevel=”Debug” maxlevel=”Error” final=”true” /> - komunikaty ze
wszystkich klas w przestrzeni nazw Name.Space na poziomie większym niż
Debug i mniejszym niż Error (czyli Debug,Info,Warn,Error) są odrzucane
(ponieważ nie podano klauzuli writeTo) i nie są przetwarzane dla nich kolejne reguły (ze względu na final=”true”)
W najprostszych aplikacjach konfiguracja logowania wygląda zwykle tak, że
mamy jeden cel i jedną regułę kierującą
komunikaty do niego w zależności od poziomu. W miarę jak aplikacja rośnie, pojawia się potrzeba dodawania kolejnych
celów i reguł. W przypadku NLoga jest to
wyjątkowo proste.
Reguły
Informacje kontekstowe
Reguły sterowania logowaniem definiujemy w sekcji <rules />. Jest to prosta tabela routingu, gdzie w każdej regule na podstawie nazwy źródła i poziomu
logowania możemy skierować wynik do
określonego celu lub listy celów. Dodatkowo możemy zdecydować czy po zapisaniu
mają być uwzględniane dalsze reguły czy
też zakończyć ich przetwarzanie.
Każdy wpis w tabeli routingu ma formę
elementu <logger />, który przyjmuje
następujące atrybuty:
·name - nazwa źródła (może zawierać
symbole wieloznaczne * na początku i/
lub na końcu)
·minlevel - minimalny poziom logo-
Największą siłą NLoga w porównaniu
z innymi narzędziami tego typu jest mechanizm formatów (ang. layouts). Są to
małe fragmenty tekstu otoczone parą nawiasów „${„ (dolar + lewy nawias klamrowy) oraz „}” (prawy nawias klamrowy),
które potrafią wstawiać do tekstu elementy informacji kontekstowej. Można ich
używać w bardzo wielu miejscach, przykładowo do sterowania formatem informacji wyświetlanych na ekran lub zapisywanych do pliku, ale także do sterowania nazwami plików!
Po co? Zobaczmy to na przykładzie. Załóżmy, że chcemy wypisywać na konsolę
tekstową następujące informacje:
</nlog>
Elementy konfiguracyjne
Wewnątrz elementu <nlog /> można
używać następujących elementów, z czego zwykle używane są dwa pierwsze elementy z poniższej listy.
·<targets /> - definiuje cele logowania
·<rules /> - definiuje reguły kierowania komunikatów diagnostycznych
·<extensions /> - ładuje rozszerzenia NLoga z pliku *.dll
·<include /> - dołącza zewnętrzny
plik konfiguracyjny
·<variable /> - ustawia wartość
zmiennej konfiguracyjnej
Cele
20
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
·bieżąca data i godzina
·nazwa klasy i metody, która spowodowała wypisanie komunikatu diagnostycznego
·poziom komunikatu diagnostycznego
·tekst komunikatu diagnostycznego
Jak to zrobić? Nic prostszego:
<target name=”c” xsi:type=”Console”
layout=”${longdate} ${callsite}
${level} ${message}”/>
Załóżmy teraz, że chcemy aby komunikaty z każdego źródła trafiały do osobnego pliku. To jest równie łatwe:
<target name=”f” xsi:type=”File” fileName=”
${logger}.txt”/>
Jak widzimy, format ${logger} został tutaj użyty w atrybucie fileName,
co sprawia, że każdy komunikat diagnostyczny trafia do pliku o nazwie takiej jak
nazwa źródła. Powyższy przykład utworzy nam pliki:
·Name.Space.Klasa1.txt
·Name.Space.Klasa2.txt
·Name.Space.Klasa3.txt
·Inny.Name.Space.Klasa1.txt
·Inny.Name.Space.Klasa2.txt
·Inny.Name.Space.Klasa3.txt
·
...
Częstym wymaganiem jest przechowywanie logów z każdego dnia w osobnym
pliku. To również jest trywialne:
<target name=”f” xsi:type=”File” filename=”
${shortdate}.txt”/>
Gdybyśmy chcieli dla każdego użytkownika naszej aplikacji zrobić osobny
plik, wystarczy poniższy fragment kodu:
<target name=”f” xsi:type=”File” filename=”
${windows-identity:domain=false}.txt”/>
Dzięki tej prostej operacji NLog utworzy nam pliki o nazwach takich jak loginy naszych użytkowników:
1.Administrator.txt
2.MaryManager.txt
3.EdwardEmployee.txt
4. ...
Oczywiście możliwe są też bardziej
złożone przypadki. Np. dla każdego użytkownika i daty tworzymy osobny plik w
katalogu takim jak data:
<target name=”f” xsi:type=”File”
filename=”${shortdate}/${windows-
identity:domain=false}.txt”/>
Powstałe pliki to:
1.2006-01-01/Administrator.txt
2.2006-01-01/MaryManager.txt
3.2006-01-01/EdwardEmployee.txt
4.2006-01-02/Administrator.txt
5.2006-01-02/MaryManager.txt
6.2006-01-02/EdwardEmployee.txt
7. ...
NLog udostępnia bardzo wiele predefiniowanych formatów. Są
one opisane pod adresem http:
//www.nlog-project.org/
layoutrenderers.html. Bardzo łatwo
jest także utworzyć swój własny format
- wymaga to zaledwie kilkunastu wierszy kodu i jest opisane w dokumentacji
projektu.
Pliki dołączane i zmienne
Czasami celowe może być rozbicie pliku konfiguracyjnego na kilka mniejszcych. NLog udostępnia w tym celu mechanizm plików dołączanych. Aby włączyć je do konfiguracji wystarczy użyć
<include file=”nazwapliku” /> .
Warto wspomnieć że nazwapliku może
zawierać elementy informacji kontekstowej, więc możliwe są zaawansowane
scenariusze takie jak dołączanie różnych
plików na różnych maszynach, jak w poniższym przykładzie, gdzie wczytujemy
plik o takiej nazwie jak nazwa bieżącej
maszyny:
<nlog>
...
<include file=”${basedir}/
${machinename}.config”/>
...
</nlog>
Zmienne pozwalają nam w skrócony
sposób zapisywać złożone lub powtarzalne wyrażenia, takie jak nazwy plików.
Aby zdefiniować zmienną posługujemy
się składnią <variable name =”var”
value =”xxx” />. Aby użyć jej wartość
posługujemy się składnią identyczną jak
przy informacjach kontekstowych, to znaczy: „${var}” (Rysunek 12).
Rekonfiguracja
Pliki konfiguracyjne są standardowo
wczytywane na początku pracy programu. W przypadku długo działających
procesów (np. usług systemowych) często
zachodzi potrzeba zwiększenia poziomu logowania w trakcie działania. Aby
umożliwić taką rekonfigurację, NLog
potrafi monitorować plik konfiguracyjny aplikacji i wczytywać go ponownie po
wykryciu jakiejkolwiej zmiany. Aby włączyć ten mechanizm, wystarczy dodać
w pliku konfiguracyjnym atrybut <nlog
autoReload =”true” />.
Rozwiązywanie problemów z
logowaniem
Czasami zdarza się tak, że pomimo
skonfigurowania, aplikacja nie zapisuje logów. Przyczyn może być wiele, najczęstszym problemem są uprawnienia,
szczególnie w aplikacji webowej. Zwykle
okazuje się, że serwer aplikacji WWW
nie ma prawa zapisu do katalogu, gdzie
Rysunek 12
chcielibyśmy gromadzić logi. Ponieważ
NLog „zjada” wyjątki występujące podczas logowania, aplikacja może się o tym
nie dowiedzieć. Jest kilka sposobów, aby
stwierdzić występowanie tego typu problemów.
·<nlog throwExceptions =”true”
/> - dodanie atrybutu throwExceptions
w pliku konfiguracyjnym powoduje,
że NLog nie maskuje wyjątków i są one
przekazywane do aplikacji. Ten atrybut
jest przydatny w fazie uruchamiania.
Po zakończeniu konfiguracji dobrze jest
włączyć maskowanie wyjątków, tak aby
przypadkowe problemy z logowaniem nie
spowodowały awarii naszej aplikacji.
·<nlog internalLogFile =”file.t
xt” /> - dodanie atrybutu internalLogFilew pliku konfiguracyjnym powoduje,
że NLog zapisze do wskazanego pliku wewnętrzne informacje ułatwiające debugowanie, w tym wszystkie wyjątki jakie
zostaną zgłoszone podczas logowania.
·<nlog internalLogLevel=”Trac
e|Debug|Info|Warn|Error|Fatal” /
> - sterujący poziomem wewnętrznego logowania
·<nlog internalLogToConsole =”
false|true” /> - wysyła zawartość we-
ColoredConsoleTarget consoleTarget = new ColoredConsole-
<nlog>
<variable name=”logDirectory” value=”${basedir}/logs/
Target();
config.AddTarget(“console”, consoleTarget);
${shortdate}”/>
<targets>
FileTarget fileTarget = new FileTarget();
<target name=”file1” xsi:type=”File” filename=”${logDirectory
config.AddTarget(“file”, fileTarget);
}/file1.txt”/>
<target name=”file2” xsi:type=”File” filename=”${logDirectory
// krok 3 - ustawiamy właściwości celów
}/file2.txt”/>
</targets>
consoleTarget.Layout = “${date:format=HH\\:MM\\:ss} ${log-
</nlog>
ger} ${message}”;
fileTarget.FileName = “${basedir}/file.txt”;
Rysunek 13
fileTarget.Layout = “${message}”;
<targets>
<target name=”n” xsi:type=”AsyncWrapper”>
// krok 4 - definiujemy reguły
<target xsi:type=”RetryingWrapper”>
<target xsi:type=”File” fileName=”${file}.txt”/>
LoggingRule rule1 = new LoggingRule(“*”, LogLevel.Debug,
</target>
consoleTarget);
</target>
config.LoggingRules.Add(rule1);
</targets>
LoggingRule rule2 = new LoggingRule(“*”, LogLevel.Debug,
Rysunek 14
fileTarget);
using NLog;
config.LoggingRules.Add(rule2);
using NLog.Targets;
using NLog.Config;
// krok 5. aktywowanie konfiguracji
using NLog.Win32.Targets;
LogManager.Configuration = config;
class Example
// przykłady użycia
{
static void Main(string[] args)
{
Logger logger = LogManager.GetLogger(“Example”);
logger.Trace(“trace log message”);
// krok 1. tworzymy obiekt konfiguracji
logger.Debug(“debug log message”);
logger.Info(“info log message”);
LoggingConfiguration config = new LoggingConfiguration();
// krok 2. tworzymy cel(e) i dodajemy je do konfiguracji
logger.Warn(“warn log message”);
logger.Error(“error log message”);
}
logger.Fatal(“fatal log message”);
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
21
TOOLS.NET
wnętrznego logu na konsolę.
·<nlog internalLogToConsoleEr
ror =”false|true” /> - wysyła zawartość wewnętrznego logu na wyjście błędów konsoli.
Przetwarzanie
asynchroniczne, cele
opakowujące i złożone
Oprócz standardowych, NLog udostępnia także tzw. wrapper targets i compound targets, czyli cele opakowujące i złożone, które modyfikują działanie innych
celów. Mogą dodawać tym samym takie
funkcje jak:
·przetwarzanie asynchroniczne (logowanie odbywa się w osobnym wątku)
·ponawianie próby w przypadku błędu
·równoważenie obciążenia (load balancing)
·buforowanie
·filtrowanie
·cele zapasowe (w przypadku awarii jedego celu przełączamy się na kolejny)
·i inne opisane na stronie http:
//www.nlog-project.org/
targets.html
Aby zdefiniować cel złożony bądź opakowujący w pliku konfiguracjnym, zagnieżdżamy cel opakowany wewnątrz
odpowiedniego celu opakowującego. Zagnieżdżanie może być wielopoziomowe,
więc możemy modyfikować działanie celu na więcej niż jeden sposób. Przykładowo, asynchroniczne logowanie do pliku z
ponawianiem próby zapisu w przypadku
błędu definiujemy w sposób przedstawiony na Rysunku 13.
Ze względu na to, że przetwarzanie
asynchroniczne jest dość często stosowane, NLog udostępnia skróconą składnię
pozwalającą włączyć to zachowanie dla
wszystkich celów. Wystarczy w elemencie <targets /> dodać atrybut async=”true”.
nikającym z poziomu logowania i dodatkowo plik zawierający te same informacje
w nieco innym formacie.
Co jeszcze można zrobić w
NLogu?
NLog udostępnia jeszcze wiele funkcji,
które ze względu na objętość nie mogły
zostać omówione w niniejszym artykule.
Każda z tych funkcji mogłaby być tematem na osobny artykuł.
·logowanie wyjątków - http://
sourceforge.net/mailarchive/
forum.php?thread _
id=6766833&forum _ id=41984
·język warunków logicznych używanych przy filtrowaniu - http://
www.nlog-project.org/conditions.html
·NLogViewer - aplikacja do podglądu logów w czasie rzeczywistym (wersja pre-alpha) - http://viewer.nlogproject.org/
Jarek Kowalski
Konfiguracja programowa
Zamiast pliku konfiguracyjnego NLog
może być konfigurowany przy pomocy
API. Pełny opis tej funkcji wykracza poza
zakres artykułu. W skrócie można powiedzieć, że polega to na:
1.utworzeniu obiektu klasy LoggingConfiguration
2.utworzeniu jednego lub więcej celów
(obiektów dziedziczących z klasy Target)
3.ustawienie właściwości celów
4.zdefiniowanie reguł
5.aktywowanie konfiguracji
Na Rysunku 14 znajduje się krótki
przykład definiujący jeden cel typu „konsola tekstowa z kolorowaniem” i drugi
plikowy oraz regułę kierującą do niego
komunikaty na poziomie Debug lub wyższym:
Wynikiem działania powyższej aplikacji jest wypisany tekst na konsoli, gdzie
każdy wiersz jest oznaczony kolorem wy-
22
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
ROZMOWA.NET
O NLogu opowiada
Jarek Kowalski.
Witamy serdecznie wszystkich
miłośników portalu developers.pl.
Będziemy opowiadać o NLogu, projekcie, którego autorem jest Jarek
Kowalski i pierwsze pytanie: Czy
sama idea powstania NLoga wzięła
się od Ciebie, Jarku, czy też byłeś
zainspirowany czyimś pomysłem?
J: Sama idea logowania lub śledzenia,
czyli tego, czym zajmuje się NLog nie jest
nowa. Już dawno temu programiści dostrzegli potrzebę zapisania śladu działania aplikacji. Dzięki takiemu śladowi
można przeanalizować funkcjonowanie
programu już po jego zakończeniu.
Ode mnie pochodzi pomysł na - moim
zdaniem - sprawniejszą niż wszystkie
dotychczasowe implementację tego typu
narzędzia. Właśnie tym jest NLog – to implementacja mechanizmu logowania w
aplikacjach dotnetowych, bardzo prosta
w użyciu.
Jeśli chodzi o narzędzia, którymi NLog
był inspirowany, to oczywiście log4net,
o którym piszę na stronie projektu. NLog
„pożyczył” od log4net interfejs programistyczny (API) do logowania. Innym narzędziem tego typu jest Enterprise Library Microsoftu, które pod pewnymi względami też może być uważane za podobne,
aczkolwiek trudno mi to jednoznacznie
stwierdzić, bo dopiero poznaję EL.
L: Dla kogo jest stworzony projekt,
tzn. kim są odbiorcy i w jakim celu
należy go używać?
J: Są trzy zasadnicze grupy odbiorców.
Pierwszą z nich są deweloperzy. Używają
oni NLoga emitując komunikaty diagnostyczne, które raportują, co ich program
robi. Te komunikaty po ich włączeniu
w trakcie działania, pozwalają innym
osobom (na ogół zajmującym się testowaniem albo wsparciem technicznym
aplikacji) określić, gdzie leży przyczyna
błędu w aplikacji lub dlaczego jakiś przypadek testowy nie zadziałał.
Innymi słowy, programista wyposaża
aplikację w instrukcje śledzenia, a testerzy i grupy wsparcia korzystają z nich.
NLog służy do tego, żeby zapewniać tym
dwóm grupom odbiorców optymalny poziom wyjścia, który ich interesuje. Oczywiście deweloperzy zwykle także korzystają z wyjścia NLoga podczas tworzenia
aplikacji – cała sztuka polega na tym, aby
komunikaty diagnostyczne, nie zostały
na końcu usunięte czy zakomentowane a
jedynie wyłączone przy pomocy mechanizmów NLoga.
L: Jeśli miałbyś określić jednym
zdaniem, czym jest NLog, to jakich
wyrazów byś użył, które by odzwierciedlały istotę
J: Jest to silnik, dzięki któremu można
sterować przepływem komunikatów diagnostycznych programu.
L: Co wchodzi w skład projektu?
J: NLog składa się z jednej głównej biblioteki (NLog.dll), którą dołącza się do
naszej aplikacji i dwóch bibliotek pomocniczych, których używamy do śledzenia
aplikacji napisanych w starszych technologiach. Dla „klasycznego ASP” i języków skryptowych takich jak JScript czy
VBScript jest NLog.ComInterop.dll. Druga
biblioteka pomocnicza, NLogC.dll służy
do śledzenia aplikacji napisanych w C i
C++, które nie używają kodu zarządzanego.
L: Skąd się wzięła sama nazwa?
J: To jest bardzo dobre pytanie.
L: N jest charakterystyczne…
J: „N” jest charakterystyczne - generalnie wszystkie popularne biblioteki i
aplikacje dotnetowe (NUnit, NDoc, NAnt,
NPlot, …) opensource’owe mają nazwy
zaczynające się od „N” (dla odróżnienia od swoich pierwowzorów z Javy, które zaczynają się od „J”) - to było łatwe,
choć NLog nie ma swojego odpowiednika w świecie Javy, a „Log” – od logowania czyli śledzenia. Jest log4net, więc w
tej przestrzeni nazw nie było już miejsca,
został więc NLog. Jako ciekawostkę mogę powiedzieć, że ostatnio w świecie Javy
pojawił się projekt NLog4J, ale zbieżność
nazw jest zupełnie przypadkowa.
L: Jakie są wymagania, żeby skorzystać z NLoga, w jakich aplikacjach
można go użyć?
J: Generalnie: we wszystkich. NLog
jest właśnie tak pomyślany, żeby dało się
go użyć minimalnym nakładem pracy
w jak największej liczbie różnego typu
aplikacji. Jest bardzo proste wsparcie dla
aplikacji webowych (ASP.NET), desktopowych (WinForms), konsolowych, aplikacji typu Windows Service, serwery COM+
udostępniane za pośrednictwem COM
Interop. W zasadzie wymieniliśmy chyba
wszystkie możliwe rodzaje. Oprócz tego wspierane są aplikacje w starym ASP,
czyli używające COM-a oraz korzystające
z niezarządzanego C/C++, bez pośrednictwa COM-a.
L: W czym napisałeś NLoga, z czego
korzystałeś?
J: Biblioteka jest napisana w C#, z niewielkimi wstawkami (dodatkami) w
Managed/C++ (dla .NET 1.x) oraz C++/
CLI (w wersji .NET 2.0). Było to potrzebne do osiągnięcia możliwości logowania z aplikacji w niezarządzanym języku
C/C++ a także COM, coś co jest unikalne
dla NLoga.
L: Jak można skonfigurować NLoga?
Czy to jest jakiś plik XML-owy, czy
jest do tego jakaś specjalna konsola?
J: Konfiguracja odbywa się przez plik
xml-owy, przy czym dużo pracy włożyłem w to, żeby ten plik był jak najprostszy w użyciu i jak najbardziej czytelny.
To jest bardzo proste –minimalną konfigurację NLoga, która robi coś użytecznego można zapisać w kilku linijkach pliku
XML. Dodatkowe funkcjonalności nie
wymagają więcej niż kilku dodatkowych
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
wierszy. Zwykle konfigurację umieszczamy wewnątrz pliku konfiguracyjnego aplikacji (App.config/web.config), ale
może to być dowolny inny plik.
Przykład konfiguracji dla NLoga (Rysunek 1)
Analogiczna konfiguracja log4net – zobaczcie jak dużo jest tutaj nieistotnego
„szumu” informacyjnego w stosunku do
konfiguracji, którą ten plik zawiera (Rysunek 2)
Zastosowanie plików daje dużą elastyczność, bo plikami konfiguracyjnymi
można wygodnie zarządzać. Możliwe są
np. pliki dołączane (ang. include files)
czy też używanie zmiennych pozwalających w krótszy sposób zapisywać złożone warunki. Poniżej przykład, który w
kilku wierszach kodu pozwala wczytać
konfigurację logowania inną dla każdej
maszyny. Dzięki temu mamy scentralizowane repozytorium konfiguracji logowania dla całej aplikacji na wszystkich
serwerach.
<nlog>
<variable name=”logsDirectory”
value=”${basedir}/nlog-config/
${machinename}” />
<include file=”${logsDirectory}/config.xml”
/>
</nlog>
L: To wszystko opisane jest w dokumentacji?
J: Tak, na stronie http://www.nlogproject.org. Dodatkowo w pakiecie instalacyjnym NLoga jest plik z dokumentacją w formacie CHM. Takie tips&tricks
można także znaleźć na moim blogu
http://blog.jkowalski.net/
L: Czyli konfiguracja odbywa się
przez edytowanie pliku konfiguracyjnego?
J: Jest też możliwa konfiguracja programowa, bez pośrednictwa pliku, ale to jest
zagadnienie bardziej zaawansowane. W
skrócie polega to na utworzeniu obiektów
reprezentujących cele logowania i reguły a następnie przypisaniu ich do obiektu
LoggingConfiguration (Rysunek 3).
Możliwa jest też uproszczona konfiguracja, kiedy w jednej linii kodu włączamy
śledzenie do pliku sterując tylko poziomem logowania.
NLog.Config.SimpleConfigurator.ConfigureForFileLogging(„file.txt”, LogLevel.Debug);
L: I wtedy ten plik się generuje? Czy
po prostu jest narzędzie, które może
wygenerować ten plik?
J: Konfiguracja samego logowania to
jest po prostu drzewo obiektów w pamięci, a plik służy do tego, żeby drzewo tych
obiektów konfiguracyjnych wytworzyć.
Koncepcja podobna jak na przykład w
XAMLu, gdzie atrybuty XML mapują się
na atrybuty obiektów. Jeśli przyjrzymy
się powyższym przykładom, łatwo zauważymy, że w NLogu także istnieje odpowiedniość pomiędzy strukturą pliku
konfiguracyjnego (nazwami atrybutów) a
strukturą obiektów w pamięci (nazwami
23
ROZMOWA.NET
właściwości).
L: Gdzie i jak zapisywane są logi,
czyli ten output, który jest rezultatem działania NLoga? W jakich
formatach można zapisywać te logi?
Używasz pojęcia ‘targets’ w dokumentacji...
J: Tak, używam pojęcia „target”. Długo szukałem jakiegoś słowa, które byłoby tu dobre. Log4net używa pojęcia „appender”. To pojęcie wydaje mi się na tyle
mylące dla kogoś, kto pierwszy raz styka
się z aplikacją, że bardzo chciałem je zastąpić. Pojęcie „append” czyli doklejania/
dołączania w przypadku celów które polegają na wysłaniu maila wydaje mi się
nietrafione.
Jest takie klasyczne angielskie na to
słowo ‘sink’, ale po polsku to jest ‘zlew’,
więc uznałem, że target jako taki ‘cel’ logowania to będzie lepsze określenie. Cele
logowania są zupełnie różnorodne. Możemy logować do pliku (to jest główne i
najbardziej typowe zastosowanie, jeśli
mamy aplikację, która jest, np. usługą
systemową), możemy logować na konsolę
– jeśli mamy aplikację konsolową, możemy logować do bazy danych, możemy nasze komunikaty wysyłać mailem, wkładać do kolejki MSMQ, w przypadku aplikacji ASP.NET możemy udostępniać je na
stronie Trace.axd. Dostępnych celów jest
dużo, a jeśli brakuje nam jakiegoś mechanizmu wyjściowego, to bardzo łatwo go
napisać i to także jest pokazane na stronie projektu.
L: Coś w rodzaju własnego ‘providera’, stworzenie własnego ‘targeta’?
J: Cel w NLogu to jedna, zwykle bardzo
prosta klasa z metodą odpowiedzialną
za zapisanie odpowiednio obrobionego
komunikatu. NLog sam, jako silnik za-
pewnia tej klasie mechanizm zewnętrznej konfiguracji. Cel, który napiszemy nie
musi troszczyć się o to, jak z pliku XMLowego wczytać wszystkie parametry, tym
zajmuje się NLog.
L: A wbudowane cele? Czy jest możliwość ich modyfikacji w jakiś sposób,
przez np. jakiś plik XML-owy.
J: Każdy target można konfigurować.
Oprócz tego, że mówimy, że logujemy do
pliku, to oczywiście musimy powiedzieć,
gdzie ten plik jest. Jeśli logujemy do konsoli, to np. mamy do wyboru, czy logujemy do stdout, czy stderr.
Ponieważ cel jest tak naprawdę klasą,
to w niektórych przypadkach warto wykorzystać dziedziczenie i stworzyć własną klasę pochodną dodając lub modyfikując standardowe funkcje. Przykładowo,
możemy opracować własny mechanizm
szyfrowania lub kompresji logów i rozszerzyć cel „File” o te funkcje.
L: Czy użytkownik ma wpływ na końcowy efekt tego, co jest zapisywane
do tych logów? Czy mógłbyś o tym
opowiedzieć?
J: Może wpływać na to co trafia do logu
definiując reguły, filtry i warunki a także
określając sposób formatowania komunikatów.
Kierowaniem komunikatów do celu sterują reguły. Każdy komunikat ma swój
poziom i nazwę źródła (loggera). Reguła
określa cele, do których określone komunikaty (w sensie źródła) na określonych
poziomach logowania mają być kierowane. Dzięki temu mamy strukturę podobną
do tabeli routingu (Rysunek 4).
Po użyciu reguły możemy zastosować
jeszcze filtry, które pozwalają odsiać komunikaty z zastosowaniem bardziej wyrafinowanych kryteriów.
Filtry mogą badać takie elementy jak
np. treści komunikatów czy inne elementy informacji kontekstowej (jak np. data/
godzina, nazwa wątku, nazwa procesu,
itp.)
Na końcu, kiedy już wiemy, że komunikat trafi do logu, to możemy zadecydować o tym, w jakiej formie on tam trafi.
Oprócz samego komunikatu, który wypisał nam programista, możemy określić,
jakie dodatkowe informacje z miejsca wywołania będą do tego logu zapisywane.
Te dodatkowe informacje to mogą być
np.: data i godzina, kiedy taki komunikat
został wysłany. To jest dosyć oczywiste,
bo zwykle taką informację chcemy mieć.
Ale może też być informacja, jaki użytkownik był w kontekście bieżącej strony
ASP.NET aktywny lub jaka była wartość
zmiennej w sesji klasycznego ASP lub
ASP.NET (Rysunek 5).
To, co stanowi największą siłę NLoga,
to możliwość wykorzystywania informacji kontekstowych nie tylko do sterowania formatem wyświetlanych danych, ale
także do sterowania parametrami samego celu.
Przykładowo, jeden cel typu File może zapisywać komunikaty do wielu plików jednocześnie, w taki sposób, że dane
każdego użytkownika, bądź każdej sesji
ASP.NET trafią do osobnego pliku.
<target
name=”session-file”
type=”File”
filename=”${basedir}/${aspnet-sessioni-
d}.txt”
layout=”${longdate} ${logger} ${level}
${message}”
/>
Analogicznie możemy na przykład dla
każdego dnia tworzyć osobny plik:
Rysunek 1. Przykładowa konfiguracja NLoga
Rysunek 3. Konfiguracja programowa
<nlog>
// create a target
<targets>
FileTarget target = new FileTarget();
<target name=”file” type=”File” filename=”file.txt”
target.FileName = “file.txt”;
layout=”${longdate}|${message}” />
// create a configuration object and append a rule
</targets>
LoggingConfiguration config = new LoggingConfiguration();
<rules>
/>
<logger name=”My.Component.*” minlevel=”Debug” writeTo=”file”
</rules>
<rules>
<appender name=”file” type=”log4net.Appender.FileAppender”>
<appendToFile value=”true” />
<logger name=”My.Component.*” minlevel=”Debug” writeTo=”file,co
nsole,sql” />
<logger name=”*” minlevel=”Error” writeTo=”email” />
<logger name=”*” minlevel=”Fatal” writeTo=”pager” />
<layout type=”log4net.Layout.PatternLayout”>
<rules>
</layout>
<target
<conversionPattern value=”%date|%message “ />
</appender>
<logger name=”ConsoleApp.LoggingExample”>
<level value=”DEBUG” />
<appender-ref ref=”file” />
</logger>
</log4net>
24
// activate the configuration
Rysunek 4
Rysunek 2. Odpowiedni plik konfiguracyjny dla log4net
<file value=”file.txt” />
config.LoggingRules.Add(rule);
LogManager.Configuration = config;
</nlog>
<log4net>
LoggingRule rule = new LoggingRule(“*”, LogLevel.Debug, target);
Rysunek 5. NLog & ASP.NET
name=”file”
type=”File”
filename=”${basedir}/file.txt”
layout=”${longdate} ${aspnet-session:variable=UserID} ${messa-
ge}”
/>
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
<target
name=”session-file”
type=”File”
filename=”${basedir}/${shortdate}.log”
layout=”${longdate} ${logger} ${level}
${message}”
/>
Co utworzy nam pliki o nazwach takich jak bieżąca data:
·2006-05-15.log
·2006-05-16.log
·2006-05-17.log
L: I jest dowolna ilość tych dodatkowych elementów?
J: Absolutnie dowolna. NLog dostarcza kilkanaście, może kilkadziesiąt (nie
wiem, nie liczyłem tego) ‘layout renderers’. Nie wiem, jakie jest dobre polskie
określenie , ale mówię o tym jako informacjach kontekstowych z miejsca wywołania, dlatego taki termin pojawia się w
dokumentacji.
L: Czym są ‘log levels’?
J: Log levels to są poziomy logowania.
W skrócie chodzi o to, że różne komunikaty mają różną ważność.
L: Czy one są predefiniowane, czy
można je ewentualnie później zmodyfikować?
J: Tak, jak najbardziej predefiniowane,
co jest zupełnie czymś innym, niż w tych
dwóch konkurencyjnych narzędziach. W
log4net jest pięć predefiniowanych (Debug, Info, Warn, Error, Fatal) poziomów
logowania, ale można zupełnie łatwo
tworzyć własne i twórcy zachęcają do tego, że jeśli jest potrzebny poziom, to go
stwórzmy. NLog w stosunku do log4net
dodaje poziom Trace poniżej Debug. Z
tego, co się dowiedziałem, to poziom logowania w LAB jest dowolną liczbą całkowitą, co daje jeszcze większą dowolność.
Twierdzę, że zbytnia swoboda w zakresie poziomów logowania jest szkodliwa. Aplikacje, które będą używać NLoga
i trzymać się tych sześciu ustalonych poziomów logowania zgodnie z ich przeznaczeniem, tak jak jest ono opisane w dokumentacji, będą po prostu lepiej ze sobą
współpracować. Aplikacje i komponenty,
które będą używać absolutnie dowolnych
poziomów logowania w dowolny sposób,
będą potem trudniejsze w konfiguracji.
NLog dostarcza wskazówek do używania poziomów logowania, czyli jaka powinna być częstotliwość jeśli objętość komunikatów diagnostycznych wysyłanych
na każdym poziomie.
L: Czy są jakieś specyficzne obiekty,
których się używa w NLogu?
J: Zwykle używa się tylko dwóch obiektów i programista do swojej pracy nie potrzebuje nic więcej. Używamy obiektu
Logger (dla osób znających log4net to jest
mniej-więcej to samo, co interfejs ILog)
i klasy LogManager, która dostarcza statycznych metod pobierających te loggery.
To jest fabryka – implementacja wzorca
Factory. Ten mechanizm jest w zasadzie
identyczny jak w log4net, ale na tyk podo-
bieństwa się kończą.
Przy konfiguracji programowej (bez
użycia pliku konfiguracyjnego) wykorzystujemy dodatkowe klasy i obiekty, ale zachęcam w miarę możliwości do posługiwania się konfiguracją w XMLu.
L: Czy masz jakieś wskazówki dla
użytkowników, jakieś szczególne
zalecenia w związku z optymalizowaniem jego działania, chodzi mi
głównie o dobre praktyki korzystania z NLoga?
J: NLog sam w sobie jest bardzo mocno
zoptymalizowany, ale jest jedna rzecz, o
której należy pamiętać.
O ile możliwa jest bardzo sprawna eliminacja komunikatów na etapie wykonania programu, o tyle problemem może
być samo wywołanie metody logowanie,
kiedy konstrukcja parametrów wywołania metody trwa długo. Zobaczmy co się
wtedy dzieje na przykładzie:
logger.Debug(„Liczba wierszy w tabeli: „ +
command.ExecuteScalar());
Ten przykład zadaje zapytanie do bazy danych tylko po to, aby zapisać jego
wynik do logu. W sytuacji kiedy mamy
poziom logowania ustawiony tak, że komunikaty Debug nie są nigdzie zapisywane, okaże się że straciliśmy tylko czas, a
skutek będzie żaden. Dużo lepiej byłoby
najpierw sprawdzić czy komunikat ma
szansę zostać gdziekolwiek zapisany.
Używamy do tego mini-wzorca o nazwie
„strażnik”.
if (logger.IsDebugEnabled)
logger.Debug(„Liczba wierszy w tabeli: „ +
command.ExecuteScalar());
To jest tak naprawdę jedyna rzecz, na
którą należy uważać, a poza tym NLog
jest całkiem sprawny w usuwaniu tych
komunikatów. Analogicznie należy uważać przy składaniu łańcuchów znakowych:
logger.Debug(„a=” + a + „ b=“ + b + “ c=“
+ c);
Zauważmy, że niezależnie od poziomu
logowania musimy zawsze skonstruować
łańcuch znakowy, co jest dość kosztowne. Dużo lepiej jest posłużyć się parametrami, analogicznie jak przy Console.WriteLine(). Dzięki temu NLog nie wykona
w ogóle operacji formatowania, o ile nie
będzie ona potrzebna, co pozwala po raz
kolejny zaoszczędzić trochę czasu.
logger.Debug(“a={0} b={1} c={2}”, a, b, c);
L: Czy istnieje jakieś narzędzie do raportowania, wbudowane w NLoga?
A może jakieś oddzielne narzędzie,
które byś polecił? Takie, które
umożliwia, żeby wewnątrz projektu
była możliwość traceingu tego co się
dzieje.
J: O ile dobrze rozumiem, to chodzi o
takie narzędzie, dzięki któremu można
analizować potem logi...
L: Ten ‘internal logging’...
J: A, o tym mówimy… Internal logging
to jest wbudowane narzędzie, które pozwala sprawdzić, co się dzieje w samym
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
mechanizmie logowania, tak aby móc
diagnozować przypadki kiedy coś nie
działa tak jak oczekujemy. Na przykład,
kiedy nie tworzą pliki z logami, albo aplikacja znacząco zwalnia, włączamy wewnętrzne logowanie i dzięki temu stwierdzamy, że baza danych, do której mają
być zapisywane logi jest niedostępna lub,
że nie mamy uprawnień do utworzenia
plików.
Jedną z zasad NLoga jest ukrywanie
wewnętrznych wyjątków przed użytkownikiem, w taki sposób, aby aplikacja nie
załamywała się z powodu błędnie działającego mechanizmu logowania. W odróżnieniu od log4net w NLogu można opcjonalnie włączyć przekazywanie wyjątku do kodu aplikacji, co pozwala bardzo
szybko zdiagnozować gdzie jest problem.
L: Kto korzysta z projektu? Czy znasz
jakieś aplikacje, które używają
NLoga?
J: Aplikacje używające NLoga to dwie
zasadnicze grupy. Większość stanowią
projekty o zamkniętym kodzie źródłowym, jest też kilka projektów otwartych.
Największym projektem otwartym
używającym NLoga jest projekt Castle
(http://www.castleproject.org),
będący szkieletem aplikacji .NET z usługami takimi jak kontener Inversion of
Control, narzędzie typu transparent proxy, itp. Udostępnia on także w przezroczysty sposób usługi logowania oparte na
NLog lub log4net.
Istnieje komercyjna aplikacja o nazwie
MAS XTrace.NET, która służy do analizowania logów i współpracuje bardzo dobrze z NLogiem, podobnie zresztą jak z
log4net. Może analizować pliki w formacie XML bądź komunikaty otrzymywane
na żywo za pośrednictwem celu o nazwie
Network..
Projektów zamkniętych używających
NLoga jest sporo, o czym świadczą osoby,
które zgłaszają się do mnie z pytaniami
i uwagami, zarówno bezpośrednio, jak i
przez publiczne forum NLoga oraz listę
dyskusyjną.
L: Jak długo pracowałeś nad pierwszą wersją?
J: Bardzo krótko. Generalnie NLog się
zrodził jako wyraz frustracji mechanizmem konfiguracyjnym, który udostępniał log4net. Dostawałem od kolegów
z zespołu sygnały, że oni po prostu nie
wiedzą jak to skonfigurować do naszych
potrzeb i to jest tak nieoczywiste, że oni
nie mają tej godziny, czy dwóch, żeby
przegryzać się przez dokumentację w Internecie.
Zaczęliśmy się zastanawiać, jak by mogła wyglądać szybka, sprawna i czytelna konfiguracja. Wtedy powstał pomysł
takiego silnika opartego o routing logów,
zamiast podejścia hierarchicznego, jakie
jest w log4net. Pierwsza wersja powstała
– o ile pamiętam – w kilka dni.
L: Na rynku istnieje wiele narzędzi
służących do generowania logów,
25
ROZMOWA.NET
np. Microsoftowy Logging Application Block, który wchodzi w
skład Enterprise Library, czy też
wspomniany przez Ciebie log4net.
Jakie są więc różnice pomiędzy tym
Twoim podejściem a tymi istniejącymi już narzędziami i dlaczego warto
korzystać akurat z NLoga? Tak, jak
już wspomniałeś, zasada działania
jest troszeczkę inna, bo tam korzyta
się z tego log routing engine...
J: NLog jest z natury rzeczy bardzo podobny do log4net. W sensie użytego interfejsu programistycznego można powiedzieć, że jest klonem, ale w zakresie tego,
co się dzieje pod spodem, czyli samych
mechanizmów sterowania, to jest zupełnie inna technologia. Log4net postawił na
rozszerzalność przez użycie olbrzymiej
ilości wzorców projektowych (moim zdaniem jest ich tam po prostu za dużo). Powoduje to, że logowanie jest spowalniane
przez taką nadmierną dbałość o potencjalną (moim zdaniem bardzo potencjalną) rozszerzalność. Kiedyś mówiłem, że
NLog od log4neta różni się tym, że NLog
nigdy nie stanie się lodówką, a log4net
dzięki swojej abstrakcji i interfejsom
– być może...
Log4Net jest też bardzo blisko spokrewniony z log4j, narzędziem, które powstało po to, aby wyeliminować niedoskonałości platformy podstawowej Javy w
zakresie logowania. Problemem było na
przykład to, że w Javie przez długi czas
nie występowały funkcje ze zmienną liczbą argumentów, narzędzie do formatowania tekstów (takie jak String.Format(),
czy też przezroczysty boxing typów prostych – wprowadzony dopiero w Java 1.5).
Moim zdaniem pewnych cech log4j nie
należało przenosić, dlatego że platforma
.NET udostępnia wiele mechanizmów,
pozwalających pewne rzeczy zrobić po
prostu lepiej.
Wracając jeszcze do abstrakcji log4net,
jeśli przyjrzymy się przykładowi najprostszego pliku konfiguracyjnego log4net, zwróćmy uwagę na to, ile zupełnie
nieistotnych pojęć zostało wykorzystanych w tym prostym przykładzie:
·type=”log4net.Appender.FileAppender” – dlaczego użytkownik musi posługiwać się nazwami klas w pliku konfiguracyjnym? W NLogu wystarczy samo „File”. Chyba łatwiej to zapamiętać, prawda?
·<layout type=”log4net.Layout.PatternLayout”> - znowu posługujemy się zupełnie nieistotną nazwą klasy. W NLogu
jest jedna klasa do layout’ów, gdyż w 99%
wszystkich przypadków mamy do czynienia z tekstami.
·<conversionPattern> – kolejne pojęcie,
które nic nie wnosi do zapisu, a jest potrzebne ze względu na wykorzystaną abstrakcję
Moim zdaniem użytkownik nie powinien spędzać na poznanie takiego narzędzia jak NLog czy log4net zbyt wiele czasu, tak, aby mógł się skupić na właściwej
26
pracy. Zmniejszanie poziomu abstrakcji i
upraszczanie tylko temu służy.
Co do Ent. Lib to trudno mi powiedzieć,
bo dopiero kilka dni temu zacząłem się
temu mocniej przyglądać, ale na pierwszy
rzut oka wygląda na to, że podczas gdy
NLog jest z założenia przenośną aplikacją, która może działać także w środowisku Mono, Ent. Lib jest mocniej związany
z Windowsem.
L: To funkcjonowalo wcześniej jako
oddzielny...
J: Logging Application Block, wiem. To
jest pakiet (czemu zresztą nie należy się
dziwić) praktycznie przywiązany do platformy Windows poprzez to, że korzysta z
mechanizmów takich specyficznych dla
Windowsa jak WMI, czy EventLog.
Konfiguracja również wygląda na mocno „przegadaną” – chyba nawet bardziej
niż log4net. Na pierwszy rzut oka widać,
że około 10% pliku konfiguracyjnego
LAB’a zajmuje wielokrotnie powtarzany
tekst: „Microsoft.Practices.EnterpriseLibrary.Logging”. Co on wnosi? Nie mam
pojęcia…
LAB sprawia wrażenie „cięższego”. Z
różnych testów opublikowanych w Internecie dowiedziałem się, że jest dużo (kilka rzędów wielkości) wolniejszy od log4neta, a ponieważ NLog jest szybszy od
log4neta, to należy się spodziewać, że jest
też szybszy od Ent.Lib. Ale więcej szczegółów nie znam, to pewnie jest temat na
osobny artykuł, kiedy uda mi się ogarnąć
LAB i przeprowadzić testy wydajnościowe.
L: Czyli testowałeś szybkość działania?
J: Tak, testowałem szybkość działania. NLog jest bardzo szybki. Twierdzę,
że dzięki odpowiedniej architekturze nie
da się zrobić tego, co robi NLog znacząco
szybciej. Prawie każda decyzja o tym, czy
coś zalogować, czy nie, polega po prostu
na sprawdzeniu jednej wartości typu boolean, która została wstępnie przeliczona.
W log4necie to jest mozolny proces wyliczania poziomów logowania, przechodzenia hierarchii loggerów, co zajmuje czas.
W NLogu, dzięki odpowiednim strukturom danych logowanie może być bardzo,
bardzo szybkie.
L: Czyli w czym najlepiej zapisywać
logi, tak aby to było optymalne pod
względem prędkości?
J: Jest kilka opcji…
L: Przypuśćmy, że mamy jakąś
bardzo dużą aplikację komercyjną.
Gdzie powinniśmy zapisywać te
logi?
J: Musimy odróżnić dwie rzeczy: szybkość zapisywania samych logów, ona
jest ze swojej natury ograniczona przez
szybkość nośnika, na który zapisujemy.
Zapis do pliku jest - wiadomo - ograniczony przez wydajność dysków, wydajność
podsystemów cache’owania, dostępną pamięć, wydajność zapisu do bazy danych
zależy od serwera tejże bazy.
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
L: Czy istnieje możliwość zapisu
asynchronicznego?
J: Istnieje, jak najbardziej. Polega na
tym, że dla każdego celu logowania możemy zdefiniować dodatkowe opakowania, które modyfikują jego działanie. Ta
funkcja została wprowadzona w wersji
0.9 i jest częścią ogólniejszego mechanizmu „wrapper targets”. Jednym z takich
„opakowań” jest logowanie asynchroniczne, który kolejkuje wszystkie komunikaty, które dostaje. Osobny wątek, działający w momencie, kiedy jest trochę wolnego
czasu, przekazuje komunikaty do właściwego celu, którym może być plik/baza
danych czy cokolwiek innego.
Tak jak powiedziałem, sam zapis odbywa się trochę później, niż logowanie
synchroniczne, ale buforowanie skutkuje
większą wydajnością i mniejszym obciążeniem wątku wywołującego. Instrukcje
logowania szybciej się kończą, a to jest
bardzo istotne.
L: Czy potrzebujesz pomocy przy
pracy i jeśli tak, to do kogo się należy
zgłosić, gdzie zasięgnąć informacji?
J: Jak najbardziej, zawsze potrzebuję.
Najbardziej zależy mi na zgłoszeniach
błędów i usterek, których można dokonywać za pomocą systemu do bug-trackingu o nazwie TRAC (http://trac.nlogproject.org/nlog)
L: Czy sam tworzyłeś NLoga, czy były
osoby, które wspomagały Twoją
pracę?
J: Można powiedzieć, że w zdecydowanej większości sam. Dostałem kilka zewnętrznych fragmentów kodu, które ktoś
napisał, dlatego, że brakowało mu jakiejś
funkcji. Po przetestowaniu włączyłem te
fragmenty ją do dystrybucji, ale nie ma
tego zbyt dużo - w sumie kilkadziesiąt
linii kodu. Cała reszta to jest moje dzieło,
ale zachęcam do tego, żeby jak najwięcej osób włączyło się do tego projektu, bo
nigdy dosyć dobrych pomysłów.
L: W jaki sposób się z Tobą skontaktować? Jest strona projektu…
J: Tak. Na tej stronie są wszystkie możliwości skontaktowania się ze mną, to jest
głównie lista dyskusyjna projektu, która
jest dosyć aktywna jak na taki - w sumie
niewielki - projekt. Jest forum, jeśli ktoś
nie chce się zapisywać na listę dyskusyjną, to może szybko na forum wysłać pytanie i szybko dostać odpowiedź
L: Możesz podać adres strony?
J: http://www.nlog-project.org/
L: Czy uważasz, że są jakieś elementy, których brakuje w NLogu, czy
myślisz o jakimś sposobie wzbogacenia projektu?
J: Co jakiś czas przychodzi taka chwila,
kiedy myślę, że w NLogu jest już wszystko. Po pewnym czasie przychodzi mi do
głowy kolejna prosta funkcja, którą zwykle dość szybko implementuję. W tym
sensie nie „długo żyjących” brakujących
elementów.
L: Jak wygląda przyszłość NLoga?
Czy zamierzasz zakończyć rozwój w
jakimś konkretnym czasie?
J: Chciałbym możliwie szybko wydać
wersję 1.0. Wersja 1.0 to takie miejsce w
rozwoju każdego projektu, kiedy wiele
firm decyduje się na używanie tego projektu. To jest dziwne, ale wiele firm decyduje się na używanie projektów, które
mają przed kropką liczbę większą od zera. Czyli, jeśli tam jest jedynka, to projekt
jest dobry, a jeśli tam jest zero, to projekt
jest zły. Nie świadczy to zupełnie o użyteczności czy jakości projektu, ale niektóre firmy takie podejście stosują.
L: Narzędzie będzie cały czas opensource’owe?
J: Tak. Licencja BSD, nie nakłada na
osobę lub firmę, która używa tego oprogramowania żadnych drastycznych wymagań. Jedyne wymaganie, jakie takie,
żeby nie zacierać źródła pochodzenia
NLoga. Czyli jeśli używamy NLoga w
swojej aplikacji, to nie ukrywamy tego.
Jeśli gdzieś wyświetlamy informację o
prawach autorskich, to jedna linijka z informacją, że używamy NLoga jest wskazana. To w zasadzie wszystko.
L: Czy zmiany w Twoim życiu zawodowym, o których wiem, nie będą
przeszkodą w rozwoju NLoga?
J: No dobrze, wydało się… [śmiech].
Zmiany w moim życiu zawodowym polegają na tym, że dostałem pracę w Microsofcie w Redmond i od października tego
roku przeprowadzam się do USA. To zapewne będzie miało wpływ na możliwość
rozwijania przeze mnie NLoga, zależy mi
na wydaniu wersji 1.0 i znalezieniu prężnej grupy następców, którzy będą dalej
ten projekt dynamicznie rozwijać.
L: Dziękuję Ci, Jarku za rozmowę, ja
mam nadzieję, że NLog będzie nadal
rozwijany i że za Twoim przykładem
w Polsce będzie coraz więcej programistów tworzących tak przydatne
narzędzia opensource’owe.
Z Jarkiem Kowalskim rozmawiali
Maja Ciemienga i Michał Grzegorzewski
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
27
TIPS & TRICKS
Inny kolor tła
w aplikacji MDI
D
omyślny kolor tła w aplikacji
MDI może wydawać się nieciekawy. Niestety nie ma standardowej
możliwości ustawienia tego koloru
– skazani jesteśmy na kolor ControlDark. Ale czy aby na pewno?
Każdy formularz Windows Forms
posiada właściwość Controls reprezentującą kolekcję kontrolek
hostowanych przez siebie. Jeśli podejrzymy zawartość tej kolekcji po
uruchomieniu programu, zauważymy kontrolkę typu MdiClient. Co
mówi na jej temat MSDN? “Reprezentuje kontener dla formularzy potomnych aplikacji MDI”. Nieźle, czyli
dokładnie to, czego szukaliśmy. Nie ma,
co prawda, możliwości dostać się do niej
bezpośrednio, ale można ją przecież wyszukać w kolekcji kontrolek formularza:
foreach (Control ctl in this.Controls)
{
Rysunek 1. Tło aplikacji MDI
client = ctl as MdiClient;
if (client != null)
break;
}
I nic nam w tej chwili nie zabrania
zmiany koloru wnętrza formularza na
bardziej przyjemny bądź umieszczenia w
środku własnego tekstu (poprzez zdefiniowanie metody obsługi zdarzenia Paint) (Rysunek 1).
Kontrolka PropertyGrid
z regulowanym
podziałem okna
Pracując z kontrolką PropertyGrid
można dość szybko zauważyć pewien
denerwujący mankament – po zainicjowaniu kontrolka ustawia splitter na domyślną pozycję 50% i nie ma możliwości
zmiany tej wartości z poziomu programu.
Można oczywiście zmieniać położenie
podziałki myszka, ale na dłuższą metę staje się męczące, kiedy przy każdym
uruchomieniu okienka z ustawieniami
aplikacji trzeba zmieniać położenie podziałki, gdyż wartości ustawień są nieczytelne.
Na szczęście istnieje możliwość rozszerzenia funkcjonalności klasy PropertyGrid o właściwość LabelRatio,. W
tym celu należy oczywiście utworzyć nowa kontrolkę, dziedziczącą z PropertyGrid .
Kontrolka PropertyGrid jest kontrolką złożoną - składa się z kilku elementów podstawowych. Ta odpowiedzialna
za wnętrze, to kontrolka typu PropertyGdidView. Niestety typ ten nie
jest dostępny publicznie, a aby móc operować na jego składowych potrzebujemy
do niego referencji. Tutaj z pomocą przy-
Rysunek 2
Rysunek 3
private Assembly wfAssembly;
private Control pgvCtl;
private Type pgvType;
private Control PropertyGridView
{
private Type PropertyGridViewType
{
get
{
get
{
if (pgvCtl == null)
{
if (pgvType == null)
{
foreach (Control ctl in Controls)
{
if (wfAssembly == null)
wfAssembly = Assembly.GetAssembly(typeof(PropertyGrid));
}
}
28
}
if (ctl.GetType() == PropertyGridViewType)
{
pgvType = wfAssembly.GetType(
dView”,
chodzi nam mechanizm refleksji
i metoda GetType() klasy Assembly. Metoda ta zwraca obiekt typu Type reprezentujący dowolny
typ .NET; wszystko co
trzeba znać to pełna
nazwa typu i assembly w którym ów typ
jest zdefiniowany
(Rysunek 2).
Zmienna wfAssembly powyżej reprezentuje tutaj referencję do komponentu Windows Forms.
Następnym krokiem będzie uzyskanie referencji do kontrolki
reprezentującej wnętrze PropertyGrid (Rysunek 3).
Jak już wcześniej wspomniano,
PropertyGridView jest typem
niedostępnym publicznie, wiec referencję do naszej kontrolki będziemy musieli
przechowywać w zmiennej typu Control.
Szybki podgląd pól naszej kontrolki,
na przykład w debuggerze VS, pokazuje
istnienie pola publicznego typu double
o nazwie labelRatio. Eksperymenty z
różnymi wartościami tego pola pokazują:
• Domyślne labelRatio równe 2
oznacza położenie splittera w połowie
kontrolki
• labelRatio równe 1 powoduje pokazanie tylko części z nazwa właściwości
• labelRatio równe 100 powoduje praktycznie pokazanie tylko części z
wartościami właściwości
Stąd można się domyśleć, że odwrotność wartości labelRatio określa jaki procent zawartości kontrolki zostanie zarezerwowany dla części z nazwą właściwości. Korzystając z tych informacji można
zmapować labelRatio do zmiennej
typu int określającej, jaką część procen-
„System.Windows.Forms.PropertyGridInternal.PropertyGrifalse, true);
}
return pgvType;
}
}
}
}
pgvCtl = ctl;
break;
return pgvCtl;
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
towo zajmuje nazwa właściwości (Rysunek 4).
Niestety labelRatio jest polem a nie
właściwością zdefiniowana w klasie. A
to oznacza, że zmiana tej wartości nie
pociąga za sobą automatycznej zmiany wyglądu kontrolki. Nie pomaga także
wywoływanie Invalidate() – podział
pozostaje taki, jaki był zdefiniowany dla
poprzedniej wartości.
Tu z pomocą przychodzi dowolne narzędzie pozwalające na podgląd zawartości pliku biblioteki .NET (np. dołączony do .NET FW ILDasm bądź darmowy .NET reflektor). Po załadowaniu
biblioteki Windows Forms (Plik System.Windows.Forms.dll jest dostępny
z linii poleceń i znajduje się w katalogu
%windir%\assembly\GAC _ MSIL\
System.Windows.Forms\2.0.0.0 _ _
b77a5c561934e089) jesteśmy w stanie
przejrzeć listę metod klasy PropertyGridView. Uwagę zwraca prywatna metoda MoveSplitterTo(), która okazuje
się być odpowiedzialna za przesuniecie
splittera w określona pozycje kontrolki.
Ponieważ jednak LabelRatio reprezentuje przesuniecie procentowe, a MoveSplitterTo() przesuwa splitter do
pozycji bezwzględnej, należałoby wcze-
ka posiada pionowy pasek przewijania.
Analiza kodu metody MoveSplitterTo() pokazuje jednak, że na początku
odczytuje ona rozmiar wewnętrzny kontrolki za pomocą prywatnej metody, GetOurSize(). I to juz wystarczy do napisania części set właściwości LabelRatio (Rysunek 5)
Definicja lokalnej własności kontrolki
GetOurSize jest praktycznie identyczna
jak ta dla MoveSplitter, wiec została
pominięta. Warto także ograniczyć zakres wartości dla LabelRatio, gdyż dla
wartości ekstremalnych jedna bądź druga
część kontrolki staje się nieczytelna (Rysunek 6).
Sprawdzenie typu wartości
Rysunek 6
śniej przeliczyć wartość procentową na
bezwzględną. I tutaj trafiamy na kolejny
problem: po wywołaniu MoveSplitterTo() labelRatio jest przeliczane na
podstawie wewnętrznego rozmiaru kontrolki, który może się różnić od rozmiaru
rzeczywistego, jeśli na przykład kontrol-
Rysunek 4
private FieldInfo fiLabelRatio;
{
get
{
}
}
}
}
if (fiLabelRatio == null)
{
}
fiLabelRatio = PropertyGridViewType.GetField(„labelRatio”);
public int LabelRatio
{
return fiLabelRatio;
get
{
}
{
{
return Convert.ToInt32(100 / labelRatio);
set
get
if (value < 10 || value > 90)
return;
double labelRatio = Convert.ToDouble(
Size s = (Size)GetOurSize.Invoke(PropertyGridView, null);
return Convert.ToInt32(100 / labelRatio);
MoveSplitter.Invoke(PropertyGridView, new object[] { newVa-
labelRatioField.GetValue(PropertyGridView));
}
double labelRatio = Convert.ToDouble(
labelRatioField.GetValue(PropertyGridView));
public int LabelRatio
}
return moveSplitterMethod;
[Category(„Appearance”)]
[Category(„Appearance”)]
{
BindingFlags.InvokeMethod);
}
private FieldInfo labelRatioField
Załóżmy następującą sytuację. Mamy
wywołać metodę. Sygnaturę tej metody
zostanie pobrana dynamicznie w programie. Może posiadać ona jeden lub więcej
parametrów, informacje o nich zostaną
odczytane poprzez MethodInfo. Wartości parametrów zostaną podane w polach
tekstowych aplikacji Windows Forms.
Na pierwszy rzut oka trywialne zadanie komplikuje się, gdy przyjrzeć mu się
bliżej. Bo choć każdy obiekt .NET oferu-
int newValue = value*s.Width / 100;
lue });
}
Rysunek 5
}
private MethodInfo moveSplitterMethod;
Rysunek 7
{
private static bool TryConvert(string Value, Type t, out object
private MethodInfo MoveSplitter
get
{
Converted)
{
if (moveSplitterMethod == null)
{
moveSplitterMethod = PropertyGridViewType.GetMethod(
„MoveSplitterTo”,
BindingFlags.Instance |
BindingFlags.NonPublic |
Converted = null;
if (t == typeof(string))
{
}
Converted = Value;
return true;
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
29
TIPS & TRICKS
Rysunek 8
Type tout = Type.GetType(t.FullName + „&”, false);
}
// TryParse first - they are cheaper
{
catch (TargetInvocationException ex)
MethodInfo mi = t.GetMethod(„TryParse”,
new Type[] {
typeof(NumberStyles),
}
typeof(IFormatProvider),
mi = t.GetMethod(„Parse”, new Type[] {
typeof(string),
object []InvokeParams = new object[] {
Value,
typeof(IFormatProvider) });
{
NumberStyles.Any,
if (mi != null)
CultureInfo.CurrentCulture,
try
Converted };
{
bool bResult = (bool)mi.Invoke(null, InvokeParams);
Converted = InvokeParams[3];
CultureInfo.CurrentCulture });
}
{
object []InvokeParams = new object[] { Value, Converted };
}
Rysunek 9
// TryParse not found, try with parse
mi = t.GetMethod(„Parse”, new Type[] {
typeof(string),
typeof(NumberStyles),
typeof(IFormatProvider) });
if (mi != null)
try
{
Converted = mi.Invoke(null, new object[] {
}
}
CultureInfo.CurrentCulture });
je metodę ToString(), która nadpisana
może przekonwertować go do łańcucha
tekstowego, to typ string nie oferuje
metody ToObject(Type t) konwertującej łańcuch na konkretną wartość. Warte podkreślenia jest, że nie możemy tutaj
użyć klasy Convert, gdyż ta wymaga
znajomości typu przed kompilacją, czyli
informację, której nie posiadamy w tym
momencie.
Jeśli typy metody zawęzimy do typów
ValueType, to możemy skorzystać z tego, ze większość z nich posiada metodę Parse(), konwertującą string do
zmiennej odpowiedniego typu. Z tą metoda jest jednak związany pewien mankament: jeśli dany łańcuch nie reprezentuje zmiennej określonego typu, zgłoszony
zostaje wyjątek FormatException. A
obsługa wyjątków w .NET jest droga. Bardzo droga... Dlatego tez, jeśli jest to tylko
30
throw ex.InnerException;
mi = t.GetMethod(„Parse”, new Type[] { typeof(string)});
if (mi != null)
try
{
}
Converted = mi.Invoke(null, new object[] { Value});
return true;
catch (TargetInvocationException ex)
{
Value,
NumberStyles.Any,
if (ex.InnerException is FormatException)
return false;
bool bResult = (bool)mi.Invoke(null, InvokeParams);
return bResult;
return true;
catch (TargetInvocationException ex)
if (mi != null)
Converted = InvokeParams[1];
Converted = mi.Invoke(null, new object[] {
Value,
return bResult;
mi = t.GetMethod(„TryParse”, new Type[] { typeof(string), tout });
{
throw ex.InnerException;
Rysunek 10
tout });
if (mi != null)
}
if (ex.InnerException is FormatException)
return false;
typeof(string),
{
return true;
if (ex.InnerException is FormatException)
return false;
}
throw ex.InnerException;
możliwe zalecane jest stosowanie wprowadzonej w .NET FW 2.0 metody TryParse(). Metoda ta, w przypadku powodzenia konwersji zwraca wartość true, a
w przypadku błędu false nie produkując przy tym wyjątku.
Znów, aby wykonać konwersję będziemy musieli odwołać się do mechanizmów
refleksji.
Przypadek pierwszy: musimy wykonać
konwersje łańcucha do łańcucha. Zadanie
proste i zawsze kończące się powodzeniem (Rysunek 7)
Przypadek drugi: użycie metody TryParse. Tutaj sprawa się komplikuje, gdyż
typy maja zazwyczaj więcej niż jedna
zdefiniowana metodę TryParse. Dodatkowo zmienna wyjściowa jest oznaczona modyfikatorem out, co oznacza, ze jej
typem w wywołaniu TryParse nie jest
t tylko referencja t, przykładowo wiec,
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
jeśli konwersja ma być przeprowadzona
do typu int, to sygnatura metody TryParse dla tego typu będzie wymagała typu int & . Co także ważne, przekazanie
zmiennej Converted do listy parametrów metody Invoke nie wystarczy, gdyż
lista ta będzie zawierała kopie obiektów
przekazanych do konstruktora. Z tego
powodu po wykonaniu metody należy
odczytać wartość bezpośrednio z tablicy
obiektów użytej jako parametr dla metody. Wszystko to jest pokazane w kodzie
na Rysunku 8.
Przypadek trzeci: metoda Parse. Znowu mamy tutaj do czynienia z metodą
wielokrotnie przeciążoną i próbować należy kilku wariantów, zaczynając od tych
najbardziej złożonych.
Ważne jest, że jeśli konwersja się nie
uda, to zwrócony zostanie wyjątek TargetInvokationException. Właściwy
wyjątek zwracany przez metodę Parse
Rysunek 11
BigInteger i1 = new BigInteger(„99999999999999999999”);
BigInteger i2 = new BigInteger(„99ADEFFFA978361ACBD”, 16);
BigInteger i3;
i3 = i1 + i2;
// nie dziala
i3 = i1.add(i2);// dziala
Rysunek 12. Kod dla btnPierwszy
string Zapytanie = „SELECT Nazwa, NIP, Adres FROM FIRMY”;
OleDbCommand Polecenie = new OleDbCommand(Zapytanie, Polacz);
OleDbDataAdapter Adapter = new OleDbDataAdapter(Polecenie);
Adapter.Fill(dataSet1, Zapytanie);
dataGridView1.DataSource = dataSet1.Tables[Zapytanie];
jest opakowany we właściwości InnerException, którą to należy sprawdzić
(Rysunek 9).
Pozostają juz tylko inne wersje Parse
(Rysunek 10).
To juz koniec listy, wiec zwracamy
false, co oznacza ze nie jesteśmy w stanie dokonać konwersji do podanego typu.
// Parse not found, cannot convert
}
return false;
Samo rozwiązanie można by zapewne spróbować poprawić i wyeliminować
przykładowo boxing występujący przy
przypisywaniu zmiennej ValueType do
zmiennej typu object. Nic oczywiście
nie stoi też na przeszkodzie, aby te metodę rozszerzyć o własną funkcjonalność,
jak np. interpretację wyliczeń czy typów
konstruowanych z łańcuchów. To juz jednak zostawiam inwencji czytelnika.
Sławek Procelewski
Z PHP na .NET
– budujemy od nowa ?
Jedną z najpopularniejszych platform
aplikacji webowych była, i jest nadal,
kombinacja określana skrótem LAMP
– Linux, Apache, MySQL, PHP. Ze względu na koszty (praktycznie zerowe koszty oprogramowania), platforma ta jest
wszechobecna.
Wyobraźmy sobie firmę, która do tej
pory całe swoje oprogramowanie, obsługujące kluczowe elementy biznesu, posiada na platformie LAMP. Pewnego dnia,
po wizycie na jednej z konferencji developerskich, kierownik projektu uznaje iż
trzeba przejść na platformę .NET, gdyż
posiada ona wiele zalet których nie da się
wykorzystać na obecnej platformie, przy
czym jest „prawie” darmowy – jedyny
koszt to licencje innego systemu operacyjnego niż dotychczas. Decyzja zapadła – przechodzimy na .NET, tylko jak ?
Migracja całego tworzonego przez wiele
miesięcy systemu na .NET i język C# zajęłaby kolejne miesiące, nie wspominając o edukacji programistów. Przecież nie
będziemy na serwerach Windowsowych
instalować Apache’a, modułu PHP a później tworzyć jakieś dziwne wrappery na
skrypty PHP. Ale zaraz – przecież istnieje
implementacja języka Python pod .NET
(IronPython) dostępna na witrynach firmy Microsoft (twórca IronPythona został
zatrudniony w zespole CLR i rozwija go
już jako produktu Microsoftu), więc pewnie jest i PHP… Odpowiedź brzmi: „i tak
i nie” – istnieje rozwiązanie, ale gigant z
Redmond jeszcze nie zdecydował się na
wsparcie.
Idea Phalanger’a, bo o nim będzie mowa, powstał rękoma programistów z Czech
w 2003 roku, jako projekt uniwersytecki. Idea jaka im przyświecała była prosta
– umożliwić integrację języka PHP z platformą .NET oraz ASP.NET. Phalanger to
między innymi kompilator PHP. Oczywiście kompilatory skryptów PHP już istnieją – jak chociażby płatny produkt firmy
Roadsend, czy też darmowy projekt eAccelerator. Jednakże dzięki kompilatorowi
Phalangera kod PHP jest kompilowany do
MSIL, języka „maszynowego” platformy
.NET. Więcej szczegółów implementacyjnych poznacie na stronach projektu, natomiast teraz spójrzmy jak możemy go wykorzystać i czy rzeczywiście warto.
Jednym ze scenariuszy wykorzystania
możliwości Phalangera, jest umieszczenie aplikacji PHP na serwerze normalnie jak do tej pory – bezpośrednie użycie
skryptów PHP poprzez serwer WWW.
Jednakże z „drobną” różnicą – zamiast
modułu PHP, przekierujemy skrypty do
mechanizmu ASP.NET, który wykorzysta
biblioteki Phalangera. Dzięki takiej kombinacji korzystamy na możliwości kompilacji - skrypty tak uruchomionej aplikacji
przy pierwszym odwołaniu są kompilowane i każde kolejne odwołanie jest szybciej obsłużone.
Kolejnym scenariuszem może być kompilacja aplikacji do :
- pliku wykonywalnego EXE, który
można normalnie uruchomić bez potrzeby posiadania interpretera PHP. Oprócz
zwykłych aplikacji konsolowych, w PHP
możemy tworzyć aplikacje okienkowe
– wykorzystując pochodzący ze środowiska „otwartego kodu” GTK (PHP-GTK),
lub też „podpiąć” się do funkcji systemu
Windows (WinBinder).
- biblioteki DLL, którą można wykorzystać w innej aplikacji platformy .NET. Wyobraźmy sobie sytuację w której posiadaZ I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
my wiele wartościowych elementów aplikacji – takich jak obiekty biznesowe, czy
też ciekawe biblioteki, i chcielibyśmy ich
użyć w aplikacji pisanej w C#. Dzięki Phalangerowi kombinacja taka jest możliwa.
- biblioteki DLL stanowiącej skompilowaną aplikację Web (WebPages.dll),
gotową do użycia wraz z mechanizmem
ASP.NET bez wstępnej kompilacji skryptów. W bibliotece tej można umieścić
praktycznie wszystkie elementy aplikacji
- łącznie z warstwą prezentacji.
A jak to jest z wydajnością rozwiązania ? Jest dobrze. Ba – nawet lepiej niż dobrze. Wspomniałem wcześniej iż uruchamiając skrypt jest on kompilowany i przy
kolejnym wywołaniu nie jest interpretowany od nowa. Dodatkowo używane są
napisane w zarządzanym języku moduły,
np. do komunikacji z SQL Serverem, co
zapewnia znacznie większą wydajność
niż użycie natywnych driverów.
Phalanger to również rozbudowana biblioteka klas, dzięki którym możliwe jest
korzystanie z funkcji i klas PHP w projektach .NET – za przykład można podać
chociażby wykorzystanie stworzonej dla
PHP biblioteki Ming do generowania animacji Flash.
Niestety – obecna wersja Phalangera (1.0) nie pozwala na użycie bibliotek
.NET pod PHP, w związku z czym nie połączymy naszej aplikacji w PHP z rozwiązaniem stworzonym w C#. Oczywiście
istnieje metoda obejścia tego – rozbudowanie biblioteki klas Phalangera, ale to
rozwiązanie nie jest polecane dla bibliotek które użyjemy tylko raz. Jednakże
problem ten zostanie rozwiązany wraz z
wersją 2.0.
Wiele obecnych, popularnych aplikacji PHP – na przykład zarządzające serwerem MySQL PhpMyAdmin, czy też forum dyskusyjne phpBB, współpracuje z
ASP.NET bez żadnych zmian w kodzie.
Niekiedy jednak zmiany w kodzie są konieczne – PHP wywodzi się w końcu z
systemów o otwartym kodzie źródłowym,
i zawiera jeszcze wiele funkcji specyficznych dla tych systemów, lub też programiści PHP na siłę używają rozwiązań zależnych od systemu operacyjnego.
Obecna wersja Phalangera – 1.0 przeznaczona jest dla .NET Framework 1.1.
Również środowisko programistycznego
jest przeznaczone tylko dla Visual Studio
2003. Jednakże użycie skompilowanych
na platformę 1.1 bibliotek w aplikacji pisanej na platformie 2.0 nie stanowi żadnego problemu. Kolejna wersja Phalangera, pod .NET Framework 2.0 oraz Visual
Studio 2005, zapowiadana jest na lipiec
tego roku.
Przykłady i więcej ciekawostek na temat projektu możecie znaleźć na witrynie
projektu, jak i parę znajdzie się na moim
blogu na developers.pl.
Link:
http://www.php-compiler.net/
Radek Zawartko
31
TIPS & TRICKS
Q&A
Jaki typ zmiennej możemy użyć dla
liczb całkowitych powyżej 28 cyfr?
Należy odwołać się do biblioteki
vjslib.dll, która jest częścią J#. W tym
celu należy oczywiście zainstalować J#
Redistributable dla danego .NET Framework (jeśli nie jest jeszcze zainstalowany). W przestrzeni nazw java.math do
dyspozycji są klasy BigInteger i BigDecimal.
Warte zauważenia jest to, że klasy te
nie definiują operatorów matematycznych i aby wykonywać działania na tych
liczbach należy wywoływać odpowiednie
metody (Rysunek 11)
Poza tym, istnieje wydajna implementacja typu BigInteger dla języka C#, dostępna pod adresem http:
//www.codeproject.com/csharp/
biginteger.asp
Należy pobierać źródła do grida za
pomocą BindingSource. Wtedy, żeby
zmienić pozycję należy po prostu uruchomić
bs.Position = 20;
gdzie bs jest obiektem BindingSource powiązanym z gridem.
Sławek Procelewski
Mam 2 buttony z ‘podpiętym’ kodem (Rysunek 12).
Jedyna różnica w kodzie dla btnDrugi to usuniecie NIP’u z zapytania:
string Zapytanie = „SELECT Nazwa, Adres
FROM FIRMY”;
Dlaczego po wykonaniu kombinacji: btnPierwszy -> btnDrugi - >
btnPierwszy, kolumna NIP ląduje mi
na końcu datagrida (skrajnie prawa
pozycja)?
Polecam następujące rozwiązanie:
Tworzymy obiekt typu BindingSource i ustawiamy własność DataSource kontrolki na ten obiekt (koniecznie w
designerze)
Następnie definiujemy sobie kolumny
przez Smart Tag „Add Columns” w gridzie pamiętajac o właściwym ustawieniu
właściwości DataPropertyName
W kodzie programu:
Wstawiamy elementy do kolekcji.
Ustawiamy własność DataSource
obiektu BindingSource na kolekcję
Bardzo ważna jest kolejność wykonania powyższych czynności. Jeśli najpierw
przypiszemy dane do źródła, a potem źródło do kontrolki, to pokażą się wszystkie własności w obiekcie źródłowym (np.
wszystkie kolumny w tabeli). A jeśli przypiszemy dane bezpośrednio do kontrolki (tj. z pominięciem BindingSource)
to kolejność kolumn będzie nieokreślona
(czyli efekt opisany w pytaniu).
Mam na formatce DataGridView
i kilka TextBoxów opartych na
jednym datasecie. Jak przewijam
datagridview (strzałka góra, dół) to
oczywiście zmienia mi się również
zawartość textboxu. Jak napisać
żeby po odpaleniu programu od
razu wskakiwał mi np na 20-ty
wiersz w datagridview i wyświetlał
dane z tego wiersza (rekordu w datacie) w texboxie?
W .NET 2003 pisałem:
datagrid1.CurrentRowIndeks = 20
datagrid1.Select(20)
i było ok, w 2005 to nie działa.
32
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
BOOKS.NET
Wyszukiwanie i usuwanie błędów z aplikacji
Korzenie
Stajnia Wintellect jest swoistą skarbnicą talentów. Jeff Prosise, Jeffrey Richter, John Robbins to zaledwie kilka nazwisk szeroko rozpoznawanych w świecie technologii około Microsoftowych.
John Robbins, były ‚Green Beret‘ w US
Army, swoje życie zawodowe w it skupił wokół usuwania błędów z aplikacji.
Pracował w NuMedze (obecnie CompuWare NuMega) przy SoftICE’ie (zalecany
był obok W32Dasm jako podstawowe narzędzie crackerskie; ciekawe kto jeszcze
pamięta serwis crackpl Gustawa Kita),
BoundsCheckerze i innych, obecnie pisuje w dziale BugsLayer MSDN Magazine,
prowadzi szkolenia i pracuje nad super
książkami.
Najbardziej znane z nich to: Debugging
Applications, która ukazała się również
w języku polskim nakładem RM, pt. Debuger usuwanie błędów z programów
oraz Debugging Applications for Microsoft .NET and Microsoft Windows, która
stanowi podstawę tego tekstu.
W przygotowaniu jest Debugging, Testing, and Tuning Microsoft .NET 2.0 Applications, zapowiedzian a na drugą połowę października tego roku.
Zajrzyjmy do środka
Usuwanie błędów z aplikacji to nie tylko obsługa F5, F9, F10, F11, itd, ale również konieczność przetestowania aplikacji, czy (co niestety czasem się zdarza)
zinterpretowanie raportu przedśmiertnego programu. Wiedza jak zrobić to sprawnie wydaje się być kluczowa.
John Robbins posiada rzadką zdolność
do przekazywania wiedzy trudnej, w sposób zrozumiały dla osób, które nie korzystają z pewnych rozwiązań na co dzień.
Na potrzeby swoich książek przygotowuje
narzędzia, którymi wspiera się tłumacząc
pewne techniki programowania i debuggowania. Poza dużą wartością edukacyjną, narzędzia te oferują funkcjonalność,
która w stosunkowo niewygórowanej cenie
książki (mi udało się ściągnąć ‘like new’
od pośrednika współpracującego z amazonem za 8 funciaków + koszty przesyłki)
jest niedostępna gdzie indziej.
Do takich narzędzi mogę na pewno zaliczyć ExceptionMon oraz FlowTrace, opisanych w rozdziałach 10 i 11. Oba programy
ilustrują wykorzystanie Profiler API w .net
1.1, dla których dokumentacja Microsoftu
występuje wyłącznie w postaci doca w pakiecie SDK do .net 1.1, bez wersji online.
Poza tym można oczywiście poczytać ciekawe artykuły na MSDN Magazine online,
gdzie obok tekstu Johna Robbinsa są jeszcze inne wprowadzające w techniki Profiler API (z update’ami do wersji 2.0).
Każdy, kto korzystał kiedykolwiek z
programu DebugView do przechwytywa-
nia komunikatów OutputDebugString w
aplikacjach win32, czy narzędzi wspierających logowanie (jak chociażby Nlog
naszego kolegi z developers.pl) i mimo
wszystko tu i tam widział komunikaty o
nieprzechwyconych wyjątkach a życzyłby sobie pełniejszą informację o tym, co
się dzieje w jego programie, będzie chciał
używać ExceptionMon. Informacje o wyjątkach badanej aplikacji trafiają do pliku
.log, którego położenie można określić
przy pomocy zmiennej środowiskowej
EXPMONFILENAME. Ogólnie cała konfiguracja sterowana jest zmiennymi środowiskowymi ze względu na architekturę
Profiling API. Microsoft podjął bowiem
słuszną decyzję, że profiler realizowany
jest jako natywny COM ładowany do przestrzeni adresowej zarządzanej aplikacji.
Staje się to istotne w przypadku monitorowania aplikacji ASPNET, gdzie worker
process aspnet_wp.exe pracuje w kontekście użytkownika o niższych uprawnieniach i nie mającego domyślnie dostępu
do zapisu w katalogu zawierającego obraz aspnet_wp.exe, a zatem bez możliwości zapisu pliku .log.
Drugim wspomnianym narzędziem jest
FlowTrace, pozwalający na obserwowanie przepływu sterowania w aplikacji
zarządzanej. Zrealizowany również jako
COM w ramach Profiling API, korzysta
ze wspólnej dla obu projektów biblioteki ProfilerLib, opakowującej całe API do
najpotrzebniejszych rzeczy. Kod źródłowy biblioteki wraz z przykładowymi startowymi projektami, które można wykorzystać do tworzenia własnych narzędzi,
gdzie skupiamy się praktycznie wyłącznie na logice, dostępne są oczywiście na
dołączonej do książki płycie CD.
Zainspirowany FlowTrace poszukałem
innych implementacji tego rozwiązania,
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
które poza nazwami metod, klas i przestrzeni dostarczy mi możliwość filtrowania tego, co ma trafiać do logu oraz będzie pokazywać wartości przekazywanych parametrów. Jest to jednak
temat na osobny artykuł.
Kolejnym ciekawym narzędziem
jest Tester, opisany w rozdziale 16. Pod
tą niewiele mówiącą nazwą jest pełne
środowisko wspierające automatyzację
testów aplikacji okienkowych. Zrealizowany jako COM pozwala na tworzenie testów w dowolnym języku skryptowym, który ma dostęp do COM-ów, co
oczywiście nie przekreśla możliwości
skorzystania z tego rozwiązania w językach bardziej lubianych. Inteligentnie
opakowuje System.Windows.Forms.SendKeys w łatwe w obsłudze klasy TWindow, TNotify, TInput i inne. Narzędzie
to znane jest czytelnikom poprzedniego
wydania (tego z polskim tłumaczeniem),
jednak teraz zostało rozbudowane i poprawione.
Obok Testera możemy znaleźć TestRec –
narzędzie, dzięki któremu bez problemu
zarejestrujemy sesję myszkowo – klawiaturową, oczywiście w postaci skryptu korzystającego z Testera. Nie będę ukrywał,
że w mojej praktyce administracyjnej to
narzędzie znajduje swoje codzienne zastosowanie. Recorder dostępny w win
3.1 zaginął bezpowrotnie, pozostawiając pustkę i miejsce dla rozwiązań firm
trzecich.
Użytkownicy Visual Studio IDE znajdą
dla siebie sporo ciekawych rzeczy w rozdziale 9. Ja osobiście czasem korzystam z
opisanego tam makra dodającego brakujące komentarze w kodzie, czyli ComenTatera. Dla osób korzystających z NDocSyntax oraz NDoca może to być dobre
narzędzie pomagające w tworzeniu dokumentacji do kodu.
Microsoft udostępnia rozbudowane środowisko do debuggowania – windbg. Dokumentacja tego narzędzia pozostawia
jednak wiele do życzenia, przez co jego
odbiór przez developerów jest co najmniej
słaby. Większość kojarzy windbg z usuwaniem błędów w sterownikach i analizą dumpów systemu. Sytuację tę można
zmienić po lekturze rozdziału 8, gdzie
John Robbins wprowadza nas jak nowicjuszy w podstawy tego bogatego narzędzia. Dodatkiem jest podrozdział poświęcony bibliotece SOS (Son of Strike) wspierającej debuggowanie aplikacji .NET.
Nie sposób jest w krótkim tekście opisać wszystko to, co przedstawił w swojej
kiążce John Robbins. Na płycie dołączonej do książki jest kilkadziesiąt programów i bibliotek, a wszystkie one opisane
są w 19 rozdziałach i kilku dodatkach.
Zawsze, gdy szukam jakiejś informacji
o usuwaniu błędów z programu, sięgam
najpierw do tej książki. Jakkolwiek książka wydana była w 2003 roku, to techniki
w niej opisane pozostają ciągle aktualne.
Michał Grzegorzewski
33
REDAKCJA
ASP.NET
Edycja struktury
organizacyjnej za
pomocą TreeView
w ASP.NET
2
ASP.NET
UMBRACO.
Wprowadzenie
i opis instalacji
WINFORMS
WINFORMS
Gra w kości
a usługi
kompilatora
7
11
Obsługa kontrolki
NotifyIcon
w Windows
Forms na
przykładzie
aplikacji
wyłączającej
monitor
14
TOOLS.NET Wprowadzenie
do śledzenia
aplikacji przy
pomocy bibliteki
NLog
17
ROZMOWA.NET O NLogu
opowiada Jarek
Kowalski
23
TIPS&TRICKS
Inny kolor tła
w aplikacji MDI
28
BOOKS.NET
Wyszukiwanie
i usuwanie
błędów z aplikacji
33
Redakcja merytoryczna:
Main concept: Michał Grzegorzewski
Działy:
ASP.NET: Ziemowit Skowroński
WINFORMS, TIPS&TRICKS:
Wojtek Gębczyk
Support: Radek Zawartko
34
Z I N E . N E T 1 (1) , C Z E R W I E C 2 0 0 6
Redakcja graficzna:
Iwona Michniewska, Teresa Oleszczuk,
Janusz Fajto, Marek Sobczak

Podobne dokumenty