SOLIDnie po łebkach

Ilu z nas wracając do starego kodu (napisanego wczoraj/ tydzień temu/ miesiąc temu) krzywi się patrząc na bałagan, który po sobie pozostawił? W zasadzie prościej będzie chyba zapytać komu się to nie zdarza. Otóż amerykańscy naukowcy znaleźli na to sposób. No dobra może nie amerykańscy, ale skrót jest z angielskiego – SOLID, rozkłada się on na pięć czynników, a każdy z nich jest znowu jakimś skrótem.

S – (SRP) Single Responsibility Principle
O – (OCP) Open / Closed Principle
L – (LSP) Liskov Substitution Principle
I – (ISP) Interface Segregation Principle
D – (DIP) Dependency Inversion Principle
(podobno wszystko co napisane Helvetica jest mądrzejsze)

O co chodzi z tymi regułami wchodzącymi w skład SOLID? Jest to pięć zasad mówiących o tym jak tworzyć kod, który będzie łatwiej utrzymywać, rozszerzać, czytać, a problem głodu na świecie zniknie. Pitu pitu, już pokazuje przykłady, które będą proste, czasem wręcz naiwne, chodzi o ukazanie idei, nie rozwiązanie jakiegoś rzeczywistego problemu.

Single Resposibility Principle wszyscy piszą że “Nigdy nie powinno być więcej niż jednego powodu do modyfikacji”. Strasznie mnie irytuje taki zapis, bo można napisać “klasa jest odpowiedzialna tylko za jedną logiczną funkcjonalność”. Skoro służy do mierzenia, to nie służy do zapisywania. Jeśli ma konwertować wartości z jednego formatu na inny, to nie powinna ich wysłać do innych części aplikacji. etc., itd., itp.

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: Consolas, “Courier New”, Courier, Monospace;
background-color: #ffffff;
max-height: 300px;
overflow: auto;
/*white-space: pre;*/
}

.csharpcode pre { margin: 0em; }

.csharpcode .rem { color: #008000; }

.csharpcode .kwrd { color: #0000ff; }

.csharpcode .str { color: #a31515; }

.csharpcode .op { color: #0000c0; }

.csharpcode .preproc { color: #cc6633; }

.csharpcode .asp { background-color: #ffff00; }

.csharpcode .html { color: #800000; }

.csharpcode .attr { color: #ff0000; }

.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}

.csharpcode .lnum { color: #606060; }

   1:   public class BadHardwareDevice
   2:      {
   3:          int hardwareReadingLevel;
   4:          void DisplayHardwareLevelAlarm()
   5:          {
   6:              Console.WriteLine("Current value of {0} is dangerous!", hardwareReadingLevel);
   7:          }
   8:   
   9:          int GetHardwareReadings()
  10:          {
  11:              // reads data from device into hardwareReagingLevel
  12:              // return value from that reading
  13:              return hardwareReadingLevel;
  14:          }
  15:      }

Prosta klasa, ale zajmuje się ona dwiema rzeczami jednocześnie; odczytuje wartość z urządzenia (GetHardwareReadings) oraz wypisuje alarm na konsolę (DisplayHardwareLevelAlarm).
Kod można zmodyfikować na przykład w taki sposób:

   1:  public class BetterHardwareDeviceConsoleLogger
   2:      {
   3:          void DisplayHardwareLevelAlarm(int aLevel)
   4:          {
   5:              Console.WriteLine("Current value of {0} is dangerous!", aLevel);
   6:          }
   7:      }
   8:   
   9:      public class BetterHardwareDeviceReader
  10:      {
  11:          int hardwareReadingLevel;
  12:          int GetHardwareReadings()
  13:          {
  14:              // reads data from device into hardwareReagingLevel
  15:              // return value from that reading
  16:              return hardwareReadingLevel;
  17:          }
  18:      }

Teraz klasa odczytu zajmuje się tylko odczytem, natomiast klasa logująca alarm tylko wypisaniem na konsolę. Dodatkowo jeśli będzie potrzeba wysłania zapisu gdzieś indziej niż na konsolę nie będzie to miało wpływu na działanie innych komponentów. Bez modyfikacji już istniejących klas można stworzyć klasę BetterHardwareDeviceNetworkLogger i wysłać informację o poziomie wartości w świat.

Open / Close Principle wszyscy piszą, że klasa powinna być otwarta na rozszerzenia, a zamknięta na modyfikacje. I bądź mądry. Już nadciągam z chłopską odsieczą i rozumowaniem. Klasa powinna być tak napisana, aby nie trzeba było jej aktualizować gdy pojawi się nowa implementacja np. klasy logującej. Rozszerzenie funkcjonalne aplikacji powinno zostać zaimplementowane poza klasą, ona sama natomiast może z niej skorzystać. Jak zawsze bez kodu ciężko jest tłumaczyć:

   1:  public class BadLogger
   2:      {
   3:          public void LogMessage(string aMessage, BadLogTarger aTarget)
   4:          {
   5:              switch (aTarget)
   6:              {
   7:                  case BadLogTarger.ConLog:
   8:                      // write to console
   9:                      break;
  10:                  case BadLogTarger.NetLog:
  11:                      // write to network
  12:                      break;
  13:                  case BadLogTarger.DevNullLog:
  14:                      // ignore writing
  15:                      break;
  16:              }
  17:          }
  18:      }
  19:    
  20:      public enum BadLogTarger
  21:      {
  22:          ConLog,
  23:          NetLog,
  24:          DevNullLog,
  25:      }

Tutaj aby dodać logowanie np. do pliku należy dodać kolejnego enuma (przewińcie przykładowy kod na dół), a następnie rozszerzyć metodą LogMessage o odpowiedniego case….. dużo roboty. Może lepiej będzie zrobić coś takiego:

   1:  public class BetterLogger
   2:  {
   3:      ILogger logger;
   4:      public BetterLogger(ILogger aLogger)
   5:      {
   6:          this.logger = aLogger;
   7:      }
   8:      
   9:      public void LogMessage( string aMessage )
  10:      {
  11:          logger.LogMessage(aMessage);
  12:      }
  13:  }
  14:   
  15:  public interface ILogger
  16:  {
  17:      void LogMessage(string aMessage);
  18:  }
  19:   
  20:  public class ConLogger: ILogger
  21:  {
  22:      void ILogger.LogMessage(string aMessage)
  23:      {
  24:          // log to console
  25:   
  26:      }
  27:  }
  28:   
  29:  public class NetLogger : ILogger
  30:  {
  31:      public void LogMessage(string aMessage)
  32:      {
  33:          // log to network
  34:      }
  35:  }
  36:   
  37:  public class DevNullLogger : ILogger
  38:  {
  39:      public void LogMessage(string aMessage)
  40:      {
  41:          // log to network
  42:      }
  43:  }

Ojej więcej kodu. Tak tak, ale teraz obczaj to (miły czytelniku), po dodaniu nowego logera (np. FileLoger) implementuje się metodą LogMessage, a następnie przekazują taką klasę do BetterLogger, której nie trzeba już modyfikować. Wszystko działa tak jak poprzednio, a jest nowa funkcjonalność. Klasa jest otwarta na rozszerzenia, ale zamknięta na zmiany – voila.

Liskov Substitution Principle wszyscy strasznie komplikują opis. Mam proste wytłumaczenie tej zasady (proste bo po co komplikować, albo proste bo nie zrozumiałem zasady), jeśli dziedziczysz lub implementujesz pewien interfejs, wszystkie klasy pochodne powinny zachowywać się w podobny (logicznie podobny) sposób. Tak aby obiekt wykorzystujący referencję (wskaźnik) do klasy bazowej, mógł spokojnie wykorzystać funkcjonalność klas bardziej pochodnych i nie zostać zaskoczonym przez nietypowym zachowaniem (patrz kod poniżej). Dodatkowym założeniem jest, że parametry akceptowane przez klasy pochodne mogą być mniej restrykcyjne niż w klasie bazowej (może obsłużyć więcej przypadków), natomiast wartości zwracana mogą być takie same lub mniejsze (nie można zwrócić czegoś, co nie jest określone przez klasa bazowa). Dygresja: Przykład z prostokątem i kwadratem dla mnie ma sens i jest poprawny, nie wiem dlaczego jest uznawany za błędną implementacje, jeśli ktoś potrafi to wytłumaczyć proszę o kontakt.

   1:  public interface BadISettings
   2:  {
   3:      void Save();
   4:      void Load();
   5:  }
   6:   
   7:  public class BadUserSettings : BadISettings
   8:  {
   9:      public void Save()
  10:      {
  11:          // save user settings
  12:      }
  13:   
  14:      public void Load()
  15:      {
  16:          // load user settings
  17:      }
  18:  }
  19:   
  20:  public class BadApplicationSettings : BadISettings
  21:  {
  22:      public void Save()
  23:      {
  24:          // throw InvalidOperationException
  25:          // cannot overwrite application settings
  26:      }
  27:   
  28:      public void Load()
  29:      {
  30:          // load application settings
  31:      }
  32:  }

Co jest nie tak? Otóż ustawienia aplikacja nie chętnie się zapisują. Co wpływa na możliwość ich wykorzystania przez interfejs BadISettings. Interfejs nie definiuje nigdzie, że będzie rzucał wyjątkiem, poza tym nie definiuje on metody tylko po to żeby jej nie wspierać. Jednym z rozwiązań jest coś takiego:

   1:  public interface BetterIReadSettings
   2:  {
   3:      void Read();
   4:  }
   5:   
   6:  public interface BetterIWritableSettings
   7:  {
   8:      void Save();
   9:  }
  10:   
  11:  public class BetterUserSettings : BetterIReadSettings, BetterIWritableSettings
  12:  {
  13:      void Read()
  14:      {
  15:          // read settings
  16:      }
  17:   
  18:      void Save()
  19:      {
  20:          // save settings
  21:      }
  22:  }
  23:   
  24:  public class BetterApplicationSettings : BetterIReadSettings
  25:  {
  26:      public void Read()
  27:      {
  28:          // load application settings
  29:      }
  30:  }

//Pozdrawiam czułe oko Jacka

Teraz można spokojnie wykorzystać wspólny interfejs BetterIWritableSettings bez obaw że coś się wywali lub wyleci w powietrze. A BetterIReadableSettings z jedną metodą? A czy jest w tym coś złego? Zerknij na SRP.

Interface Segregation Principle – różnie o tym piszą, akurat nie ma informacji w rodzimym języku. Klienci naszej klasy nie powinni mieć dostępu do elementów z których nie korzystają. Będzie lepiej jeśli nawet nie będzie o nich wiedzieć. Klient powinien dostać tylko tyle ile potrzebuje, nic więcej. Jak zwykle abstrakcja i interfejsy są jor friend.

   1:  public class BadPerson
   2:  {
   3:      public string FirstName;
   4:      public string LastName;
   5:      public string EmailAddress;
   6:      public string PhoneNumber;
   7:      public string Address;
   8:  }
   9:   
  10:  public class BadEmailer
  11:  {
  12:      public void SendEmail( BadPerson aBadPerson )
  13:      {
  14:          // send email using just email address
  15:      }
  16:  }
  17:   
  18:  public class PhoneCaller
  19:  {
  20:      public void PhoneCall( BadPerson aBadPerson )
  21:      {
  22:          // make phone call using just phone number
  23:      }
  24:  }
  25:   
  26:  public class LetterSender
  27:  {
  28:      public void SendLetter (BadPerson aBadPerson )
  29:      {
  30:          // send letter using firstname, lastname and an address
  31:      }
  32:  }

Tutaj widać że klasa BadPerson agreguje wszystko co dotyczy jakiegoś człowieczka, co dał się wbić w internet za kubek kawy ;). Następnie wszystkie te informacje są przekazywane do innych obiektów, który wykorzystują tylko część z tych danych. Kto wie co one tak naprawdę robią z resztą z nich? Czy nie lepiej zamienić to na coś takiego:

   1:  public interface BetterIEmailable
   2:  {
   3:      string EmailAddress{get;set;}
   4:  }
   5:   
   6:  public interface BetterIPhoneable
   7:  {
   8:      string PhoneNumber{get;set;}
   9:  }
  10:   
  11:  public interface BetterILetterable
  12:  {
  13:      string FirstName{get;set;}
  14:      string LastName{get;set;}
  15:      string Address{get;set;}
  16:  }
  17:   
  18:  public class BetterPerson : BetterIEmailable, BetterILetterable, BetterIPhoneable
  19:  {
  20:      public string EmailAddress { get; set; }
  21:      public string PhoneNumber { get; set; }
  22:      public string FirstName { get; set; }
  23:      public string LastName { get; set; }
  24:      public string Address { get; set; }
  25:  }
  26:   
  27:  public class BetterEmailer
  28:  {
  29:      public void SendEmail(BetterIEmailable aBetterPerson)
  30:      {
  31:          // send email using just email address
  32:      }
  33:  }
  34:   
  35:  public class BetterSender
  36:  {
  37:      public void SendLetter(BetterILetterable aBetterPerson)
  38:      {
  39:          // send letter using firstname, lastname and an address
  40:      }
  41:  }
  42:   
  43:  public class BetterCaller
  44:  {
  45:      public void PhoneCall(BetterIPhoneable aBetterPerson)
  46:      {
  47:          // make phone call using just phone number and first name
  48:      }
  49:  }

Teraz każdy dostaje tylko to czego potrzebuje. Nie zaistnieje problem, w którym klasa wysyłająca pocztę elektroniczną zacznie zapisywać dane teleadresowe, podczas gdy miała wysłać tylko email. Wiecie rozumiecie.

Dependency Inversion Principle – mówi o tym, aby powiązania pomiędzy klasami były luźne. Klasy nie powinny zależeć od właściwej implementacji, a działać na wcześniej zdefiniowanym interfejsie. Dzięki temu prościej jest rozszerzać funkcjonalność lub całkowicie ją zmieniać, oczywiście w ramach interfejsu i zasady LSP.

   1:  public class BadButton
   2:  {
   3:      public BadButton( BadLamp aLamp)
   4:      {
   5:          Lamp = aLamp;
   6:      }
   7:   
   8:      public void Click()
   9:      {
  10:          this.Lamp.Switch();
  11:      }
  12:   
  13:      public BadLamp Lamp { get; private set; }
  14:  }
  15:   
  16:  public class BadLamp
  17:  {
  18:      readonly BadButton button;
  19:      public BadLamp()
  20:      {
  21:          this.button = new BadButton(this);
  22:      }
  23:   
  24:      public void Switch()
  25:      {
  26:          // switch the light on/off
  27:      }
  28:  }

Przykład powyżej to wyjątkowo mocne powiązanie klasy BadButton oraz BadLamp. Nie będą one współpracować z żadną inną klasą niż te zdefiniowane, chyba że dziedziczące po BadLamp, natomiast już na pewno nie zaistnieje możliwość zmiany guzika na inny. Oczywiście istnieje lepsze rozwiązanie:

   1:  public interface BetterIButtonClient
   2:  {
   3:      void Switch();
   4:  }
   5:   
   6:  public interface BetterIButton
   7:  {
   8:      void Click();
   9:  }
  10:   
  11:  public class BetterButton : BetterIButton
  12:  {
  13:      BetterIButtonClient client;
  14:      public BetterButton(BetterIButtonClient aClient)
  15:      {
  16:          client = aClient;
  17:      }
  18:   
  19:      public void Click()
  20:      {
  21:          this.client.Switch();
  22:      }
  23:  }
  24:   
  25:  public class FancyBetterButton : BetterIButton
  26:  {
  27:      BetterIButtonClient client;
  28:      public FancyBetterButton(BetterIButtonClient aClient)
  29:      {
  30:          client = aClient;
  31:      }
  32:   
  33:      public void Click()
  34:      {
  35:          this.client.Switch();
  36:      }
  37:  }
  38:   
  39:  public class BetterLamp : BetterIButtonClient
  40:  {
  41:      BetterIButton betterButton;
  42:      public BetterLamp(BetterIButton aBetterButton)
  43:      {
  44:          betterButton = aBetterButton;
  45:      }
  46:   
  47:      public void Switch()
  48:      {
  49:          // switch the light on/off
  50:      }
  51:  }

Co dostajemy? Otóż button już nie musi działać tylko i wyłącznie z lampą, zadziała także z pralką, lodówką i innym AGD o ile sprzęt będzie obsługiwać interfejs BetterIButtonClient i prostą metodą Switch. A dalej widać, że lampa także może być już bardziej fikuśna, nie zależy ona już tylko od jednego rodzaju Buttona. W dodatku to nie ona jest odpowiedzialna za decydowanie z którego skorzysta (od siebie dodałem idee: DI). Dzięki temu fabryka lamp może w prosty sposób przerzucić się na nowszy model z wykorzystaniem FancyBetterButton-ów. Widać tu wpływy idei OCP. Taki kod prościej jest wykorzytać w innym projektach, ponieważ nie jest on mocno powiązany z bezpośrednią implementacją klasy guzików czy innych obiektów, zdefiniowanych z projekcie.

Podczas pisania kodu należy przede wszystkim korzystać z G.Ł.O.W.Y. to jest najważniejsza zasada! Dopiero później warto podpierać się różnymi ideami i regułami, które proponują inni. Zasady są po to aby je łamać, a nic tak nie uczy człowieka jak popełniane błędy. Zachęcam do łamania zasad SOLID, a potem sprawdzenia czy miało to sens i podzielenia się tym doświadczeniem ze mną i innymi 😉

Podczas wpisu sporą część (bardzo dużą część wiedzy) oparłem na
[1] http://www.blackwasp.co.uk/SOLIDPrinciples.aspx
[2] www.wikipedia.org polska i angielska wersja
[3] http://www.objectmentor.com Tutaj trzeba się porządnie naszukać aby znaleźć materiały
Szczególnie polecam punkt pierwszy.

Uwagi jak zawsze chętnie przyjmę.

ps. Jutro do pracy
ps2. C# jest czytelny, nawet dla nie programujących w C#.
ps3. Link do źródełek ze wszystkimi zasadami  https://www.sugarsync.com/pf/D6056980_3239101_969599
 

WPF pasek postępu na ikonie aplikacji, dodatkowe guziki w podlądzie apliacji – TaskbarItemInfo w natarciu.

Nauka WPF idzie całkiem dobrze, a to oznacza kolejną porcję informacji. Dzisiaj trochę o ficzerze z Windows7 (które jest w Viście), a nie wiem czy będzie nadal aktualny w nowej wersji Windows, która pewnie ukaże niebawem.
Opowiem trochę o pasku postępu, który można wyświetlać na ikonie aplikacji, która jest na systemowym pasku zadań, oraz o tym jak dodać kilka (maksymalnie 7 widocznych) guzików do podglądu aplikacji. Przez podgląd rozumiem, najechanie myszą na ikonę działającej aplikacji,
znajdującą się na pasku zadań, ale bez klikania na niej. Spowoduje to
wyświetlenie się małego okienka z podglądem naszej aplikacji.
Media Player Classic to ma, Zune to ma, Windows Media Player to, więc dlaczego i ja miałbym z tego nie skorzystać.

Podgląd aplikacji, z wyświetlonym podpisem dla pierwszego guzika.

Obie funkcjonalności (pasek postępu oraz dodatkowe guziki) opierają się na TaskbarItemInfo, który jest częścią wyświetlanego okna. Jak prawie wszystko w WPF, można to zdefiniować w XAML oraz code-behind. Wszyscy pamiętamy o tym, że samodzielne definiowane właściwości dla TaskbarItemInfo w code-behind zaczynamy od zaalokowania pamięci na ten obiekt, ponieważ początkowo TaskbarItemInfo jest NULLem. Ja o tym pamiętam po pierwszym uruchomienia programu.
Tak deklaruje się w XAML dodatkowe guziki, które mają być widoczne w podglądzie okna.

  1. <Window.TaskbarItemInfo>
  2.     <TaskbarItemInfo>
  3.       <TaskbarItemInfo.ThumbButtonInfos>
  4.         <ThumbButtonInfo x:Name=”ReverseThumb” ImageSource=”imagesrev.png” Description=”Reverse progress”
  5.                          Click=”ReverseThumbClick” />
  6.         <ThumbButtonInfo x:Name=”PauseThumb” ImageSource=”imagespause.png” Description=”Pause progress”
  7.                          Click=”PauseThumbClick” IsEnabled=”False” />
  8.         <ThumbButtonInfo x:Name=”RunThumb” ImageSource=”imagesplay.png” Description=”Run progress”
  9.                          Click=”RunThumbClick” />
  10.         <ThumbButtonInfo x:Name=”ForwardThumb” ImageSource=”imagesffd.png” Description=”Forward progress”
  11.                          Click=”ForwardThumbClick” IsEnabled=”False” />
  12.       </TaskbarItemInfo.ThumbButtonInfos>
  13.     </TaskbarItemInfo>
  14.   </Window.TaskbarItemInfo>

Nie ma w tym nic trudnego, ani skomplikowanego. Jenocześnie można wyświetlić nie więcej niż siedem, jeśli na liście będzie ich więcej, to tylko pierwszych siedem widocznych będzie wyświetlonych. Dynamiczne zachowanie się guzików, można osiągnąć poprzez zmianę wartości Visibility na Collapsed, ukrywając jedne, a odkrywając inne.
W przykładnie postanowiłem obsłużyć zdarzenie Click i wtedy wykonać odpowiedni kod. Do TaskbarItemInfo można również przypiąć wpf’owy Command.
ThumbButtonInfo ma pewną przypadłość, lub nie potrafiłem znaleźć tej informacji, otóż w C# nie można (nie wiem jak?) odwołać się do właściwości Name, przez co musiałem obsługę zdarzeń rozdzielić na pojedyncze metody. Funkcjonalność na której mi zależało, to uruchomienie paska postępu, pauza postępu, oraz zmiana kierunku postępu (np. na symulowanie rollback instalacji).

Oprócz tego, na kolorowym oknie aplikacji (to różowe GUI powstało w ramach praktyki WPF i ćwiczeń z layoutu) widać 5 pseudo guzików, które umożliwiają ustawienie właściwości paska postępu, wyświetlanego na ikonie aplikacji.
Stan postępu można sygnalizować na cztery (pięć) sposob:


  • Zielony (TaskbarItemProgressState.Normal), wszystko w porządeczku
  • Niezdecydowany (TaskbarItemProgressState.Indeterminate), gdy
    programista nie jest pewien ile czasu zajmie operacja, może wyświetlić
    pasek informujący o tym, że operacja jest w trakcie wykonywania i
    będzie gotowa gdy będzie gotowa (tak samo jak Diablo III). W tym
    przypadku aktualizowanie paska postępu nie wpływa w żaden sposób na
    sposób wyświetlania postępu)
  • Zółty (TaskbarItemProgressState.Paused) – brak postępu, pauza. Nie mniej jednak nic nie powstrzymuje aplikacji przed zwiększaniem wartości postępu, 
  • Czerwony (TaskbarItemProgressState.Error) – pojawił się błąd, nadal można aktualizować pasek.
  • Brak paska postępu (TaskbarItemProgressState.None)

Powtórzę się: w każdym przypadku wartość postępu może wzrastać czy maleć (cofać się).
Poszczególne stany można zmieniać w trakcie działania aplikacji, nie trzeba zerować czy resetować  postępu. Oczywiście wszyscy pamiętamy, że kontrolki modyfikować wolno tylko w głównym wątku, TaskbarItemInfo nie jest wyjątkiem. Wartości przyjmowane przez pasek postępu mieszcząc się od 0.0 do 1.0 (double).
Wątek który symuluje postęp paska:

  1. new Action(
  2. () =>
  3. {
  4.     this.TaskbarItemInfo.ProgressValue = progressValue;
  5.     progressValue += progress;
  6.     if (progressValue > 1.0)
  7.     {
  8.         progressValue = 0.0;
  9.     }
  10.     else if (progressValue < 0.0)
  11.     {
  12.         progressValue = 1.0;
  13.     }
  14. })

Tyle – cała filozofia. Przyznam szczerze, że tą funkcjonalność wcześniej, to myślałem że będzie więcej zabawy, jednak WPF kolejny raz mnie pozytywnie zaskakuje.
W świątecznej promocji dopiszę jeszcze na krótko, że dodatkowo na ikonie można dorysować jeszcze mniejszą ikonkę tzw. Overlay. Miniaturka pojawia się i znika płynnie, natomiast jeśli jest już widoczna i zostanie zmieniona na inną, to zmiana ta jest natychmiastowa.

  1. <Window.TaskbarItemInfo>
  2.     <TaskbarItemInfo Overlay=”imagesthree.png”>
  3. </Window.TaskbarItemInfo>

Znowu prosto – WPF nas rozpieszcza 🙂

Wszelkie uwagi i krytykę jak zwykle chętnie przyjmę w komentarzach.

WPF – rotacja, transformacja i przechylanie

W ramach uczestnictwa w kursie WPF czytam ksiązkę “WPF 4 Unleashed” i przygotowuje jakieś przykłady sprawdzające co tam pan autor pisze w tej książce. Jeden z początkowych rozdziałów (tak, dopiero rozpoczynam kurs i książkę) poświęcony jest tematyce transformacji kontrolek, w związku z tym napisałem prosty przykład ukazyjący omawiane tranformacje. W zasadzie testuje tylko trzy z pięciu, ponieważ przesunięcie obiektów nie jest zbyt widowiskowe, a wykorzystanie macierzy nie jest na moją głowę. Zostają tylko obracanie, skalowanie i przechylanie.
Do implementacji wykorzystałem trzy wątki, a jak wszyscy wiemy .NET nie lubi, jak ktoś dobiera się do kontrolek z innego wątku niż głównego. Wykorzystany został tutaj obiekt Dispatcher z WPF, który ułatwia sprawę. Najbardziej denerwującą rzeczą okazał się brak możliwości skorzystania z wyrażeń lambda do implementacji ciała metody zajmującej się aktualizacją kontrolek. Trzeba było skorzystać z delegatów. Na stackoverflow widziałem przykład z rzutowaniem lambdy na Action. Po krótkiej i przegranej walce z przykładem, postanowiłem że zostanę przy delegatach.

Przykład jest na tyle prosty, że nie pokuszę się o wrzucanie go na jakiś serwer, zostanie pokazany światu tu i tylko tu.
Na początek UI (bez szału):

  1. <Window x:Class=”wpfTransformations.MainWindow”
  2.         xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
  3.         xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
  4.         Title=”MainWindow” Height=”466″ Width=”1000″>
  5.     <Grid>
  6.         <!– Rotating –>
  7.         <Button x:Name=”r_00_00″ Content=”0,0″ Height=”23″ HorizontalAlignment=”Left” Margin=”69,64,0,0″
  8.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”0,0″>
  9.             <Button.RenderTransform >
  10.                 <RotateTransform Angle=”33″ x:Name=”RotateTransform”/>
  11.             </Button.RenderTransform>
  12.         </Button>
  13.  
  14.         <Button x:Name=”r_10_00″ Content=”1,0″ Height=”23″ HorizontalAlignment=”Left” Margin=”255,64,0,0″
  15.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”1,0″>
  16.             <Button.RenderTransform>
  17.                 <RotateTransform Angle=”33″/>
  18.             </Button.RenderTransform>
  19.         </Button>
  20.  
  21.         <Button x:Name=”r_10_10″ Content=”1,1″ Height=”23″ HorizontalAlignment=”Left” Margin=”447,64,0,0″
  22.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”1,1″>
  23.             <Button.RenderTransform>
  24.                 <RotateTransform Angle=”33″/>
  25.             </Button.RenderTransform>
  26.         </Button>
  27.  
  28.         <Button x:Name=”r_00_10″ Content=”0,1″ Height=”23″ HorizontalAlignment=”Left” Margin=”631,64,0,0″
  29.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”0,1″>
  30.             <Button.RenderTransform>
  31.                 <RotateTransform Angle=”33″/>
  32.             </Button.RenderTransform>
  33.         </Button>
  34.  
  35.         <Button x:Name=”r_05_05″ Content=”0.5,0.5″ Height=”23″ HorizontalAlignment=”Left” Margin=”803,64,0,0″
  36.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”0.5,0.5″>
  37.             <Button.RenderTransform>
  38.                 <RotateTransform Angle=”33″/>
  39.             </Button.RenderTransform>
  40.         </Button>
  41.         <!– Scaling –>
  42.           <Button x:Name=”s_00_00″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”69,179,0,0″
  43.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”0,0″>
  44.             <Button.RenderTransform>
  45.                 <ScaleTransform ScaleX=”.6″ ScaleY=”-.6″ />
  46.             </Button.RenderTransform>
  47.         </Button>
  48.         <Button x:Name=”s_00_10″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”255,179,0,0″
  49.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”0,1″>
  50.             <Button.RenderTransform>
  51.                 <ScaleTransform ScaleX=”.6″ ScaleY=”-.6″ />
  52.             </Button.RenderTransform>
  53.         </Button>
  54.         <Button x:Name=”s_10_10″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”447,179,0,0″
  55.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”1,1″>
  56.             <Button.RenderTransform>
  57.                 <ScaleTransform ScaleX=”.6″ ScaleY=”-.6″ />
  58.             </Button.RenderTransform>
  59.         </Button>
  60.         <Button x:Name=”s_10_00″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”631,179,0,0″
  61.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”1,0″>
  62.             <Button.RenderTransform>
  63.                 <ScaleTransform ScaleX=”.6″ ScaleY=”-.6″ />
  64.             </Button.RenderTransform>
  65.         </Button>
  66.         <Button x:Name=”s_05_05″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”803,179,0,0″
  67.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”0.5,0.5″>
  68.             <Button.RenderTransform>
  69.                 <ScaleTransform ScaleX=”.6″ ScaleY=”-.6″ />
  70.             </Button.RenderTransform>
  71.         </Button>
  72.         <!– Skewing –>
  73.           <Button x:Name=”sk_00_00″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”69,315,0,0″
  74.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”0,0″>
  75.             <Button.RenderTransform>
  76.                 <SkewTransform AngleX=”33″ AngleY=”3″ />
  77.             </Button.RenderTransform>
  78.         </Button>
  79.         <Button x:Name=”sk_10_00″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”255,315,0,0″
  80.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”1,0″>
  81.             <Button.RenderTransform>
  82.                 <SkewTransform AngleX=”33″ AngleY=”3″ />
  83.             </Button.RenderTransform>
  84.         </Button>
  85.         <Button x:Name=”sk_10_10″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”447,315,0,0″
  86.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”1,1″>
  87.             <Button.RenderTransform>
  88.                 <SkewTransform AngleX=”33″ AngleY=”3″ />
  89.             </Button.RenderTransform>
  90.         </Button>
  91.         <Button x:Name=”sk_00_10″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”631,315,0,0″
  92.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”0,1″>
  93.             <Button.RenderTransform>
  94.                 <SkewTransform AngleX=”33″ AngleY=”3″ />
  95.             </Button.RenderTransform>
  96.         </Button>
  97.         <Button x:Name=”sk_05_05″ Content=”Button” Height=”23″ HorizontalAlignment=”Left” Margin=”803,315,0,0″
  98.                 VerticalAlignment=”Top” Width=”75″ RenderTransformOrigin=”0.5,0.5″>
  99.             <Button.RenderTransform>
  100.                 <SkewTransform AngleX=”33″ AngleY=”3″ />
  101.             </Button.RenderTransform>
  102.         </Button>
  103.     </Grid>
  104. </Window>

Tak wygląda code-behind i trzy wątki odpowiedzialne, za aktualizację kontrolek (też bez szału):

  1. using System;
  2. using System.Windows;
  3. using System.Windows.Controls;
  4. using System.Windows.Media;
  5.  
  6. namespace wpfTransformations
  7. {
  8. using System.Threading;
  9.  
  10. /// <summary>
  11. /// Interaction logic for MainWindow.xaml
  12. /// </summary>
  13. public partial class MainWindow : Window
  14. {
  15.     // delegates to handle UI controls update from background threads
  16.     public delegate void SetAngleD(float f, Button b);
  17.     public delegate void SetScaleD(float sx, float sy, Button b);
  18.     public delegate void SetSkewD(float ax, float ay, Button b);
  19.  
  20.     // helper methods to update the controls    
  21.     public void SetAngle(float f, Button b)
  22.     {
  23.         RotateTransform r = new RotateTransform(f);
  24.         b.RenderTransform = r;
  25.     }
  26.  
  27.     public void SetScale(float sx, float sy, Button b)
  28.     {
  29.         ScaleTransform st = new ScaleTransform(sx, sy);
  30.         b.RenderTransform = st;
  31.     }
  32.  
  33.     public void SetSkew(float ax, float ay, Button b)
  34.     {
  35.         SkewTransform sk = new SkewTransform(ax, ay);
  36.         b.RenderTransform = sk;
  37.     }
  38.  
  39.     public MainWindow()
  40.     {
  41.         InitializeComponent();
  42.  
  43.         // This one will take care of rotation of the elements
  44.         Thread tRotatingThread = new Thread(
  45.             () =>
  46.             {
  47.                 float angle = 33.0f;
  48.                 while (true)
  49.                 {
  50.                     SetAngleD d = new SetAngleD(this.SetAngle);
  51.                     r_00_00.Dispatcher.Invoke(d, new object[] { angle, r_00_00 });
  52.                     r_00_10.Dispatcher.Invoke(d, new object[] { angle, r_00_10 });
  53.                     r_10_10.Dispatcher.Invoke(d, new object[] { angle, r_10_10 });
  54.                     r_10_00.Dispatcher.Invoke(d, new object[] { angle, r_10_00 });
  55.                     r_05_05.Dispatcher.Invoke(d, new object[] { angle, r_05_05 });
  56.  
  57.                     angle += 1.0f;
  58.                     if (angle > 360.0f)
  59.                     {
  60.                         angle = 0.0f;
  61.                     }
  62.  
  63.                     Thread.Sleep(10);
  64.                 }
  65.             });
  66.         tRotatingThread.IsBackground = true;
  67.  
  68.         // This one will play with scaling properties of controls
  69.         Thread tScalingThread = new Thread(
  70.             () =>
  71.             {
  72.                 float sxm = 0.019f;
  73.                 float sym = -0.011f;
  74.  
  75.                 float sx = 0.6f;
  76.                 float sy = -0.6f;
  77.  
  78.                 while (true)
  79.                 {
  80.                     SetScaleD d = new SetScaleD(this.SetScale);
  81.                     s_00_00.Dispatcher.Invoke(d, new object[] { sx, sy, s_00_00 });
  82.                     s_10_00.Dispatcher.Invoke(d, new object[] { sx, sy, s_10_00 });
  83.                     s_10_10.Dispatcher.Invoke(d, new object[] { sx, sy, s_10_10 });
  84.                     s_00_10.Dispatcher.Invoke(d, new object[] { sx, sy, s_00_10 });
  85.                     s_05_05.Dispatcher.Invoke(d, new object[] { sx, sy, s_05_05 });
  86.  
  87.                     if (Math.Abs(sx) > 2.1f)
  88.                     {
  89.                         sxm *= -1.0f;
  90.                     }
  91.  
  92.                     if (Math.Abs(sy) > 2.1f)
  93.                     {
  94.                         sym *= -1.0f;
  95.                     }
  96.  
  97.                     sx += sxm;
  98.                     sy += sym;
  99.                     Thread.Sleep(30);
  100.                 }
  101.             });
  102.  
  103.         tScalingThread.IsBackground = true;
  104.  
  105.  
  106.         // This one will skew them grrrrr
  107.         Thread tSkewingThread = new Thread(
  108.             () =>
  109.             {
  110.                 float ax = 33.0f;
  111.                 float ay = 3.0f;
  112.  
  113.                 while (true)
  114.                 {
  115.                     SetSkewD d = new SetSkewD(this.SetSkew);
  116.                     sk_00_00.Dispatcher.Invoke(d, new object[] { ax, ay, sk_00_00 });
  117.                     sk_10_00.Dispatcher.Invoke(d, new object[] { ax, ay, sk_10_00 });
  118.                     sk_10_10.Dispatcher.Invoke(d, new object[] { ax, ay, sk_10_10 });
  119.                     sk_00_10.Dispatcher.Invoke(d, new object[] { ax, ay, sk_00_10 });
  120.                     sk_05_05.Dispatcher.Invoke(d, new object[] { ax, ay, sk_05_05 });
  121.  
  122.                     ax += 1.0f;
  123.                     if (ax > 90.0f)
  124.                     {
  125.                         ax = 0.0f;
  126.                     }
  127.  
  128.                     ay += 1.0f;
  129.                     if (ay > 60.0f)
  130.                     {
  131.                         ay = 0.0f;
  132.                     }
  133.  
  134.                     Thread.Sleep(30);
  135.                 }
  136.             });
  137.  
  138.         tSkewingThread.IsBackground = true;
  139.  
  140.  
  141.         // Because everyone of them is set as background thread
  142.         // They should end nicely at the end of application
  143.         tRotatingThread.Start();
  144.         tScalingThread.Start();
  145.         tSkewingThread.Start();
  146.     }
  147.  
  148. }
  149. }

Na koniec aplikacji w akcji (odrobina szału jest):

Pierwszy rząd guzików jest obracany, kolejny skalowany, a ostatni przechylany. Chyba lepiej to wygląda gdy działa, po wklejeniu do msvc powinien śmigać bez zająknięcia.
To tyle. Do zapamiętania: Dispatcher nie przyjmie lamby i nie walcz z tym, od razu korzystaj z delegatów. Chyba, że wiesz o co chodzi z tym Action() i wiesz jak z tego skorzystać (wiesz? – napisz w komentarzu), albo masz inne rozwiązanie. Podobno w 4.5 można z każdego wątku aktualizować UI, jeszcze nie sprawdziłem.

ps. Rozpieszczam was tym zawijaniem wierszy.

Mono Cecil – przykład użycia

W poprzednim poście wspomniałem o mojej walce w Mono Cecil, dzisiaj chciałbym się podzielić moimi wrażeniami i doświadczeniem. Dodam tylko, że o Mono usłyszałem na spotkaniach wrocławskiej grupy .net, wykład prowadził Paweł Łukasik, slajdy z wykładu dostępne są na jego blogu: http://pawlos.blogspot.com. Jak zwykle zapraszam na spotkania i wykłady.

Źródła które pokaże, są tylko prostym przykładem, zamysłem tego co chciałem zrobić w docelowym rozwiązaniu. Powinny jednak wystarczyć by ukazać jak działa Mono Cecil.
Sztuczka miała polegać na dodaniu funkcjonalności do istniejącej już aplikacji; jestem leniwy i nie chce klikać myszą w guziczki góra, dół, prawo i lewo, chciałbym mieć możliwość nawigowania za pomocą strzałek na klawiaturze. Rozwiązania są dwa, można napisać taką implementacji od zera w IL i doklejeniu jej do docelowej aplikacji. Druga możliwość, to skopiowanie gotowej implementacji z innej aplikacji. Wszyscy jesteśmy leniwi, tak więc rozwiązanie drugie było bardziej kuszące.
Cały projekt jest dostępy na git hubie dostępy dla każdego.

Zabrałem się więc do roboty i oto do czego doszedłem:
Kod wykonujący brudną robotę:

  1. using System.Linq;
  2.  
  3. namespace mcWorker
  4. {
  5.     using Mono.Cecil;
  6.     using Mono.Cecil.Cil;
  7.  
  8.     class Program
  9.     {
  10.         static void Main(string[] args)
  11.         {
  12.             string sourceExe = @”contentSourceForm.exe”;
  13.             string targetExe = @”contentTargetForm.exe”;
  14.  
  15.             string srcMethod1 = “Form1_KeyDown”;
  16.             string srcMethod2 = “Form1_KeyUp”;
  17.  
  18.             // Source of code
  19.             AssemblyDefinition adSrc = AssemblyDefinition.ReadAssembly(sourceExe);
  20.             TypeDefinition typeSrc = adSrc.MainModule.Types
  21.                 .Where(t => t.Name == “SourceForm”).First();
  22.  
  23.  
  24.             // Place where code will be added
  25.             AssemblyDefinition adDst = AssemblyDefinition.ReadAssembly(targetExe);
  26.             TypeDefinition typeDst = adDst.MainModule.Types
  27.                 .Where(t => t.Name == “TargetForm”).First();
  28.  
  29.             // Copy both methods from src to dst
  30.             MethodDefinition m1 = CopyMethod(adSrc, typeSrc, srcMethod1, adDst, typeDst);
  31.             MethodDefinition m2 = CopyMethod(adSrc, typeSrc, srcMethod2, adDst, typeDst);
  32.  
  33.             // Now they should be marked as event handlers
  34.             AddEventHandlers(adDst, typeDst, “KeyDown”, m1);
  35.             AddEventHandlers(adDst, typeDst, “KeyUp”, m2);
  36.  
  37.             adDst.Write(@”newTarget.exe”);
  38.  
  39.         }
  40.  
  41.         private static void AddEventHandlers(AssemblyDefinition adDestination,
  42.                                             TypeDefinition typeDestination,
  43.                                             string aEventName,
  44.                                             MethodDefinition aEventHandler)
  45.         {
  46.             // For the simpliciyty of code the event handler will be connected
  47.             // to the events in default constructors
  48.             // Just before leaving it
  49.  
  50.             // I know that there is just one constructor, but this is only an example!
  51.             var ctor = typeDestination.Methods.Where(m => m.IsConstructor).First();
  52.  
  53.             // Find last return code
  54.             // We will put our code just before that opcode
  55.             var lastRet = ctor.Body.Instructions.Reverse()
  56.                 .Where(i => i.OpCode == OpCodes.Ret).First();
  57.  
  58.             // Now we need an IL generator
  59.             var ilg = ctor.Body.GetILProcessor();
  60.  
  61.             // and now the magic
  62.             ilg.InsertBefore(lastRet, Instruction.Create(OpCodes.Ldarg_0));
  63.             ilg.InsertBefore(lastRet, Instruction.Create(OpCodes.Ldarg_0));
  64.             ilg.InsertBefore(lastRet, Instruction.Create(OpCodes.Ldftn, aEventHandler));
  65.  
  66.             // I did check here also that there is only one construcor
  67.             ilg.InsertBefore(
  68.                 lastRet,
  69.                 Instruction.Create(
  70.                     OpCodes.Newobj,
  71.                     adDestination.MainModule
  72.                     .Import(typeof(System.Windows.Forms.KeyEventHandler)
  73.                     .GetConstructors().First())));
  74.  
  75.             ilg.InsertBefore(
  76.                 lastRet,
  77.                 Instruction.Create(
  78.                     OpCodes.Callvirt,
  79.                     adDestination.MainModule
  80.                     .Import(typeof(System.Windows.Forms.Control)
  81.                     .GetEvent(aEventName).GetAddMethod())));
  82.         }
  83.  
  84.         private static MethodDefinition CopyMethod(AssemblyDefinition adSource,
  85.                                                     TypeDefinition typeSource,
  86.                                                     string mthdName,
  87.                                                     AssemblyDefinition adDestination,
  88.                                                     TypeDefinition typeDestination)
  89.         {
  90.             // source
  91.             MethodDefinition srcMethod = typeSource.Methods
  92.                 .Where(m => m.Name == mthdName).First();
  93.  
  94.             // now create a new place holder for copy
  95.             MethodDefinition target = new MethodDefinition(srcMethod.Name,
  96.                                                         srcMethod.Attributes,
  97.                                                         adDestination.MainModule
  98.                                                         .Import(srcMethod.ReturnType));
  99.  
  100.             // Copy all method parameters
  101.             // I could use var, but I did this on purpose to show the type used.
  102.             foreach (ParameterDefinition pd in srcMethod.Parameters)
  103.             {
  104.                 target.Parameters.Add(
  105.                     new ParameterDefinition(pd.Name, pd.Attributes, adDestination.MainModule
  106.                         .Import(pd.ParameterType)));
  107.             }
  108.  
  109.             // Now copy all local variables that are defined withing method body
  110.             // I could use var, but I did this on purpose to show the type used.
  111.             foreach (VariableDefinition vd in srcMethod.Body.Variables)
  112.             {
  113.                 target.Body.Variables
  114.                     .Add(new VariableDefinition(adDestination.MainModule
  115.                         .Import(vd.VariableType)));
  116.             }
  117.  
  118.             // copy the state
  119.             target.Body.InitLocals = srcMethod.Body.InitLocals;
  120.  
  121.             /* copy all instructions from SRC to DST */
  122.             foreach (Instruction instruction in srcMethod.Body.Instructions)
  123.             {
  124.                 // Case when method call another method defined withing SRC type/assembly
  125.                 MethodReference mr = instruction.Operand as MethodReference;
  126.                 // Case when method load field from type/assembly
  127.                 FieldReference fr = instruction.Operand as FieldReference;
  128.                 TypeReference tr = instruction.Operand as TypeReference;
  129.                 if (mr != null)
  130.                 {
  131.                     if (mr.DeclaringType == typeSource)
  132.                     {
  133.                         // That would mean that here we have a
  134.                         // method call to method within source type
  135.                         // And this need to be redirected to source type
  136.                         // or handled in some other way
  137.                         // But in this example is not used
  138.                         // If you want some examples please contace me
  139.                     }
  140.                     else
  141.                     {
  142.                         target.Body.Instructions.Add(
  143.                             Instruction.Create(instruction.OpCode,
  144.                             adDestination.MainModule.Import(mr)));
  145.                     }
  146.                 }
  147.                 else
  148.                 {
  149.                     if (fr != null)
  150.                     {
  151.                         // So we migth found our selfs in position that we need
  152.                         // to redirect this load to some other field or remove it.
  153.                         // For now lets redirect for different field
  154.                         // Please try to remove the code between TRY ME
  155.                         // and check what peverify.exe will tell
  156.                         /*TRY ME*/
  157.                         if (fr.Name == “sourceStatus”)
  158.                         {
  159.                             target.Body.Instructions.Add(
  160.                                 Instruction.Create(
  161.                                     instruction.OpCode,
  162.                                     adDestination.MainModule.Import(typeDestination.Fields
  163.                                     .Where(f => f.Name == “targetStatus”).First())));
  164.                         }
  165.                         else/*TRY ME*/
  166.                         {
  167.                             target.Body.Instructions.Add(Instruction
  168.                                 .Create(instruction.OpCode, adDestination.MainModule.Import(fr)));
  169.                         }
  170.  
  171.                     }
  172.                     else if (tr != null)
  173.                     {
  174.                         target.Body.Instructions.Add(Instruction
  175.                             .Create(instruction.OpCode, adDestination.MainModule.Import(tr)));
  176.                     }
  177.                     else
  178.                     {
  179.                         target.Body.Instructions.Add(instruction);
  180.                     }
  181.                 } // else
  182.             } // foreach
  183.  
  184.             typeDestination.Methods.Add(target);
  185.             return target;
  186.         }
  187.     }
  188. }

Źródło z którego chce wziąć kod (zależy mi na obsłudze klawiszy).

  1. using System.Windows.Forms;
  2.  
  3. namespace SourceForm
  4. {
  5.     public partial class SourceForm : Form
  6.     {
  7.         public SourceForm()
  8.         {
  9.             InitializeComponent();
  10.         }
  11.  
  12.         private void Form1_KeyDown(object sender, KeyEventArgs e)
  13.         {
  14.             if (e.KeyCode == Keys.Escape)
  15.             {
  16.                 this.sourceStatus.Text = string.Format(“I did forget to mention that {0} ends the game.”, e.KeyCode.ToString());
  17.             }
  18.             else
  19.             {
  20.                 this.sourceStatus.Text = string.Format(“Key {0} (down).”, e.KeyCode.ToString());
  21.             }
  22.  
  23.         }
  24.  
  25.         private void Form1_KeyUp(object sender, KeyEventArgs e)
  26.         {
  27.             if (e.KeyCode == Keys.Escape)
  28.             {
  29.                 this.Close();
  30.             }
  31.             else
  32.             {
  33.                 this.sourceStatus.Text = string.Format(“Key {0} (up).”, e.KeyCode.ToString());
  34.             }
  35.         }
  36.  
  37.         private void Form1_MouseDown(object sender, MouseEventArgs e)
  38.         {
  39.             this.sourceStatus.Text = string.Format(“Mouse: {0} down”, e.Button.ToString());
  40.         }
  41.  
  42.         private void Form1_MouseUp(object sender, MouseEventArgs e)
  43.         {
  44.             this.sourceStatus.Text = string.Format(“Mouse: {0} up”, e.Button.ToString());
  45.         }
  46.     }
  47. }

Tak wygląda implementacja, którą chce rozszerzyć:

  1. using System.Windows.Forms;
  2.  
  3. namespace TargetForm
  4. {
  5.     public partial class TargetForm : Form
  6.     {
  7.         public TargetForm()
  8.         {
  9.             InitializeComponent();
  10.         }
  11.  
  12.         private void TargetForm_MouseDown(object sender, MouseEventArgs e)
  13.         {
  14.             this.targetStatus.Text = string.Format(“down: {0}”, e.Button.ToString());
  15.         }
  16.  
  17.         private void TargetForm_MouseUp(object sender, MouseEventArgs e)
  18.         {
  19.             this.targetStatus.Text = string.Format(“up: {0}”, e.Button.ToString());
  20.         }
  21.     }
  22. }

Komentarze w kodzie 🙂 Mam nadzieję że są w miarę zrozumiałe, jeżeli będzie coś niejasnego, zawsze służę pomocą.
Co ciekawe, warto spojrzeć (w źródła), że pola sourceStatus oraz targetStatus nie są tego samego typu, ale oba posiadają te same pola (Text) i oba dziedziczą po Control, dzięki temu pięknie zadziałał polimorfizm.

Oczywiste oczywistości:

  • Kod który podłącza event handlery do eventów nie wymyśliłem sam, z pomocą przyszedł reflektor. Zobaczyłem (czytaj skopiowałem) kod do obsługi myszy i wstawiłem analogiczny do obsługi klawiatury
  • Za pierwszym razem też wydawało mi się to strasznie zakręcone i okrutnie trudne, ale po trzecim podejściu do problemu, wszystko nabiera sensu. W sumie nawet fajnie się przegląda IL 😉 żarcik taki.
  • Nie wszystko się od razu udaje, przykład z TRY ME, za pierwszym razem (w trzecim podejściu) zapomniałem o tym i coś nie zadziałało. Na szczęście narzędzie peverify.exe potrafi o tym przypomnieć. Także możliwość debugowania dużo ułatwia, gdy można podejrzeć dokładnie wartości zmiennych i w razie potrzeby dla testów zmieniać jest w trakcie działania programu.
  • Google – szukajcie rozwiązań, jest spora szansa, że ktoś już miał problem podobny do waszego i został on rozwiązany. Jeżeli nie, to być może naprowadzi was na rozwiązanie waszego problemu. Nie warto odpuszczać, bo na pewno jakoś się da 🙂

Pokazany przykład jest prostym rozwiązaniem, a kod który przenosi funkcjonalność z aplikacji do aplikacji nie jest najbardziej rozbudowany i przemyślany. W przypadku, gdy pojawiają się dodatkowe wywołania metod w źródłowym assembly, zaczyna komplikować wszystko, trzeba sprawdzać nazwy metod, przypisywać referencję na docelowe assembly, pilnować typów, etc. Trzeba się bardziej nagimnastykować. Tutaj akurat nie chciałem się na tym skupiać.
Jeżeli coś nie działa sprawdzajcie reflektorem czy innym programem do podglądania kodu, czy nie zapomnieliście za importować gdzieś typu, lub czy wywołania się zgadzają. Czy wszystkie zmienne zostały za deklarowane w ciele (body) metody. Jeszcze raz przypominam o narzędziu peverify, które wskazuje co i gdzie jest nie tak.

Powodzenia i niech moc będzie z wami.
Jarek

//EDIT
Klient nasz pannn. Poprawiłem główny kod, teraz powinno być łatwiej go czytać. Ale nie chciało mi się tego robić dla form. Tam zresztą nie ma wiele ciekawego do oglądania.

Tips’n’Tricks – peverify

Od pewnego czasu walczę z mono cecil z mniejszym i większym powodzeniem, w trakcie walk znajduje na forach różne przydatne podpowiedzi, postanowiłem je zebrać w ramach krótkich wpisów. O mono też się pojawi wpis, ale jak już zrobię to na czym mi zależy i w dodatku będzie działać 🙂
Teraz słów kilka o peverify.exe.

PEVERIFY.EXE

Gdy chcemy się pobawić trochę systemem refleksji w c# czy hakować zasoby, czy w jakikolwiek inny sposób ingerować w skompilowany kod aplikacji pisanej w .net, a następnie mieć pewność że wszystko jest w porządku i nie zakłóciliśmy jej wewnętrznej harmonii warto sprawdzić ją przy pomocy peverify.
Sposób użycia jest banalnie prosty, jako parametr aplikacji podaje się nazwę programu do sprawdzenia i czeka na wynik.
Tak wygląda wynik takiej operacji:

Zakolorowałem część nazwy aplikacji, nigdy nie wiesz czy ktoś się nie wkurzy jak mu wytkniesz jego błędy 😉

Pierwszy widoczny błąd (stack underflow) to akurat moja wina, dodałem do kodu wywołania swojej funkcji, ale zapominałem wcześniej załadować pewien argument i proszę wszystko widać, nawet podaje linijkę gdzie wystąpił błąd. Reszta to rzeczy odziedziczone, co ciekawe widać aplikacja nie musi być idealna by można było z niej korzystać.
Aby uruchomić peverify wystarczy odpalić Visual Studio Command Prompt i działa, oczywista oczywistość można dodać sobie odpowiednie ścieżki do PATH i nie bawić w linie poleceń do msvc.

To tyle, krótko i na temat.