Showing posts with label dobre praktyki. Show all posts
Showing posts with label dobre praktyki. Show all posts

07/07/2011

Bug w Visual Studio

Home

Niedawno spotkałem się z zabawnym bugiem w Visual Studio. Zauważyłem go w VS 2005, ale udało mi się go odtworzyć w VS 2010, zresztą nie jest to trudne. Poniżej bardzo prosty kawałek kodu, który pozwoli wyjaśnić o co chodzi:
public class Fun
{
  private string s = null;

  public void Test(string s)
  {
    Test2();
  }


  public void Test2()
  {
    Console.WriteLine(s);
  }
}
...
new Fun().Test("Ala ma kota");
Stawiamy pułapkę na początku metody Test oraz Test2 i uruchamiamy program. Po zatrzymaniu programu na pierwszej pułapce sprawdzamy wartość parametru s oraz prywatnej składowej klasy o tej samej nazwie. Otrzymamy taki wynik:



Powtarzając tą samą operację po zatrzymaniu programu na drugiej pułapce otrzyma natomiast taki wynik:



W "magiczny" sposób składowa prywatna, która powinna mieć wartość null przyjęła nagle wartość "Ala ma kota". Analogicznie parametr s przyjął wartość null zamiast "Ala ma kota". Na tym nie koniec. Poniższy obrazek pokazuje jak środowisko VS raportuje NullReferenceException i równocześnie pokazuje, że podejrzana referencja nie jest pusta.



Oczywiście nie ma w tym żadnej magii. Moim zdaniem to ewidentny bug w Visual Studio, spowodowany tym, że środowisko nie uwzględnia zasięgu obowiązywania składowych klas czy też parametrów metod. Z drugiej strony nazywanie w taki sam sposób zmiennych lokalnych, parametrów czy składowych klas, których zasięgi leksykalne pokrywają się, nie jest dobrym pomysłem bo prowadzi do bałaganu i pomyłek. W każdym razie wbrew pozorom w szale debugowania łatwo się na to złapać.

05/06/2011

Aplikacje wielojęzyczne - WPF

Home

Przystępując do tłumaczenia aplikacji WPF miałem dokładny plan jak się za to zabrać. Mianowicie postanowiłem użyć narzędzia LocBaml, o którym dowiedziałem się z training kit'a do egzaminu 70-502. Opis całej procedury można znaleźć tutaj. Praktyka pokazała jednak, że narzędzie to pozostawia bardzo dużo do życzenia. Dalej opiszę kolejne kroki pracy z LocBaml wraz z komentarzem jak to wygląda w praktyce.

UIDs

Mechanizm wielojęzyczności aplikacji w WPF koncepcyjnie zbliżony jest to tego co znamy z ASP.NET. W szczególności każda kontrolka, która posiada jakieś zasoby do przetłumaczenia powinna mieć odpowiedni identyfikator tzw. UID. Identyfikatory te można nadać automatycznie przy pomocy polecenia:

msbuild /t:updateuid NAZWA_PROJEKTU.csproj

Wszystkie pliki XAML, które mają zostać przetworzone przez msbuild powinny być checkoutowane (brzmi okropnie ale nie przychodzi mi do głowy dobry polski odpowiednik). Narzędzie to działa i dobrze i źle. Dobrze bo rzeczywiście wygeneruje dla wszystkich kontrolek UID. Źle bo zrobi to dla wszystkich kontrolek, a właściwie powinienem napisać dla wszystkich elementów dokumentu XAML. msbuild nie wykonuje jakiejkolwiek analizy przetwarzanych elementów. Skutkuje to dużym bałaganem. Po przeprowadzeniu tej operacji w plikach XAML pojawi się bardzo dużo, niepotrzebnych, śmieciowych UID'ów, na przykład po co nadawać identyfikator UID kontrolce Border, Line lub StackPanel. Potem aż bolą oczy jak się patrzy na tak przetworzony XAML. Moja rada jest taka. Jeśli chcemy stosować LocBaml to zawczasu, tworząc interfejs użytkownika, powinniśmy nadawać kontrolkom UID'y.

Wyciąganie zasobów do przetłumaczenia

Po wygenerowaniu UID'ów przechodzimy do następnego kroku czyli używamy LocBaml aby wyciągnać zasoby do przetłumaczenia do pliku CSV. Służy do tego takie polecenie:

LocBaml.exe /parse NAZWA_PROJEKTU.resources.dll /out:NAZWA_PLIKU_WYJSCIOWEGO.CSV

Skompilowane zasoby czyli pliki dll umieszczane są w podkatalogach o nazwach zgodnych z kulturą (językiem) zasobów np.: en-US, pl-PL itp. Jeśli nasz projekt nazywa isę SimpleApplication to plik dll z zasobami będzie nazywał się SimpleApplication.resources.dll. W praktyce wydanie tego polecenia skończy się błędem:

Could not load file or assembly '...' or one of its dependencies. The system cannot find the file specified.

Rozwiązanie jest proste ale trochę upierdliwe. Otóż plik LocBaml.exe, plik z zasobami NAZWA_PROJEKTU.resources.dll oraz plik exe/dll powstały po skompilowaniu aplikacji/biblioteki muszą znajdować się w tym samym folderze. Najszybciej to oskryptować. Podobny ale trochę inny komunikat o błędzie:

Could not load file or assembly '...' or one of its dependencies. An attempt was made to load a program with an incorrect format.

Otrzymamy jeśli nasza aplikacja ma ustawioną docelową platformę na x86. W takim wypadku możemy zmienić ustawienia naszej aplikacji, przekompilować LocBaml na x86 albo użyć narzędzia corflags o czym już zresztą pisałem tutaj lub tutaj.

Tak wygenerowany plik CSV powinniśmy teraz przetłumaczyć. Jeśli użyliśmy msbuild do wygenerowania UID'ów to plik ten będzie zawierał dla dużej aplikacji nawet kilkanaście tysięcy wierszy. W moim przypadku było to około 13 tysięcy wierszy z czego jakieś 10% wymagało przetłumaczenia!!! Po prawdzie tak duża liczba wierszy jest również związana ze sposobem działania LocBaml. Otóż dla każdej kontrolki z UID'em w pliku CSV znajdziemy wiele wierszy. Na przykład dla kontrolki TextBlock możemy zlokalizować, co oczywiste, właściwość Text ale również mniej sensowne jak Foreground czy Margin.

Stworzenie pliku dll z przetłumaczonymi zasobami

To jest chyba najtrudniejszy, a zarazem najmniej wygodny krok w całym procesie. W teorii jest to proste. Bierzemy przetłumaczony plik CSV i używamy LocBaml do wygenerowania dll'ki z zasobami o nazwie NAZWA_PROJEKTU.resources.dll, a następnie umieszczamy ją w odpowiednim katalogu aplikacji np.: en-US. Za wczytanie odpowiedniej wersji zasobów odpowiada już silnik WPF, a decyzje podejmuje podobnie jak w ASP.NET czy WinForms na podstawie właściwości Thread.CurrentThread.CurrentUICulture.

Problem pierwszy związany jest z bugiem w implementacji LocBaml, który objawia się tym, że identyfikatory zasobów w pliku CSV mogą się powtarzać. W związku z tym proces odwrotny czyli wygenerowanie dll'ki na podstawie pliku CSV się nie powiedzie. Rozwiązanie problemu można znaleźć tutaj i jest ono trywialne ale wymaga rekompilacji projektu. Można też, ale tego nie próbowałem, zapewnić, że wszystkie pliki XAML w naszym projekcie mają inne nazwy nawet jeśli znajdują się w innych katalogach.

Pierwszy problem w porównaniu z drugim to nic. Wyobraźmy sobie taką sytuację. Mamy projekt aplikacji WPF i grzecznie, zgodnie z zasadami wszystkie komunikaty wyświetlane użytkownikowi trzymamy w plikach zasobów resx. Po jakim czasie chcemy przygotować angielską wersję aplikacji. Zaczynamy od przetłumaczenia tych zasobów, a więc plik np.: msg.resx kopiujemy i zmieniamy mu nazwę na msg.en-US.resx, a następnie tłumaczymy.

Teraz zabieramy się za XAML. Nadajemy kontrolką UID'y, wyciągamy zasoby do pliku CSV, tłumaczymy i ponownie używamy narzędzie LocBaml do wygenerowania dll'ki z zasobami. Wszystko poszło jak po maśle i zadowoleni chcemy skopiować świeżutką dll'ke z przetłumaczonymi zasobami do katalogu en-US, a tu figa z makiem. W katalogu znajduje się już dll'ka o takiej nazwie. Skąd się wzięła? Została wygenerowana przez VS i zawiera komunikaty, które umieściliśmy w pliku msg.en-US.resx. Mamy więc dwie dll'ki o takiej samej nazwie, jedną z przetłumaczonymi komunikatami, a drugą z przetłumaczonym GUI i musimy umieścić je w tym samym katalogu.

I tutaj zaczynają się schody, wysokie, wąskie, ciemne i niewygodne. Nie pozostaje nic innego jak użyć linkera al.exe. Da się to zrobić ale tak jak powiedziałem jest to bardzo niewygodne. Nie będę tutaj tego opisywał bo moim zdaniem jest to strasznie nudne. Jeśli ktoś tego potrzebuje to zapraszam do kontaktu.

Inne

Takie podejście do lokalizowania aplikacji WPF ma również inne wady. Tak jak wspomniałem wersja zasobów jaka zostanie wczytana zależy od właściwości Thread.CurrentThread.CurrentUICulture. Trzeba więc pamiętać aby ustawić ją na odpowiednią kulturę zanim zaczniemy robić cokolwiek z interfejsem użytkownika. W przeciwnym wypadku możemy doprowadzić do wczytania złej wersji zasobów i okaże się, że cześć okien będzie w języku polskim, a część w angielskim. Po drugie, modyfikacja Thread.CurrentThread.CurrentUICulture nie powoduje automatycznego przełączenia się pomiędzy jedną, a drugą wersją zasobów. Dopiero zamknięcie okna i ponowne jego wyświetlenie spowoduje pobranie odpowiedniej wersji językowej zasobów.

Podsumowanie

LocBaml można użyć, pytanie czy z wszystkimi wadami i ograniczeniami tego narzędzia warto. W omawianym przypadku skończyło się to użyciem wyszukanej przez kolegę biblioteki WPFLocalizeExtension. Też ma swoje wady, też nie jest idealna, w szczególności w żaden sposób nie automatyzuje procesu lokalizowania aplikacji ale nie wymaga UID'ów, nie trzeba pisać skryptów, linkować itd. Tutaj zresztą kłania się to co już pisałem dwa razy:

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


26/05/2011

Aplikacje wielojęzyczne - WinForms

Home

Przyszła pora wrócić do tematu aplikacji wielojęzycznych. Tym razem skupię się na WinForms. Zacznę od tego, że część rzeczy, o których pisałem we wcześniejszym poście na temat aplikacji ASP.NET można zastosować do innych technologii, w szczególności do WinForms. Dla przypomnienia:
  • Jeśli chcemy aby nasza aplikacja miała wiele wersji językowych to przygotowujmy się do tego od pierwszej linijki tej aplikacji.
  • Stałe znakowe w kodzie są złe, bardzo złe, niewyobrażalnie złe... 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.: klasie o nazwie Constans.
Tyle tytułem wstępu. Kiedy przystępowałem do prac nad aplikacją WinForms byłem o tyle w gorszej sytuacji w porównaniu do wcześniejszych prac nad aplikacją ASP.NET, że nie wiedziałem o żadnych narzędziach wbudowanych w Visual Studio wspomagających lokalizację WinForms. Co do samego mechanizmu przełączania się pomiędzy różnymi wersjami językowymi zasobów to wygląda to tak samo jak w ASP.NET. Mamy więc odpowiednio ponazywane pliki np.: Resources.resx, Resources.en.resx itd. zawierające zasoby dla poszczególnych języków (kultur). Teraz w zależności od tego jaką kulturę ustawimy na właściwości Thread.CurrentThread.CurrentUICulture taka wersja zasobu zostanie wczytana. Na samym początku pomyślałem więc aby przejrzeć kod wygenerowany przez designer i w nim pozamieniać stałe znakowe na odwołania do zasobów. Podobnie postąpiłem przecież dla ASP.NET. Czyli poniższy kod:
...
// 
// cancelButton
// 
this.cancelButton.Location = new System.Drawing.Point(85, 103);
this.cancelButton.Name = "cancelButton";
this.cancelButton.Size = new System.Drawing.Size(75, 23);
this.cancelButton.Text = "Anuluj";
...
Zamienić na taki:
...
// 
// cancelButton
// 
this.cancelButton.Location = new System.Drawing.Point(85, 103);
this.cancelButton.Name = "cancelButton";
this.cancelButton.Size = new System.Drawing.Size(75, 23);
this.cancelButton.Text = Resources.Cancel;
...
To nie jest jednak dobry pomysł przynajmniej z kilku powodów. Kod generowany przez designer WinForms nie jest przeznaczony do samodzielnej modyfikacji. Podejście to zadziała ale wystarczy, że ktoś użyje designera aby zmienić położenie kontrolki lub zrobić coś równie prostego, a kod zostanie ponownie wygenerowany, a nasze zmiany usunięte. Po drugie przeglądanie kodu designera w poszukiwaniu stałych znakowych to żmudna, nudna i błędogenna robota. W następnej kolejności pomyślałem więc o innym rozwiązaniu:
public Form1()
{
  InitializeComponent();
  cancelButton.Text = Resources.Cancel;
  ...
}
Czyli zanim wyświetlimy formę, pobieramy zasoby i lokalizujemy GUI. Podejście bardzo proste, wręcz prymitywne. Można je trochę udoskonalić na przykład dodać do bazowej klasy metodę LocalizeGUI:
public class BaseForm : Form
{
  protected override void OnShown(EventArgs e)
  {
    LocalieGUI();
    base.OnShown(e);
  }

  protected virtual void LocalieGUI()
  {}
}
...
public partial class Form1 : BaseForm
{
  protected override void LocalieGUI()
  {
    cancelButton.Text = Resources.Cancel;
    ...
  }
}
Również bardzo proste rozwiązanie i można jeszcze dużo w nim zmienić. Zasadniczy problem polega jednak na tym, że takie rzeczy sprawdzają się kiedy używa się ich od początku. Ja dostałem gotową aplikację i jak wyobraziłem sobie przeglądanie całego kodu w poszukiwaniu etykiet, przycisków itd., a następnie przenoszenie ustawiania tekstu wyświetlanego użytkownikowi do metody LocalizeGUI to mi się odechciało.

Na szczęście istnieje dużo lepsze rozwiązanie problemu. Otóż Visual Studio posiada wsparcie dla wielojęzycznych WinForms i w przeciwieństwie do narzędzia Tools->Generate Local Resources dla ASP.NET działa całkiem dobrze. Używa się go bardzo prosto:
  • Otwieramy designer dla formy (kontrolki użytkownika).
  • Otwieramy okno właściwości (Properties) dla formy (kontrolki użytkownika).
  • Pole Localizable z kategorii Design ustawiamy na True i zapisujemy zmiany. W tym momencie designer wyniesie do pliku o nazwie NAZWA_FORMY.resx wszystkie stałe znakowe, które podlegają lokalizacji. Jeśli zajrzymy teraz do kodu generowanego przez designer to będzie on wyglądał trochę inaczej niż wcześniej. W szczególności nie znajdziemy tam kodu takie jak this.cancelButton.Text = "Anuluj";, a taki resources.ApplyResources(this.cancelButton, "cancelButton"); gdzie resources to obiekt klasy ComponentResourceManager. Co bardzo ważne dla kontrolek, które dodamy do formy później będzie to wyglądało tak samo.
  • Wracamy do okna właściwości (Properties).
  • Zmieniamy pole Language z Default na docelowy język np.: English.
  • Przechodzimy do okna designera i dokonujemy tłumaczenia. Czyli przycisk 'Anuluj' zamieniamy na 'Cancel', a etykietę 'Nazwa' na 'Name' itd.
  • Zapisujemy zmiany. W projekcie pojawi się nowy plik z zasobami o nazwie NAZWA_FORMY.JEZYK.resx.
  • Przywracamy poprzednią wartość pola Language czyli Default. Zawartość wszystkich zlokalizowanych kontrolek powinna wrócić do stanu wyjściowego.
  • Proces powtarzamy dla innych języków.
Z narzędziem tym pracuje się naprawdę przyjemnie. Trzeba tylko pamiętać, że w designerze na pierwszy rzut oka nie widać wszystkich rzeczy do przetłumaczenia na przykład pozycji menu. Praktyka pokazało również, że narzędzie to współpracuje z kontrolkami zewnętrznych dostawców. Mam tylko dwa zastrzeżenia. W jednym przypadku zmodyfikowany przez designer kod nie chciał się potem kompilować i musiałem go poprawić. Po drugie, o czym pisałem już w poście dotyczącym ASP.NET, jeśli nasza aplikacja składa się z wielu okien to otrzymamy wiele plików z zasobami. Moim zdaniem wprowadza to niestety bałagan do projektu i zmusza nas do wielokrotnego tłumaczenie tych samych tekstów.

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.

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).

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;
  }
}


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?

08/01/2011

Co to jest?!?!?!

Home

Co to jest?!?!?!. Kiedyś otrzymałem mail o takim właśnie tytule, a zawierający fragment mojego kodu. Kod ten, przyznam szczerze, zbyt elegancki nie był, ale napisałem go w sytuacji rodzaju "Masz to napisać na wczoraj". Ważne, że działał i robił to co miał robić. Nie jestem człowiekiem, który uważa swój kod za święty i sądzę, że umiem przyjąć krytykę, ale ten mail nie spodobał mi się z kilku powodów.

Po pierwsze oprócz mnie został wysłany do dwóch innych osób. Po drugie w kodzie zamieściłem komentarz wyjaśniający co ten krótki fragment kodu robi. Po trzecie treść wiadomości nie zawierała żadnych wyjaśnień jak to zrobić lepiej. Innymi słowy celem tego maila było nie tyle zwrócenie mi uwagi, że coś zrobiłem nie tak, ale bardziej pokazanie innym, że coś robię nie tak.

Całą sytuację nie przejąłem no bo i po co. Na "zaczepki" tego rodzaju mam w zwyczaju nie odpowiadać chyba, że się powtarzają. Kilka godzin później otrzymałem jednak wiadomość od jednej z dwóch osób, do której skierowany był mail Co to jest?!?!?!. Wiadomość ta była skierowana bezpośrednio do autora wiadomości Co to jest?!?!?! i wyglądała mniej więcej tak:

A to CO TO JEST?!?!?!:
  1. Opis błędu numer 1.
  2. Opis błędu numer 2.
  3. ....
Opis błędów wskazywał, że autor kodu poprzestał tylko na jego skompilowaniu i nawet jeden raz go nie uruchomił. Jak mówi stare przysłowie kto mieczem wojuje ten od miecza ginie. Krytyka jest potrzebna i często wskazana, ale krytykować trzeba umieć. Ja kiedy napotkam jakiś błąd w cudzym kodzie to trzymam się kilku zasad:
  1. Nie krytykuję publicznie czyli wysyłam mail tylko do jednej osoby. Jeśli rozmawiam z osobą, w której kodzie znalazłem błąd to mówię jej o tym w dyskretny sposób.
  2. Jeśli widzę, że błąd jest powszechnie popełniany to wysyłam maila to potencjalnie zainteresowanych osób i pokazuję przykład błędnego kodu ale nie wskazuję kto go napisał.
  3. Krytykując staram się zawsze wyjaśnić czemu uważam, że coś jest zrobione źle i jak to zrobić lepiej.
  4. Staram się aby moja wiadomość/wypowiedź nie była odebrana jako atak. Na przykład zamiast sformułowania Co to jest?!?!?! mail zatytułowałbym Błąd w metodzie SomeMethod. Taki tytuł wiadomość niesie z sobą jakąś informację i sądzę, że jest neutralny w odbiorze.
  5. Zanim wyślę maila lub pójdę porozmawiać upewniam się jeszcze raz, że błąd to rzeczywiście błąd.
Publiczna krytyka niestety jest czasem potrzebna, ale tylko w "beznadziejnych" przypadkach czyli wtedy kiedy ktoś zupełnie ignoruje nasze uwagi.

02/12/2010

Dlaczego należy być upierdliwym...

Home

... kiedy dostaje się jakieś zadanie do wykonania. Albo wręcz bardzo upierdliwym i dopytywać się o wszystkie szczegóły, a przede wszystkim o te, które wydają się nam oczywiste. Odpowiedź brzmi, aby nie tracić czasu. Zasada jest prosta ale pomimo to ciągle o niej zapominam kiedy sądzę, że już wszystko rozumiem i zadanie nie jest skomplikowane. Przykład z życia.

Otrzymałem do wykonania łatwe zadanie, które upraszczając polegało na tym aby, użytkownicy domenowi nie musieli logować się ponownie do aplikacji WWW używanej wewnątrz sieci klienta. Zdecydowałem, że zastosuję Windows Authentication. Testy w środowisku lokalnym przebiegły gładko, a więc pozostało zainstalowanie aplikacji na serwerze testowym. Myślałem, że to już koniec roboty podczas gdy w rzeczywistości zajęło mi to jeszcze dużo, dużo czasu. Z jakichś powodów automatyczne logowanie nie chciało działać na docelowej maszynie. Czemu? Bo nie byłem upierdliwy i nie zadałem jednego pytania:

Czy serwer testowy został dodany do domeny?

Niestety ale przyjąłem, że znajduje się w domenie, do której należą użytkownicy (warunek konieczny automatycznego logowania) i przyczyny kłopotów szukałem wszędzie tylko nie tam gdzie potrzeba. Bądźmy, więc upierdliwi i kiedy dostajemy zadanie pytajmy, pytajmy i jeszcze raz pytajmy.

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ć.

07/05/2010

ShowDialog i zwalnianie zasobów

Home

TestForm form = new TestForm();

if (form.ShowDialog() == DialogResult.OK)
{
  ...
}
Czy powyższy króciutki fragment kodu powodujący wyświetlenie okna dialogowego jest poprawny? Niestety, jeszcze do niedawna powiedziałbym bez mrugnięcia oka, że oczywiście tak. Niestety ponieważ ta odpowiedź jest niepoprawna. Do tego aby ten kod był poprawny brakuje niewielkiej ale za to bardzo istotnej rzeczy i zostało to pokazane poniżej:

using(TestForm form = new TestForm())
{
  if (form.ShowDialog() == DialogResult.OK)
  {
    ...
  }
}
Zamiast klauzuli using można oczywiście wywołać Dispose. Dlaczego jednak jawne zwolnienie zasobów jest w ogóle potrzebne? Czy to oznacza, że za każdym razem kiedy chcemy wyświetlić jakieś okno musimy pamiętać o klauzuli using?

Odpowiedź brzmi nie, jest to konieczne tylko jeśli wyświetlamy okno dialogowe. W takim wypadku po jego zamknięciu nie następuje wywołanie metody Close, która zwolni za nas zasoby. Dzięki temu możemy użyć tego samego okna ponownie. Nie jest to żadna wiedza tajemna i można o tym przeczytać w dokumentacji metody ShowDialog:

Unlike modeless forms, the Close method is not called by the .NET Framework when the user clicks the close form button of a dialog box or sets the value of the DialogResult property.

Kolejny raz przekonuję się, że nawet jeśli czegoś używamy bardzo często i jest to dla nas coś prostego i oczywistego to nie znaczy to, że robimy to na 100% dobrze :)

11/12/2009

Ciekawe zgłoszenie błędu

Home

Jest to druga, poprawiona wersja tego postu. Za wcześniejsze pomyłki przepraszam.

W poście tym chciałbym opisać interesujący błąd. Wszystko zaczęło się od zgłoszenia od klienta dotyczącego problemów z wydrukami. Nie wchodząc w szczegóły okazało się, że cały problem sprowadza się do utworzenia dostatecznie dużej bitmapy. Co jednak ciekawe analiza pokazała, że system dysponuje znaczną ilością wolnej pamięci (ok 1.5 GB) podczas gdy do utworzenia bitmapy potrzebne było "raptem" kilkaset megabajtów. Tutaj dodam, że mówimy o systemie 32 bitowym.

Z pomocą przyszedł tutaj program vnmap, który służy do analizy pamięci wirtualnej i fizycznej procesu. Pokazał on, że proces rzeczywiście dysponuje znaczną ilością pamięci ale największy ciągły jej obszar to tylko 200 MB. Do zaalokowania bitmapy potrzeba natomiast właśnie ciągłego obszaru pamięci. Nie dotyczy to zresztą tylko bitmap, podobny problem możemy wystąpić przy ładowaniu bibliotek dll.

Taką sytuację nazywamy defragmentacją pamięci. Co było jej przyczyną? Zgodnie z tym co pokazał wspomniany program vnamp pamięć w dużym stopniu była poszatkowana przez biblioteki dynamiczne. Nie bez znaczenia jest tutaj fakt, że rozpatrywany przypadek dotyczył dość dużego systemu zbudowanego z wielu modułów.

Problem próbowałem zaleczyć przy użyciu narzędzia rebase.exe, które służy do ustawienia preferowanego adresu pod jaki ma zostać załadowana dll'ka. Testy niestety pokazały, że to nic nie daje.

Pytanie co jest przyczyną takiego położenia bibliotek w pamięci? Tutaj nie pozostaje mi nic innego jak rozłożyć ręce. Wcześniej byłem przekonany, że jest to związane z mechanizmem bezpieczeństwa, który losowo rozrzuca biblioteki po pamięci. Zwrócono mi jednak uwagę, że taki mechanizm (ASLR) pojawił się dopiero w Windows Vista. Sprawa jest więc otwarta. Jakieś pomysły?

Jak sobie z tym poradzić? Generalnie jednoznacznej odpowiedzi nie ma, ja znam trzy podejścia. Po pierwsze przejście na system 64 bitowy rozwiąże problem ale nie jest to zawsze możliwe. Po drugie można próbować wyeliminować konieczność alokacji tak dużej bitmapy ale może to być bardzo trudne. Można też użyć przełącznika /3GB, który pozwala procesom użytkownika użyć 3 GB pamięci wirtualnej zamiast domyślnych 2 GB ale nie jest to zalecane rozwiązanie.

Na zakończenie chciałbym podziękować koledze Damianowi z pracy, który analizował zgłoszenie klienta i podsunął mi pomysł na ten post.

19/10/2009

Trochę o zwalnianiu zasobów

Home

Każdy dobry programista wie, że po skończeniu pracy z obiektem klasy implementującej interfejs IDisposable należy wywołać metodę Dispose (jawnie bądź nie jawnie). Dlatego kiedy ostatnio zobaczyłem kod, w którym programista beztrosko raz po raz tworzy ikonę, a następnie radośnie o niej zapomina powodując wzrost liczby obiektów GDI przez usta przeszły mi dość niecenzuralne słowa. Oczywiście od razu poprawiłem kod w mniej więcej taki sposób:
using(Icon icon = GetIcon())
{
   ...
}
Nic prostszego można powiedzieć. Jednak przy następnym uruchomieniu aplikacji ku mojemu zdziwieniu liczba obiektów GDI znowu zaczęła rosnąć. Zaglądam, więc do metody GetIcon. A tam widzę coś takiego:
return Icon.FromHandle(handle);
Nie kojarząc za bardzo metody FromHandle zaglądam do dokumentacji, a tam jest napisane coś takiego:

When using this method you must dispose of the resulting icon using the DestroyIcon method in the Win32 API to ensure the resources are released.

Kolejny mój krok to oczywiście sprawdzenie czy wywołanie DestroyIcon działa. Metodę tą należy zadeklarować w następujący sposób:
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet=CharSet.Auto)]
extern static bool DestroyIcon(IntPtr handle);
Jej użycie oczywiście rozwiązało problem. Co dociekliwsi mogą jeszcze zapytać czemu wywołanie Dispose nie wystarcza. Sprawa jest dość ciekawa. Okazuje się, że klasa Icon używa wewnętrznie DestroyIcon do zwalniania zasobów ale tylko i wyłącznie wtedy kiedy jest właścicielem tych zasobów to jest kiedy sama je zaalokuje. W momencie tworzenia ikony przy pomocy uchwytu dostarczonego z zewnątrz trzeba samemu zadbać o jego zwolnienie.

Reasumując w opisanym przypadku dwóch programistów zrobiło dwa poważne błędy. Pierwszy zapomniał o zwolnieniu zasobów, a drugi o dokładnym przeczytaniu dokumentacji. Metoda GetIcon powinna zostać napisana tak aby korzystający z niej programista nie musiał posiadać wiedzy "tajemnej" aby dobrze jej użyć.

26/08/2009

Przykład tego jak nie należy pisać kodu

Home

Czasami jak patrzę na niektóre kody to zastanawiam się o czym myślał piszący je programista. Ostatnio natknąłem się na kod mniej więcej jak poniżej (zmieniłem nazwy zmiennych, metod itd.). Pomińmy jaka jest jego logika i gdzie został użyty. Proponuję natomiast przyjrzeć się przez chwilę temu fragmentowi i spróbować odpowiedzieć na pytanie co jest nie tak.
...
for (int i = 0; i < values.Length; i++)
{
  if (values[i] != null)
  {
    if (hash[values[i]] != null)
      Process(hash[values[i]]);

    if (hash[values[i]] != null)
    { 
      Process(values[i]);
      Process(values[i], hash[values[i]]);
      hash.Remove(values[i]);
    }
  }
}
...
Mnie uderzyła duża i zupełnie zbyteczna liczba odwołań do zmiennych values (tablica) i hash (Hashtable). Odwołanie do tej pierwszej występuje 8 razy, a do drugiej 5 razy! Po chwili pracy i wprowadzeniu bardzo prostych poprawek kod wygląda tak:
...
for (int i = 0; i < values.Length; i++)
{
  string val = values[i];
  if (val != null)
  {
    object obj = hash[val];
    if (obj != null)
      Process(obj);

    if (obj != null)
    {
      Process(val);
      Process(val, obj);
      hash.Remove(obj);
    }
  }
}
...
Odwołanie do zmiennych values oraz hash występuje tylko 1 raz. Czemu o tym piszę? Odpowiedź jest tak prosta jak wprowadzone poprawki: wydajność. Dodam jescze, że kod ten można jeszcze ulepszyć. W tym poście skupiam się jednak na tej jednej rzeczy.

W tym przypadku poprawiona wersja działa jakieś 3x/4x razy szybciej! Przyznam jednak, że całościowy efekt tej optymalizacji jest niezauważalny z dwóch powodów. Po pierwsze długość używanej w kodzie tablicy i liczba elementów w tablicy hashującej jest generalnie niewielka. Przeważnie nie przekracza kilkudziesięciu, może kilkuset elementów. W takim scenariuszu czas wykonania tego kodu nie przekracza milisekundy. Po drugi częstotliwość użycia tego kodu jest niewielka.

Z drugiej jednak strony programista, który napisał omawiany kod stosuje (zastosował) takie konstrukcje w wielu miejscach. Być może wywołuje jedną, ciężką, metodę wielokrotnie zamiast zapamiętać jej wynik wywołania na później. Przykładów takich można mnożyć.

Reasumując należy uczyć się na cudzych błędach i strzec się takich konstrukcji.

18/08/2009

Ciekawy przypadek optymalizacji

Home

Ostatnio zajmowałem się optymalizacją aplikacji mapowej do planowania i inwentaryzacji sieci telekomunikacyjnych. Problem polegał na tym, że przy dużej liczbie warstw aplikacja zaczynała działać powoli. Warstwa to taki pojemnik na dane tego samego rodzaju np.: warstwa kabli, warstwa wzmacniaczy itd.

W określonych scenariuszach liczba takich warstw mogła sięgać nawet kilku tysięcy. Zacząłem od wrzucenia aplikacji do profilera (AQTime). Jak to często bywa w takich sytuacjach okazało się, że czas pożera kilka metod, które same z siebie wykonują się bardzo szybko ale są wołane bardzo dużo razy - proporcjonalnie do liczby rastrów. W rzeczywistości dojście to tego o jakie metody chodzi nie było oczywiście takie proste ale nie będę zanudzał szczegółami.

Po pierwsze postanowiłem ograniczyć liczbę wywołań tych metod. Niestety okazało się, że z różnych powodów nie da się tego zrobić. W drugim podejściu postanowiłem, więc ograniczyć czas potrzebny na ich wykonanie. Co ciekawe okazało się, że metody te pozornie prawie nic nie robią czyli zawierają tylko odwołania do właściwości Count czy też indeksera jakiejś kolekcji.

Zanim przejdę dalej wyjaśnię, że jako platformę mapową używam rozwiązań firmy ESRI opartych o technologię COM. W związku z tym konieczne jest użycie mechanizmów interoperacyjności pomiędzy kodem zarządzanym i niezarządzanym. W omawianym przypadku wspomniana kolekcja nie była kolekcją zarządzaną ale wrapper'em na obiekt COM. Postanowiłem, więc zrezygnować z odwołań do tej kolekcji i użyć dobrze znanej klasy List<T>. W praktyce utworzyłem instancję listy zarządzanej istniejącą równolegle do kolekcji niezarządzanej. Elementy do obu kolekcji dodawane są równocześnie, podobnie usuwane. Nie jest to problem ponieważ czynność ta wykonywana jest jednorazowo.

Postępując podobnie w kilku innych miejscach, to znaczy eliminując odwołania do wrapper'ów do obiektów COM, zmniejszyłem czas wykonywania niektórych czynności o połowę! Wydaje mi się, że to niezły wynik szczególnie, że dalsze optymalizację cały czas są możliwe. Wniosek z tego taki, że czasem warto wprowadzić redundancję aby zyskać na wydajności.

22/07/2009

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

Home

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

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

07/05/2009

"Autentykacja" - Czy takie słowo istnieje?

Home

Nie jestem purystą językowym ale niektóre rzeczy mi przeszkadzają i uważam, że powinno się mówić, a szczególnie pisać po polsku, a nie po "polskiemu". Właśnie jestem po lekturze kilku dokumentów dotyczących bezpieczeństwa, zarządzania użytkownikami oraz uprawnieniami, w których nagminnie używano słowa autentykacja. Niby wiadomo, że chodzi o proces uwierzytelniania (ang. authentication) ale takiego słowa nie ma po prostu w języku polskim. Jeśli ktoś nie wierzy to zapraszam do słownika języka polskiego, dostępny również on-line. Pół biedy kiedy używamy tego sformułowania w luźnej rozmowie ale słowo pisane jest bardziej wrażliwe na takie neologizmy, dziwne sformułowania itd.

06/05/2009

Dopowiedzenie

Home

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

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

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

28/04/2009

Słów kilka o terminologii

Home

Ostatnio odniosłem wrażenie, że jest spora grupa osób, która dość niechlujnie używa terminów szyfrowanie (deszyfrowanie) i kodowanie (dekodowanie). Prowadzić to może natomiast do niezrozumienia u rozmówcy lub czytelnika.

Kodowanie to proces przekształcenie danych wejściowych do innej, bardziej pożądanej i użytecznej w danym kontekście postaci. W jednym przypadku użyteczne będą to dane skompresowane, w innym zapisane przy użyciu odmiennego alfabetu, a w jeszcze innym zaszyfrowane. Zgadza się, szyfrowanie jest specyficzną formą kodowania. Podstawowa różnica pomiędzy szyfrowaniem, a innymi formami kodowania polega natomiast na tym, że szyfrowanie wymaga podania klucza. Zarówno w przypadku innych form kodowania jak i szyfrowania algorytm może być dobrze znany i dostępny publicznie. Tylko, że nie znając klucza, a znając algorytm szyfrujący nie zdziałamy dużo. Pomijam tutaj sytuację kiedy algorytm został skompromitowany (złamany) lub kiedy użyty klucz jest zbyt słaby, łatwy do odgadnięcia.

Pojęcie kodowania jest, więc bardzo ogólne. Podsumowując mówiąc - te dane są zakodowane - tak naprawdę nie przekazujemy żadnej informacji, a już na pewno nie o tym, że dane zostały zaszyfrowane - utajnione.