27/07/2011

IntelliTrace - Reaktywacja

Home

Już kawał czasu temu w artykule Używanie IntelliTrace poza Visual Studio 2010! opisałem w jaki sposób uruchomić narzędzie IntelliTrace poza środowiskiem Visual Studio 2010. Przedstawiona przeze mnie metoda miała jednak wadę. Log z nagranym przebiegiem wykonania programu zawierał "tylko" informację o wywołaniach metod czyli drzewo wywołań, a brakowało w nim informacji o zdarzeniach diagnostycznych czyli ważnych punkty w historii wykonania programu np.: wykonanie zapytania do bazy danych.

Ostatnio udało mi się znaleźć ostatni element układanki. Otóż aby log IntelliTrace zawierał wszystkie niezbędne informacje wystarczy wykonać, oprócz opisanych już przeze mnie rzeczy, jeszcze jeden krok czyli zmodyfikować plik CollectionPlan.xml z planem działania/konfiguracją narzędzia. Znajdujemy w nim linię:
...
<DiagnosticEventInstrumentation enabled="false">
...
i zamieniamy na:
...
<DiagnosticEventInstrumentation enabled="true">
...

22/07/2011

Londyn - przechodzenia przez ulicę

Home

W Polsce od maleńkiego uczono mnie, że przez ulicę należy przechodzić na pasach i kiedy pali się zielone światło. Swego czego przekonałem się również ile wynosi mandat za przechodzenie na czerwonym świetle:) Kiedy wyjeżdżam za granicę z założenia stosuję te same zasady, choćby dlatego, żeby nie płacić potencjalnie wyższych mandatów niż w Polsce.

W Londynie (nie wiem czy jest tak w całym Zjednoczonym Królestwie), co mnie bardzo zaskoczyło, sprawa ma się zupełnie inaczej. Zdecydowana większość osób przechodzi na czerwonym świetle i jest to właściwie zasadą. Początkowo miałem jednak pewne skrupuły. O tym, że jest to "dozwolone" przekonałem sie kiedy zobaczyłem, że policja nie reaguje na ten niecny proceder nawet jeśli przechodzi się na czerwonym świetle tuż koło właśnie ruszającego radiowozu!

Z czego wynika to przyzwolenie na łamanie prawa? Od kolegi dowiedziałem się, że wbrew pozorom wszystko dzieje się w majestacie prawa. Z ciekawości poszukałem informacji na ten temat i wygląda na to, że kolega miał rację. Otóż przepisy dotyczące przechodzenia przez jezdnię sformułowano przy użyciu wyrażeń "should/should not" albo "do/do not" (rada/zalecenie), a nie "must/must not" (zakaz/nakraz). Innymi słowy napisano, że "nie powinno" się przechodzić na czerwonym świetle zamiast, że "jest zabronione/nie wolno" przechodzić na czerwonym świetle.

Takie podejście do przechodzenia przez ulicę z pewnością przyspiesza poruszanie się po Londynie. Trzeba jednak pamiętać, że robimy to na swoją odpowiedzialność. Jeśli zostaniemy potrąceni przez samochód to będzie to nasza wina.

Linki do poprzednich postów z serii na temat życia w Londynie:

19/07/2011

See[Mike]Code

Home

Dzisiaj rozmawiając z kolegami zeszliśmy na temat rekrutacji programistów. Między innymi rozmawialiśmy o narzędziach wspomagających ten proces takich jak Codility. Rozmowa ta przypomniała mi, że swego czasu natknąłem się ba bardzo proste ale pomysłowe narzędzie pozwalające na żywo, zdalnie sprawdzić jak potencjalny kandydat radzi sobie z programowaniem. Miałem trudności z przypomnieniem sobie adresu strony dlatego ku pamięci publikuję ten post. Przy okazji sądzę, że narzędzie to może przydać się innym. Mianowicie chodzi o See[Mike]Code.

Zasada działania jest bardzo prosta. Wchodzimy na stronę See[Mike]Code, klikamy przycisk New Interview Site i otrzymujemy dwa adresy. Jeden wysyłamy do kandydata, a drugi zachowujemy dla siebie. O wyznaczonej porze prosimy aby kandydat uruchomił przeglądarkę i wszedł na podany adres. Jego oczom ukarze się taki widok:



Kiedy wejdziemy pod drugi adres wyświetlona zostanie dość podobna strona. Teraz prosimy kandydata aby wykonał jakieś proste zadanie programistyczne np.: słynne FizzBuzz, a jego poczynania możemy obserwować na ekranie naszego komputera. See[Mike]Code nie udostępnia takich dobrodziejstw jak IntelliSense ale w przypadku prostych zadań, takich jak wspomniane FizzBuzz, nie jest to konieczne.

Jestem przekonany, że na polskim rynku nie mamy sytuacji, w której jak to napisał Jeff Atwood na swoim blogu:

Like me, the author is having trouble with the fact that 199 out of 200 applicants for every programming job can't write code at all. I repeat: they can't write any code whatsoever.

Sądzę jednak, że dobrze mieć w swoim repertuarze takie narzędzie.

13/07/2011

Londyn - pogotowie

Home

Dzisiaj idąc do pracy byłem świadkiem jak pogotowie udzielało pomocy kobiecie, która zasłabła na moście London Bridge. Co w tym takiego ciekawego, ze postanowiłem o tym napisać? Moją uwagę zwrócił środek transportu jakim przyjechał ratownik czyli rower. Słyszałem o tym ale po raz pierwszy widziałem.

Temat podrążyłem trochę bardziej i dowiedziałem się, że w Londynie pogotowie posługuje się oczywiście samochodami ale również motocyklami (motorcycle responders) i rowerami (cycle responders). Dwa ostatnie środki transportu używane są w najbardziej ruchliwych/zakorkowanych dzielnicach tak aby nawet w godzinach szczytu dotrzeć do poszkodowanego w jak najkrótszym czasie. Za takim rowerowym pogotowiem wysyłany jest również ambulans. Jeśli jednak okaże się, ze poszkodowany nie musi zostać przetransportowany do szpitala to ratownik może odwołać wyjazd. Dzięki takiemu podejściu, od momentu wprowadzenia pogotowania rowerowego (10 lat temu) ambulanse musiały wyjechać o 20 tysięcy razy mniej, co przekłada się przecież na konkretne pieniądze. Inny niespotykany w Polsce "wynalazek" to Ambulance community responder czyli jednoosobowe pogotowie samochodowe, w którym pracują ochotnicy. Ich celem jest dotarcie do pacjenta i utrzymanie go przy życiu, aż do przybycia ambulansu, których liczba jest ograniczona.

O Brytyjskiej służbie zdrowia słyszałem dwie opinie, ze jest zła albo, że jest bardzo zła. Mam nadzieję, ze nie będę miał okazji tego zweryfikować ale przynajmniej pogotowie wygląda tutaj na dobrze zorganizowane i sprawnie działające.

Linki do poprzednich postów z serii na temat życia w Londynie:

10/07/2011

Londyn - ceny jedzenia

Home

Dzisiaj wrócę do tego jak żyje się w Londynie, a konkretniej do cen artykułów spożywczych i innych. Trochę zakupów już zrobiłem i na podstawie paragonów sporządziłem listę wybranych produktów wraz z cenami w supermarketach (Tesco, Lidl, Sainsbury's). Lista ta znajduje się na końcu postu. Oczywiście nie jest kompletna ale pi razy oko pozwala zorientować się w poziomie cen. Po miesiącu pobytu sporządzę natomiast podsumowanie ile przez ten czas kosztowało mnie jedzenie.

Czy jest drogo? Trochę na pewno ale ogólnie możemy być dumni bo ceny w Polsce z pewnością osiągnęły Europejski albo prawie Europejski poziom. Szkoda, że to samo nie dotyczy pensji :) Dodam jeszcze, że dość powszechne są promocje rodzaju kup 3 paczki wędliny, a zapłać za dwie co przy większych zakupach pozwala znacząco obniżyć ich koszt.

Sporo droższe jest na pewno żywienie się na mieście np.: za kebab zapłaciłem 5£, za narodowe danie Brytyjczyków czyli Fish&Chips 10£, a za świeżo zrobioną kanapkę z szynką (takie 2 trójkąty) 3.5£. Duży kubełek ryżu z kurczakiem curry lub zestaw obiadowy Sushi w sieciówce Wasabi (bardzo polecam) to koszt odpowiednio ok. 5£ i ok. 6,5£. Są to ceny z centrum Londynu, więc z pewnością trochę zawyżone ale tak czy inaczej jest drożej niż w Polsce. Jeśli chodzi o bardzo ważną rzecz czyli cenę piwa to 1 pinta (~568 ml) w pubie kosztuje ok. 3,5£. Do tematu piwa i pubów wrócę jeszcze później.
  • Coca cola 2L 1,5£ - 1,89£
  • Woda mineralna 2L 0,45£
  • Piwo 0,81£-1,1£
  • Sok pomarańczowy 1 L 1,19£ - 1,56£
  • 1 kg jabłek 1,5£
  • 1 kg pomidorów 1,99£
  • 1 kg cebuli1,2 £
  • 1 banan 0,18£
  • Mała puszka pomidorów 0,41£
  • Słoik ogórków konserwowych 0,99£
  • Mały słoik oliwek 0,59£
  • Paczka Spaghetti 0,79£
  • 0,5 kg soli 0,15£
  • 50g pieprzu 0,55£
  • 0,5 kg wołowego mięsa mielonego 2,69£
  • 4 steki wieprzowe (nie pamiętam wagi, tak na 2 obiady) 3,29£
  • Chleb pełnoziarnisty 1,25 £
  • Zupka w proszku 0,19£
  • Czteropak jogurtów Activia 1,66£
  • masło 1,6£
  • 1 Kg paczka frytek McCane 1,86£
  • Paczka wędliny 140g 1,15£
  • 15 jajek 1,25£
  • 1 kg proszku do prania Persil
  • batonik Snicker 0,50£ - 0,60£


Linki do poprzednich postów z serii na temat życia w Londynie:

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

06/07/2011

Londyn - początek

Home

Życie podąża różnymi ścieżkami, a mnie ostatnio przywiało do Londynu. Zawsze lubiłem czytać o życiu i pracy w innych krajach dlatego postanowiłem, że korzystając z okazji będę dzielił się swoimi wrażeniami na blogu.

Zacznę od tego, że jestem w podróży służbowej, a więc jestem w tej komfortowej sytuacji, że przyjechałem na gotowe. Z tego powodu nie mogę jednak opisać z autopsji jak wygląda szukanie pracy czy mieszkania w Londynie. Skupię się, więc na innych rzeczach: ile kosztuje mnie życie z wyłączeniem kosztów około mieszkaniowych, jak wyglądają dojazdy do pracy, czy miasto mi się podoba itd. Tym samym mocno odbiegnę od zwyczajnej tematyki bloga ale mam nadzieję, że się spodoba. Posty na ten temat chciałbym publikować co kilka dni ale będzie to raczej zlepek luźno powiązanych z sobą wpisów niż z góry zaplanowana seria.

Na początek kilka zdań o tym jak idzie mi dogadywanie się z autochtonami. Wyjeżdżając do Londynu miałem z jednej strony wiele powodów sądzić, że nie będę miał z tym większych problemów ale miałem również trochę obaw. Jeśli ktoś chciałby porównać swój poziom znajomości angielskiego z moim to uczę się go od czasów podstawówki przy czym intensywną naukę zacząłem tak naprawdę w liceum. Mam też zdany egzamin CAE. Do tej pory angielskiego używałem głównie do czytania dokumentacji technicznej, książek, forów czy blogów. Rzadziej pisałem po angielsku, a już bardzo rzadko miałem okazję rozmawiać po angielsku, w szczególności na tematy inne niż techniczne.

Przechodząc do setna to po tych kilku dniach pobytu muszę powiedzieć, że jest dobrze. W przeważającej liczbie przypadków nie mam żadnych albo prawie żadnych problemów z porozumiewaniem się po angielsku. Równocześnie zdarzyło się jednak, że w czasie rozmowy, pomimo poproszenia o powtórzenie, z powodu akcentu nie byłem w stanie wyłowić z wypowiedzi bardzo wielu słów i mogłem zrozumieć tylko ogólny sens wypowiedzi. Mam jednak nadzieję, że to tylko kwestia czasu i osłuchania. Muszę również przyznać, że jestem mile zaskoczony ponieważ większość do tej pory spotkanych przeze mnie osób nie mówi z bardzo silnym akcentem, który utrudniałby zrozumienie.

Jeśli chodzi o mówienie to otrzymałem nawet komplement od rodowitego Anglika, że mój angielski jest bardzo dobry :) Co prawda moim zdaniem rozmawiając z rzeczonym Anglikiem popełniłem sporo błędów gramatycznych i czasem brakowało mi słów ale to pokazuje, że najważniejsze to żeby się nie bać i mówić, mówić i jeszcze raz mówić. Dla mieszkańców danego kraju, w tym przypadku dla Brytyjczyków, najważniejsze jest przecież to, że mówi się w ich języku, a więc nie powinniśmy być dla siebie zbyt surowi. To przypomina mi anegdotę jaką opowiedział mi kolega z Polski. Otóż kiedy ubiegał się o pracę to odbył rozmowę z headhunter'em, który stwierdził, że z tym jego angielskim to mogło by być dużo lepiej, że popełnia błędy itd. Pomimo sceptycyzmu headhunter'a odbył rozmowę z Anglikami, a oni nie mieli zastrzeżeń i go zatrudnili. Dodam jeszcze, że jego angielski jest moim zdaniem lepszy od mojego.

Na koniec osobom, które wyjeżdżają za granicę i podobnie jak ja rzadko rozmawiają po angielsku polecam przed samym wyjazdem wykupić kilka godzin konwersacji z lektorem aby się rozgadać. Ja tak zrobiłem i sądzę, że pomogło.

09/06/2011

Bardzo wymagająca rekrutacja 2

Home

W ostatnim poście opisałem przebieg pewnej rekrutacji, w której uczestniczyłem, aż do rozmowy z kierownikiem projektu. Post ten stanowi dokończenie tego tematu, a w szczególności zawiera odpowiedź na pytania jakie pojawiły się w komentarzach.

Po jakimś czasie po rozmowie z kierownikiem projektu, w tej chwili już nie pamiętam szczegółów, zostałem zaproszony do kolejnego etapu rekrutacji. Tym razem musiałem pofatygować się do innego miasta na serię rozmów z przedstawicielami firmy z zagranicy. O takiej konieczności zostałem zresztą poinformowany dużo wcześniej i dlatego również na każdym z wcześniejszych etapów rekrutacji była sprawdzana moja znajomość angielskiego. Nie ukrywam, że taka podróż nie do końca mi się podobała ale ponieważ praca wyglądała bardzo obiecująca to zdecydowałem się na wyjazd.

Na rozmowę pojechałem samochodem ze względu na elastyczność, jadę kiedy chcę, nie przejmuję się godziną odjazdu pociągu... Decyzja była dobra i zła. Zła ponieważ 6 godzin jazdy to męcząca sprawa, a dobra bo pociągiem nie byłoby wiele krócej, a musiałbym jeszcze płacić za taksówkę itd. Na rozmowę pojechałem dzień wcześniej aby się porządnie wyspać. Nocleg miałem prawie za darmowo, a za benzynę zwrócono mi pieniądze. Tutaj dodam, że z perspektywy uważam, że po tak długiej jeździe samochodem, a chyba nawet pociągiem porządny odpoczynek to konieczność. Innymi słowy nie ma sensu umawiać się na rozmowę o pracę tuż po długiej podróży. Zapewne można wypaść dobrze ale jestem przekonany, że zawsze będzie to gorzej niż kiedy będziemy wypoczęci.

Rozmowy z przedstawicielami z zagranicy trwały 3 godziny. W sumie odbyłem trzy rozmowy. Każda z nich była mocno techniczna ale dotyczyła trochę innych rzeczy. Rozmawiałem o algorytmach np.: programowanie dynamiczne, implementacji funkcji wirtulanych w C++, złożoności obliczeniowej, a także o rzeczach bardziej biznesowych. Muszę przyznać, że po tych rozmowach nabyłem większej pewności co do mojego mówionego języka angielskiego. To jednak co innego rozmawiać po angielsku na wczasach czy prowadzić luźną rozmowę, a co innego mieć kilkugodzinną rozmowę o pracę w tym języku z ludźmi, z którymi w inny sposób sie nie dogadasz.

Z przebiegu tych rozmów byłem zadowolony, a moje wrażenie zostało wkrótce potwierdzone bo zaproponowano mi pracę. Tutaj dodam, bo zapomniałem o tym wcześniej napisać, że rekrutacja dotyczyła stanowiska programisty czy jak to się nazywało w nomenklaturze tejże firmy. Z tą istotną różnicą, że było to stanowisku tzw. programisty algorytmicznego czyli takiego, który zajmuje się na co dzień przede wszystkim implementacją i analizą algorytmów np.: data miningowych, sortowania itp., a nie realizacją typowo biznesowych wymagań.

Dalszy etap rekrutacji to oczywiście negocjacje, które trwały dość długo (dwa spotkania, rozmowy telefoniczne). Jak się skończyły? Finalnie odrzuciłem ofertę pracy. Czemu? Wbrew pozorom nie chodziło o kwestie finansowe, a na pewno nie miały one decydującej roli. Głównym argumentem przeciw były powody natury rodzinno osobistej. Co tu dużo mówić, przeprowadzka do innego miasta kiedy całe życie spędziło się w Warszawie, ma się tutaj rodzinę, przyjaciół, pracę, mieszkanie i jest się zadowolony z życia to bardzo ciężka decyzja. Pozostaje mi tylko żałować, że oferta nie dotyczyła pracy w Warszawie.

Jako podsumowanie chciałbym wymienić te cechy/zalety tego procesu rekrutacyjnego, które spowodowały, że go zapamiętałem:
  • Bardzo kompetentne osoby sprawdzające wiedzę techniczną.
  • Dyskusja, a nie tylko ocena przedstawionych przeze mnie rozwiązań.
  • Praca domowa do zrobienia.
  • Sprawdzenie znajomości algorytmów i rozwiązywania zadań algorytmicznych.
  • Prowadzenie części rozmowy w języku angielskim.
  • Szczerość w odpowiedziach na moje pytania nawet jeśli odpowiadający wiedział, że taka odpowiedź może mnie zniechęcić.
  • Jeśli firma wkłada tyle wysiłku w znalezienie pracowników to znaczy, że planuje dłuższą współpracę.
  • Jeśli firma wkłada tyle wysiłku w znalezienie pracowników to znaczy, że potencjalni współpracownicy są bardzo kompetentni.
Opisana rekrutacja jest dla mnie wzorem rekrutacji dobrze sprawdzającej umiejętności i wiedzę technologiczną kandydata i porównuję do niej inne. Nie twierdzę jednak, że zawsze powinno to wyglądać w taki sposób. Nie każdej firmy na to stać, a po drugie nie zawsze potrzebne jest tak gruntowne sprawdzenie potencjalnego pracownika. Każdy proces rekrutacyjny na stanowisko programistyczne powinien jednak charakteryzować się kilkoma rzeczami (łatwymi do osiągnięcia):
  • Pisanie jakiegoś kodu przez kandydata.
  • Test z wiedzy technicznej.
  • Rozmowa z osobą techniczną i biznesową.
  • Sprawdzenie znajomość języka angielskiego przynajmniej w stopniu pozwalającym czytać dokumentację.

06/06/2011

Bardzo wymagająca rekrutacja

Home

W swojej dotychczasowej karierze wziąłem udział w wielu rekrutacjach. W przeważającej liczbie przypadków zostałem zaproszony na rozmowę ale czasami skończyło się na wysłaniu CV. Bardzo często zaproszenie na rozmowę poprzedzone było wywiadem telefonicznym. Wielokrotnie rozwiązywałem różnego rodzaju testy, dużo rzadziej byłem proszony o wykonanie pracy domowej. Część rekrutacji organizowana była przez firmy HR'owe inne bezpośrednio przez zatrudniającą firmę. Spośród tych wszystkich rekrutacji kilka zapadło mi w pamięci, a szczególnie jedna niezwykle wymagająca. Aby nie budzić wątpliwości napiszę, że rekrutacja ta odbyła się już sporo czasu temu i finalnie nie podjąłem współpracy z tą firmą. Chciałbym jednak opisać jak wyglądała ponieważ dużo się dzięki niej nauczyłem i jest dla mnie dzisiaj wzorem, do którego porównuję inne rekrutacje. Post ze względu na jego długość postanowiłem rozbić na dwie części, tutaj prezentuję pierwszą.

Wszystko rozpoczęło się w sposób standardowy od zapytania na portalu internetowym czy jestem zainteresowany taką, a taką ofertą pracy. Propozycja była na tyle interesująca, że odpowiedziałem na nią pozytywnie. Po jakimś czasie zadzwoniła do mnie Pani z HR'ów aby potwierdzić moje zainteresowanie i umówić się na dłuższą rozmowę telefoniczną.

O ile mnie pamięć nie myli rozmowa odbyła się w ciągu kolejnego tygodnia i trwała około półgodziny. W tym czasie zostałem poproszony o opisanie swojego dotychczasowego doświadczenia, w jakich projektach brałem udział, jakich technologii używałem... jednym słowem standard. Padło również pytanie o oczekiwania finansowe, na które to mimo starań i prób aby to druga strona wyszła najpierw z propozycją w końcu udzieliłem odpowiedzi. Rozmowę wyróżnia trochę to, że już na tym etapie została zweryfikowana moja znajomość języka angielskiego. Z drugiej strony taka praktyka jest chyba coraz powszechniejsza. Na koniec dowiedziałem się, że dalszy etap rekrutacji będzie polegał na rozwiązaniu dwóch zadań algorytmicznych i jednego biznesowego. Przy czym większą uwagę miałem zwrócić na zadania algorytmiczne.

Następnego dnia otrzymałem drogą mailową treść zadań. Niestety ze względu na klauzulę poufności nie przytoczę ich tutaj, chociaż chciałbym ponieważ były bardzo ciekawe. Pierwsze zadanie algorytmiczne rozwiązałem następnego wieczoru. Moje pierwsza implementacja bazowała na przeszukiwaniu przestrzeni stanów i dawało poprawne wyniki ale kiedy już miałem je wysłać zauważyłem, że do problemu można podejść w zupełnie innych sposób. Finalne rozwiązanie zajmowało kilka linii kodu zamiast kilkuset! Za drugie zadanie zabrałem się następnego dnia i doszedłem do wniosku, że to pytanie z hakiem ponieważ przedstawiony problem należy do klasy NP, co też napisałem w odpowiedzi.

Rozwiązanie pierwszego zadania została zaakceptowane. Co do drugiego to zostałem poproszony żebym się jeszcze mu przyjrzał ponieważ postawiony problem można rozwiązać w czasie wielomianowym. Początkowo byłem przekonany, że to układający/oceniający zadanie pomylił się. Błąd tkwił jednak po mojej stronie. Na czym polegał? Przedstawiony problem można było sprowadzić do znalezienie ścieżki Hamiltona w grafie i to rzeczywiście jest problem NP. Ja zapomniałem jednak o tym, że wśród wszystkich możliwych grafów są takie ich odmiany dla, których problem znalezienia ścieżki Hamiltona można sprowadzić do znalezienia ścieżki Eulera, a to można zrobić w czasie wielomianowym. Co do zadania biznesowego to w końcu z braku czasu go nie rozwiązałem ale tak jak pisałem nie było ono bardzo istotne.

Co dalej? Zostałem zaproszony na rozmowę, połączoną z testem. Test składał się z 20 otwartych pytań i na jego rozwiązanie było około 30 minut. Pytania były tak ułożone, że można na nie było odpowiedzieć w jednym/dwóch zadaniach. Czy były trudne? Dla mnie raczej nie, chociaż nad niektórymi musiałem się dłużej zastanowić. Pomimo, że pytań było tylko 20 sądzę, że dobrze weryfikowały ogólną znajomość platformy .NET. Było coś o zwalnianiu zasobów, użyciu słowa kluczowego using, synchronizacji, LINQ'u itd. Te 30 minut starczyło dokładnie na tyle aby napisać odpowiedzi, ktoś nie orientujący się w temacie miałby z tym problem.

Po rozwiązaniu testu przyszła kolej na rozmowę z dwoma "technicznymi" osobami. Rozmowa odbyła się za pośrednictwem Skype. Początkowo zapraszano mnie na wizytę do innego miasta ale kiedy powiedziałem, że będzie z tym ciężko zorganizowano mi video konferencję. Rozmowa miała być połączona z omówieniem testu ale widać wypadłem dobrze ponieważ zadano mi raptem 1 albo 2 pytania na ten temat. Całą rozmowę zapamiętałem z 2 powodów. Po pierwsze trwała bite 3 godziny, a po drugie zadawano mi naprawdę trudne pytania. Pytania nad którymi musiałem się dogłębnie zastanowić przed podaniem odpowiedzi. W gruncie rzeczy nie były to po prostu pytania ale spore problemy do rozwiązania. Fajne było to, że nawet po podaniu prawidłowej odpowiedzi mówiono mi żebym się zastanowił bo można to zrobić jeszcze lepiej, bardziej optymalnie. W czasie tej rozmowy powtórnie zweryfikowano moją znajomość języka angielskiego tyle, że tym razem rozmawialiśmy o zagadnieniach technicznych. Po tych 3 godzinach byłem równie zmęczony jak po całym dniu pracy.

W czasie tej rozmowy miałem też okazję zadać kilka pytań, na które w miarę możliwości udzielono mi odpowiedzi. Jak zwykle w takich przypadkach poprosiłem również o feedback. Powiedziano mi, że pełnego raportu raczej nie otrzymam ale na pewno dostanę informację zwrotna czy było coś nie tak, a jeśli tak to co. Dwa dni później dostałem zaproszenie na następną rozmowę, tym razem z kierownikiem projektu. Rozmowa ta również odbyła się za pośrednictwem video konferencji i trwała około półgodziny. I tym razem sprawdzono moją znajomość języka angielskiego i zadano kilka "biznesowych" pytań. Miałem też okazję dopytać o interesujące rzeczy. Na koniec kierownik projektu powiedział, że w razie jakichś wątpliwości zaprasza do kontaktu. Z możliwości tej skorzystałem kilkukrotnie i za każdym razem otrzymałem wyczerpującą odpowiedź...

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.

19/05/2011

Toad, parametry typu OUT i kursory

Home

Będzie krótko i zwięźle. Pracujemy z aplikacją Toad for Oracle i napisaliśmy procedurę składowaną, która ma kilka parametrów oraz zwraca kursor przez parametr typu OUT. Dla ustalenia uwagi niech jej deklaracja wygląda następująco:

TYPE cursorType IS REF CURSOR;
...
PROCEDURE SOMEPROCEDURE(param1 VARCHAR2, param2 VARCHAR2, outParam OUT cursorType); 

Chcemy ją wywołać i przetestować w łatwy i szybki sposób. Zależy nam na tym aby aplikacja pokazała nam "zawartość" kursora. Rozwiązanie jest proste, a pokazałem je poniżej.

BEGIN
  SOMEPACKAGE.SOMEPROCEDURE (:PARAM1, :PARAM2, :CURSOR);
END;

Wystarczy zaznaczyć ten kod i nacisnąć Ctrl+Enter. Pojawi się okienko, w którym będziemy mogli zdefiniować wartości poszczególnych parametrów. W przypadku parametru :CURSOR środowisko powinno wykryć, że to kursor i zająć się jego obsługą. Po zamknięciu okienka kod zostanie wykonany, a "zawartość" kursora zostanie wyświetlona w gridzie (zakładka Data Grid). Proste, łatwe i przyjemne. Wcześniej nie znałem ten funkcjonalności ale bardzo mi się podoba.

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.

08/05/2011

Float(1) = Float(24) ???

Home

Jakiś czas temu pracowałem nad komponentem, który odpowiadał za generacją tabel w bazie danych na podstawie zadanej definicji. Dodatkowo kod ten potrafił wykryć różnicę pomiędzy definicją, a faktyczną strukturą tabeli w bazie danych i poinformować o tym użytkownika. Przynajmniej w teorii bo w praktyce czasami twierdził, że definicja nie jest spójna ze stanem faktycznym w bazie danych chociaż na pierwszy rzut oka wyglądała, że jest.

Wspomniany kod zawierał oczywiście kilka błędów, które udało mi się szybko poprawić ale jeden zapadł mi w pamięci. Pewnie dlatego, że jego znalezienie było trochę trudniejsze. Błąd ten pojawiał się kiedy definicja tabeli zawierała kolumnę typu float. W takim wypadku komponent zawsze twierdził, że definicja i strukturą tabeli w bazie danych są inne.

Diagnozę błędu rozpocząłem od przygotowania definicji trywialnej tabeli z jedną kolumną typu float o zadanej precyzji 10. Komponent wygenerował tabelę przy użyciu poniższej komendy DDL i oczywiście stwierdził, że coś jest nie tak.
CREATE TABLE TestTable
(
 SomeNumber float(10) NULL
) 
Do wykrywaniu różnic pomiędzy definicją, a stanem bazy danych skorzystałem z widoków systemowych, a w szczególności z widoku sys.all_columns. Postanowiłem, więc zobaczyć jak wygląda wiersz odpowiadający kolumnie SomeNumber i otrzymałem taki wynik:

object_id name column_id system_type_id user_type_id max_length precision scale ...
2137058649 SomeNumber 1 59 59 4 24 0 ...

Moją uwagę od razu zwróciła zawartość kolumny precision równa 24. Zgodnie z definicja powinno być natomiast 10. Chwila konsternacji, szybkie spojrzenie do dokumentacji i wszystko okazało się jasne. SQL Server ze względu na zgodność ze standardem ISO pozwala napisać float(10), float(20) itp. ale wewnętrznie wartości od 1 do 24 traktuje jak 24, a wartości od 25 do 53 jako 53. Po uwzględnieniu tej informacji komponent zaczął działać prawidłowo.

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

17/04/2011

Błąd przy dodawaniu przystawki do konsoli zarządzania

Home

Przystawka certyfikatów (ang. Certificates Snap-in) pozwala na przeglądanie magazynu certyfikatów dla użytkownika, usługi czy też komputera. Aby uruchomić to narzędzie wystarczy w wierszu poleceń lub do okienka Uruchom wpisać polecenie certmgr.msc. Problem polega na tym, że tak uruchomiona przystawka certyfikatów pokaże nam tylko magazyn dla bieżącego użytkownika.

Jeśli chcemy zobaczyć magazyn certyfikatów komputera, tak było w moim przypadku, czeka nas trochę więcej pracy. Zaczynamy od uruchomienia konsoli zarządzania (ang. Microsoft Management Console) wpisując w wierszu poleceń lub do okienka Uruchom komendę mmc.exe. Następnie wybieramy Plik -> Dodaj/Usuń przystawkę. Przystawka certyfikatów znajduje się na początku listy i po jej wybraniu zostaniemy poproszeni o zaznaczenie jakimi certyfikatami chcemy zarządzać: użytkownika, usługi czy komputera. Na koniec klikamy Ok.

Tak to wygląda w teorii, w praktyce po naciśnięciu przycisku Ok konsola zarządzania raportowała błąd (pokazany poniżej) i kończyła pracę, bez względu na to jaka przystawkę wybrałem.
  Nazwa zdarzenia problemu: APPCRASH
  Nazwa aplikacji: mmc.exe
  Wersja aplikacji: 6.0.6002.18005
  Sygnatura czasowa aplikacji: 49e02760
  Nazwa modułu z błędem: StackHash_7ae8
  Wersja modułu z błędem: 6.0.6002.18327
  Sygnatura czasowa modułu z błędem: 4cb74dd3
  Kod wyjątku: c0000374
  Przesunięcie wyjątku: 00000000000aca57
  Wersja systemu operacyjnego: 6.0.6002.2.2.0.256.6
  Identyfikator ustawień regionalnych: 1045
  Dodatkowe informacje 1: 7ae8
  Dodatkowe informacje 2: fab1f7793b8a08e05290bb8ef1ca5c9e
  Dodatkowe informacje 3: 1607
  Dodatkowe informacje 4: 3b4ea5c6cc4724ebe1b8e0ae80fae1cf
Pan Google nie był zbyt pomocny. Radził aby zainstalować SP2 dla Visty, który już mam zainstalowany. Na innej stronie ktoś twierdził, że problem pojawia się jeśli na jednej maszynie zainstalowane są dwie wersje MSSQL i że trzeba jedną z nich odinstalować. Ja mam akurat zainstalowane dwie wersje MSSQL ale nie miałem najmniejszej ochoty usuwać z dysku żadnej z nich. Trzecia osoba radziła aby na komputerze o zbliżonej do problematycznego konfiguracji, na którym mmc.exe działa, wyeksportować klucze rejestru dotyczące konsoli i zaimportować je na komputerze, na którym występuje problem. Ta rada też nie przypadła mi do gustu ponieważ nie miałem takiego komputera pod ręką. Nie wiedziałem także, na ile ta konfiguracja powinna być "zbliżona" aby było dobrze.

Postanowiłem jednak pójść tropem zawartości rejestru i konfliktu pomiędzy różnymi wersjami MSSQL. Na początek zauważyłem, że na liście dostępnych przystawek znajdują się dwie przystawki o takiej samej nazwie SQL Server Configuration Manager. Zapewne dedykowane dla różnych wersji MSSQL. Następnie postanowiłem zajrzeć do rejestru do klucza, który przechowuje listę wszystkich dostępnych przystawek:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MMC\SnapIns\

Jest ich tam kilkadziesiąt i mają niewiele mówiące nazwy np.: d52e5f54-75d9-4a93-91b7-2215ea5cbed2 ale szybko udało mi się znaleźć klucze odpowiadające przystawce SQL Server Configuration Manager. Pomyślałem sobie "raz kozie śmierć", zobaczę co się stanie jak usunę jeden z nich. Oczywiście najpierw wyeksportowałem kopię klucza, a dopiero potem go usunąłem. Okazało się to strzałem w dziesiątkę. Po tej operacji dodanie nowej przystawki w końcu zadziałało. Co ciekawe nie ma znaczenia czy usuniemy klucz przystawki dla MSSQL 2005, czy dla MSSQL 2008.

Reasumując. Jeśli operacja dodania nowej przystawki konsoli zarządzania kończy sie błędem, a masz na komputerze zainstalowane dwie (lub więcej) wersji MSSQL to z dużym prawdopodobieństwem problem spowodowany jest konfliktem pomiędzy przystawkami SQL Server Configuration Manager dla różnych wersji MSSQL. Można go rozwiązać usuwając odpowiedni wpis z rejestru. Nie jest to idealne rozwiązanie, ale z braku laku dobry kit.

14/04/2011

CURRENT_USER , SYSTEM_USER...

Home

W poście Problem z domyślnym schematem wspomniałem, że w celu wyjaśnienia problemu posłużyłem się funkcją CURRENT_USER, która zwraca nazwę bieżącego użytkownika. Czym jednak funkcja ta różni się od funkcji SYSTEM_USER, SESSION_USER czy innych o podobnych nazwach. Sprawa jest prosta. Część z tych funkcji zwraca login, który służy do uwierzytelnienie się względem serwera, a część nazwę użytkownika, która określa co możemy zrobić w ramach poszczególnych baz danych (autoryzacja). Każdy login jest skojarzony (zmapowany) z jednym użytkownikiem dla danej instancji bazy danych. Nie jest to nic skomplikowanego ale można się zgubić w gąszczu nazw. Dlatego ku pamięci, głównie dla siebie, zebrałem te informacje w postaci poniższej tabelki.

CURRENT_USERZwracają nazwę użytkownika
USER_NAME
SESSION_USER
USER

SYSTEM_USERZwracają login
SUSER_SNAME

Więcej szczegółów np.: parametry wywołania, wyjaśnienie skąd wzięło się tyle funkcji robiących to samo, można znaleźć tutaj.

13/04/2011

Problem z domyślnym schematem

Home

Kilka dni temu chciałem zmodyfikowałem domyślny schemat dla jednego z użytkowników, niech nazywa się SomeUser. W tym celu otworzyłem Management Studio, wybrałem interesującą mnie bazę danych i przeszedłem do listy użytkowników. Dalej wybrałem interesującego mnie użytkownika i wyświetliłem dla niego okno właściwość i w polu Default schema: wpisałem nazwę schematu, dla ustalenia uwagi niech nazywa się testSchema.

Żeby być pewnym, że wszystko jest w porządku nawiązałem połączenie z serwerem korzystając z wspomnianego użytkownika i spróbowałem wykonać trywialne zapytanie pobierające dane z tabeli znajdującej się w schemacie testSchema:
select *
from TestTable
Ku swojemu zdziwieniu otrzymałem komunikat o treści Invalid object name 'TestTable'. Sprawdziłem, więc zapytanie zawierające pełną nazwę tabeli (razem ze schematem) i zadziałało:
select *
from testSchema.TestTable
Aby upewnić się, że pracuję w kontekście właściwego użytkownika posłużyłem się poleceniem select current_user, które ponownie ku mojemu zdziwieniu wypisało na ekran dbo zamiast SomeUser. No cóż może się pomyliłem. Ponownie połączyłem się z serwerem upewniając się, że korzystam z dobrego użytkownika ale nic się nie zmieniło.

Problem pomógł mi rozwiązać kolega, który zwrócił uwagę, że SomeUser ma przypisaną rolę sysadmin, a więc w rzeczywistości był widziany jako użytkownik dbo (dla którego domyślny schemat to dbo). Po zabraniu użytkownikowi roli sysadmin, która nie była mu zresztą potrzebna, wszystko zaczęło działać jak trzeba.

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.

23/03/2011

Przygody ze Steam'em

Home

Shogun: Total War to jedna z pierwszych gier jaką odpaliłem na swoim pierwszym komputerze PC i zarazem jedna z najlepszych gier w jakie grałem. Bardzo podobała mi się również inna gra z tej serii czyli Rome: Total War. Ostatnio postanowiłem nadrobić zaległości i kupiłem ostatnią grę z tej serii czyli Napoleon: Total War.

Napoleon: Total War jak wiele obecnie sprzedawanych gier wymaga do działania konta w serwisie Steam. Żaden problem ponieważ takie konto posiadam już od dawna. Niestety Steam potrafi być bardzo irytujący. Zacznę od tego, że po uruchamieniu klient Steam sprawdza czy są dostępne aktualizacje i pobiera je nie pytając użytkownika o zdanie. W tej chwili dysponuję połączeniem modemowym, a więc jest to dla mnie tym bardziej uciążliwe.

Na tym nie koniec. Zanim udało mi się uruchomić właściwy proces instalowania gry napotkałem jeszcze jeden problem. Po uruchomieniu instalatora pojawił sie błąd Failed to run install script. Szczęśliwie udało mi się go szybko rozwiązać, uruchamiając proces instalacji z wiersza poleceń przy pomocy komendy steam.exe -install h: gdzie h: to napęd DVD.

Kiedy instalacja zakończyła się i myślałem, że w końcu zagram okazało się, że Steam ma jeszcze coś do powiedzenia i rozpoczął aktualizację dopiero co zainstalowanej gry. Wyłączenie automatycznej aktualizacji we właściwościach Napoleon: Total War, odłączenie Internetu, ponowne uruchomienie klienta Steam nie pomogło. W tej chwili proces instalacji zakończył się i udało mi się uruchomić grę :) Tak sobie jednak myślę, że stare dobre czasy kiedy kupowało się grę, przychodziło do domu, instalowało i grało bez potrzeby aktywowania przez Internet, przymusowej aktualizacji itp. mijają albo już minęły.

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.

21/02/2011

Domeny globalne

Home

Od kilku miesięcy jestem szczęśliwym posiadaczem własnej domeny michalkomorowski.com. Jakiś czas temu moje szczęście zostało jednak zakłócone kiedy dowiedziałem się, że usługa whois udostępnia publiczne moje pełne dane adresowe!!! Szybko skontaktowałem się z firmą home.pl, za pośrednictwem której wykupiłem domenę aby wyjaśnić sprawę.

Co tu dużo mówić byłem nastawiony wojowniczo ale mój zapał został szybko zgaszony. Otóż okazało się, że dla domen globalnych dane właścicieli są domyślnie jawne. Wymaganie takie stawia rejestratorom organizacja ICANN czyli główny zarządca domen na świecie. W regulaminie znajduje się oczywiście stosowny punkt, który przeoczyłem przeglądając umowę. Szczęście w nieszczęściu aby ukryć dane wystarczy wypełnić elektroniczny formularz. Po jego wypełnieniu moje dane zostały ukryte w ciągu 24 godzin, tutaj plus dla home.pl za szybką reakcję.

Z drugiej strony sądzę, że w dobrym tonie byłoby gdyby firmy zajmujące się rejestracją domen informowały o takich rzeczach przy pomocy dużego czerwonego napisu ponieważ sprawa nie jest oczywista. Z ciekawości sprawdziłem kilka domen globalnych dla polskich blogów i dla większości z nich wynik zwrócony przez usługę whois, zawierał dane adresowe właścicieli. Może sie mylę ale z dużą pewnością ich autorzy oraz wielu innych nie zdaje sobie z tego sprawy.

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?

21/01/2011

Czy to na pewno takie proste?

Home

Czy można zrobić coś źle dodając nowy plik do projektu aplikacji WWW w Visual Studio? Dla ustalenia uwagi niech będzie do plik z definicją raportu wczytywanego w czasie działania tejże aplikacji. Plik ten chcemy w razie potrzeby zmodyfikować bez konieczności ponownej kompilacji projektu. Sprawa wydaje się prosta (Add -> Existing item... itd.) ale jest pewien haczyk. Otóż, Visual Studio dla plików z nieznanymi rozszerzeniami ustawia ich właściwość Build Action na wartość None. Jeśli uruchomimy aplikację z poziomu Visual Studio to nie zauważymy żadnych błędów. Problemy pojawi się jeśli zainstalujemy aplikację na serwerze IIS przy pomocy narzędzia Publish....

Narzędzie to udostępnia kilka sposobów kopiowania plików, a domyślnie zaznaczona jest opcja Only files needed to run application. Sęk w tym, że pliki, dla których właściwość Build Action ma wartość None nie są traktowane jako niezbędne do działania aplikacji i zostaną pominięte w czasie kopiowania. Oczywiście skończy się to komunikatem o błędzie przy pierwszej próbie wczytania pliku. Problem jest bardzo prosty do rozwiązania, wystarczy ustawić Build Action na Content, ale łatwo o tym zapomnieć w ferworze kodowania.

14/01/2011

Trojanie

Home

"Trojanie" to opera Hectora Berlioza, którą można zobaczyć na deskach Teatru Wielkiego. Bilety na nią wpadły mi w ręce trochę przypadkiem, a decyzję czy iść musiałem podjąć bardzo szybko i mówiąc szczerze miałem pewne wątpliwości. Bynajmniej nie dlatego, że na słowo opera cierpnie mi skóra ale dlatego, że 5 godzinne przedstawienie tuż po pracy, nastrojowa muzyka, przygaszone światła... skłaniają do drzemki. Pomimo początkowych wątpliwości zdecydowałem, że skorzystam z okazji i nie żałuję. Powiem nawet więcej, jestem bardzo zadowolony z podjętej decyzji i szczerze zachęcam każdego do kupna biletów na to wspaniałe widowisko przy następnej nadarzającej się okazji.

Czemu ta opera opowiadająca o bitwie o Troję i dalszych losach jej mieszkańców jest tak wyjątkowa? Orkiestra, dyrygent, aktorzy, tancerze, śpiewacy byli wspaniali, zresztą innych w Teatrze Wielkim się nie zobaczy, ale mnie zachwyciła przede wszystkim scenografia. Co tu dużo mówić momentami zbierałem szczękę z podłogi!

Niech wspomnę o kilkudziesięciu szturmowcach Imperium w charakterystycznych białych zbrojach, walce bokserskiej, koniu trojańskim zbudowanym z laptopów, kosmonautach, aktorach/statystach unoszących się w powietrzu i Elvisie... Takich rzeczy nie widuje się codziennie, a już na pewno nie w operze!

W bardzo ciekawy sposób wykorzystano animację komputerową. Tło ogromnej sceny stanowił jeden ekran (kurtyna), a drugi, przezroczysty w 90% został umieszczony z przodu sceny, pomiędzy widownią i aktorami. Na obu ekranach wyświetlano to samo np.: kłębowisko węży, statki kosmiczne... Wykorzystanie dwóch ekranów pozwoliło wywołać efekt przestrzenności. Trudno to opisać ale wyglądało super. W kilku animacjach udało mi się nawet dostrzec słynny Blue Screen of Death. Z animacją bardzo fajnie współgrała dekoracja. Momentami można było się pogubić co jest wyświetlane na ekranie, a co znajduje się na scenie. Moje uznanie wzbudziła też scena, w której kilkanaście osób buduje jeden wielki ekran złożony z kilkunastu zsynchronizowanych laptopów.

Cała opera ogólnie przepełniona jest różnymi nawiązaniami do popkultury i współczesnego zinformatyzowanego świata. Na scenie cały czas coś się dzieje i naprawdę ciężko się nudzić. Jeszcze raz polecam!

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.