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