Fody ciekawa ptaszyna – część 1

Może słyszeliście o takim ptaszku Fody. Ostatnim czasem ptaszek ten podbił moje serce. Korzystam z jego usług tak często jak się tylko da. Ogólnie muszę podziękować Pawłowi Łukasikowi za prezentacje na KGD.NET, która otworzyła mi oczy 😉

PropertyChanged.Fody

PropertyChangedFodyLogo
To chyba mój ulubiony pakiet, ale zanim go opiszę muszę zrobić drobne wprowadzenie.
Od początku mojej przygody z WPFem ( i ogólnie XAMLem) strasznie żmudnym procesem było bindowanie pomiędzy DataModelem a widokiem (niech zasłona milczenia zakryje fakt, że w początkowym etapie nie wykorzystywałem MVVM). Ponieważ nie chcę być gołosłowny przygotowałem prostą aplikację (kod dostępny na githubie).

Klasyczne podejście

Kod dostepny pod tagiem ClassicSolution.
Każdy kto pisał kiedyś aplikację w WPF pamięta zapewne kod przypominający ten:

class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string _firstName;
    public string FirstName
    {
        get { return _firstName; }
        set
        {
            if (value != _firstName)
            {
                _firstName = value;
                OnPropertyChanged("FirstName");
                OnPropertyChanged("FullName");
            }
        }
    }

    private string _lastName;
    public string LastName
    {
        get { return _lastName; }
        set
        {
            if (value != _lastName)
            {
                _lastName = value;
                OnPropertyChanged("LastName");
                OnPropertyChanged("FullName");
            }
        }
    }

    public string FullName
    {
        get { return string.Format("{0} {1}", FirstName, LastName); }
    }

    private string _adress;
    public string Adress
    {
        get { return _adress; }
        set
        {
            if (value != _adress)
            {
                _adress = value;
                OnPropertyChanged("Adress");
            }
        }
    }

    public virtual void OnPropertyChanged(string propertyName)
    {
        var propertyChanged = PropertyChanged;
        if (propertyChanged != null)
        {
            propertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Rączki w górę kto tak kiedyś pisał. No dobrze ale czy ktoś tak jeszcze piszę? Jeśli tak to niech zastanowi się nad wadami takiego podejścia bo moim zdaniem są dwie poważne wady: Magic string oraz mnóstwo kodu zaśmiecającego klasę (dodatkowo powtarzanego w innych klasach).
Nigdy nie mieliście problemów z Magic String? To moje gratulację. Mi niestety takie problemy zdarzyły się stanowczo za często więc jestem szczególnie uczulony na taki kod.
W celu wyeliminowania z kodu magic stringów możemy zastosować kilka metod.

CallerMemberName

Kod dostepny pod tagiem CallerMemberNameSolution.

class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string _firstName;
    public string FirstName
    {
        get { return _firstName; }
        set
        {
            if (value != _firstName)
            {
                _firstName = value;
                OnPropertyChanged();
                OnPropertyChanged("FullName");
            }
        }
    }

    private string _lastName;
    public string LastName
    {
        get { return _lastName; }
        set
        {
            if (value != _lastName)
            {
                _lastName = value;
                OnPropertyChanged();
                OnPropertyChanged("FullName");
            }
        }
    }

    public string FullName
    {
        get { return string.Format("{0} {1}", FirstName, LastName); }
    }

    private string _adress;
    public string Adress
    {
        get { return _adress; }
        set
        {
            if (value != _adress)
            {
                _adress = value;
                OnPropertyChanged();
            }
        }
    }

    public virtual void OnPropertyChanged([CallerMemberName]string propertyName = "")
    {
        var propertyChanged = PropertyChanged;
        if (propertyChanged != null)
        {
            propertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Niby jest lepiej. Obsługę OnPropertyChanged dla prostych właściwości mamy bez użycia stringów. Jednak i tak musimy wywoływać OnPropertyChanged ze stringiem jako parametr jeśli chcemy bindować do właściwości FullName. Kolejnym minusem jest to, że CallerMemberName występuje od .net 4.5 a nie zawsze możemy oczekiwać, że tworzymy kod dla najnowszej wersji.
(atrybut CallerMemberName wykorzystywany jest wyłącznie przez kompilator, podglądając kod w ILSpy i tak zobaczy się stringa z nazwą właściwości)

Lamda Expression

Kod dostepny pod tagiem LambdaSolution.

class Person : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string _firstName;
    public string FirstName
    {
        get { return _firstName; }
        set
        {
            if (value != _firstName)
            {
                _firstName = value;
                OnPropertyChanged(() => FirstName);
                OnPropertyChanged(() => FullName);
            }
        }
    }

    private string _lastName;
    public string LastName
    {
        get { return _lastName; }
        set
        {
            if (value != _lastName)
            {
                _lastName = value;
                OnPropertyChanged(() => LastName);
                OnPropertyChanged(() => FullName);
            }
        }
    }

    public string FullName
    {
        get { return string.Format("{0} {1}", FirstName, LastName); }

    }

    private string _adress;
    public string Adress
    {
        get { return _adress; }
        set
        {
            if (value != _adress)
            {
                _adress = value;
                OnPropertyChanged(() => Adress);
            }
        }
    }

    protected virtual void OnPropertyChanged(Expression<Func<object>> expression)
    {
        this.PropertyChanged.Raise(this, expression);
    }
}

Oczywiście aby powyższy kod zadziałał musimy rozszerzyć delegata PropertyChangedEventHandler o metodę Raise

static class PropertyChangedExtension
{
    public static void Raise(this PropertyChangedEventHandler handler, object sender, Expression<Func<object>> expression)
    {
        if (handler != null)
        {
            if (expression.NodeType != ExpressionType.Lambda)
            {
                throw new ArgumentException();
            }
            var body = expression.Body as MemberExpression;
            if (body == null)
            {
                throw new ArgumentException();
            }
            string propertyName = body.Member.Name;
            handler(sender, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Osiągnęliśmy pierwszy cel. Nie ma Magic stringów. Kod jest lepiej przystosowany do późniejszych zmian i działa nawet z .net 3.5. Tylko że kod jest dalej brzydki. I dodatkowo wolniejszy, ale bądźmy szczerzy, że to nie jest kluczowa kwestia.
(w przeciwieństwie do poprzedniego rozwiązania tym razem ILSpy jako parametry pokazywać będzie lambdy zamiast stringów)

PropertyChanged.Fody

Kod dostepny pod tagiem FodySolution.
Dochodzimy wreszcie do bohatera tego postu.
Zaczynamy od ściągnięcia nugetem właściwego pakietu

Install-Package PropertyChanged.Fody

Ściąga się nam niewielki pakiet a do naszego solution dodany zostaje plik FodyWeavers.xml (do references nic nie zostaje dodane). Wybieramy nazwę dla eventu, który będzie wywoływany np. tak jak proponuje Basia Fusińska raisePropertyChanged.

<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
  <PropertyChanged EventInvokerNames="raisePropertyChanged"/>
</Weavers>

Po tej drobnej zmianie kod klasy Person wygląda tak:

[ImplementPropertyChanged]
class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string FullName
    {
        get { return string.Format("{0} {1}", FirstName, LastName); }
    }
    public string Adress { get; set; }
}

Jak to w takim wypadku działa? Otóż PropertyChanged.Fody zrobił code weaving. Czyli w czasie kompilacji dołożył on ten brzydki kod z klasycznego rozwiązania (po prostu odwalił za nas brudną robotę). To oczywiście bardzo prosty przykład, ale stosując PropertyChanged.Fody oszczędziłem wiele swojego cennego czasu. Chętnym zgłębienia niuansów tego pakietu polecam lekturę dokumentacji z githuba gdzie można poczytać m.in. o dodatkowych atrybutach np. [DoNotNotify].

6 thoughts on “Fody ciekawa ptaszyna – część 1

  1. Pingback: dotnetomaniak.pl
    1. Nie dość, że zaciekawiłeś to jeszcze pomogłeś mi oszczędzić mnóstwo czasu

  2. … a to zainteresowanie rozprzestrzenia się po znajomych Wojtka i im też oszczędza sporo pracy:)

    Thx Wojtku i Pawle

    Pozdrawiam
    Jarek

  3. Ciekawy artykul.

    Jednak podobne rozwiazanie uzyskamy w duzo prostszy sposob z uzyciem PostSharp:
    http://www.postsharp.net/model/inotifypropertychanged

    Aspect-oriented Programming w Postsharp posiada jeszcze wiele innych przydatnych atrybutow,
    jak OnMethodBoundaryAspect, ktory pomoze np. latwiej zaimplementowac logowanie czasu wykonania danej metody.

    1. Zgadzam się z tobą PostSharp też umożliwia wygodną implementację INotifyPropertyChanged (i znacznie więcej ale AOP to temat na odrębny post). Używałem go zresztą w powodzeniu w kilku projektach, ale moim zdaniem rozwiązanie z Fody jest prostsze (ale to już kwestia gustu). Dodatkowo kod generowany przez Fody jest czytelniejszy. Podglądając kod w ILSpy mamy (dla klasy Person) 82 linie kodu przy użyciu Fody i 470 lini kodu przy użyciu PostSharpa. And last but not least PostSharp dodaje referencje do siebie a Fody nie.

Comments are closed.