Posted on 1. July 2010

Варианты реализации INotifyPropertyChanged

Инфраструктура Silverlight имеет мощную модель связывания данных. Одним из главных элементов данной возможности является интерфейс INotifyPropertyChanged. Он используется для уведомления клиента(это может быть интерфейс приложения или внутренние объекты приложения) об изменении свойств определенного объекта. В этом посте я хочу описать несколько возможных вариантов реализации INotifyPropertyChanged, которые появились в процессе его использовния.

Базовая реализация

При самом простом подходе реализации этого интерфейса нам необходимо объявить событие PropertyChanged. А также метод для запуска события. Пример реализации:

public class Customer : INotifyPropertyChanged
{
	public event PropertyChangedEventHandler PropertyChanged;
 
	protected void RaisePropertyChanged(string propertyName)
        {
            PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler != null && !String.IsNullOrWhiteSpace(propertyName))
            {
                 handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
}

Главная проблема, которая возникает при реализации INotifyPropertyChanged заключается в том, что бы значения параметра propertyName в методе RaisePropertyChanged сдалеть "дружественным" к рефакторингу.

Как правило, реализация свойства, которое извещается о своем изменении имеет следующий вид:

private string _firstName;
 
public string FirstName
{
	get
	{
		return _firstName;
	}
	set
	{
		if (!object.Equals(_firstName, value))
		{
			_firstName = value; 
			RaisePropertyChanged("FirstName");
		}
	}
}

 Иногда, код внутри "сеттера" заменяют на использование обобщенного метода:

protected bool SetValue(ref T property, T value, string propertyName)
{
	bool result = false;
	if (!Object.Equals(property, value))
	{
		property = value;
		OnPropertyChanged(propertyName);
		result = true;
	}

	return result;
}

Кроме этого необходимо изменить наше свойство

public string FirstName
{
	get
	{
		return _firstName;
	}
	set
	{
		SetValue(ref _firstName, value, "FirstName");
	}
}

Данное решение не является дружественным к рефакторингу: когда вы меняете название свойства вам необходимо вручную изменять название свойства которое находится внутри строки. Если вы этого не сделаете, то сборка проекта пройдет успешно, но в последствии могут появится ошибки, которые будет трудно исправлять. Для решения этой проблемы Josh Smith предложил следующее решение:

[Conditional("DEBUG")]
private void VerifyProperty(string propertyName)
{
	Type type = this.GetType();
 
	// Look for a public property with the specified name.
	PropertyInfo propInfo = type.GetProperty(propertyName);
 
	Debug.Assert(propInfo != null, string.Format(CultureInfo.InvariantCulture, "{0} is not a property of {1}", propertyName, type.FullName));
}

Следовательно нам необходи теперь изменить метод запуска события:

protected void RaisePropertyChanged(string propertyName)
{
	VerifyProperty(propertyName);
 
	PropertyChangedEventHandler handler = this.PropertyChanged;
            if (handler != null && !String.IsNullOrWhiteSpace(propertyName))
            {
                 handler(this, new PropertyChangedEventArgs(propertyName));
            }

}

Данный подход не выявит ошибок при компиляции проекта, если в параметре придет название свойства, которое отсутствует у объекта, но будет создана ошибка в режиме отладки, когда метод RaisePropertyChanged вызовется с несуществующим свойством. Тем не менее данный подход не покажет ошибки связанной с неправильным использованим параметра. Например:

private string _firstName;
private string _lastName;
 
public string FirstName
{
	get { return _firstName; }
	set { SetValue(ref _firstName, value, "FirstName"); }
}
 
public string LastName
{
	get { return _lastName; }
	set { SetValue(ref _lastName, value, "FirstName"); }
}

При использовании copy/past вероятность появления такого кода очень высока.

Использование Lambda Expressions и Expression Trees

Этот вариант заключается в использовании Lambda Expressions и Expression Trees для определения названия свойства. Ключевой точкой в этом подходе является следующий метод:

public static string GetPropertyNameFromExpression(Expression> property)
{
	var lambda = (LambdaExpression)property;
	MemberExpression memberExpression;
 
	if (lambda.Body is UnaryExpression)
	{
		var unaryExpression = (UnaryExpression)lambda.Body;
                memberExpression = (MemberExpression)unaryExpression.Operand;
	}
	else
	{
		memberExpression = (MemberExpression)lambda.Body;
	}
 
	return memberExpression.Member.Name;
}

с последующим добавлением обобщенного метода RaisePropertyChanged:

protected void RaisePropertyChanged(Expression> property)
{
RaisePropertyChanged(GetPropertyNameFromExpression(property));
}

Теперь свойство будет иметь вид:

private string _firstName;
 
public string FirstName
{
	get
	{
		return _firstName;
	}
	set
	{
		if (!Object.Equals(_firstName, value))
		{
			_firstName = value;
 
			RaisePropertyChanged(() => FirstName);
		}

	}
}

или же метод SetValue будет иметь следующий код: 

protected bool SetValue(ref T property, T value, Expression> propertyDelegate)
{
	var result = false;
	if (!Object.Equals(property, value))
	{
		property = value;
		RaisePropertyChanged(propertyDelegate);
		result = true
	}
	return result;
}

 мы можем изменить наше свойство следующим образом:

public string FirstName
{
	get
	{
		return _firstName;
	}
	set
	{
		SetValue(ref _firstName, value, () => FirstName);
	}
}

Положительная часть представленного решения заключается в дружественности к рефакторингу: если когда-нибудь имя свойства измениться используя интрумент рефакторинга, делегат также будет изменен и корректное имя будет использоваться в событии PropertyChanged. Еще одним преимуществом является то, что при опечатках мы получаем ошибки компиляции. Отрицательная сторона заключается в использовании отражения. Еще один негативный момент - при использовании copy/paste можно допустить ошибку:

private string _firstName;
private string _lastName;
 
public string FirstName
{
	get { return _firstName; }
	set { SetValue(ref _firstName, value, () => FirstName); }
}
 
public string LastName
{
	get { return_lastName; }
	set { SetValue(ref _lastName, value, () => FirstName); }
}

Использование MethodBase.GetCurrentMethod()

Karl Shifflett предложил иной подход в своей статье. Он предложил брать имя свойства используя MethodBase.GetCurrentMethod(). Таким образом реализация свойства приобретает следующий вид:

public string FirstName
{
	get
	{
		return _firstName;
	}
	set
	{
		SetValue(ref _firstName, value, MethodBase.GetCurrentMethod().Name.Remove(0,4));
	}
}

Это решение работает немного лучше, но все равно в каждом свойстве необходимо добавлять строку: MethodBase.GetCurrentMethod().Name.Remove(0,4)

А какие варианты передачи имени свойства используете вы?

 

Материалы по теме:



Comments

trackback Сергей Лутай
1:20 AM on Thursday, July 1, 2010

Варианты реализации INotifyPropertyChanged

Инфраструктура Silverlight имеет мощную модель связывания данных. Одним из главных элементов данной возможности

Ivan Korneliuk Ivan Korneliuk Ukraine
8:18 AM on Thursday, July 1, 2010

Если есть возможность, я использую AOP. Тогда классы выглядят как POCO.

[NotifyPropertyChanged]
public class SomeClassCreatedWithIoC
{
    public virtual int SomeData { get; set; }

    [DoNotNotify]
    public virtual string SomeAnotherData { get; set; }
}

Anton Gudin Anton Gudin Ukraine
12:14 PM on Thursday, July 1, 2010

Привет, очень хорошая статья по этому поводу http://csharperimage.jeremylikness.com/2010/06/tips-and-tricks-for-inotifypropertychanged.html, хотя ты ее наверное читал ;)

Dima Pasko Dima Pasko Ukraine
12:27 PM on Thursday, July 1, 2010

Мне нравятся варианты с использованием AOP, например PostSharp:

http://www.lesnikowski.com/blog/index.php/inotifypropertychanged-with-postsharp/

http://www.codeproject.com/KB/cs/NotifyingAttribute.aspx?msg=3001451

alexkr.net alexkr.net Germany
10:29 AM on Friday, July 2, 2010

На самом деле вместо
MethodBase.GetCurrentMethod().Name.Remove(0,4))

удобней писать
MethodBase.GetCurrentMethod().Name.Substring(4)

Sergey Lutay Sergey Lutay Ukraine
2:58 PM on Friday, July 2, 2010

to alexkr.net: На вкус и цвет...

Сергей Сергей Russia
8:06 PM on Tuesday, July 27, 2010

В таких случаях непроизвольно вспомниаются макросы из С/С++. Может они и правда - зло, но в некоторых случаях их отсутствие еще большее зло.

trackback Alex Krakovetskiy blog
4:15 PM on Tuesday, October 5, 2010

Дайджест технических материалов для разработчиков #1

Этот дайджест содержит ссылки на технические статьи, которые были написаны, в большинстве, участниками

Сергей Лутай Сергей Лутай Ukraine
12:24 PM on Wednesday, January 5, 2011

Kind Of Magic MSBuild Task - в основе используется MSBuild для упрощения реализации интерфейса http://kindofmagic.codeplex.com/

Ivan Korneliuk Ivan Korneliuk Ukraine
1:06 PM on Wednesday, January 5, 2011

NotifyPropertyWeaver - http://code.google.com/p/notifypropertyweaver/

В останньому проекті я використовую його. І я в захваті від нього.

Add comment


(Will show your Gravatar icon)

  Country flag

biuquote
  • Comment
  • Preview
Loading