2015-12-19 14:51:54

Nadmiarowo blokujące się przyciski z MVVM Light

MVVM Light jest bilioteką ułatwiającą tworzenie aplikacji wykorzystujących wzorzec MVVM. Jak do tej pory cieszy się moją sympatią w stopniu znacznie wyższym niż inne frameworki. Głownie dlatego, że nie robi rzeczy o które go nie proszę. Ostatnio jednak znalazłem ciekawe zachowanie tej biblioteki, które może powodować pojawianie się niedeterministycznych bugów.
MVMM Light posiada klasę RelayCommand pozwalającą szybko stworzyć pole typu ICommand (opis mechanizmu można znaleźć w wielu miejscach w internetach). Klasa ta posiada 2 parametry konstruktora: Execute typu Action - referencja do metody/lambdy wywoływanej po kliknięciu w przycisk, canExecute typu Func<bool> odwołanie do metody/lambdy wywoływane w celu określenia czy przycisk ma być włączony. Dodatkowo klasa posiada metodę RaiseCanExecuteChanged służącą do informowania przycisku o konieczności ponownego wywołania metody CanExecute.

Problem, który odkryłem, dotyczy drugiego parametru konstruktora. I aby go lepiej naświetlić przedstawię przypadek użycia, w którym on wystąpił.
Aplikacja posiada ustawienia zapisywane na dysku, które mogą być zmieniane w dedykowanym module. Moduł posiada przyciski "Zapisz" i "Anuluj" - pozwalające opuścić moduł. Operacja zapisz zamyka moduł tylko gdy ustawienia przechodzą walidację. Aplikacja na starcie wykrywa czy zapisane ustawienia spełniają te reguły (nie spełniają przy świeżej instalacji oraz jak ktoś usunie plik z dysku), jeżeli nie otwiera moduł ustawień, w tym konkretnym przypadku przycisk anuluj ma być trwale wyłączony. I ten przypadek działał bez zarzutów. Czasami (powtarzalność około 30%) przycisk "Anuluj" był wyłączony przy "ręcznym" odpaleniu modułu. Błąd dał się odtworzyć na mojej maszynie. Jednak gdy przyjrzałem mu się w trybie Debug zobaczyłem że komenda po wywołaniu CanExecute zawsze zwraca true.

Podgląd w Visual Studio

Jednak przycisk mimo to bywał wyłączony. Błąd wynikał z połączenia kilku czynników. Po pierwsze flagę określającą czy przycisk ma być włączony wyliczałem w konstruktorze i wrzucałem do lambdy w komendzie, nigdzie jej nie przechowując bo nie była mi nigdzie indziej potrzebna. Po drugie referencja do metody przekazanej jako parametr CanExecute w RelayCommand jest przetrzymywana z wykorzystaniem WeakReference i jeżeli znikną wszystkie inne referencje do niej zostanie sprzątnięta przez Garbage Collector. W tym przypadku po wywołaniu metody CanExecute na obiekcie RelayCommand zostanie zwrócona wartość false (asekuracyjnie). Błąd występował jeżeli GC zdążył sprzątnąć moją lambdę zanim została użyta. Jeżeli nie zdążył to moduł działał poprawnie.
Jak rozwiązać ten problem? Po pierwsze można stworzyć własną implementację ICommand nie korzystającą z WeakReference. To rozwiązanie będzie niewątpliwie będzie działać. Problem w tym, że może powodować inne bugi, głównie wycieki pamięci w pewnych skrajnych przypadkach (np. gdy jako parametr przekażemy wyrażenie Lambda wykorzystujące pole statyczne). Po za tym nie po to dodaliśmy MVVM Light żeby rezygnować z jednej z najfajniejszych zabawek. Drugim rozwiązaniem jest powiązanie parametru canExecute z ViewModelem, co sprawi że GC nie posprząta jej tak długo jak ViewModel będzie żył. Można to zrobić przynajmniej na 2 sposoby: w wyrażeniu lambda przekazywanym do RelayCommand użyć przynajmniej 1 pola z klasy (może być to pole ReadOnly) albo jako parametr przekazać niestatyczną metodę z klasy.
Na koniec podaję linka do aplikacji obrazującej błąd: https://github.com/szogun1987/MvvmLightBugExample

Edit 27.01.2017:

Do MVVM Light zostało dodane obejście pozwalające naprawić ten błąd. Do konstruktora RelayCommand został dodany parametr "keepTargetAlive". Jeżeli zostanie tam przekazane true błąd przestanie występować.

Tagi

C Sharp MVVMLight

Komentarze: