Na následujících několika řádcích se vás pokusím seznámit s velice užitečným a mocným rozhraním. Řeč bude o rozhraní INotifyPropertyChanged, které je obsažené ve jmenném prostoru Systém.ComponentModel. Konkrétně se podíváme na různé způsoby implementace v jazyce C#.

Nejdříve si ale o rozhraní něco řekněme. Dalo by se říci, že se v podstatě jedná o možnou implementaci návrhového vzoru Observer. Pakliže jste se již s implementací tohoto vzoru setkali – tato bude lehce odlišná. INotifyPropertyChanged je postaveno na vyvolání události. Oproti klasické implementaci vzoru, kde je nutné dědit ze třídy Observable, zde tato nutnost odpadá.

V podstatě toto rozhraní slouží k tomu, aby jeden objekt mohl sledovat druhý. Klienti jsou pak informováni o každé změně ve sledovaném objektu. Informaci o změně stavu dostanou formou události PropertyChanged. Klienti mohou na nová data okamžitě reagovat. Jedná se tedy o jakési vázání (svazování) dat.

Rozhraní INotifyPropertyChanged je jádrem DataBindingu. Slouží k synchronizaci vázaného objektu s UI komponentou (umožňuje automatické aktualizace komponent, apod.).

Implementace

Jako modelující příklad si zkusíme vytvořit třídu Student a následně se pokusit implementovat notifikační rozhraní INotifyPropertyChanged. Nejprve si připravme samotnou třídu, bez rozhraní.

class Student
{
    private string name = String.Empty;
    private string email = String.Empty;
    private int age;

    #region Properties

    public string Name
    {
        get { return this.name; }
        set { this.name = value; }
    }

    public string Email
    {
        get { return this.email; }
        set { this.email = value; }
    }

    public int Age
    {
        get { return this.age; }
        set { this.age = value; }
    }

    #endregion
}

Třída obsahuje pouze tři vlastnosti (jméno, email a věk).

Nyní implementujme samotné rozhraní. Naše třída dostane veřejnou událost (event) PropertyChanged. Tento event je potřeba při každé změně vlastnosti vyvolat. Abychom nemuseli psát dokolečka pořád stejný kód pro handler, připravíme si pro tento úkol novou metodu – OnPropertyChanged(), které budeme přidávat řetězcem název vlastnosti.

class Student : INotifyPropertyChanged
{
    private string name = String.Empty;
    private string email = String.Empty;
    private int age;

    public string Name
    {
        get { return this.name; }
        set
        {
            this.name = value;
            OnPropertyChanged("Name");
        }
    }

    public string Email
    {
        get { return this.email; }
        set
        {
            this.email = value;
            OnPropertyChanged("Email");
        }
    }

    public int Age
    {
        get { return this.age; }
        set
        {
            this.age = value;
            OnPropertyChanged("Age");
        }
    }

    #region INotifyPropertyChanged Members

    /// <summary>
    /// Veřejná událost
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged(string propertyName)
    {
        // zjistíme, zda je někdo k události přihlášen
        // musí existovat nějaký delegát, který bude event zpracovávat
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    #endregion
}

Refactoring kódu

Nyní by již vše mělo fungovat, tak jak má. Jedná se o nejběžnější implementaci tohoto rozhraní. K tomuto kódu bychom ale mohli mít ještě několik výhrad. Například použití Stringu pro předávání názvu vlastnosti. Použití řetězců (jde-li to i jinak) není obecně vhodné řešení, neboť je to velmi náchylné k chybám – typicky překlepy v řetězci nebo opomenutí změny řetězce při rename refactoringu vlastnosti.

Jedním řešením by mohlo být například nastavením konstantních řetězců pro názvy vlastností, ale to není zrovna hezké a ideální řešení. Nabízejí se nám ještě další dva způsoby jak tento problém vyřešit.

1. Do těla metody přidáme kontrolu existence vlastnosti, před vyvoláním události.

#region INotifyPropertyChanged Members

public event PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged(string propertyName)
{
    if (GetType().IsVisible)
    {
        if (!string.IsNullOrEmpty(propertyName) && GetType().GetProperty(propertyName) == null)
            throw new ArgumentException("Error", "propertyName");
    }

    if (PropertyChanged != null)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

#endregion

Tento způsob není o moc lepší než původní. Hlavním problémem mechanismu je, že se celý proces kontroly vlastnosti provádí až v samotném běhu aplikace.

2. Druhou možností je využití výrazu Linq, který bude vstupem metody. Toto řešení má oproti předchozím zásadní výhodu a to fakt, že nás na chyby upozorňuje již kompilátor. Řešení považuji za velmi elegantní a bezpečné. Metodě nyní předáváme LambdaExpression, je potřeba tedy změnit i kód volání metody u jednotlivých vlastností.

class Student : INotifyPropertyChanged
{
    private string name = String.Empty;
    private string email = String.Empty;
    private int age;

    public string Name
    {
        get { return this.name; }
        set
        {
            this.name = value;
            OnPropertyChanged(() => Name);
        }
    }

    public string Email
    {
        get { return this.email; }
        set
        {
            this.email = value;
            OnPropertyChanged(() => Email);
        }
    }

    public int Age
    {
        get { return this.age; }
        set
        {
            this.age = value;
            OnPropertyChanged(() => Age);
        }
    }

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    protected void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
    {
        if (propertyExpression.Body.NodeType == ExpressionType.MemberAccess)
        {
            var memberExpression = propertyExpression.Body as MemberExpression;
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(memberExpression.Member.Name));
        }
    }
    #endregion
}

Hlavní nedostatek tohoto návrhu vidím ve velkém množství reflexe, která se děje na pozadí. Může pak tento způsob být v některých případech mnohem pomalejší. Nicméně ve většině případů by to mělo být zanedbatelné.

Pakliže bychom chtěli pokračovat v refactoringu kódu dále, kde může ještě vzniknout problém? Jediná věc, která mě nyní napadá je, že zapomeneme událost vůbec vyvolat. Proto se doporučuje odsouvat nastavování vlastností do speciální metody, která vlastnost změní a zároveň vyvolá událost. Nemůže se pak stát, že bychom přidali novou vlastnost a zapomněli přidat řádek s OnPropertyChange().

Nové metodě předáme veškeré informace, které je potřeba znát – název vlastnosti, referenci na vlastnost a novou hodnotu. V metodě si navíc ověříme, zda vůbec dojde ke změně hodnoty. Událost vyvoláme jen v případě změny.

#region INotifyPropertyChanged Members

public event PropertyChangedEventHandler PropertyChanged;

protected void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
{
    if (propertyExpression.Body.NodeType == ExpressionType.MemberAccess)
    {
        var memberExpression = propertyExpression.Body as MemberExpression;
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(memberExpression.Member.Name));
    }
}

protected void SetProperty<T>(ref T field, T value, Expression<Func<T>> propertyExpression)
{
    if (!EqualityComparer<T>.Default.Equals(field, value))
    {
        field = value;
        OnPropertyChanged(propertyExpression);
    }
}

#endregion

Volání metody pak vypadá následovně:

public int Age
{
    get { return this.age; }
    set
    {
        SetProperty(ref age, value, () => Age);
    }
}

K této metodě lze jen dodat, že by mohla být i statická a použita ve všech třídách, které máte v projektu. Bylo by ale nutné přidat další parametr, kterým by byl ještě vlastník dané vlastnosti.

Možnosti implementace použití rozhraní INotifyPropertyChanged je velké množství a je jen na vás, jakou cestou se vydá vaše aplikace. Výše zmíněné jsou ty nejvíce používané. Možná vás už v tento okamžik napadají nápady, jak kód výše vylepšit či jak rozhraní implementovat zcela odlišně.