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.