Showing posts with label programowanie. Show all posts
Showing posts with label programowanie. Show all posts

15/05/2011

Aplikacje wielojęzyczne - ASP.NET

Home

Wstęp

Ostatnimi czasy zajmowałem się przygotowanie obcojęzycznej wersji systemu, na który składa się cała plejada aplikacji napisanych w różnych technologiach, od ASP.NET przez WinForms po WPF. Do tej pory zagadnienie to znałem przede wszystkim ze strony teoretycznej. To znaczy wiedziałem o różnych mechanizmach wbudowanych w .NET wspierających ten proces, nie omieszkałem ich wypróbować ale nie miałem okazji zastosować tej wiedzy do dużego i skomplikowanego systemu. Powiem więcej, proces lokalizacji aplikacji napisanej w .NET wydawał mi się relatywnie łatwy i prosty do przeprowadzenia, przecież platforma daje tyle za darmo. Teoria, teorią, a w praktyce okazało się, że nie jest to takie łatwe. Tym wstępem rozpoczynam serię postów, w której chcę się podzielić moimi doświadczeniami i przemyśleniami na ten temat.

Na początek ASP.NET. Przystępując do pisania tego postu zastanawiałem się czy zacząć od opisania podstaw związanych z przygotowaniem obcojęzycznej wersji aplikacji ASP.NET czy od razu przejść do opisania swoich doświadczeń. Zdecydowałem się na drugie podejście, trochę z lenistwa, a przede wszystkim dlatego, że nie lubię robić tego co zostało już zrobione, odsyłam na przykład do ASP.NET Globalization and Localization.

Stałe znakowe w kodzie

Zacznę od tego, że używanie w kodzie (mam tutaj na myśli kod C#, VB.NET, a nie markup) stałych znakowych powinno być karane zesłaniem na Sybir. Stałe zawierające komunikaty dla użytkownika itp. powinny znajdować się w zasobach aplikacji, a pozostałe, nazwijmy je techniczne powinny zostać zdefiniowany w jednym konkretnym miejscu np.:
public static class Constans
{
  ...
  public static const string Name = "Name";
  ...
}
Wróćmy jednak do treści wyświetlanych użytkownikom i załóżmy, że w kodzie znajdujemy cos takiego:
...
Msg.ShowMessage("Operation successful!");
...
Coś takiego jest złe, bardzo złe, niewyobrażalnie złe... Jeśli napiszemy coś takiego to potem osoba odpowiedzialna za przygotowanie obcojęzycznej wersji aplikacji (w tym przypadku ja) będzie musiał znaleźć w kodzie wszystkie miejsca tego rodzaju i przenieść komunikat do pliku z zasobami czyli wykonać naszą pracę. Takich miejsc może być bardzo dużo i ciężko jest znaleźć.

Użycie okienka Find, wyrażeń regularnych jest przydatne ale trzeba wiedzieć czego szukać, raz będzie to Msg.ShowMessage(...), innym razem lbl.Text = ..., a w jeszcze innym przypadku coś innego. Jeśli dodamy do tego wiele bibliotek to otrzymamy prawie syzyfowe zadanie. To oczywiście dotyczy aplikacji każdego rodzaju, a nie tylko ASP.NET. Powyższy kod powinien wyglądać tak jak poniżej (Msg to nazwa pliku z zasobami). Jeśli plik z zasobami zostanie przetłumaczony nie trzeba robić nic innego.
...
Msg.ShowMessage(Msg.OperationSuccessful);
...

Stałe znakowe w markup'ie

Przejdźmy teraz do kodu strony czyli tego co znajduje się w plikach *.aspx, *.ascx itd. Otóż myślałem, że z tym nie będzie problemu i planowałem użyć narzędzia dostarczanego razem z Visual Studio Tools->Generate Local Resources. Narzędzie to generuje dla strony/kontrolki plik zawierający "wszystkie" zasoby, które mogą podlegać lokalizacji i modyfikuje kod w ten sposób aby odpowiednia wersja zasobów była wczytywana automatycznie. Sądziłem, że będzie to bardzo proste, parę sekund na stronę i po zabawie. Praktyka wygląda tak:
  • Narzędzie to potrafi zamulić VS i to nawet dla stosunkowo prostych strony i na mocnej maszynie. Objawia się to tym, że środowisko przestaje odpowiadać na kilkadziesiąt sekund, a nawet dłużej.
  • Narzędzie to nie współpracuje ze wszystkimi kontrolkami i tak czy inaczej w niektórych przypadkach trzeba "ręcznie" tworzyć zasoby.
  • Jeśli nasza aplikacja składa się z wielu stron otrzymamy również wiele plików z zasobami. Liczbę tą należy jeszcze przemnożyć przez liczbę języków jakie chcemy obsługiwać. Uważam, że prowadzi to do bałaganu w projekcie. Osobiście wolę aby zasoby były zgromadzone w jednym, kilku plikach, a nie w kilkunastu lub kilkudziesięciu. Moim zdaniem ułatwia to zarządzanie nimi, tłumaczenie czy wprowadzanie poprawek.
  • Generate Local Resources wyniesienie do pliku z zasobem wszystko co się da. Może się zdarzyć, że dla danej strony musimy przetłumaczyć raptem kilka elementów natomiast plik z zasobami będzie ich zawierał wielokrotnie więcej.
Z tych względów postanowiłem zrezygnować z tego narzędzia i niestety samemu przeszukać kod stron i wynieść stałe znakowe do plików z zasobami. Aby odwołać się do tych zasobów z poziomu kodu strony wykorzystałem wyrażenia <% ... %>. Tutaj możemy wyróżnić dwa przypadki
Przypadek 1
<asp:Button ID="deleteButton" runat="server" Text="Usuń" />
można zamienić na:
<asp:Button ID="deleteButton" runat="server"  Text="<%$ Resources : lbl, Delete %>" />
lub na :
<asp:Button ID="deleteButton" runat="server"  Text="<%# Resources.lbl.Delete %>" />
W drugim wypadku trzeba pamiętać o wywołaniu metody DataBind. lbl to nazwa pliku z zasobami. Ja korzystałem głównie z pierwszej opcji.
Przypadek 2
<div>
  Nie znaleziono pliku
</div>
można zamienić na:
<div>
  <%= Resources.msg.FileNotFound %>
</div>
lub na:
<div>
  <asp:Literal runat="server" Text="<%$ Resources: msg, FileNotFound %>" />
</div>
lub na:
<div>
  <asp:Literal runat="server" Text="<%# Resources.msg.FileNotFound %>" />
</div>
lub na:
<div>
  <%# Resources.msg.FileNotFound %>
</div>
W dwóch ostatnich przypadkach trzeba pamiętać o wywołaniu DataBind. msg to nazwa pliku z zasobami. Ja korzystałem głównie z <%= ... %>, ma to jednak swoje ograniczenia. W pewnych, w cale nie rzadkich, przypadkach natrafimy na wyjątek The Controls collection cannot be modified because the control contains code blocks (i.e. % ... %). O tym jak sobie z tym poradzić i dlaczego tak jest pisałem w tym poście.

Inne

Tak jak napisałem na początku zajmowałem się przygotowaniem obcojęzycznej wersji dużego systemu. System ten ma swoją długą historię, a przejawem tego jest między innymi własny mechanizm lokalizowania. Mechanizm ten można było zastosować tylko do niektórych elementów interfejsu użytkownika. Co też zrobiłem bo skoro coś jest i działa to czemu z tego zrezygnować. Z perspektywy muszę przyznać, że nie była do dobra decyzja. Wykorzystanie więcej niż jednego mechanizmu lokalizowania aplikacji spowodowało, że konfiguracja tej aplikacji stała się trudniejsza. Jestem pewny, że za jakiś czas ktoś będzie się zastanawiał czemu tutaj widzę napisy po angielsku, a tutaj po polsku!?!?

Podsumowanie

W podsumowaniu powiem tylko jedną rzecz, którą zapewne powtórzę jeszcze nie raz:

Jeśli chcemy aby nasza aplikacja miała wiele wersji językowych to przygotowujmy się do tego od pierwszej linijki tej aplikacji.

Przez przygotowujmy się mam na myśli umieszczanie komunikatów w zasobach itp. ale również wypróbowanie dostępnych mechanizmów i sprawdzenie jak działają. Nie odkryłem tutaj Ameryki ale dopiero kiedy poczułem na własnej skórze co to znaczy lokalizacja dużej, starej aplikacji, która nie była do tego gotowa uświadomiłem sobie w pełni jak ważne jest myślenie o wielu wersjach językowych od samego początku.

03/05/2011

The Controls collection cannot be modified because the control contains code blocks (i.e. % ... %)

Home

The Controls collection cannot be modified because the control contains code blocks (i.e. <% ... %>). to paskudny błąd, który generowany jest przez klasę System.Web.UI.ControlCollection. Ma to miejsce przy próbie modyfikacji kolekcji, na przykład przez wywołanie Add(Control control), w przypadku kiedy kontrolki w niej zawarte korzystają z wyrażeń postaci <% ... %> lub <%= ... %>. Istnieje kilka sposób ominięcia tego problemu:
  • Kod zawierający wyrażenia <% ... %> lub <%= ... %> umieścić w dodatkowym bloku <div runat="server">...<div/>. Na przykład taki kod:
    <form id="form1" runat="server">
        <% for (int i = 0; i < 5; ++i) %>
        <% { %>
        <br />
        <asp:label runat="server" Text="Test" />
        <%= i.ToString() %>
        <% } %>
    </form>
    
    zamienimy na taki:
    <form id="form1" runat="server">
        <div runat="server">
        <% for (int i = 0; i < 5; ++i) %>
        <% { %>
        <br />
        <asp:label runat="server" Text="Test" />
        <%= i.ToString() %>
        <% } %>
        </div>
    </form>
    
    W drugim przypadku modyfikacja kolekcji (form1.Controls.Add(new Label());) nie spowoduje błędu ponieważ kontrolki form oraz div posiadają oddzielne kolekcje ControlCollection i tylko kolekcja kontrolki div jest w trybie tylko do odczytu.
  • Jeśli mamy dostęp do kontrolek Telerika to możemy skorzystać z RadCodeBlock.
Poniżej jeszcze dwa, mniej ogólne, rozwiązania problemu:
  • Zastąpienie <%= ... %> przez <%# ... %>. Na przykład taki kod:
    <%= Environment.MachineName %>
    
    możemy zastąpić takim:
    <%# Environment.MachineName %>
    
  • Użyć kontrolki Literal i taki kod:
    <%= Resources.lbl.Title %>
    
    zastąpić takim:
    <asp:Literal runat="server" Text="<%$ Resources: lbl, Title %>"/>
    
Rozwiązanie problemu to jedna sprawa. Mnie zaciekawiło dlaczego tak się dzieje. Poszukiwania rozpocząłem od przyjrzenia się implementacji kolekcji ControlCollection. Tutaj jak zwykle pomocny okazał się .NET Reflector. Na początki dowiedziałem się, że kolekcja ma prywatne pole _readOnlyErrorMsg; . Jeśli jest ono różne od null i nastąpi próba modyfikacji kolekcji to generowany jest wyjątek:
...
if (this._readOnlyErrorMsg != null)
{
  throw new HttpException(SR.GetString(this._readOnlyErrorMsg));
}
...
Pole to ustawiane jest tylko w jednym miejscu, w metodzie ControlCollection.SetCollectionReadOnly(string errorMsg). Metoda ta wywoływana jest w kilku miejscach, ale tylko w jednym przypadku parametr errorMsg odpowiada komunikatowi The Controls collection cannot be modified.... Ma to miejsce w metodzie Control.SetRenderMethodDelegate(RenderMethod renderMethod). I tu pojawił się problem. Okazało się, że ta metoda nie jest nigdzie wywoływana, a przynajmniej tak twierdził .NET Reflector (i w sumie się nie pomylił).

Wywołanie tej metody znalazłem w plikach generowanych dynamicznie dla stron aspx. Znajdziemy je w katalogu z plikami tymczasowymi C:\WINDOWS\Microsoft.NET\Framework\v4.0.30319\Temporary ASP.NET Files\.... Dokładej lokalizacji nie podaję ponieważ nazwy tych plików generowane są w sposób losowy. Dla przykładu z początku postu (z pętlą for) zostanie wygenerowany taki kod:
...
@__ctrl.SetRenderMethodDelegate(new System.Web.UI.RenderMethod(this.@__Renderform1));
...
Metoda @__Renderform1 wygląda natomiast tak (po usunięciu dyrektyw kompilatora):
...
private void @__Renderform1(System.Web.UI.HtmlTextWriter @__w, System.Web.UI.Control parameterContainer) 
{
  @__w.Write("\r\n    \r\n        ");
  for (int i = 0; i < 5; ++i) 
  { 
    @__w.Write("\r\n    <br />\r\n    ");
    parameterContainer.Controls[0].RenderControl(@__w);
    @__w.Write("\r\n    ");
    @__w.Write( i.ToString() );
    @__w.Write("\r\n    ");
  }
}
...
Co w tym takiego zdrożnego, że silnik ASP.NET nie pozwala na modyfikowanie kolekcji? Moim zdaniem kluczowa jest linijka zawierająca odwołanie to kolekcji Controls. Jak widać indeks kontrolki do jakiej się odwołujemy jest ustawiony na sztywno w kodzie. W takim przypadku modyfikowanie kolekcji kontrolek byłoby albo bezcelowe bo i tak kontrolka nie została by wyrenderowana albo doprowadziło to wyrenderowania innej kontrolki niż trzeba.

Ciekawe posty na ten temat można znaleźć również na blogu Rick Strahl's Web Blog. Podaje tylko tytuły artykułów, a nie linki ponieważ w momencie pisania tego postu artykuły te były dostępne tylko w cache Google'a:
  • Reliably adding Controls or Markup before or after an ASP.NET Control?
  • Understanding how <% %> expressions render and why Controls.Add() doesn't work

26/04/2011

Problem z magazynem certyfikatów

Home

Jakiś czas temu pracowałem nad aplikacją WWW, która między innymi zajmowała się wysyłaniem SMS'ów. SMS'y wysyłałem przy pomocy Web Service'u udostępnionego przez operatora. Aby wysłać taki SMS musiałem uwierzytelnić się korzystając z infrastruktury klucza publicznego - dokładniej mówiąc aby wysłać wiadomość musiałem przedstawić certyfikat podpisany przez operatora. Jak to zazwyczaj bywa testy przy użyciu serwera WWW wbudowanego w Visual Studio powiodły się. Niestety po zainstalowaniu aplikacji na serwerze IIS 7.0 aplikacja nie potrafiła znaleźć certyfikatu w magazynie kluczy i próba wysłania SMS'a kończyła się wyjątkiem:

System.Security.Cryptography.CryptographicException: Zestaw kluczy nie istnieje.

Początkowo myślałem, że to błąd konfiguracji czyli, że szukam certyfikatu o błędnej nazwie. Szybko okazało się jednak, że konfiguracja jest poprawna. W tym momencie przypomniałem sobie o kwestii uprawnień. Przecież tożsamość puli aplikacyjnej w jakiej działa aplikacja musi mieć odpowiednie uprawnienia aby uzyskać dostęp do magazynu certyfikatów.

Problem rozwiązałem bardzo szybko zmieniając tożsamość puli aplikacyjnej na System Lokalny (Local System). Rozwiązanie to nie spodobało mi się jednak ponieważ w ten sposób pula aplikacyjna dostała dużo, dużo więcej uprawnień niż tylko prawo dostępu do magazynu certyfikatów. Krótkie poszukiwania naprowadziły mnie na poprawnego rozwiązanie. Otóż należy nadać puli aplikacyjnej (tożsamości puli aplikacyjnej) uprawnienia TYLKO do wybranych certyfikatów w magazynie.

W tym celu uruchamiamy konsolę zarządzania (mmc.exe) i dodajemy do niej przystawkę zarządzania certyfikatami (pisałem o tym w poście Błąd przy dodawaniu przystawki do konsoli zarządzania). Następnie w kategorii Osobisty (My) znajdujemy interesujący nas certyfikat i z menu kontekstowego wybieramy Wszystkie zadania -> Zarządzaj kluczami prywatnymi.... W okienku jakie się pojawi nadajemy użytkownikowi z jakiego korzysta pula prawa do odczytu. Zalecane jest konto wbudowane Usługa sieciowa (Network Service).

01/04/2011

Zabawy z pozycjonowaniem okna w WPF

Home

Czasami zachodzi potrzeba "ręcznego" pozycjonowania okna w aplikacji WPF. Sprawa jest prosta i sprowadza się do odpowiedniego ustawienia właściwości Left oraz Top. Jeśli chcemy aby pozycja okna dziecka zależała od położenia rodzica to możemy odwołać się do rodzica przez właściwość Owner i napisać taki kod jak poniżej. Kod ten powoduje przesunięcie okno dziecka o 50 jednostek w prawo względem lewej krawędzi okna rodzica.
...
myWindow.Left = myWindow.Owner.Left + 50;
...
Nic prostrzego. Jest tu jednak mały haczyk. Jeśli okno rodzica jest zmaksymalizowane to właściwość Left i Top dla rodzica będą miały wartość wskazującą położenie okna przed zmaksymalizowaniem. Poniżej kod, który to uwzględnia:
...
myWindow.Left = (myWindow.Owner.WindowState == WindowState.Maximized ? 0 : myWindow.Owner.Left) + 50;
...
Nic trudnego ale trzeba o tym pamiętać. Sprawa robi się jeszcze ciekawsza kiedy pracujemy z wieloma monitorami. W takim wypadku powyższy kod będzie działał tylko dla głównego monitora. Jeśli okno rodzica będzie zmaksymalizowane i będzie znajdować się na innym monitorze niż główny to wykonanie tego kodu spowoduje wyświetlenie okna dziecka na głównym monitorze! Poniżej rozwiązanie tego problemu:

if (myWindow.Owner.WindowState == WindowState.Maximized)
{
  Screen s = Screen.FromHandle(new WindowInteropHelper(myWindow.Owner).Handle);                      
  myWindow.Left = s.WorkingArea.Left + 50;
}
else
{
  myWindow.Left = myWindow.Owner.Left + 50;
}
                    
Korzystam tutaj ze starej poczciwej klasy Screen, przy pomocy której znajduję monitor, na którym wyświetlane jest okno rodzica. Następnie odczytuję położenie lewej krawędzi tego monitora. Wartość ta będzie różna w zależności od tego, z którym monitorem mamy do czynienia.

19/03/2011

Jak właściwość może napsuć krwi

Home

Właściwości to jedna z moich ulubionych cech języka C#. W poście tym nie będę jednak przekonywał opornych, jeśli tacy są, do ich używania. Opiszę natomiast przypadek złego wykorzystania właściwości, który ostatnio napsuł mi trochę krwi. Załóżmy, że mamy aplikację WWW lub grubego klienta. Aplikacja ta komunikuje się z bazą danych za pośrednictwem tzw. dostawców danych czyli klas implementujących dobrze określone interfejsy. W zależności od konfiguracji dostawca może być dostępny lokalnie w ramach procesu aplikacji lub zdalnie czyli być uruchomiony na innej maszynie np. jako usługa WCF. Z perspektywy aplikacji jest to przezroczyste.

Dla ustalenia uwagi przyjmijmy, że dostawca udostępnia metodę Customer GetCustomer(int customerId), która zwraca obiekt klasy Customer reprezentujący klienta o podanym identyfikatorze. Problem polega na tym, że w zależności od tego czy dostawca danych działa lokalnie czy zdalnie to dla tego samego identyfikatora zwraca trochę inne dane. Dokładniej mówiąc jeśli mamy do czynienia z dostawcą lokalnym wszystko jest w porządku. Natomiast dla dostawcy zdalnego zwracane dane są niekompletne w tym sensie, że nie wszystkie właściwości obiektu Customer są ustawione. Co ciekawe po zdebugowaniu procesu serwera okazuje się, że zwracane przed dostawcę dane są poprawne ale po dotarciu do aplikacji okazują się jednak niekompletne.

Początkowo przecierałem oczy ze zdziwienia bo jak to możliwe, że część danych nagle znika. Po zagłębieniu się w problem znalazłem jednak winnego, którym okazała się błędne zaimplementowana właściwość w połączeniu z serializacją XML'ową. Istotę problemu przedstawia poniższy kod:

private string _StringValue;
public string StringValue
{
  get
  {
    if(_StringValue == "aaa")
      return "Ala ma kota";
    else if(_StringValue == "bbb")
      return "Kot ma Alę";
    ...
    else
      return null;
  }
  set
  {
    _StringValue = value;
  } 
}

Jak widać wartość zwracana przez getter właściwości jest inna niż wartość ustawiana przy pomocy settera. W przypadku kiedy dostawca działa lokalnie nie stanowi to żadnego problemu ponieważ pracujemy bezpośrednio z klasą dostawcy danych. W przypadku kiedy dostawca uruchomiony jest zdalnie to w rzeczywistości po stronie aplikacji pracujemy z obiektem proxy. W momencie wywołania metody GetCustomer do serwera wysyłane jest odpowiedni komunikat i wywoływana jest "prawdziwa" metoda GetCustomer. W wyniku otrzymujemy obiekt klasy Customer, która musi zostać przesłany do proxy. W tym celu obiekt musi zostać zserializowany czyli zapisany do strumienia i wysłany przez sieć.

I tutaj jest pies pogrzebany. Jeśli zostanie użyta serializacja XML'owa czyli taka, która "zamieni" obiekt na dokument XML to do proxy zostanie wysłana wartość zwrócona przez getter właściwości czyli coś innego niż wartość, która została ustawiona przy pomocy settera np.: jeśli do ustawienia właściwości StringValue użyjemy łańcucha "aaa" to w dokumencie XML zostanie zapisana wartość "Ala ma kota".

Po odebraniu odpowiedzi proxy spróbuje odtworzyć obiekt na podstawie odebranego dokumentu XML. Do zainicjowania właściwości StringValue użyje wartości z dokumentu czyli łańcucha "Ala ma kota". Teraz jeśli odczytamy właściwości StringValue to dostaniemy null ponieważ kod w getterze nie rozpozna łańcucha "Ala ma kota". Jeśli użyjemy serializacji binarnej problem nie wystąpi ponieważ w strumieniu zostanie zapisana wartość prywatnego pola _StringValue. Jeśli chcemy pozostać przy serializacji XML'owej to należy trochę zmodyfikować kod. Właściwość StringValue powinna być tylko do odczytu. Należy również stworzyć dodatkową właściwość do ustawiania danych.

public string StringValueForSerialization
{
  get; set;
}

public string StringValue
{
  get
  {
    if(StringValueForSerialization == "aaa")
      return "Ala ma kota";
    else if(StringValueForSerialization == "bbb")
      return "Kot ma Alę";
    ...
    else
      return null;
  }
}


11/03/2011

Kilka słów o operatorach

Home

Dzisiaj kolega podesłał mi swoistą zagadkę w postaci dwóch, pozornie robiących to samo, fragmentów kodu. Jeden napisany w C#, a drugi w VB.NET:
int counter = 0;
while (++counter < 10)
{
  Console.WriteLine(counter.ToString());
}
Dim counter As Integer = 0
While (++counter < 10)
  Console.WriteLine(counter.ToString())
End While
Różnica polega na tym, że pętla w C# wykona się 9 razy, a w VB.NET otrzymamy pętlę nieskończoną. Dlaczego? Przyjrzyjmy się dokładniej pętli napisanej w języku VB.NET. Ci z was, którzy programują lub programowali w tym języku zapewne zwrócili uwagę na użycia operatora preinkrementacji. No właśnie, od kiedy w VB.NET mamy operator preinkrementacji? I tu tkwi pies pogrzebany, w VB.NET nie ma operatora preinkrementacji czy też predekrementacji. W takim razie czemu ten kod w ogóle się kompiluje? Początkowo pomyślałem, że to błąd kompilatora. W przekonaniu tym utwierdził mnie dodatkowo reflector, który po wrzuceniu do niego dll'ki pokazał coś takiego:
...
Do While (counter < 10)
...
Efekt zaobserwowałem w VS 2010, a więc eksperyment przeprowadziłem jeszcze w VS 2008 z tym samym wynikiem. W tym momencie pomyślałem, że to jednak niemożliwe aby przez tyle lat taki bug pozostał niewykryty. Po chwili mnie olśniło i wszystko okazało się proste i oczywiste. Zanim rozwiążę zagadkę do końca powiem, że poniższy kod również się skompiluje:
Dim counter As Integer = 0

While (++-+---++--+-+-counter < 10)
  Console.WriteLine(counter.ToString())
End While
C# nie jest gorszy i w nim również da się stworzyć podobnego dziwoląga:
int counter = 0;
while (+-+-+-+-+--counter < 10)
{
  Console.WriteLine(counter.ToString());
}
W tej chwili wszystko powinno być już jasne. + i - to nie tylko operatory binarne ale również operatory unarne. Poniżej fragment dokumentacji z MSDN:

Unary + operators are predefined for all numeric types. The result of a unary + operation on a numeric type is just the value of the operand.

Unary - operators are predefined for all numeric types. The result of a unary - operation on a numeric type is the numeric negation of the operand.

W języku VB.NET, tak jak pisałem wcześniej, nie ma operatorów preinkrementacji oraz predekrementacji, a więc zmienną liczbową możemy poprzedzić dowolną kombinacją + i -. W języku C# takie operatory istnieją, a więc zmienna liczbowa może zostać poprzedzona ciągiem + i - pod warunkiem, że występują one naprzemiennie (z wyjątkiem ostatniej pary).


26/02/2011

Jak zagłodzić Timer?

Home

Okazuje się, że bardzo prosto, ale zacznijmy od początku. Niedawno zakończyłem pracę nad serwerem zajmującym się wykonywaniem tzw. zadań wsadowych. Definicje zadań do wykonania pobierane są z bazy danych, a w danym momencie może działać wiele serwerów. Każdy serwer rezerwuje sobie swoje zadania na pewien kwant czasu. Po upływie tego czasu inne serwery mają prawo przejąć to zadanie. Może się tak zdarzyć na przykład jeśli jakiś serwer ulegnie awarii. Jeśli wykonanie danego zadania zajmuje więcej czasu niż czas rezerwacji to serwer musi przedłużyć dzierżawę.

W tym celu stworzyłem komponent, który monitoruje zadania przetwarzane przez serwer i kiedy zbliża się czas wygaśnięcia dzierżawy, przedłuża ją. Komponent ten korzysta z klasy Timer z przestrzeni nazw System.Timers, która co określony kwant czasu generuje zdarzenie. W metodzie obsługującej to zdarzenia nie robię nic innego jak po prostu aktualizuję czas wygaśnięcia dzierżawy. Tyle w telegraficznym skrócie.

W czasie testowania stworzonego rozwiązania zauważyłem, że w niektórych przypadkach czas wygaśnięcia dzierżawy nie jest aktualizowany. Wyglądało na to jakby klasa Timer nie generowała zdarzeń albo generowała je z dużym opóźnieniem! Wbrew pozorom rozwiązanie zagadki okazało się bardzo proste. Otóż klasa Timer generuje zdarzenie Elapse w wątku pochodzącym z puli wątków ThreadPool. Jeśli dodamy do tego fakt, że zadania wsadowe również są wykonywane przez wątki z puli to wszystko staje się oczywiste. Jeśli serwer umieści w puli odpowiednio dużo zadań wsadowych to może okazać sie, że brakuje wątków dla klasy Timer.

Poniżej prosty kod prezentujący ten efekt. Na początku ograniczam liczbę wątków do 10 i zlecam wykonanie 10 zadań. Zanim wszystkie zadania zostaną uruchomione na ekran zostanie wypisanych kilka napisów Hello World from System.Timers.Timer!. Następnie kiedy wszystkie 10 zadań zostanie uruchomionych napis przestanie się pokazywać. Zobaczymy go ponownie kiedy przynajmniej jedno zadanie zostanie zakończone i tym samym zwolni sie jeden wątek.
class Program
{
  static void Main(string[] args)
  {
    ThreadPool.SetMaxThreads(10, 10);
    
    System.Timers.Timer t = new System.Timers.Timer();
    t.Interval = 1000;
    t.Elapsed += new System.Timers.ElapsedEventHandler(t_Elapsed);
    t.Start();

    for (int i = 0; i < 10; ++i)
        ThreadPool.QueueUserWorkItem((o) =>
            {
              Console.WriteLine("Start " + o);
              Thread.Sleep(10000);
              Console.WriteLine("End " + o);
            }, i);

    Console.ReadLine();
  }

  static void t_Elapsed(object sender, System.Timers.ElapsedEventArgs e)
  {
    Console.WriteLine("Hello World from System.Timers.Timer!");
  }
}
Taki sam efekt otrzymamy jeśli użyjemy klasy System.Threading.Timer zamiast System.Timers.Timer. Jak można poradzić sobie z tym problemem. Uważam, że są trzy rozwiązania:
  • Zliczać liczbę zadań umieszczanych w puli wątków i pilnować aby zawsze, przynajmniej jeden wątek z puli był wolny i mógł być użyty przez klasę Timer. Maksymalną liczbę wątków możemy pobrać przy pomocy metody GetMaxThreads. Jest to jednak podejście sprzeczne z ideą puli wątków, do której wrzucamy zadania do wykonania i nie zastanawiamy się kiedy dokładnie zostanie uruchomione, przez jaki wątek itd.
  • Zrezygnować z klasy ThreadPool i samemu zarządzać wątkami.
  • Użyć (napisać) zegar odpalający zdarzenia Elapse poza pulą wątków.
W opisie problemu pominąłem Task Parallel Library ponieważ w omawianym przypadku użycie .NET 4.0 nie było możliwe. Szybki test pokazał jednak, że jeśli mógłbym użyć Task Parallel Library to problem nie wystąpiłby. Z tego co wiem Task Parallel Library nie używa klasy ThreadPool, a więc przyczyna problemu nie występuje.

16/02/2011

DockPanel i ScrollViewer

Home

Dlaczego lubię kontener DockPanel? Ponieważ dobrze (intuicyjnie) współpracuje z kontrolką ScrollViewer, która dostarcza pionowych i poziomych pasków przewijania. Załóżmy, że główne okno naszej aplikacji zawiera listę (kontrolka ListView) z kilkuset wierszami. Wierszy jest na tyle dużo, że w danym momencie na ekranie widoczna jest tylko część z nich. Aby użytkownik mógł przewijać listę i zobaczyć wszystkie wiersze używamy kontrolki ScrollViewer

ScrollViewer zachowa się jednak inaczej w zależności od kontenera w jakim zostanie osadzony. Jeśli będzie to np.: StackPanel lub Grid to paski przewijania nie zostaną wyświetlone i otrzymamy efekt jak poniżej.



Można sobie z tym poradzić ustawiając na kontrolce ScrollViewer właściwość Height ale spowoduje to, że lista będzie miała zawsze tą samą wysokość bez względu na wielkość okna. Podobnie można ustawić właściwość Height dla listy ale efekt będzie taki sam.



Dużo lepszym rozwiązaniem jest osadzenia kontrolki ScrollViewer wewnątrz kontenera DockPanel. Dzięki temu uzyskamy efekt jak poniżej. Paski przewijania są widoczne, a wysokość listy zależy od wielkości okna.


29/01/2011

Być czy nie być, pisać czy nie pisać komentarze?

Home

Programiści dzielą się na:
  • Tych, którzy piszą bardzo dużo komentarzy, czasami prawie w każdej linijce. Tych spotkałem niewielu.
  • Tych, którzy w ogóle ich nie piszą, nawet jeśli napisali kod, którego nie da się zrozumieć bez choćby odrobiny komentarza. Tych ortodoksów jest już więcej.
  • Największa grupę stanowią natomiast programiści, którzy sytuują się gdzieś pomiędzy tymi dwiema skrajnościami.
Kiedy zaczynałem programować pisałem bardzo dużo komentarzy. Wiele z nich było zupełnie niepotrzebnych, na przykład komentarz "Konstruktor domyślny" dla domyślnego, bezparametrowego konstruktora. Z biegiem lat, kiedy nabrałem doświadczenia przesunąłem się gdzieś w okolicę środka na skali pomiędzy ortodoksyjnymi zwolennikami komentarzy i ortodoksyjnymi przeciwnikami komentarzy. Wciąż bliżej mi jednak do tych pierwszych.

Jednak, ostatnio dwa zdarzenia spowodowały, że częściowo przewartościowałem swój sposób patrzenia na tę sprawę i moje nastawienie przesunęło się znacząco w kierunki nie pisania komentarzy. Co to za zdarzenia? Po pierwsze postanowiłem wrócić do swojego starego projektu napisanego w C# ponad 3 lata temu. W skrócie program realizuje wnioskowanie wstecz. Właściwy silnik wnioskujący ma około 800 linii kodu (nie licząc GUI i definicji prostych struktur danych). Przy czym ponad połowa z tego przypada na komentarze, definicje regionów i znaki nowe linii! Problem w tym, że pomimo dużej liczby komentarzy miałem dużą trudność w zrozumieniu tego kodu i niektórych rozwiązań jakie przyjąłem. Drugie zdarzenie dotyczyło cudzego kodu, w którym skomentowana była prawie każda linijka. Początkowo myślałem, że ułatwi to sprawę ale myliłem się. Tak duża liczba komentarzy skutecznie utrudniała zrozumienie kodu. Usunięcie znaczącej części tych komentarzy (i refactoring) sprawiło, że kod stał się dużo łatwiejszy w odbiorze.

W obecnej chwili uważam, że większe znaczenie ma odpowiedni podział kodu na klasy, a przede wszystkim na metody. Zamiast komentarza lepiej zamknąć kod, którego dotyczy w metodę o samo tłumaczącej się nazwie. Komentarze w kodzie staram się ograniczyć do miejsc, które rzeczywiście tego wymagają: skomplikowany algorytm, szybko wprowadzane poprawki itd. Jeśli chodzi o komentarze do metod to uważam, że są one niezbędne tam gdzie z nazwy metody i nazw jej parametrów nie można wywnioskować jej przeznaczenia. Jeśli mamy skłonność to niepisania komentarzy to warto spytać innego programisty co jego zdaniem robi dana metoda. Jeśli odpowie poprawnie to komentarz z dużym prawdopodobieństwem nie jest potrzebny. Komentarz jest również potrzebny jeśli metoda rzuca jakieś wyjątki. Co jednak z metodami publicznymi i chronionymi czyli tymi, z których potencjalnie będą korzystać inni. Tutaj mam mieszane uczucia. Z jednej strony dobra praktyka mówi, że dla tych metod komentarze są potrzebne. Z drugiej po co komentować metodę o takim nagłowku IList<User> GetAllUsersWithBankAccount(), która zwraca listę użytkowników z kontem bankowym?

25/11/2010

Czemu VB.NET jest be?

Home

Z językiem VB.NET da się pracować, co z powodzeniem czynię od blisko roku, ale niektóre rzeczy doprowadzają mnie do szału. Aby wyjaśnic o co chodzi posłużę się bardzo, bardzo prostym interfejsem pokazanym poniżej:
Public Interface IFun
  Function Fun() As Integer
  Function SuperFun() As Integer
End Interface
Implementacja funkcji Fun oraz SuperFun jest trywialna i wyglądają jak poniżej:
Public Function Fun() As Integer
  Return 0
End Function

Public Function SuperFun() As Integer
  Return 1
End Function
Jeszcze kod, który korzysta z tej implementacji:
Sub Main()
  Dim fun As Fun = New Fun()
  Dim ifun As IFun = fun

  Console.WriteLine(fun.Fun() = ifun.Fun())
  Console.WriteLine(fun.SuperFun() = ifun.SuperFun())
  Console.WriteLine(fun.Fun() = ifun.SuperFun())
  Console.WriteLine(fun.SuperFun() = ifun.Fun())
End Sub
A na koniec pytanie, jaki wynik zostanie wypisany na ekan? Założę się, że nikt się nie zawacha i odpowie:
True
True
False
False
Taki wynik jest oczywiście logiczny i prawidłowy ale nie zawsze, w przypadku VB.NET można uzyskać również taki wynik:
False
False
True
True
Nie trzeba do tego stosować żadnych sztuczek, wystarczy zwykła pomyłka. Pokazując implementację metod Fun oraz SuperFun celowo pominąłem słowo kluczowe Implements, które określa jaki element intefejsu implementuje dana metoda. Kod klasy powinien wyglądać tak:
Public Class Fun
  Implements IFun

  Public Function Fun() As Integer Implements IFun.Fun
    Return 0
  End Function

  Public Function SuperFun() As Integer Implements IFun.SuperFun
    Return 1
  End Function
End Class
Niestety ponieważ metody Fun oraz SuperFun mają takie same sygnatury to nic nie stoi na przeszkodzie aby zamienić miejscami Implements IFun.Fun z Implements IFun.SuperFun i otrzymać coś takiego:
Public Class Fun
  Implements IFun

  Public Function Fun() As Integer Implements IFun.SuperFun
    Return 0
  End Function

  Public Function SuperFun() As Integer Implements IFun.Fun
    Return 1
  End Function
End Class
Mały, wkurzajacy i bardzo trudny do wykrycia błąd wynikający z cech języka VB.NET.

09/11/2010

Poznaj swój program

Home

Tematykę IntelliTrace poruszałem już kilkukrotnie. Dzisiaj chciałbym powrócić do zagadnienia opisanego w poście Logi IntelliTrace bez tajemnic czyli analizy logu przy pomocy IntelliTrace API. Tym razem napiszę w jaki sposób dostać się do informacji o tym kiedy została wywołana jakaś metoda, jaki wynik zwróciła i jakie były parametry wywołania. Informacje te są prezentowane w oknie Calls View w Visual Studio 2010 ale można je analizować w ograniczonym stopniu. Poniższe informacje przydadzą się każdemu kto będzie chciał na przykład załadować te dane do bazy danych w celu później analizy, na przykład przy pomocy algorytmów odkrywania wiedzy (data mining).

Zacznijmy od wzorca kodu, który posłuży nam do odczytania pliku z logiem:
using (IntelliTraceFile file = new IntelliTraceFile(pathToIntelliTraceLog))
{
  //Kolekcja procesów będzie miała tylko jedną pozycję
  foreach (IntelliTraceProcess process in traceFile.Processes)
  {
    //Przetworzenie kolejnych wątków
    foreach (IntelliTraceThread thread in process.Threads)
    {
      //Tworzymy strumień ze zdarzeniami. Każde zdarzenie odpowiada np.: wywołaniu metody
      Chain chain = thread.CreateThreadChain<ThreadChain>();
  
      EventToken eventToken = chain.FirstValidToken;
      //Przetwarzamy zdarzenie po zdarzeniu
      while (eventToken != chain.AfterLastToken)
      {
        //Pobranie zdarzenia
        IntelliTraceEvent ev = chain.GetEvent(eventToken);

        //To zdarzenie reprezentujące wywołanie metody
        if(ev is MethodEnterEvent)
          ProcessMethodEnterEvent(ev);
        //To zdarzenie reprezentujące zakończenie wywołania metody
        else if(ev is MethodExitEvent)
          ProcessMethodExitEvent(ev);
          
        eventToken = chain.GetNextToken(eventToken);
      }
    }
  }
}
Powyższy kod zawiera dużo komentarzy dlatego nie powinno być trudności z jego zrozumieniem. Chciałbym zwrócić uwagę tylko na jedną rzecz. Lista zdarzeń jakie możemy obsłużyć zawiera znacznie więcej pozycji niż tylko dwie pokazane powyżej (MethodEnterEvent, MethodExitEvent). Niestety ale nie ma dobrej dokumentacji z pełną listą jaki "łańcuch zdarzeń" jakie zdarzenia udostępnia. Pozostaje droga eksperymentalna oraz przyjrzenie się projektowi iTraceReader, o którym już pisałem w poście Logi IntelliTrace bez tajemnic.

Zobaczmy, więc jak wyglądają metody ProcessMethodEnterEvent oraz ProcessMethodExitEvent. Na samym początku należy zamienić zdarzenia MethodEnterEvent oraz MethodExitEvent, które zawierają tylko suchy ciąg bajtów na bardziej przyjazne ResolvedMethodEnterEvent oraz ResolvedMethodExitEvent, z których łatwo wyciągniemy interesujące nas informacje.
private void ProcessMethodEnterEvent(IntelliTraceEvent ev)
{
  ResolvedMethodEnterEvent methodEnterEvent = new ResolvedMethodEnterEvent(CurrentProcess, ev as MethodEnterEvent);
  //...
}
Klasy ResolvedMethodEnterEvent oraz ResolvedMethodExitEvent zawierają właściwość Method, która zwraca obiekt klasy ResolvedMethod. Klasa ta reprezentuje wywołaną metodę i udostępnia następujące bardzo intuicyjne właściwości:
  • ContainingTypeName - Pełna nazwa typu (razem z przestrzenią nazw) w ramach, którego metoda została zdefiniowana.
  • MethodName - Nazwa metody.
  • ParameterNames - Nazwy (ale nie wartości) parametrów.
  • ParameterTypeNames - Pełne nazwy typów parametrów.
  • ReturnTypeName - Pełna nazwa typu (ale nie wartość) zwracanego przez metodę.
Teraz przejdźmy do odczytania parametrów przekazanych do metody przy jej wywołaniu. Do tego celu posłuży nam metoda:

IList<IDataElement> ResolvedMethodEnterEvent.GetParameters()

W celu reprezentowania różnych bytów IntelliTrace API udostępnia interfejs o nazwie IDataElement. IDataElement może reprezentować typy proste i referencyjne. W przypadku typu referencyjnego IDataElement może zawierać elementy podrzędne reprezentujące składowe obiektu, które również będą reprezentowane przez IDataElement. Napisałem może, a nie musi ponieważ IntelliTrace analizuje tylko obiekty pierwszego poziomu. Czyli jeśli parametr jakiejś metody to obiekt klasy A, który zawiera składowe typu string, int oraz wskazanie na obiekt klasy B to IntelliTrace zapisze w logu wartości wszystkich składowych typów prostych obiektu klasy A ale już nie obiektu klasy B. Na temat obiektu klasy B dowiemy się tylko tyle, że jest. Poniżej zamieszczam opis interfejsu IDataElement. Zaznaczam jednak, że tworząc ten opis bazowałem na doświadczeniu i przeprowadzonych eksperymentach dlatego może on zawierać nieścisłości lub błędy:
  • HasChildren - true jeśli mamy do czynienia z typem referencyjnym i jest to obiekt pierwszego poziomu, false w przypadku typów prostych i obiektów zagnieżdżonych.
  • Name - Nazwa parametru, nazwa składowej obiektu, '[Return value]' dla wartości zwracanej przez metodę, this lub base.
  • TypeName - Pełna nazwa typu reprezentowanego bytu.
  • Value - W przypadku typów prostych właściwa wartość, a w przypadku typów referencyjnych nazwę typu. Jeśli wartość nie została zapisana przez IntelliTrace otrzymamy wynik 'unknown'.
  • GetChildren - Metoda, którą możemy wywołać jeśli właściwość HasChildren jest równa true. Metoda ta zwraca elementy podrzędne w stosunku do bieżącego.
Warto również wiedzieć o poniższej metodzie, która służy do pobrania wyniku zwróconego przez metodę:

IDataElement ResolvedMethodExitEvent.GetReturnValue()

Znacznie rzadziej będą nas interesowały parametry typu out, do których uzyskamy dostęp przy pomocy poniżej metody. Niestety ale wygląda na to, że metoda ta zawiera jakiś błąd. Próbowałem wywołać ją kilkukrotnie dla rożnych metod z parametrem out typu string. Za każdym razem kończyło się to błędem. Raz był to wyjątek ArgumentOutOfRangeException innym razem ApplicationException.

IList<IDataElement> ResolvedMethodExitEvent.GetOutputs()

Podsumowując pomimo ograniczeń IntelliTrace API zachęcam do zabawy z logami.

28/10/2010

Znikające Activity

Home

Na poniższym obrazku widać dwa schematy pochodzące z rożnych aplikacji, a przedstawiające fragment pewnego procesu biznesowego. Z prawej strony można zobaczyć definicję tego procesu, a z lewej powołaną do życia i uruchomioną jego instancję.



Niestety ale z jakichś powodów instancja różni się od swojej definicji. Pytanie dlaczego? Początkowo myślałem, że to jakiś problem z zapisywaniem lub odczytywaniem definicji/instancji procesu z bazy danych. Pomysł ten jednak szybko odrzuciłem ponieważ gdyby problem tkwił w tym miejscu to odczyt z bazy danych, deserializacja obiektu i w końcu wyświetlenie definicji/instancji procesu w standardowej kontrolce WorkflowView nie udało by się.

W dalszej kolejności zacząłem szukać różnic pomiędzy wspomnianymi aplikacji. Szybko udało się wykryć (dziękuję za pomoc Tomkowi), że kod brakującego Activity znajduje się w bibliotece niedostępnej dla aplikacji. Po skopiowaniu odpowiedniej dll'ki do katalogu z binariami aplikacji problem ustąpił.

Zacząłem się jednak zastanawiać czemu nie został zgłoszony żaden wyjątek informujący o braku dll'ki. Od razu pomyślałem, że ktoś musiał go przechwycić i zgasić. W sumie to się nie pomyliłem. Po włączeniu Managed Debugging Assistant dla wszystkich wyjątków (Debug -> Exceptions) i ponownym uruchomieniu aplikacji asystent pokazał, że wyjątek pojawia się w czasie deserializowania instancji procesu, gdzieś w środku klasy ActivityMarkupSerializer. Pojawienie się wyjątku nie przeszkadza jednak dokończyć deserializacji i zwrócić obiekt reprezentujący proces tylko, że bez kilku Activity.

Można powiedzieć, że ActivityMarkupSerializer jest bardzo sprytny ponieważ potrafi sobie poradzić nawet jeśli brakuje jakiejś biblioteki. Moim zdaniem ta klasa jest jednak za sprytna. Teraz już będę o tym wiedział ale na takim błędzie można stracić mnóstwo czasu.

14/09/2010

Internal .Net Framework Data Provider Error 30

Home

Ostatnio mam szczęście do "dziwnych" błędów. Dwa dni temu pisałem o htmlfile: Access Denied. Przy okazji podziękowania dla Tomka i Procenta za wskazówki jak sobie z nim poradzić. Teraz natknąłem się na błąd z zupełnie innej beczki związany z komunikacją z bazą danych. Zadanie do wykonania: wywołać procedurę składowaną. Zabrałem się do tego jak zawsze czyli nawiązałem połączenie do bazy danych, utworzyłem DbCommand, dodałem parametry, ustawiłem wartości parametrów i wywołałem procedurę. Niestety tym razem zamiast przejść do dalszej części zadania otrzymałem błąd Internal .Net Framework Data Provider Error 30. Konia z rzędem temu kto powie co ten błąd oznacza. Przeglądam kod jeszcze raz ale wszystko wygląda prawidłowo. Zresztą robiłem to już setki razy i działało. Jedyna różnica to taka, że teraz pracuję z bazą danych Oracle, a nie z bazą Microsoftu.

Od czego ma się jednak kolegów? Wysłałem maila z pytaniem czy ktoś już spotkał się z tym problemem i opłaciło się. Szybko uzyskałem wskazówkę, że błąd może być związany z błędną nazwą parametru i okazało sie to, że tak jest. W swoim kodzie użyłem metody, która w zależności od typu bazy danych zwraca odpowiednio przygotowany parametr nazwany. W przypadku Oracle parametry nazwane poprzedzone są dwukropkiem np.: :name, :date ale nie w przypadku przekazywania parametrów do procedury składowanej!!! Wspomniana metoda nie uwzględniała tego i stąd błąd. Inna sprawa to dlaczego sterownik Oracle nie ukrywa tej różnicy przed programistą oraz czemu ten błąd jest komunikowany w taki mało zrozumiały sposób.

Na zakończenie chciałbym podziękować Pawłowi za pomoc w rozwiązaniu problemu.

09/09/2010

htmlfile: Access Denied

Home

FileUploader to kontrolka, która umożliwia wybranie pliku przez użytkownika i przesłanie go na serwer. Po umieszczeniu na stronie renderuje się do pola tekstowego i przycisku Przeglądaj. Ja chciałem uzyskać efekt, w którym kontrolka ta jest ukryta, użytkownik naciska pewien przycisk, wyświetlane jest okienko do wyboru pliku, a następnie plik przesyłany jest na serwer. Zrobiłem to przy pomocy odrobiny JavaScript'u.
    function chooseFile()
    {
        var uploader = document.getElementById("<%= this.Uploader.ClientID %>");
        uploader.click();
    }

    function submitFile() 
    {
        document.forms[0].submit();
    }
Pierwsza funkcja chooseFile znajduje na stronie kontrolkę i wymusza kliknięcie na nią czyli powoduje wyświetlenie okienka do wyboru pliku. Druga funkcja po prostu przesyła stronę na serwer. Kontrolka i przycisk zostały osadzone na stronie w sposób pokazany poniżej:
    ...
    <asp:Button ID="Upload" runat="server" OnClientClick="chooseFile();" Text="Wybierz plik"/>
    ...
    <asp:FileUpload ID="Uploader" runat="server" style="display:none" onchange="submitFile();" />
     ...
Dzięki ustawieniu atrybutu display na wartość none kontrolka renderuje się do kodu HTML nie jest natomiast widoczna dla użytkownika. Po napisaniu kodu uruchamiam stronę, wybieram przycisk Wybierz plik i ku mojemu zadowoleniu pojawia się okienko do wyboru pliku. Wybieram interesujący mnie plik i na tym niestety kończy się zabawa, a pojawia się problem. W linii zawierającej document.forms[0].submit(); raportowany jest błąd htmlfile: Access Denied.

Sprawdzam jeszcze raz te kilka linijek kodu ale wyglądają prawidłowo. No dobrze, a może błąd jest spowodowany tym, że schowałem kontrolkę. Usuwam atrybut style i próbuję jeszcze raz ale błąd znowu się pojawia. Teraz kontrolka jest widoczna, więc sprawdzam co się stanie kiedy nacisnę przycisk Przeglądaj samemu, a nie przy pomocy JavaScript'u. Próbuję i tym razem wybrany plik został przesłany na serwer! Dla pewności powtarzam eksperyment jeszcze raz i ponownie sukces. Innymi słowy błąd pojawia się jeśli kontrolka do wyboru pliku zostanie kliknięta z poziomu kodu.

Więcej pomysłów nie mam, a więc zaglądam do Google i od razu widzę kilku nieszczęśników z podobnym problemem jak mój. Niestety zamiast rozwiązania problemu znajduję tylko informację, że błąd ten związany jest z jakimś mechanizmem bezpieczeństwa, który nie pozwala wysyłać plików bez wiedzy użytkownika. Sprawdzam jeszcze jak sytuacja wygląda w innych przeglądarkach (Opera i Firefox) i błąd się nie pojawia ale to dlatego, że nie działa kod uploader.click(); i okienko do wyboru pliku nie wyświetla się.

Jedyne co mi jeszcze przychodzi do głowy do wypróbowanie innych kontrolek do wyboru plików np.: Talerik'a. Dopóki nie znajdę obejścia problemu będę natomiast stosował tradycyjne podejście do zagadnienia czyli pozwolę użytkownikowi samemu nacisnąć przycisk.

31/08/2010

Jeden duży projekt, czy może wiele małych 2

Home

Publikując artykuł na temat "małych" i "dużych" byłem ciekawy opinii innych ludzi z branży. Pierwszy odzew otrzymałem od kolegi ze studiów Marka Kozłowskiego i za jego zgodą przedstawiam go poniżej.

Marek po przeczytaniu postu wyraził zdziwienie, że taki temat w ogóle pojawił się w rozmowie, a rozbicie kodu na podprojekty jest dla niego czymś naturalnym i oczywistym. Stwierdził również, że nie wyobraża sobie pracy z jednym dużym projektem (Aspekt psychologiczny), na który składa się na przykład 100 plików z kodem zawierających wszystko począwszy od logiki biznesowej, przez konwersję obiektów biznesowych na obiekty bazy danych, po warstwę dostępu do danych oraz interfejsy i implementację obiektów zdalnych. Przytoczył również taki przykład.

Wyobraźmy sobie, że piszemy projekt, który ma realizować funkcjonalność zarządzania magazynem i składa się (w skrócie) z warstwy prezentacji, warstwy logiki biznesowej oraz warstwy dostępu do bazy danych. Jest to prosta aplikacja desktopowa, a implementacje poszczególnych warstw znajdują sie w osobnych projektach (Wymusza poprawną architekturę). Po roku klient prosi o przygotowanie tej samej aplikacji ale w wersji WWW. Co robimy? Budujemy interfejs użytkownika WWW ale dalej korzystamy z tego samej logiki biznesowej i warstwy dostępu do bazy danych (Łatwiejsze użycie tego samego kodu w innym projekcie biznesowym). Innymi słowy bierzemy sobie dwie dll'ki i dodajemy do referencji aplikacji WWW.

Jak by to wyglądało w przypadku jednego dużego projektu? Albo musielibyśmy kopiować kod między projektami co doprowadziło by do redundancji i konieczności poprawiania tych samych błędów w dwóch projektach albo dodalibyśmy do projektu interfejsu WWW referencję do jednej dużej biblioteki albo nawet pliku exe!!!, która zawierać będzie wiele niepotrzebnych rzeczy.

Na koniec Marek słusznie zauważył, że wraz z wprowadzeniem obiektowych języków programowania nastąpił czas tworzenia pakietów, bibliotek, które tworzą sieć powiązań oraz, że jest to wręcz intuicyjne i naturalne.

Opinia Marka jest więc zgodna z moim zdaniem, co mnie bardzo cieszy, ale kij ma zawsze dwa końce. W najbliższej przyszłości postaram się, więc znaleźć i zachęcić do wypowiedzi kogoś z przeciwnego obozu.

15/08/2010

Jeden duży projekt, czy może wiele małych

Home

Kilka dni temu dyskutowałem z kolegą na temat tego czy całość/większość kodu powinna być umieszczona w jednym dużym projekcie (jak On uważa) czy rozłożona pomiędzy mniejsze projekty (jak uważam Ja). Przez projekt rozumiem tutaj jednostkę organizacji kodu np.: csproj w Visual Studio. Duży projekt to dla mnie taki, który zawiera wszystko czyli: implementację GUI, logikę biznesową, interfejsy, struktury danych, klasy dostępu do danych itd. Może się to przekładać na liczbę linii kodu ale nie musi. Dalej, aby odróżnić projekt jako jednostkę organizacji kodu od projektu jako przedsięwzięcia biznesowego będę używał sformułowania projekt biznesowy dla tego drugiego.

Wracając do wspomnianej dyskusji to zakończyła się impasem ponieważ żaden z nas nie zmienił swojego zdania. Spowodowała jednak, że postanowiłem jeszcze raz gruntownie przemyśleć sprawę. W ten sposób powstała poniższa lista zalet i potencjalnych wad małych projektów. Listy te są skonstruowałem w ten sposób, że na początki znajdują się najważniejsze/najpoważniejsze zalety i wady.

Zalety
  • Łatwiejsze użycie tego samego kodu w innym projekcie biznesowym. Przy dużych projekcie również jest to możliwe ale oznacza dodanie referencji do wielu innych niepotrzebnych w danym kontekście rzeczy czyl bałagan.
  • Wymusza poprawną architekturę. Na przykład jeśli GUI, logika biznesowa i dostęp do bazy danych znajdują się w innych projektach to projekt z GUI będzie miał referencję na projekt z logiką biznesową ale nie na odwrót ponieważ referencje cykliczne nie są dozwolone.
  • Ułatwia dalszy rozwój. Wyobraźmy sobie sytuację, w której projekt biznesowy jest na etapie stabilizacji i zbliża się termin oddania. Z drugiej strony istnieje konieczność dalszego rozwijania jakiejś jego części. Po wdrożeniu klient zapewne będzie zgłaszał błędy. Po jakimś czasie może pojawić się potrzeba złączenia (merge) dwóch (lub więcej) ścieżek rozwoju czyli przeniesienia poprawek błędów z wersji produkcyjnej na rozwojową i dodanie nowych funkcji z wersji rozwojowej do wersji produkcyjnej. W przypadku małych projektów łatwiej zorientować się co się zmieniło, co trzeba przenieść, a co nie, czy merge spowoduje jakieś błędy itd.
  • Aspekty psychologiczne. Nie przytłacza liczbą plików i folderów. Łatwiej zorientować się "o co biega" - łatwiej jest pracować z małym projektem odpowiedzialnym za jedną konkretna rzecz niż z dużym odpowiedzialnym za dziesiątki różnych rzeczy.
  • Łatwiejsze utrzymanie testów jednostkowych. W przypadku podejścia, w którym testy jednostkowe są umieszczane w innym projekcie niż testowany kod będzie mieli kilka małych projektów z testami jednostkowymi. W podejściu przeciwstawnym w danym projekcie będziemy mieli ograniczoną liczbę testów dotyczących tego jednego projektu. Należy to przeciwstawić dużym projektom gdzie powstanie nam albo kolejny duży projekt na testy jednostkowe albo bardzo duży projekt zawierający wszystko plus jeszcze testy jednostkowe tego wszystkiego.
  • Prostsze i łatwiejsze w utrzymaniu pliki konfiguracyjne. Ma to znaczenie jeśli używamy technologii wymagających wielu plików konfiguracyjnych, najczęsciej dokumentów XML np.: Spring.
  • Krótsza kompilacja. Jeśli nie zmieniły się interfejsy to można skompilować pojedynczy, mały projekt.
  • Wykonywanie brancha małego projektu trwa szybciej
Wady
  • Defragmentacja pamięci. Pisałem o tym już wcześniej. Problem polega na tym, że przy ładowaniu do pamięci moduły nie są ustawiane jeden po drugim ale są umieszczane w "losowo" wybranych miejscach co powoduje poszatkowanie pamięci. W większości wypadków nie jest to problemem ale na przykład przy alokacji dużej bitmapy potrzebny jest ciągły obszar pamięci. W wyniku defragmentacji system będzie dysponował dostatecznie dużą ilością pamięci ale nie w jednym kawałku. Problem nie występuje na systemach 64-bitowych, które są coraz powszechniejsze.
  • Dłuższe uruchamianie VS. Jeśli utworzymy Solution i dodamy do niego kilkadziesiąt projektów to jego otwieranie będzie trwać długo. Z drugiej strony czy aby na pewno praca z kilkudziesięcioma projektami ma sens?
  • Konieczność zarządzania dużą liczbą referencji. Każdy lub prawie każdy projekt będzie miał kilka lub więcej referencji do innych projektów. Zgadzam się, że może to być problem. Z drugiej strony pracowałem przy rozwijaniu naprawdę dużego systemu składającego się z kilkunastu podsystemów, każdy z kilkunastoma małymi projektami z czego część była współdzielona i radziliśmy sobie.
  • Trudniejsza instalacja. Wynika to z dużej ilości bibliotek dll, które powstają w wyniku kompilacji wielu małych projektów. Mogą również wystąpić konflikty wersji. Podobnie jak wyżej zgadzam się, że jest to możliwe ale podobnie jak wyżej przeciwstawiam tej wadzie swoje doświadczenie, które mówi co innego.
  • Dłuższa kompilacja całego systemu. Zgadzam się, przy wielu małych projektach kompilacja wydłuży się i to znacznie. Jednak i tutaj dołożę swoje trzy grosze. Jak często istnieje potrzeba przekompilowania całego systemu? Jeśli w danym momencie pracujemy z kilkoma konkretnymi projektami to po co wykonywać build wszystkich pozostałych? Ma to sens, jeśli zostały zmienione klasa, struktury lub interfejsy używane w wielu innych projektach.
  • Problemy z konfiguracją tych samych rzeczy w różnych projektach. Przy odpowiedniej architekturze systemu i zastosowaniu odpowiednich wzorców projektowych (singleton, fabryka) nie jest to dla mnie żaden problem.
  • Zwiększony czas uruchamiania aplikacji. Tak ale o ułamki sekund.
Ponieważ w powyższych wyliczeniach powoływałem się na to, że coś trwa tyle, a tyle przytoczę bardzo fajne zestawienie porównujące czasy kompilacji, ładowawania solution'a przez Visual Studio itd. dla różnej liczby projektów, które pojawiło sie w dyskusji na portalu stackoverflow - odpowiedź autorstwa jerryjvl'a.

Reasumując jestem przekonany, że zalety dzielenia kodu na małe projekty przeważają potencjalne wady. Co więcej uważam, że problemy z małymi projektami wynikają głównie ze złego podejścia i przyjęcia nieodpowiednich rozwiązań takich jak: budowanie wszystkich projektów zamiast kilku wybranych lub z dogmatycznego trzymania się małych projektów czyli bezrefleksyjnego tworzenia małego projektu na wszystko co się da. Co za dużo to jednak nie zdrowo :)

Na koniec jedna uwaga. Na początku projektu biznesowego może się wydawać, że lepiej trzymać wszystko w dużym projekcie bo tak prościej, bo kodu mało. Ale o ile nie jest to rzeczywiście malutki projekt biznesowy to szybko okaże się, że podzielenie kodu na mniejsze projekty będzie wymagać tyle pracy, że nikomu nie będzie się tego chciało zrobić.

09/08/2010

Logi IntelliTrace bez tajemnic

Home

IntelliTrace to jedno z najciekawszych narzędzi jakie pojawiło się w Visual Studio 2010. Dla tych, którzy jeszcze go nie znają w skrócie służy do nagrywania działania programu w celu późniejszej jego analizy. Nagrywane są wywołania metod oraz tzw. zdarzenia diagostyczne czyli ważne punkty w historii działania programu np.: nawiązanie połączenia z bazą danych, wykonanie zapytania do bazy danych, załadowanie modułu czy uruchomienie wątku. Visual Studio 2010 udostępnia kilka sposobów ich analizowania.

Listę wszystkich zarejestrowanych zdarzeń znajdziemy w oknie IntelliTrace Events View. Możemy je przefiltrować na podstawie wątku w jakim wystąpiły lub kategorii do jakiej należą. W oknie Calls View zobaczymy natomiast zdarzenia wplecione pomiędzy wywołania metod. Na tej podstawie łatwo zorientować się co doprowadziło do ich wygenerowania. Możliwości są więc całkiem spore ale jednak ograniczone. Z pomocą przychodzi IntelliTrace API, które pozwala na programową analizę logów IntelliTrace (*.iTrace).

Do czego może się to przydać? Do głowy przychodzi mi kilka pomysłów. Pierwszy z brzegu to program to graficznej wizualizacji zdarzeń. Dalej analiza logu pod kątem poprawności działania programu na podstawie analizy częstotliwości występowania poszczególnych typów zdarzeń. Sądzę, że w podobny sposób można analizować wydajność aplikacji. Może się również zdarzyć, że dane prezentowane przez Visual Studio będą dla nas zbyt ubogie i jedynym wyjściem będzie napisanie własnego analizatora.

Aby skorzystać z interfejsu programistycznego IntelliTrace powinniśmy zacząć od dodania do projektu referencji do biblioteki Microsoft.VisualStudio.IntelliTrace.dll. Znajdziemy ją w standardowym oknie Add Reference w zakładce .NET. Właściwą analizę logu rozpoczynamy od jego wczytania przy pomocy klasy IntelliTraceFile.
using (IntelliTraceFile file = new IntelliTraceFile(pathToIntelliTraceLog))
{
...
}
Klasa ta ma tylko jedną interesującą właściwość o nazwie Processes. Przy jej pomocy uzyskujemy dostęp do listy procesów, dla których log zawiera jakieś zdarzenia. W praktyce lista ta będzie zawierać tylko jedną pozycję. Proces reprezentowany jest przez klasę IntelliTraceProcess, która jest bardziej interesująca. Dzięki niej możemy dowiedzieć się o modułach załadowanych do procesu (właściwość Modules), uruchomionych wątkach (właściwość Threads) czy środowisku w jakim został uruchomiony proces (właściwość SystemInformationEvent). Wątki reprezentowane są przez klasę IntelliTraceThread.

Najważniejsze jest to, że klasy IntelliTraceProcess oraz IntelliTraceThread pozwalają na dobranie się do zdarzeń IntelliTrace. Służą do tego odpowiednio metody IntelliTraceProcess.CreateProcessChain oraz IntelliTraceThread.CreateThreadChain. Pierwszej z nich użyjemy jeśli interesują nas "łańcuchy zdarzeń" globalne dla całego procesu, a drugiej jeśli "łańcuchy zdarzeń" specyficzne dla danego wątku. Druga kategoria jest mniejsza i zawiera tylko trzy pozycje: ThreadChain, ThreadStreamChain oraz ThreadCheckpointChain. Warto zaznaczyć, że na tym poziomie wszystko jest zdarzeniem czyli zarówno wspomniane zdarzenia diagnostyczne jak i wywołania metod będą obsługiwane w taki sam sposób. Różnica polega tylko na innej klasie zdarzenia. Poniżej przykład wywołania metody IntelliTraceThread.CreateThreadChain, które zwróci łańcuch ze zdarzeniami odpowiadającymi min.: wywołaniu metod:
Chain chain = thread.CreateThreadChain<ThreadChain>();
Teraz nie pozostaje nic innego jak odczytać i przetworzyć kolejne zdarzenia:
EventToken eventToken = chain.FirstValidToken;
while (eventToken != chain.AfterLastToken)
{
  IntelliTraceEvent ev = chain.GetEvent(eventToken);

  //...

  eventToken = chain.GetNextToken(eventToken);
}
Klasa IntelliTraceEvent jest klasą bazową dla wszystkich typów zdarzeń, a więc aby uzyskać dokładniejsze dane należy wykonać rzutowanie na jeden z wydziedziczonych z niej typów.

Warto zwrócić uwagę na jedną szczególnie interesująca klasę z perspektywy analizy logu. Mam tutaj na myśli klasę DiagnosticStreamChain przy pomocy, której uzyskamy dostęp do tzw. zdarzeń diagnostycznych. Przykładem zdarzenia diagnostycznego jest wspomniane wcześniej wykonanie zapytania do bazy danych, naciśnięcie przycisku, wypisanie czegoś na konsolę czy przeładowanie strony w aplikacji ASP.Net. Zdarzeń tych jest dużo, a pełną ich listę znajdziemy w ustawieniach Visual Studio: Tools->Options->IntelliTrace->IntelliTrace Events. Możliwość analizy tego typu zdarzeń jest tym ciekawsza jeśli uwzględnimy możliwość rozszerzania tej listy o swoje własne zdarzenia! Zainteresowanych odsyłam do moich dwóch wcześniejszych tekstów Własne zdarzenia IntelliTrace oraz Własne zdarzenia IntelliTrace 2 .

Użycie IntelliTrace API nie jest skomplikowane ale nasuwa się pytanie:

Gdzie znaleźć dokumentację, szczegółowy opis poszczególnych klas reprezentujących zdarzenia, "łańcuchy zdarzeń" itd.?

Niestety ale dostępna dokumentacja na ten temat jest bardzo uboga. Znajdziemy w niej co prawda listę typów zdarzeń czy "łańcuchów" ale bez jakiegokolwiek opisu. Chyba, że opisem można nazwać coś takiego: "Describes a DiagnosticEvent." (dla klasy DiagnosticEvent) albo coś takiego: "Describes the ThreadCreateEvent." (dla klasy ThreadCreateEvent). Dla mnie to masło maślane. Konie z rzędem temu kto domyśli się na podstawie dostępnych informacji czym na przykład różnią się klasy ThreadChain oraz ThreadStreamChain? Można do tego dojść analizując kod biblioteki przy pomocy reflektora lub eksperymentalnie ale wymaga to dodatkowego wysiłku.

W zgłębianiu IntelliTrace API pomocny okaże się natomiast bardzo fajny projekt o nazwie iTraceReader, który znajdziemy tutaj. iTraceReader to nakładka na surowe IntelliTrace API, która ułatwia wykonanie wielu czynności. Zapoznając się z tym projektem można nauczyć się bardzo wielu rzeczy. Wszystkich, których ten temat zaciekawił zachęcam do bliższego przyjrzenia się.

28/07/2010

Używanie IntelliTrace poza Visual Studio 2010!

Home

W oficjalnej dokumentacji IntelliTrace można przeczytać, że narzędzie to jest dostępne tylko i wyłącznie z poziomu środowiska Visual Studio 2010. To bardzo źle ponieważ z góry przekreśla użycie IntelliTrace do diagnozowania błędów u klienta. Wyobraźmy sobie, że dostajemy zgłoszenie trudnego do powtórzenia błędu. Czy nie byłoby wspaniale uruchomić aplikację w środowisku produkcyjnym, pod kontrolą IntelliTrace i w razie wystąpienia błędu poprosić klienta o przekazanie nam plików z logami (*.iTrace). Nie wszystko jest jednak stracone. Pomimo, że nie jest to oficjalnie wspierane można uruchomić IntelliTrace z poza Visual Studio 2010!

Na początek należy odpowiedzieć na pytanie czym jest IntelliTrace: wątkiem działającym w ramach procesu Visual Studio 2010, niezależnym procesem, a może jeszcze czymś innym? Odpowiedź na to pytanie można uzyskać przy pomocy programu Process Explorer. Na poniższym obrazku widać fragment drzewa procesów dla węzła devenv.exe (czyli dla Visual Studio).



Pod nim znajdziemy węzeł odpowiadający debug'owanemu programowi oraz węzeł IntelliTrace, a w jego właściwościach ścieżkę z jakiej jest uruchamiany (VS_2010_INSTALL_DIR to katalog instalacyjny środowiska) np.:

VS_2010_INSTALL_DIR\Team Tools\TraceDebugger Tools\IntelliTrace.EXE

Reasumując IntelliTrace to niezależny proces, który komunikuje się z Visual Studio przy użyciu jakiegoś mechanizmu IPC (ang. Inter-process communication ). Jeśli spróbujemy go uruchomić z linii poleceń na ekran zostanie wypisana lista dostępnych komend. Z kilku dostępnych interesująca jest komenda launch, która zgodnie z podanym opisem uruchamia podaną aplikację i rozpoczyna jej monitorowanie. Po wpisaniu w linię poleceń: IntelliTrace help launch uzyskamy bardziej szczegółowe informacje na jej temat.

Z dostępnych parametrów najważniejsze to /logfile (w skrócie /f) przy pomocy, którego wskazujemy docelowy plik z logiem oraz /collectionplan (w skrócie /cp) przy pomocy, którego wskazujemy plik z planem działania, konfiguracją IntelliTrace. Skąd go wziąć? Domyślny plik używany przez Visual Studio 2010, a który jest dokumentem XML, znajdziemy w lokalizacji:

VS_2010_INSTALL_DIR\Team Tools\TraceDebugger Tools\en\CollectionPlan.xml

Mając już wszystkie elementy układanki, spróbujmy uruchomić jakiś program pod kontrolą IntelliTrace ale bez pomocy Visual Studio. Poniżej pokazano takie przykładowe wywołanie. W miejsce LOG_PATH należy wstawić ścieżkę do wyjściowego pliku z logiem (najlepiej nadać mu rozszerzenie iTrace), w miejsce COLLECTION_PLAN_PATH ścieżkę do pliku CollectionPlan.xml (może nazywać się inaczej i mieć inne rozszerzenie). PROGRAM_PATH to oczywiście ścieżka do programu jaki chcemy monitorować.

VS_2010_INSTALL_DIR\Team Tools\TraceDebugger Tools\IntelliTrace.EXE launch /f:LOG_PATH /cp:COLLECTION_PLAN_PATH PROGRAM_PATH

Po wykonaniu tego polecenia we wskazanym przez nas katalogu pojawi się plik z logiem. Jeśli będzie miał rozszerzenie iTrace to po dwukliku uruchomi się Visual Studio 2010. W tym momencie czeka nas jednak niemiła niespodzianka ponieważ okaże się, że plik z logiem nie zawiera żadnych informacji tak jakby program nie był monitorowany.



Na brakujący element układanki naprowadzi nas ponownie Process Explorer, który umożliwia podejrzenie parametrów wywołania programu. Interesuje nas oczywiście proces IntelliTrace. Jeśli odczytamy jego parametry wywołania to dowiemy się, że został uruchomiony z komendą run, a nie launch. Różnica polega na tym, że run w przeciwieństwie do launch uruchamia tylko proces monitorujący i nie wskazuje konkretnej aplikacji do monitorowania. To jaka aplikacja będzie monitorowana zależy od Visual Studio 2010. Kolejna, ważna dla nas różnica to wartość parametru /cp, który nie wskazuje na domyślny plan wykonania ale na jakiś "dziwny" plik np.:

C:\Users\user\AppData\Local\Microsoft\VisualStudio\10.0\TraceDebugger\Settings\frueouq2.vfs

Jest to plik tymczasowy, a jego nazwa generowana jest w sposób losowy. Jeśli spróbujemy go otworzyć to okaże się, że bardzo przypomina domyślny plik CollectionPlan.xml. Dokładniej mówiąc schemat XML jest taki sam, inne są natomiast wartości atrybutów, liczba węzłów itd. czyżby brakujący element układanki? Tworzymy, więc kopię tego pliku, wskazujemy go w parametrze \cp i ponownie wydajemy wcześniej pokazane polecenie. Tym razem po otworzeniu wynikowego pliku z logiem zobaczymy listę monitorowanych wątków, drzewo wywołań itd.



Pełny sukces? Niestety nie do końca. Po bliższym przyjrzeniu się zauważymy, że mamy dostęp tylko do drzewa wywołań, a brakuje informacji o zdarzeniach diagnostycznych (ważne punkty w historii wykonania programu np.: wykonanie zapytania do bazy danych). Tego ograniczenia nie udało mi się jeszcze ominąć. Przypuszczam, że przy uruchamianiu IntelliTrace przez Visual Studio 2010 do programu przekazywane są jakieś dodatkowe opcje. Być może do rejestrowania zdarzeń potrzebne jest Visual Studio, a logger IntelliTrace nie potrafi tego robić?

Najważniejsze jest jednak to, że już teraz IntelliTrace można uruchomić niezależnie od środowiska programistycznego, chociaż z pewnymi ograniczeniami. Biorę to za dobrą monetę i mam nadzieję, że pełne wsparcie dla uruchamiania IntelliTrace z poza Visual Studio jest w planach Microsoft'u i pojawi się, jeśli nie z którymś service pack'iem to w kolejnej edycji Visual Studio.

Artukuł opublikowałem również w serwisie software.com.pl.

27/06/2010

Słów kilka o ASP.NET, IIS, corflags i opcjach kompilacji

Home

Jakiś czas temu przenosiłem aplikację ASP.NET z środowiska developerskiego do testowego i jak często bywa w takich sytuacjach migracja nie obyła się bez pewnych kłopotów. Przy próbie uruchomienia aplikacji użytkownik otrzymywał informację o tym, że nie udało się załadować jednej z bibliotek. Po chwili zauważyłem, że bezpośrednim winowajcą był wyjątek BadImageFormatException. Z podobnym problem już się spotkałem dlatego szybko skojarzyłem, że przyczyną problemu może być próba załadowania 32 bitowej biblioteki do procesu 64 bitowego (środowisko testowe było 64 bitowe). Dla przypomnienia nie jest możliwe aby proces 64 bitowy korzystał z bibliotek 32 bitowych i na odwrót.

Przyjrzałem się więc bliżej kłopotliwej bibliotece przy pomocy programu corflags (pisałem już o nim w tym poście) i okazało się, że ma ona ustawiona flagę 32BIT wskazującą, że biblioteka może być uruchamiana tylko w trybie 32 bitowym. Sprawdziłem również inne biblioteki ale nie miały ustawione tej flagi. W tym momencie wszystko było już jasne, aplikacja ASP.NET hostowana była przez 64 bitowy serwer i w związku z tym nie mogła skorzystać z biblioteki 32 bitowej. Wyczyściłem flagę, również przy pomocy programu corflags i zgodnie z oczekiwaniami problem ustąpił.

Następnie postanowiłem wyjaśnić przyczynę czemu jedna biblioteka miała ustawioną flagę 32BIT. W tym celu przyjrzałem się ustawieniom projektu w Visual Studio i okazało się, że opcja Platform target ustawiona jest na x86 zamiast na Any CPU jak w innych projektach. W tym momencie przypomniałem sobie, że sam to zrobiłem żeby móc używać funkcji Edit and Continue, a po skończeniu pracy zapomniałem przywrócić odpowiednią konfigurację.

Na koniec pozostaje wyjaśnić czemu aplikacja działała w 64 bitowym środowisku developerskim, a w testowym już nie. Otóż, w środowisku testowym pula aplikacyjna w jakiej została umieszczona aplikacja miała wyłączoną flagę Włącz aplikacje 32 bitowe, która umożliwia hostowanie takich aplikacji na 64 bitowym serwerze IIS. Flaga ta dostępna jest w zaawansowanych ustawieniach puli aplikacji w grupie Ogólne. W środowisku developerskim używałem natomiast serwera zintegrowanego z Visual Studio. Nie jestem tego pewien ale prawdopodobnie ma on domyślnie włączoną tą flagę albo środowisko samo określa czy ta flaga ma być włączona w zależności od ustawień uruchamianych projektów.

Reasumując warto znać narzędzie corflags, opcję konfiguracji projektów Platform target oraz przełącznik Włącz aplikacje 32 bitowe .

16/06/2010

Własne zdarzenia IntelliTrace 2

Home

Dzisiaj, zgodnie z wcześniejszą obietnicą, chciałem pokazać w jaki sposób zdefiniować nowe zdarzenie diagnostyczne dla naszej własnej metody. W tym celu użyję bardzo prostej klasy pokazanej poniżej:

namespace SmallTest
{
    public class Fun
    {
        public void Hello(int count, string msg)
        {
            for (int i = 0; i < count; ++i)
                Console.WriteLine(msg);
        }
    }
}

Przyjmijmy, że klasa ta znajduje się w projekcie o nazwie SmallTest po skompilowaniu, którego powstanie plik SmallTest.dll. Skoro mamy już z czym pracować przystąpmy do modyfikacji pliku ColllectionPlan.xml. Cały proces będzie bardzo podobny do tego co pokazałem w poprzednim poście. Zaczynamy od znalezienia węzła o nazwie ModuleList i umieszczamy pod nim węzeł wskazujący na naszą bibliotekę:

  <ModuleSpecification Id="SmallTest">SmallTest.dll</ModuleSpecification>

Co istotne biblioteka nie musi być podpisana, ani znajdować się w jakimś konkretnym katalogu. Następnie tworzymy nową kategorię dla zdarzeń. W tym celu pod węzłem Categories powinniśmy umieścić pokazany poniżej węzeł XML. Jeśli nie chcemy tworzyć nowej kategorii możemy pominąć ten krok.

  <Category Id="my" _locID="category.My">My</Category>

Na koniec najważniejsza rzecz, zdefiniowanie nowego zdarzenia. Postępujemy dokładnie tak samo jak przy definiowaniu zdarzenia dla metody platformy .NET. Potrzebny kod XML został pokazany poniżej. Umieszczamy go pod węzłem DiagnosticEventSpecifications. Jeśli w poprzednim kroku nie tworzyliśmy nowej kategorii wartość węzła CategoryId powinna odpowiadać nazwie innej, już istniejącej kategorii.

    <DiagnosticEventSpecifications>
      <DiagnosticEventSpecification>
        <CategoryId>my</CategoryId>
        <SettingsName _locID="settingsName.Fun.Hello">Fun.Hello</SettingsName>
        <SettingsDescription _locID="settingsDescription.Fun.Hello">Fun.Hello</SettingsDescription>
        <Bindings>
          <Binding>
            <ModuleSpecificationId>SmallTest</ModuleSpecificationId>
            <TypeName>SmallTest.Fun</TypeName>
            <MethodName>Hello</MethodName>
            <MethodId>SmallTest.Fun.Hello(System.Int32, System.String):System.Void</MethodId>
            <ShortDescription _locID="shortDescription.Fun.Hello">Fun.Hello({0},{1})</ShortDescription>
            <LongDescription _locID="longDescription.Fun.Hello">Fun.Hello(count={0},msg={1})</LongDescription>
            <DataQueries>
              <DataQuery index="1" maxSize="0" type="Int32" _locID="dataquery.Fun.Hello.count" _locAttrData="name" name="count" query="" />
              <DataQuery index="2" maxSize="256" type="String" _locID="dataquery.Fun.Hello.msg" _locAttrData="name" name="msg"  query=""></DataQuery>
            </DataQueries>
          </Binding>
        </Bindings>
      </DiagnosticEventSpecification>

Szczegółowy opis znaczenia poszczególnych węzłów XML użytych w tej definicji podałem w poprzednim poście, a więc nie będę go tutaj powielał. Uruchamiany Visual Studio i jeśli nie popełniliśmy żadnego błędu możemy już korzystać z nowego zdarzenia. W celu sprawdzenia czy wszystko jest w porządku najpierw zaglądamy do Tools->Options->IntteliTrace->IntteliTrace Events. Powinniśmy zobaczyć nową kategorię, a po jej rozwinięciu nowe zdarzenie:



Teraz możemy stworzyć nowy projekt konsolowy i dodać referencję to biblioteki SmallTest. Kiedy to zrobimy, nie pozostaje nic innego jak utworzyć obiekt klasy Fun i wywołać metodę Hello:

            Fun f = new Fun();
            f.Hello(2, "Hello world!");

Po skompilowaniu i uruchomieniu programu pod kontrolą debugger'a i z włączonym mechanizmem IntteliTrace w oknie z zarejestrowanymi zdarzeniami zobaczymy nasze zdarzenie:



W następnym poście pokażę w jaki sposób analizować wystąpienia zdarzeń w sposób programowy.