Pleciemy INotifyPropertyChanged

Implementacja INotifyPropertyChanged w aplikacjach wykorzystujących MVVM potrafi przysporzyć o niemały ból głowy. Redundantny kod, monotonia zapisu, rozwlekłe klasy, a wszystko to opatrzone niezliczoną ilością niepotrzebnego kodu. Wraz z ewolucją języka i rozwojem bibliotek starano się optymalizować użycie tego mechanizmu poprzez coraz to nowocześniejsze rozwiązania. Lambdy, refleksje, wykorzystanie atrybutu [CallerMemberName] z C# 5.0 – wszystko to jednak ciągle zmuszało nas do powtarzania określonej sekwencji. Czy rozwiązaniem w takim przypadku może okazać się paradygmat programowania aspektowego i tkanie kodu (ang. code weaving)?

Code weaving

Rozwiązanie to nie jest niczym nowym i istniało od wielu lat. Ze względu na brak powszechnie dostępnych bibliotek nie zyskało jednak zbyt dużej popularności. Na czym polega tytułowe plecienie kodu? Należy tutaj przyjrzeć się bliżej procesowi kompilacji. Kompilator, podczas przetwarzania naszego kodu napisanego w języku C#, tworzy plik binarny zawierający kod języka pośredniego zwanego IL (Intermediate Language). Następnie, podczas uruchamiania programu środowisko uruchomieniowe języka .Net kompiluje kod IL do kodu maszynowego. Wykorzystując tytułowe „plecienie kodu” możemy po kompilacji przetworzyć otrzymany kod IL i wstrzyknąć do pliku binarnego dodatkowe instrukcje.

indeks

Kod IL

Kod IL to pewnego rodzaju rozbudowany składniowo asembler. Za pomocą programu ILDasm.exe dostarczonego razem ze środowiskiem możemy podejrzeć w jaki sposób nasz kod napisany w języku C# przetworzony zostaje do języka pośredniego. Aby lepiej to zilustrować stwórzmy najprostszy możliwy program:

Następnie skompilujmy go za pomocą polecenia:

Kompilacja spowodowała powstanie pliku binarnego zawierającego nasz kod pośredni, wykorzystując do tego format PE (struktura Portable Executable jest ciekawa sama w sobie i warto się z nią zapoznać).

Kod IL wygenerowanego pliku możemy podejrzeć za pomocą polecenia przedstawionego poniżej (polecam również spróbować użyć przełącznika /all, dzięki czemu uzyskamy czytelną wersje struktury pliku PE):

W pliku możemy znaleźć fragment kodu podobny do tego poniżej.

Najbardziej interesująca jest dla nas metoda Main:

  • metodę rozpoczyna .entrypoint, który definiuje która metoda z modułu jest metodą wejściową,
  • dyrektywa .maxstack definiuje maksymalną liczbę zmiennych, które mogą być odłożone na stos – domyślną wartością jest wartość 8,
  • następnie instrukcja ldstr odkłada wartość Hello, world! na stos,
  • instrukcja call wywołuje metodę WriteLine obiektu System.Console, a ta wykorzystując wcześniej odłożoną na stos wartość wyświetla ją użytkownikowi,
  • na koniec instrukcja ret powoduje powrót z funkcji.

W opisie pominięte zostały instrukcję nop (no operation), które nie robią nic. Są one umieszczane w kodzie, aby np. umożliwić umiejscowienie breakpointa podczas debugowania kodu. Użycie przełącznika /optimize podczas kompilacji spowoduje, że nie zostaną one dodane do naszego kodu. Pełną listę kodów operacji (ang. opcode) możecie znaleźć tutaj.

Właściwości w IL

Od momentu powstania języka C# jego nierozłącznym towarzyszem są właściwości. Zwalniają one programistę z obowiązku ręcznego tworzenia akcesorów poprzez zastosowanie słów kluczowych get set. W wersji 3.0 języka C# usprawniono ich działanie dodając do języka możliwość tworzenia właściowści automatycznych. C# 6.0 umożliwia dodatkowo dodanie domyślnej ich wartości:

Przyjrzyjmy się jaki kod zostanie wygenerowany przez kompilator:

Jak widzimy dla potrzeb właściwości stworzone zostało specjalne pole przechowujące jej wartość oraz dwie metody: get_Demo() oraz set_Demo(), w których za pomocą instrukcji ldsfld oraz stsfld odczytywane lub zapisywane są wartości specjalnej zmiennej.

W samym konstruktorze statycznym klasy(cctor) następuje odłożenie na stos wartości 4 (ldc.i4.4) i ustawienie wartości pola.

Następnie chcąc uzyskać wartość pola, kompilator użyje metody get_Demo do jej pobrania. Pytanie dodatkowe. Czy poniższy kod się skompiluje?

Mono.Cecil

Najprostsza implementacje INotifyPropertyChanged wygląda następująco:

Jednak chcielibyśmy pozbyć się nadmiarowego kodu, zachowując to samo zachowanie i skrócić zapis do:

Możemy to osiągnąć za pomocą Mono.Cecil. Wspomniana biblioteka pozwala na analizę i modyfikację programów wykorzystujących IL. Wykorzystując ją, wstrzykniemy brakujące instrukcje IL do naszych właściwości. Jedyne co musimy zrobić to wywołać metodę OnPropertyChanged w wygenerowanym kodzie IL. Poniżej znajduje się przykładowy kod generujący wspomniane zawołanie:

  • Na początku za pomocą funkcji ReadModule wczytujemy nasz moduł,
  • Następnie w naszym module wyszukujemy i zapisujemy referencję do metody OnPropertyChanged, którą będziemy wstrzykiwać do właściwości,
  • Dalej wyszykujemy wszystkie modele widoku które powinny implementować INotifyPropertyChanged,
  • Iterując po kolejnych właściwościach pobieramy aktualny kod metody set,
  • Pierwsza dodawana przez nas instrukcja to ldarg.0, która spowoduje odłożenie na stos obiektu this,
  • W drugiej kolejności za pomocą instrukcji ldstr odkładamy na stos nazwę właściwości (potrzebna metodzie OnPropertyChanged),
  • Na koniec dopisujemy instrukcję call z referencją do naszej metody OnPropertyChanged,
  • Tworzymy nowy plik PE.

W efekcie początkowy kod właściwości:

Zostaje zamieniony na:

W ten sposób stworzyliśmy automatycznie notyfikujące o zmianach właściwości.

PropertyChanged.Fody

Niestety. W powyższym przykładzie rozważyliśmy tylko najprostszy z możliwych przypadków.

  • Co, jeżeli właściwości są zależne od siebie i powinny notyfikować o zmianach innych?
  • Co, jeżeli nie chcemy, aby jakaś właściwość notyfikowała o zmianie?
  • W jaki sposób wstrzyknąć kod IL w procesie budowania?

Jak widać problem nie jest trywialny. Na szczęście nie musimy zajmować się jego rozwiązaniem, gdyż istnieje już gotowa biblioteka, która zrobi to za nas. PropertyChanged.Fody, bo o niej mowa pozwala pozbyć się redundantnego kodu implementacji INotifyPropertyChanged i wstrzykując kod IL przy użyciu Mono.Cecil, robiąc to automatycznie. Jest ona częścią większego pakietu zwanego Fody (którego nazwa pochodzi od nazwy ptaka z rodziny wikłaczowatych, słynącego z wikłania gniazd). W następnym wpisie na blogu opiszę możliwości jakie oferuje Fody.

Podsumowanie

W powyższym wpisie przedstawiona została idea działania biblioteki PropertyChanged.Fody Zachęcam również do zapoznania się możliwościami Mono.Cecil i poeksperymentowania z językiem IL jak i również narzędziami dostarczonymi wraz ze środowiskiem.

Facebooktwitter

3 komentarze

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *