Jirka Pénzeš Blog – Neznáma krajina, kde i sáňkovat do kopce jde zlehka
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.).
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 }
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ě.
Nyní jsem studentem Informačních technologií na Univerzitě Pardubice. Dlouhodobě se věnuji programování na platformě .NET, zejména ASP.NET a jazyk C#.NET. V současnosti je mým velkým zájmem technologie LINQ a WPF, objektově orientované programování a počítačová grafika. Dále se věnuji i jazyku JAVA a architektuře aplikací.
Augi
Červenec 24th, 2011 at 14.13
Nejvíc se mi líbí implementace pomocí AOP – pogoogluj třeba „PostSharp INotifyPropertyChanged“.
Jirka Pénzeš
Červenec 24th, 2011 at 15.35
Řešení pomocí AOP mě vůbec nenapadlo, díky za typ. Koukám, implementace od PostSharpu vypadá daleko lépe než nepěkné Lambda expression
toronto seo affordable
Únor 18th, 2012 at 17.28
What i do not understood is in fact how you are not actually a lot more neatly-appreciated than you might be now. You are so intelligent. You understand thus considerably in relation to this subject, made me individually imagine it from so many varied angles. Its like men and women are not fascinated except it’s one thing to do with Lady gaga! Your own stuffs outstanding. All the time take care of it up!