14/09/2012

Dziwne zachowanie konstruktora statycznego - ciąg dalszy 2

Home

Niedawno kolega opowiedział mi o jeszcze jednym przypadku kiedy opisane przeze mnie zachowanie konstruktora statycznego w środowiskach x86/x64 doprowadziło do kłopotów. Scenariusz był dość ciekawy, dlatego go opiszę na uproszczonym przykładzie. Zacznijmy od tego, że napisaliśmy zarządzany komponent COM. Komponent ten w konstruktorze statycznym czyta wartość jakiegoś parametru konfiguracyjnego z pliku i na tej podstawie coś robi. W poniższym przykładzie, żeby nie komplikować sprawy, po prostu zapisuje jego wartości do pliku:

[ComVisible(true)]
public class MySimpleCOM : ServicedComponent
{
 static MySimpleCOM()
 {
  File.AppendAllText(
   @"c:\log.txt",
   String.Format("Value of configuration parameter = '{0}' in the domain '{1}'\n.", ConfigurationManager.AppSettings["Test"], AppDomain.CurrentDomain.FriendlyName));
 }
 
 public MySimpleCOM() {}
 public void Fun() {}
}

Dodam jeszcze, że do pliku AssemblyInfo.cs w projekcie z naszym zarządzanym komponentem zostały dodane takie atrybuty:

[assembly: ApplicationActivation(ActivationOption.Server)]
[assembly: ApplicationAccessControl(false)]

ApplicationActivation zapewnia, że nasz komponent będzie hostowany poza procesem, w którym się do niego odwołamy, dokładniej przez proces dllhost.exe. Drugi atrybut jest opcjonalny ale ułatwia testy i mówi, że nie jest wymagana żadna kontrola dostępu (uprawnień) kiedy ktoś będzie próbował użyć naszego komponentu. Kod testujący nasz komponent jest jeszcze prostszy:

class Program
{
 static void Main(string[] args)
 {
  using (var host = new MySimpleCOM())
  {
   host.Fun();
  }
 }
}

Warto zwrócić uwagę, że nie trzeba samemu rejestrować komponentu. Zostanie to zrobione za nas.

Na koniec dodajemy jeszcze do pliku konfiguracyjnych programu testującego (app.config) oraz komponentu COM (Application.config) sekcję appSettings z parametrem test. Dla rozróżnienia niech wartość tego parametru różni się w obu przypadkach np.:

<appSettings>
 <add key="test" value="Tester"/>
</appSettings>

oraz

<appSettings>
 <add key="test" value="COM"/>
</appSettings>

Aby zobaczyć na czym dokładnie polega problem najłatwiej postąpić w następujący sposób. Najpierw kompilujemy oba projekty z opcją x86, uruchamiamy i zaglądamy do pliku z log.txt. Dalej kompilujemy ponownie oba projekty z opcją x64 i znowu uruchamiamy pamiętając aby uprzednio odinstalować wersję x86 komponent COM. Można to zrobić na przykład przy pomocy narzędzia Usługi składowe (dcomcnfg) wbudowanego w system Windows.

W pierwrzym przypadku (x86) plik log.txt będzie wyglądał tak:

Value of configuration parameter = 'COM' in the domain 'DefaultDomain'.

A w drugim (x64) w następujący sposób:

Value of configuration parameter = 'COM' in the domain 'DefaultDomain'.
Value of configuration parameter = 'Tester' in the domain 'Tester.vshost.exe'.

Jak widać w przypadku środowiska x64 kod inicjalizujący w konstruktorze statycznym został wywołany dwa razy, do tego z różnymi wartościami parametru konfiguracyjnego. W prawdziwym systemie mogłoby to doprowadzić do powstania jakichś błędów i chyba sami przyznacie, że ich przyczyna nie byłaby trywialna do wykrycia.

PS.

Jeśli przy własnoręcznym odtwarzaniu tego scenariusza napotkacie problem, że komponent COM nie będzie czytał konfiguracji ze pliku Application.config, to odsyłam na przykład do tego wpisu, w którym wszystko jest wyjaśnione.

22/07/2012

Jeden dziwny znak, a kilka rzeczy do zapamiętania

Home

Niedawno zetknąłem się z raportem, który wypluwał dane do pliku. Przy czym jednym z wymagań było aby trim'ować pola, które zostały dopełnione do wymaganej długości. W tym celu użyto funkcji rtrim. Przeglądając wyprodukowane raporty, zauważyłem jednak, że w niektórych przypadkach zawartość jednego pola zawiera jakieś białe znaki. Na pierszy rzut oka wyglądało to na spacje. Na wszelki wypadek sprawdziłem czy napewno zastosowano rtrim i wszystko się zgadzało.

Rozpocząłem więc poszukiwania czemu rtrim mogło nie zadziałać i od razu nauczyłem się pierwszej rzeczy, do której nie przywiązywałem wcześniej uwagi. rtrim obsługuje tylko spacje i nie poradzi sobie z tabulatorem, znakiem powrotu karetki itd. Poniżej krótki przykład demonstrujący problem (char(9) to tabulator).

DECLARE @string char(10)
SET @string = 'abc' + char(9)

SELECT LEN(@string), LEN(RTRIM(@string))

Rozwiązaniem problemu może być na przykład użycie funckji replace:

SELECT LEN(@string), LEN(RTRIM(@string)), LEN(REPLACE(@string,char(9),SPACE(0)))

W tym przypadku to jednak nie pomogło. W pliku wyjściowym cały czas pojawiały się te "dziwne" spacje. Użyłem więc prostego skryptu aby zobaczyć kody znaków dla pola, które sprawiało problemy. Okazało się, że te dziwne "spacje" to znak o kodzie 255. Teraz w zależności od tego do jakiej tablicy znaków zajrzałem uzyskałem inną informację. Na przykład według ISO 8859-1 to znak łacińska mała litera Y z diarezą/umlautem.

Tutaj dochodzimy do kolejnej ważnej rzeczy, którą łatwo przeoczyć kiedy przez większość czasu pracujemy w dobrze określonym środowisku z takimi, a nie innymi ustawieniami regionalnymi itd. Otóż ten sam znak może wyglądać inaczej kiedy wykonamy zapytanie w Microsoft SQL Server Management Studio ze względu na collation, a inaczej po zapisaniu do pliku i otwarciu w jakimś edytorze ze względu na wybraną w programie stronę kodową itd. Poniższy skrypt pokazuje jak ten sam znak zostanie zaprezentowany dla różnych collation.

DECLARE @string char(10)
SET @string = 'abc' + char(255)
SELECT @string
Collate SQL_Latin1_General_CP1_CI_AS
SELECT @string
Collate SQL_Polish_CP1250_CI_AS

Nie ma w tym nic trudnego i przez większość czasu kwestie kodowania, tablicy znaków... nas nie obchodzą ale warto pamiętać o takich rzeczach bo potem mogą pojawić się cokolwiek zaskakujący efekty, nie zawsze oczywiste do wyjaśnienia.

Post ten dotyczy MSSQL 2008.

08/07/2012

WPF i opóźnione wykonanie (deferred execution)

Home

Najpierw spójrzmy na poniższy fragment XAML'a z prostą implementacją combobox'a z wieloma kolumnami:

...
<ComboBox ItemsSource="{Binding PerformedAnalysis}" SelectedValuePath="Id" SelectedValue="{Binding SelectedItem}">
 <ComboBox.ItemTemplate>
  <DataTemplate>
   <StackPanel Orientation="Horizontal">
    <Border BorderThickness="1" BorderBrush="Black" Margin="1" Padding="1">
     <TextBlock Text="{Binding Id}" Width="100"></TextBlock>
    </Border>
    <Border BorderThickness="1" BorderBrush="Black" Margin="1" Padding="1">
     <TextBlock Text="{Binding Name}" Width="500"></TextBlock>
    </Border>
   </StackPanel>
  </DataTemplate>
 </ComboBox.ItemTemplate>
</ComboBox>
...

Oraz kod z view model'u zasilający tą kontrolkę. Kod ten używa Entity Framework, a Context to nic innego jak obiekt dziedziczący po ObjectContext.

...
private IEnumerable _PerformedAnalysis = null;

public IEnumerable PerformedAnalysis
{
    get
    {
        try
        {
            if (_PerformedAnalysis == null)
            {
                _PerformedAnalysis = 
                        from a in Context.Analyses
                        orderby a.Id
                        select new { Id = a.Id, Name = a.Name };
            }
        }
        catch (Exception ex)
        {
            ServiceProvider.GetService<IWindowService>().ShowError(ex);
            _PerformedAnalysis = new[] { new { Id = -1, Name = ex.Message } };
        }

        return _PerformedAnalysis;
    }
}
...

Kod ten zawiera dość poważną usterką. Jaką? Ktoś może powiedzieć, że błędem jest użycie bezpośrednio EF'a, zamiast ukryć dostęp do danych za warstwą interfejsów. W prostej aplikacji nie ma to moim zdaniem sensu, w bardziej rozbudowanej też można nad tym dyskutować. Ktoś może również powiedzieć, że przy bindowaniu należy użyć ObservableCollection. W tym jednak przypadku do kolekcji nie będą dodawane lub usuwane żadne elementy, więc nie jest to konieczne.

Błąd jest dużo poważniejszy i może spowodować, potocznie mówiąc, wywalenie się całej aplikacji. Odpowiedzmy na pytanie co się stanie jeśli nawiązanie połączenia z bazą danych jest niemożliwie? Oczywiście zostanie zgłoszony błąd ale czy zostanie obsłużony? Pozornie powyższy kod poradzi sobie z takim scenariusz, przecież zawiera blok try/catch.

Zapomniano jednak o tzw. opóźnionym wykonaniu (ang. deffered execution) czyli o tym, że właściwe zapytanie do bazy danych zostanie wykonane dopiero kiedy dane będą rzeczywiście potrzebne czyli kiedy silnik WPF wykona bindowanie. Inaczej mówiąc wyjątek spowodowany brakiem połączenia z bazą danych zostanie rzucony gdzieś wewnątrz silnika WPF. Będzie można go przechwycić korzystając z DispatcherUnhandledException ale z perspektywy działania aplikacji to już nic nie da.

Jak to często bywa naprawienie błędu jest bardzo proste. Należy zapewnić wykonanie zapytania jeszcze w naszym kodzie np.:

...
 _PerformedAnalysis = 
  (from a in Context.Analyses
  orderby a.Id
  select new { Id = a.Id, FileName = a.FileName, Start = a.StartTime }).ToList();
...

24/06/2012

Informatyka w służbie medycyny ;)

Home

Nie wiem jak większość z Was, ale ja na co dzień, jako programista agenta transferowego dla dużych instytucji finansowych, nie mam okazji obserwować jak wynik moich wysiłków przekłada się na pracę klienta. Sądzę, że nie jestem odosobniony i w mniejszym lub większym stopniu doświadcza tego każdy kto pracuje nad projektami przeznaczonymi dla dużych firm i korporacji. Z tego powodu lubię wykorzystywać swoje umiejętności i pomagać przyjaciołom i rodzinie.

Ostatnio moja szwagierka, która jest na specjalizacji z anestezjologii, spytała czy pomógłbym jej zautomatyzować papierkową robotę związaną ze specjalizacją. Papierologia ta przejawia się tym, że dla każdego zabiegu w jakim uczestniczy musi wypełnić odpowiedni formularz. Zabiegów takich w ciągu całego stażu musi wykonać, o ile mnie pamięć nie myli, około 2 tyś. Oznacza to konieczność wypełnienia i wydrukowania takiej samej liczby formularzy i dodatkowego zestawienia podsumowującego.

Wzór formularz przygotowała sobie sama w Excelu. Początkowe kilkadziesiąt kart wypełniła ręcznie. Robota łatwa ale żmudna i zawsze łatwo o pomyłkę. Dodatkowo ciężko potem przeglądać dużą liczbę takich kart i wyszukać tą, która nas w danej chwili interesuje. Formularz wygląda w następujący sposób:



Do tego informację o każdej takiej karcie zapisuje się w zestawieniu podsumowującym. Pomysł na automatyzację był następujący: "Chciałabym mieć arkusz w programie Excel z kolumnami, w które wpisywałabym informacje potrzebne do wypełnienia kart i zestawienia podsumowującego. Na tych kolumnach założę sobie filtry itp. Z arkusza tego generowane będę karty oraz zestawienie podsumowujące."

Zrobienie czegoś takiego nie jest trudne ale wymaga umiejętności pisania makr. Z chęcią przystąpiłem więc do pracy. Jedno popołudnie i nowe rozwiązanie było gotowe. Składa się na nie:
  • Arkusz z wzorem formularza. Z tego arkusza wzór jest kopiowany i wypełniany.
  • Arkusz z danymi. Każdy wiersz odpowiada jednemu formularzowi. Aby za każdym razem nie generować wszystkich kart zawiera dodatkową kolumnę ze znacznikiem czy już generowano daną kartę.
  • Arkusz z podsumowaniem.
  • Arkusz z wygenerowanymi kartami.
  • Skrypt (140 linii) generujący podsumowanie i karty, uruchamiany kombinacją klawiszy Ctrl+G.
Coś bardzo prostego, chwila wysiłku, ale sporo satysfakcji.

05/06/2012

CLR Profiling API - dobrego złe początki

Home

Jakiś czas temu odpowiadając na komentarz pod postem przebąknąłem, że przymierzam się do wzięcia byka za rogi i chcę napisać własny profiler korzystając z CLR Profiling API. Zagadnienie nie jest trywialne dlatego postanowiłem zacząć od zapoznania się z "literaturą". Na pierwszy ogień poszedł artykuł Write Profilers With Ease Using High-Level Wrapper Classes. Swoje lata ma, chociaż na liście mam jeszcze starsze artykuly, ale akurat w przypadku CLR  Profiling API lata biegną wolniej niż w przypadku innych technologii.

Do artykułu, jakby inaczej, dołączony jest kod, który postanowiłem jak najszybciej uruchomić aby zobaczyć czy to rzeczywiście działa. W tym momencie natknąłem się na pierwszy problem i o tym będzie ten post. Problem wynikał z tego, że chciałem "sprofilować" aplikację używającą .NET 4.0 kodem napisanym z myślą o .NET 2.0. Z biegu nie jest to możliwe i objawia się takim błędem w logu systemowym:

.NET Runtime version 4.0.30319.225 - Loading profiler failed. The profiler that was configured to load was designed for an older version of the CLR. You can use the COMPLUS_ProfAPI_ProfilerCompatibilitySetting environment variable to allow older profilers to be loaded by the current version of the CLR. Please consult the documentation for information on how to use this environment variable, and the risks associated with it. Profiler CLSID: '{B98BC3F3-D630-4001-B214-8CEF909E7BB2}'. Process ID (decimal): 5128. Message ID: [0x2517].


Łatwo jednak sobie z tym poradzić, wystarczy ustawić zmienną środowiskową COMPLUS_ProfAPI_ProfilerCompatibilitySetting. Uruchomienie programu pod kontrolą profiler'a będzie więc wyglądało w następujący sposób:

rem Rejestrujemy dll'kę z profiler'em
regsvr32 /s MyProfiler.dll

rem Włączenie profilowania
set COR_ENABLE_PROFILING=1

rem Ustawiamy profiler, który chcemy użyć (zarejestrowaliśmy go 2 linijki wcześniej)
set COR_PROFILER={32E2F4DA-1BEA-47ea-88F9-C5DAF691C94A}

rem Włączamy kombatybilność wstecz
COMPLUS_ProfAPI_ProfilerCompatibilitySetting=EnableV2Profiler

rem Uruchamiamy program
MyProgram.exe

rem Profiler nie jest już potrzebny, więc go wyrejestrowujemy
regsvr32 /s /u MyProfiler.dll

Ale to nie wszystko. Drugi rodzaj błędu z jakim się spotkałem objawia się następującym wpisem w logu systemowym:

.NET Runtime version 4.0.30319.225 - Loading profiler failed during CoCreateInstance. Profiler CLSID: '{B98BC3F3-D630-4001-B214-8CEF909E7BB2}'. HRESULT: 0x80070002. Process ID (decimal): 2620. Message ID: [0x2504].

I może wynikać z dwóch rzeczy. Po pierwsze jeśli nasz program jest programem 64 bitowym, do jego profilowania musimy użyć 64 bitowej wersji profilera. Sprawa ma się podobnie, jeśli program jest 32 bitowy. W przeciwnym wypadku środowisko nię będzie potrafiło załadować odpowiedniej biblioteki. Błąd pojawi się również jeśli spróbujemy uruchomić profiler nie posiadając praw administratora (Windows Vista z włączonym UAC). A więc uruchamiając skrypt należy skorzystać z opcji Uruchom jako administrator.