Реализация INotifyPropertyChanged - существует ли лучший способ?
Microsoft должна была реализовать что-то быстрое для INotifyPropertyChanged
, как в автоматических свойствах, просто укажите {get; set; notify;}
Я думаю, что в этом есть смысл. Или есть какие-то осложнения для этого?
Можем ли мы сами реализовать что-то вроде "уведомить" в наших свойствах. Есть ли изящное решение для реализации INotifyPropertyChanged
в вашем классе или единственный способ сделать это, подняв PropertyChanged
событие в каждой собственности.
Если нет, то можем ли мы написать что-то, чтобы автоматически сгенерировать кусок кода PropertyChanged
событие?
35 ответов
Без использования что-то вроде postsharp, минимальная версия, которую я использую, использует что-то вроде:
public class Data : INotifyPropertyChanged
{
// boiler-plate
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, string propertyName)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
// props
private string name;
public string Name
{
get { return name; }
set { SetField(ref name, value, "Name"); }
}
}
Каждое свойство тогда просто что-то вроде:
private string name;
public string Name
{
get { return name; }
set { SetField(ref name, value, "Name"); }
}
который не огромен; его также можно использовать как базовый класс, если хотите. bool
вернуться из SetField
говорит вам, если это не было, на случай, если вы хотите применить другую логику.
или даже проще с C# 5:
protected bool SetField<T>(ref T field, T value,
[CallerMemberName] string propertyName = null)
{...}
который можно назвать так:
set { SetField(ref name, value); }
с помощью которого компилятор добавит "Name"
автоматически.
C# 6.0 облегчает реализацию:
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
... а теперь с C#7:
private string name;
public string Name
{
get => name;
set => SetField(ref name, value);
}
Начиная с.Net 4.5, наконец-то есть простой способ сделать это.
.Net 4.5 представляет новые атрибуты информации о вызывающем абоненте.
private void OnPropertyChanged<T>([CallerMemberName]string caller = null) {
// make sure only to call this if the value actually changes
var handler = PropertyChanged;
if (handler != null) {
handler(this, new PropertyChangedEventArgs(caller));
}
}
Вероятно, хорошей идеей будет добавить в функцию также компаратор.
EqualityComparer<T>.Default.Equals
Также см. Информацию о вызывающем абоненте (C# и Visual Basic).
Мне действительно нравится решение Марка, но я думаю, что его можно немного улучшить, чтобы избежать использования "волшебной строки" (которая не поддерживает рефакторинг). Вместо использования имени свойства в качестве строки легко сделать его лямбда-выражением:
private string name;
public string Name
{
get { return name; }
set { SetField(ref name, value, () => Name); }
}
Просто добавьте следующие методы к коду Марка, это поможет:
protected virtual void OnPropertyChanged<T>(Expression<Func<T>> selectorExpression)
{
if (selectorExpression == null)
throw new ArgumentNullException("selectorExpression");
MemberExpression body = selectorExpression.Body as MemberExpression;
if (body == null)
throw new ArgumentException("The body must be a member expression");
OnPropertyChanged(body.Member.Name);
}
protected bool SetField<T>(ref T field, T value, Expression<Func<T>> selectorExpression)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;
OnPropertyChanged(selectorExpression);
return true;
}
Кстати, это было вдохновлено этим сообщением в блоге обновленный URL
Есть также Fody, который имеет надстройку PropertyChanged, которая позволяет вам написать это:
[ImplementPropertyChanged]
public class Person
{
public string GivenNames { get; set; }
public string FamilyName { get; set; }
}
... и во время компиляции вставляет свойство измененных уведомлений.
Я думаю, что люди должны уделять немного больше внимания производительности, это действительно влияет на пользовательский интерфейс, когда нужно связать много объектов (например, сетка с 10 000+ строками) или если значение объекта часто меняется (приложение для мониторинга в реальном времени),
Я взял различные реализации, найденные здесь и в других местах, и провел сравнение, проверил сравнение производительности реализаций INotifyPropertyChanged.
Вот взгляд на результат
Я представляю класс Bindable в своем блоге по адресу http://timoch.com/blog/2013/08/annoyed-with-inotifypropertychange/ Bindable использует словарь в качестве пакета свойств. Достаточно просто добавить необходимые перегрузки для подкласса, чтобы управлять его собственным полем поддержки, используя параметры ref.
- Нет волшебной струны
- Нет отражения
- Может быть улучшено для подавления поиска в словаре по умолчанию
Код:
public class Bindable : INotifyPropertyChanged {
private Dictionary<string, object> _properties = new Dictionary<string, object>();
/// <summary>
/// Gets the value of a property
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="name"></param>
/// <returns></returns>
protected T Get<T>([CallerMemberName] string name = null) {
Debug.Assert(name != null, "name != null");
object value = null;
if (_properties.TryGetValue(name, out value))
return value == null ? default(T) : (T)value;
return default(T);
}
/// <summary>
/// Sets the value of a property
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
/// <param name="name"></param>
/// <remarks>Use this overload when implicitly naming the property</remarks>
protected void Set<T>(T value, [CallerMemberName] string name = null) {
Debug.Assert(name != null, "name != null");
if (Equals(value, Get<T>(name)))
return;
_properties[name] = value;
OnPropertyChanged(name);
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) {
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) {
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Это можно использовать так:
public class Contact : Bindable {
public string FirstName {
get { return Get<string>(); }
set { Set(value); }
}
}
На самом деле у меня еще не было возможности попробовать это самостоятельно, но в следующий раз, когда я настраиваю проект с большим требованием для INotifyPropertyChanged, я собираюсь написать атрибут Postsharp, который будет внедрять код во время компиляции. Что-то вроде:
[NotifiesChange]
public string FirstName { get; set; }
Станет:
private string _firstName;
public string FirstName
{
get { return _firstname; }
set
{
if (_firstname != value)
{
_firstname = value;
OnPropertyChanged("FirstName")
}
}
}
Я не уверен, сработает ли это на практике, и мне нужно сесть и попробовать, но я не понимаю, почему нет. Мне может понадобиться, чтобы он принимал некоторые параметры для ситуаций, когда нужно запускать более одного OnPropertyChanged (если, например, у меня было свойство FullName в классе выше)
В настоящее время я использую собственный шаблон в Resharper, но даже после этого мне надоели все мои свойства так долго.
Ах, быстрый поиск в Google (который я должен был сделать до того, как написал это) показывает, что по крайней мере один человек сделал что-то подобное здесь. Не совсем то, что я имел в виду, но достаточно близко, чтобы показать, что теория хороша.
Да, лучший способ, безусловно, существует. Вот:
Шаг за шагом обучающий курс сжался мной, основываясь на этой полезной статье.
- Создать новый проект
- Установить основной пакет замка в проект
Install-Package Castle.Core
- Устанавливайте только легкие библиотеки mvvm
Установочный пакет MvvmLightLibs
- Добавьте два класса в проект:
NotifierInterceptor
public class NotifierInterceptor : IInterceptor
{
private PropertyChangedEventHandler handler;
public static Dictionary<String, PropertyChangedEventArgs> _cache =
new Dictionary<string, PropertyChangedEventArgs>();
public void Intercept(IInvocation invocation)
{
switch (invocation.Method.Name)
{
case "add_PropertyChanged":
handler = (PropertyChangedEventHandler)
Delegate.Combine(handler, (Delegate)invocation.Arguments[0]);
invocation.ReturnValue = handler;
break;
case "remove_PropertyChanged":
handler = (PropertyChangedEventHandler)
Delegate.Remove(handler, (Delegate)invocation.Arguments[0]);
invocation.ReturnValue = handler;
break;
default:
if (invocation.Method.Name.StartsWith("set_"))
{
invocation.Proceed();
if (handler != null)
{
var arg = retrievePropertyChangedArg(invocation.Method.Name);
handler(invocation.Proxy, arg);
}
}
else invocation.Proceed();
break;
}
}
private static PropertyChangedEventArgs retrievePropertyChangedArg(String methodName)
{
PropertyChangedEventArgs arg = null;
_cache.TryGetValue(methodName, out arg);
if (arg == null)
{
arg = new PropertyChangedEventArgs(methodName.Substring(4));
_cache.Add(methodName, arg);
}
return arg;
}
}
ProxyCreator
public class ProxyCreator
{
public static T MakeINotifyPropertyChanged<T>() where T : class, new()
{
var proxyGen = new ProxyGenerator();
var proxy = proxyGen.CreateClassProxy(
typeof(T),
new[] { typeof(INotifyPropertyChanged) },
ProxyGenerationOptions.Default,
new NotifierInterceptor()
);
return proxy as T;
}
}
- Создайте модель представления, например:
-
public class MainViewModel
{
public virtual string MainTextBox { get; set; }
public RelayCommand TestActionCommand
{
get { return new RelayCommand(TestAction); }
}
public void TestAction()
{
Trace.WriteLine(MainTextBox);
}
}
Поместите привязки в xaml:
<TextBox Text="{Binding MainTextBox}" ></TextBox> <Button Command="{Binding TestActionCommand}" >Test</Button>
Поместите строку кода в файл с выделенным кодом MainWindow.xaml.cs следующим образом:
DataContext = ProxyCreator.MakeINotifyPropertyChanged<MainViewModel>();
- Наслаждаться.
Внимание!!! Все ограниченные свойства должны быть украшены виртуальным ключевым словом, потому что они используются прокси-замком для переопределения.
На дворе 2022 год. Теперь есть официальное решение.
Используйте генераторы исходного кода MVVM из набора Microsoft MVVM Toolkit.
Этот
[ObservableProperty]
private string? name;
будет генерировать:
private string? name;
public string? Name
{
get => name;
set
{
if (!EqualityComparer<string?>.Default.Equals(name, value))
{
OnNameChanging(value);
OnPropertyChanging();
name = value;
OnNameChanged(value);
OnPropertyChanged();
}
}
}
// Property changing / changed listener
partial void OnNameChanging(string? value);
partial void OnNameChanged(string? value);
protected void OnPropertyChanging([CallerMemberName] string? propertyName = null)
{
PropertyChanging?.Invoke(this, new PropertyChangingEventArgs(propertyName));
}
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
Он поддерживает стандарт .NET 2.0 и .NET >= 5.0.
Очень похожий на AOP подход заключается в том, чтобы внедрить содержимое INotifyPropertyChanged в уже созданный объект на лету. Вы можете сделать это с помощью Castle DynamicProxy. Вот статья, которая объясняет технику:
Позвольте мне представить мой собственный подход под названием Yappi. Он принадлежит генераторам производных классов Runtime proxy|, добавляя новые функциональные возможности к существующему объекту или типу, например, к Dynamic Proxy Caste Project.
Это позволяет реализовать INotifyPropertyChanged один раз в базовом классе, а затем объявить производные классы в следующем стиле, все еще поддерживая INotifyPropertyChanged для новых свойств:
public class Animal:Concept
{
protected Animal(){}
public virtual string Name { get; set; }
public virtual int Age { get; set; }
}
Сложность конструкции производного класса или прокси может быть скрыта за следующей строкой:
var animal = Concept.Create<Animal>.New();
И вся работа по реализации INotifyPropertyChanged может быть выполнена следующим образом:
public class Concept:INotifyPropertyChanged
{
//Hide constructor
protected Concept(){}
public static class Create<TConcept> where TConcept:Concept
{
//Construct derived Type calling PropertyProxy.ConstructType
public static readonly Type Type = PropertyProxy.ConstructType<TConcept, Implementation<TConcept>>(new Type[0], true);
//Create constructing delegate calling Constructor.Compile
public static Func<TConcept> New = Constructor.Compile<Func<TConcept>>(Type);
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(PropertyChangedEventArgs eventArgs)
{
var caller = PropertyChanged;
if(caller!=null)
{
caller(this, eventArgs);
}
}
//define implementation
public class Implementation<TConcept> : DefaultImplementation<TConcept> where TConcept:Concept
{
public override Func<TBaseType, TResult> OverrideGetter<TBaseType, TDeclaringType, TConstructedType, TResult>(PropertyInfo property)
{
return PropertyImplementation<TBaseType, TDeclaringType>.GetGetter<TResult>(property.Name);
}
/// <summary>
/// Overriding property setter implementation.
/// </summary>
/// <typeparam name="TBaseType">Base type for implementation. TBaseType must be TConcept, and inherits all its constraints. Also TBaseType is TDeclaringType.</typeparam>
/// <typeparam name="TDeclaringType">Type, declaring property.</typeparam>
/// <typeparam name="TConstructedType">Constructed type. TConstructedType is TDeclaringType and TBaseType.</typeparam>
/// <typeparam name="TResult">Type of property.</typeparam>
/// <param name="property">PropertyInfo of property.</param>
/// <returns>Delegate, corresponding to property setter implementation.</returns>
public override Action<TBaseType, TResult> OverrideSetter<TBaseType, TDeclaringType, TConstructedType, TResult>(PropertyInfo property)
{
//This code called once for each declared property on derived type's initialization.
//EventArgs instance is shared between all events for each concrete property.
var eventArgs = new PropertyChangedEventArgs(property.Name);
//get delegates for base calls.
Action<TBaseType, TResult> setter = PropertyImplementation<TBaseType, TDeclaringType>.GetSetter<TResult>(property.Name);
Func<TBaseType, TResult> getter = PropertyImplementation<TBaseType, TDeclaringType>.GetGetter<TResult>(property.Name);
var comparer = EqualityComparer<TResult>.Default;
return (pthis, value) =>
{//This code executes each time property setter is called.
if (comparer.Equals(value, getter(pthis))) return;
//base. call
setter(pthis, value);
//Directly accessing Concept's protected method.
pthis.OnPropertyChanged(eventArgs);
};
}
}
}
Он полностью безопасен для рефакторинга, не использует отражения после построения типа и достаточно быстр.
Посмотрите здесь: http://dotnet-forum.de/blogs/thearchitect/archive/2012/11/01/die-optimale-implementierung-des-inotifypropertychanged-interfaces.aspx
Он написан на немецком языке, но вы можете скачать ViewModelBase.cs. Все комментарии в CS-файле написаны на английском языке.
С помощью этого ViewModelBase-Class можно реализовать привязываемые свойства, похожие на хорошо известные свойства зависимости:
public string SomeProperty
{
get { return GetValue( () => SomeProperty ); }
set { SetValue( () => SomeProperty, value ); }
}
Хотя, очевидно, существует множество способов сделать это, за исключением магических ответов AOP, ни один из ответов, похоже, не рассматривает установку свойства Model непосредственно из модели представления без наличия локального поля для ссылки.
Проблема в том, что вы не можете ссылаться на недвижимость. Однако вы можете использовать Action для установки этого свойства.
protected bool TrySetProperty<T>(Action<T> property, T newValue, T oldValue, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(oldValue, newValue))
{
return false;
}
property(newValue);
RaisePropertyChanged(propertyName);
return true;
}
Это может быть использовано как следующий фрагмент кода.
public int Prop {
get => model.Prop;
set => TrySetProperty(x => model.Prop = x, value, model.Prop);
}
Ознакомьтесь с этим репозиторием BitBucket для полной реализации метода и нескольких различных способов достижения того же результата, включая метод, который использует LINQ, и метод, который использует отражение. Обратите внимание, что эти методы медленнее по производительности.
Все эти ответы очень хороши.
Мое решение - использовать фрагменты кода для выполнения работы.
Это использует простейший вызов события PropertyChanged.
Сохраните этот фрагмент и используйте его как фрагмент "fullprop".
расположение можно найти в меню "Инструменты \ Диспетчер фрагментов кода..." в Visual Studio.
<?xml version="1.0" encoding="utf-8" ?>
<CodeSnippets xmlns="http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet">
<CodeSnippet Format="1.0.0">
<Header>
<Title>inotifypropfull</Title>
<Shortcut>inotifypropfull</Shortcut>
<HelpUrl>http://ofirzeitoun.wordpress.com/</HelpUrl>
<Description>Code snippet for property and backing field with notification</Description>
<Author>Ofir Zeitoun</Author>
<SnippetTypes>
<SnippetType>Expansion</SnippetType>
</SnippetTypes>
</Header>
<Snippet>
<Declarations>
<Literal>
<ID>type</ID>
<ToolTip>Property type</ToolTip>
<Default>int</Default>
</Literal>
<Literal>
<ID>property</ID>
<ToolTip>Property name</ToolTip>
<Default>MyProperty</Default>
</Literal>
<Literal>
<ID>field</ID>
<ToolTip>The variable backing this property</ToolTip>
<Default>myVar</Default>
</Literal>
</Declarations>
<Code Language="csharp">
<![CDATA[private $type$ $field$;
public $type$ $property$
{
get { return $field$;}
set {
$field$ = value;
var temp = PropertyChanged;
if (temp != null)
{
temp(this, new PropertyChangedEventArgs("$property$"));
}
}
}
$end$]]>
</Code>
</Snippet>
</CodeSnippet>
</CodeSnippets>
Вы можете изменить вызов, как вам нравится (чтобы использовать вышеуказанные решения)
Основываясь на ответе Томаса, который был адаптирован из ответа Марка, я превратил измененный код свойства отражения в базовый класс:
public abstract class PropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
protected void OnPropertyChanged<T>(Expression<Func<T>> selectorExpression)
{
if (selectorExpression == null)
throw new ArgumentNullException("selectorExpression");
var me = selectorExpression.Body as MemberExpression;
// Nullable properties can be nested inside of a convert function
if (me == null)
{
var ue = selectorExpression.Body as UnaryExpression;
if (ue != null)
me = ue.Operand as MemberExpression;
}
if (me == null)
throw new ArgumentException("The body must be a member expression");
OnPropertyChanged(me.Member.Name);
}
protected void SetField<T>(ref T field, T value, Expression<Func<T>> selectorExpression, params Expression<Func<object>>[] additonal)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return;
field = value;
OnPropertyChanged(selectorExpression);
foreach (var item in additonal)
OnPropertyChanged(item);
}
}
Использование такое же, как и у ответа Томаса, за исключением того, что вы можете передать дополнительные свойства для уведомления. Это было необходимо для обработки вычисляемых столбцов, которые необходимо обновить в сетке.
private int _quantity;
private int _price;
public int Quantity
{
get { return _quantity; }
set { SetField(ref _quantity, value, () => Quantity, () => Total); }
}
public int Price
{
get { return _price; }
set { SetField(ref _price, value, () => Price, () => Total); }
}
public int Total { get { return _price * _quantity; } }
У меня есть это вождение коллекции элементов, хранящихся в BindingList, представленных через DataGridView. Это избавило меня от необходимости делать ручные вызовы Refresh() в сетку.
Я держу это как фрагмент. C# 6 добавляет хороший синтаксис для вызова обработчика.
// INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
private void Set<T>(ref T property, T value, [CallerMemberName] string propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(property, value) == false)
{
property = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Я создал метод расширения в моей базовой библиотеке для повторного использования:
public static class INotifyPropertyChangedExtensions
{
public static bool SetPropertyAndNotify<T>(this INotifyPropertyChanged sender,
PropertyChangedEventHandler handler, ref T field, T value,
[CallerMemberName] string propertyName = "",
EqualityComparer<T> equalityComparer = null)
{
bool rtn = false;
var eqComp = equalityComparer ?? EqualityComparer<T>.Default;
if (!eqComp.Equals(field,value))
{
field = value;
rtn = true;
if (handler != null)
{
var args = new PropertyChangedEventArgs(propertyName);
handler(sender, args);
}
}
return rtn;
}
}
Это работает с.Net 4.5 из-за CallerMemberNameAttribute. Если вы хотите использовать его с более ранней версией.Net, вы должны изменить объявление метода с: ...,[CallerMemberName] string propertyName = "", ...
в ...,string propertyName, ...
Использование:
public class Dog : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
string _name;
public string Name
{
get { return _name; }
set
{
this.SetPropertyAndNotify(PropertyChanged, ref _name, value);
}
}
}
Другое комбинированное решение использует StackFrame:
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void Set<T>(ref T field, T value)
{
MethodBase method = new StackFrame(1).GetMethod();
field = value;
Raise(method.Name.Substring(4));
}
protected void Raise(string propertyName)
{
var temp = PropertyChanged;
if (temp != null)
{
temp(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Использование:
public class TempVM : BaseViewModel
{
private int _intP;
public int IntP
{
get { return _intP; }
set { Set<int>(ref _intP, value); }
}
}
Я решил таким образом (это немного трудоемко, но, безусловно, быстрее во время выполнения).
В VB (извините, но я думаю, что это не сложно перевести на C#), я делаю эту замену с помощью RE:
(?<Attr><(.*ComponentModel\.)Bindable\(True\)>)( |\r\n)*(?<Def>(Public|Private|Friend|Protected) .*Property )(?<Name>[^ ]*) As (?<Type>.*?)[ |\r\n](?![ |\r\n]*Get)
с:
Private _${Name} As ${Type}\r\n${Attr}\r\n${Def}${Name} As ${Type}\r\nGet\r\nReturn _${Name}\r\nEnd Get\r\nSet (Value As ${Type})\r\nIf _${Name} <> Value Then \r\n_${Name} = Value\r\nRaiseEvent PropertyChanged(Me, New ComponentModel.PropertyChangedEventArgs("${Name}"))\r\nEnd If\r\nEnd Set\r\nEnd Property\r\n
Это преобразование всего кода, как это:
<Bindable(True)>
Protected Friend Property StartDate As DateTime?
В
Private _StartDate As DateTime?
<Bindable(True)>
Protected Friend Property StartDate As DateTime?
Get
Return _StartDate
End Get
Set(Value As DateTime?)
If _StartDate <> Value Then
_StartDate = Value
RaiseEvent PropertyChange(Me, New ComponentModel.PropertyChangedEventArgs("StartDate"))
End If
End Set
End Property
И если я хочу иметь более читаемый код, я могу быть наоборот, просто сделав следующую замену:
Private _(?<Name>.*) As (?<Type>.*)[\r\n ]*(?<Attr><(.*ComponentModel\.)Bindable\(True\)>)[\r\n ]*(?<Def>(Public|Private|Friend|Protected) .*Property )\k<Name> As \k<Type>[\r\n ]*Get[\r\n ]*Return _\k<Name>[\r\n ]*End Get[\r\n ]*Set\(Value As \k<Type>\)[\r\n ]*If _\k<Name> <> Value Then[\r\n ]*_\k<Name> = Value[\r\n ]*RaiseEvent PropertyChanged\(Me, New (.*ComponentModel\.)PropertyChangedEventArgs\("\k<Name>"\)\)[\r\n ]*End If[\r\n ]*End Set[\r\n ]*End Property
С
${Attr} ${Def} ${Name} As ${Type}
Я бросаю, чтобы заменить код IL метода set, но я не могу написать много скомпилированного кода в IL... Если я напишу день, я скажу вам!
Я написал статью, которая помогает с этим ( https://msdn.microsoft.com/magazine/mt736453). Вы можете использовать пакет NuSet SolSoft.DataBinding. Тогда вы можете написать код так:
public class TestViewModel : IRaisePropertyChanged
{
public TestViewModel()
{
this.m_nameProperty = new NotifyProperty<string>(this, nameof(Name), null);
}
private readonly NotifyProperty<string> m_nameProperty;
public string Name
{
get
{
return m_nameProperty.Value;
}
set
{
m_nameProperty.SetValue(value);
}
}
// Plus implement IRaisePropertyChanged (or extend BaseViewModel)
}
Выгоды:
- базовый класс не является обязательным
- не задумываться о каждом "установленном значении"
- может иметь свойства, которые зависят от других свойств, и все они автоматически вызывают соответствующие события (в статье есть пример этого)
Я только что нашел ActiveSharp - автоматический INotifyPropertyChanged, я еще не использовал его, но выглядит хорошо.
Цитировать с его веб-сайта...
Отправлять уведомления об изменении свойства без указания имени свойства в виде строки.
Вместо этого напишите такие свойства:
public int Foo
{
get { return _foo; }
set { SetValue(ref _foo, value); } // <-- no property name here
}
Обратите внимание, что нет необходимости включать имя свойства в виде строки. ActiveSharp надежно и правильно вычисляет это для себя. Он работает на основе того факта, что ваша реализация свойства передает поле поддержки (_foo) по ссылке. (ActiveSharp использует этот вызов "по ссылке", чтобы определить, какое поле поддержки было передано, и из поля оно идентифицирует свойство).
Вот версия NotifyPropertyChanged для Unity3D или не CallerMemberName
public abstract class Bindable : MonoBehaviour, INotifyPropertyChanged
{
private readonly Dictionary<string, object> _properties = new Dictionary<string, object>();
private static readonly StackTrace stackTrace = new StackTrace();
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Resolves a Property's name from a Lambda Expression passed in.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="property"></param>
/// <returns></returns>
internal string GetPropertyName<T>(Expression<Func<T>> property)
{
var expression = (MemberExpression) property.Body;
var propertyName = expression.Member.Name;
Debug.AssertFormat(propertyName != null, "Bindable Property shouldn't be null!");
return propertyName;
}
#region Notification Handlers
/// <summary>
/// Notify's all other objects listening that a value has changed for nominated propertyName
/// </summary>
/// <param name="propertyName"></param>
internal void NotifyOfPropertyChange(string propertyName)
{
OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
/// <summary>
/// Notifies subscribers of the property change.
/// </summary>
/// <typeparam name="TProperty">The type of the property.</typeparam>
/// <param name="property">The property expression.</param>
internal void NotifyOfPropertyChange<TProperty>(Expression<Func<TProperty>> property)
{
var propertyName = GetPropertyName(property);
NotifyOfPropertyChange(propertyName);
}
/// <summary>
/// Raises the <see cref="PropertyChanged" /> event directly.
/// </summary>
/// <param name="e">The <see cref="PropertyChangedEventArgs" /> instance containing the event data.</param>
internal void OnPropertyChanged(PropertyChangedEventArgs e)
{
var handler = PropertyChanged;
if (handler != null)
{
handler(this, e);
}
}
#endregion
#region Getters
/// <summary>
/// Gets the value of a property
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="name"></param>
/// <returns></returns>
internal T Get<T>(Expression<Func<T>> property)
{
var propertyName = GetPropertyName(property);
return Get<T>(GetPropertyName(property));
}
/// <summary>
/// Gets the value of a property automatically based on its caller.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
internal T Get<T>()
{
var name = stackTrace.GetFrame(1).GetMethod().Name.Substring(4); // strips the set_ from name;
return Get<T>(name);
}
/// <summary>
/// Gets the name of a property based on a string.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="name"></param>
/// <returns></returns>
internal T Get<T>(string name)
{
object value = null;
if (_properties.TryGetValue(name, out value))
return value == null ? default(T) : (T) value;
return default(T);
}
#endregion
#region Setters
/// <summary>
/// Sets the value of a property whilst automatically looking up its caller name.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
internal void Set<T>(T value)
{
var propertyName = stackTrace.GetFrame(1).GetMethod().Name.Substring(4); // strips the set_ from name;
Set(value, propertyName);
}
/// <summary>
/// Sets the value of a property
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
/// <param name="name"></param>
internal void Set<T>(T value, string propertyName)
{
Debug.Assert(propertyName != null, "name != null");
if (Equals(value, Get<T>(propertyName)))
return;
_properties[propertyName] = value;
NotifyOfPropertyChange(propertyName);
}
/// <summary>
/// Sets the value of a property based off an Expression (()=>FieldName)
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
/// <param name="property"></param>
internal void Set<T>(T value, Expression<Func<T>> property)
{
var propertyName = GetPropertyName(property);
Debug.Assert(propertyName != null, "name != null");
if (Equals(value, Get<T>(propertyName)))
return;
_properties[propertyName] = value;
NotifyOfPropertyChange(propertyName);
}
#endregion
}
Этот код позволяет вам писать поля поддержки свойств следующим образом:
public string Text
{
get { return Get<string>(); }
set { Set(value); }
}
Кроме того, в resharper, если вы создадите фрагмент шаблона / поиска, вы также можете автоматизировать рабочий процесс, преобразовав простые поля проп в вышеуказанную основу.
Шаблон поиска:
public $type$ $fname$ { get; set; }
Заменить шаблон:
public $type$ $fname$
{
get { return Get<$type$>(); }
set { Set(value); }
}
Я придумал этот базовый класс для реализации наблюдаемого паттерна, который в значительной степени делает то, что вам нужно ("автоматически" реализует set и get). Я потратил на это целый час в качестве прототипа, поэтому он не имеет много модульных тестов, но подтверждает концепцию. Обратите внимание, что он использует Dictionary<string, ObservablePropertyContext>
убрать необходимость в приватных полях.
public class ObservableByTracking<T> : IObservable<T>
{
private readonly Dictionary<string, ObservablePropertyContext> _expando;
private bool _isDirty;
public ObservableByTracking()
{
_expando = new Dictionary<string, ObservablePropertyContext>();
var properties = this.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).ToList();
foreach (var property in properties)
{
var valueContext = new ObservablePropertyContext(property.Name, property.PropertyType)
{
Value = GetDefault(property.PropertyType)
};
_expando[BuildKey(valueContext)] = valueContext;
}
}
protected void SetValue<T>(Expression<Func<T>> expression, T value)
{
var keyContext = GetKeyContext(expression);
var key = BuildKey(keyContext.PropertyName, keyContext.PropertyType);
if (!_expando.ContainsKey(key))
{
throw new Exception($"Object doesn't contain {keyContext.PropertyName} property.");
}
var originalValue = (T)_expando[key].Value;
if (EqualityComparer<T>.Default.Equals(originalValue, value))
{
return;
}
_expando[key].Value = value;
_isDirty = true;
}
protected T GetValue<T>(Expression<Func<T>> expression)
{
var keyContext = GetKeyContext(expression);
var key = BuildKey(keyContext.PropertyName, keyContext.PropertyType);
if (!_expando.ContainsKey(key))
{
throw new Exception($"Object doesn't contain {keyContext.PropertyName} property.");
}
var value = _expando[key].Value;
return (T)value;
}
private KeyContext GetKeyContext<T>(Expression<Func<T>> expression)
{
var castedExpression = expression.Body as MemberExpression;
if (castedExpression == null)
{
throw new Exception($"Invalid expression.");
}
var parameterName = castedExpression.Member.Name;
var propertyInfo = castedExpression.Member as PropertyInfo;
if (propertyInfo == null)
{
throw new Exception($"Invalid expression.");
}
return new KeyContext {PropertyType = propertyInfo.PropertyType, PropertyName = parameterName};
}
private static string BuildKey(ObservablePropertyContext observablePropertyContext)
{
return $"{observablePropertyContext.Type.Name}.{observablePropertyContext.Name}";
}
private static string BuildKey(string parameterName, Type type)
{
return $"{type.Name}.{parameterName}";
}
private static object GetDefault(Type type)
{
if (type.IsValueType)
{
return Activator.CreateInstance(type);
}
return null;
}
public bool IsDirty()
{
return _isDirty;
}
public void SetPristine()
{
_isDirty = false;
}
private class KeyContext
{
public string PropertyName { get; set; }
public Type PropertyType { get; set; }
}
}
public interface IObservable<T>
{
bool IsDirty();
void SetPristine();
}
Вот использование
public class ObservableByTrackingTestClass : ObservableByTracking<ObservableByTrackingTestClass>
{
public ObservableByTrackingTestClass()
{
StringList = new List<string>();
StringIList = new List<string>();
NestedCollection = new List<ObservableByTrackingTestClass>();
}
public IEnumerable<string> StringList
{
get { return GetValue(() => StringList); }
set { SetValue(() => StringIList, value); }
}
public IList<string> StringIList
{
get { return GetValue(() => StringIList); }
set { SetValue(() => StringIList, value); }
}
public int IntProperty
{
get { return GetValue(() => IntProperty); }
set { SetValue(() => IntProperty, value); }
}
public ObservableByTrackingTestClass NestedChild
{
get { return GetValue(() => NestedChild); }
set { SetValue(() => NestedChild, value); }
}
public IList<ObservableByTrackingTestClass> NestedCollection
{
get { return GetValue(() => NestedCollection); }
set { SetValue(() => NestedCollection, value); }
}
public string StringProperty
{
get { return GetValue(() => StringProperty); }
set { SetValue(() => StringProperty, value); }
}
}
Если вы используете динамику в.NET 4.5, вам не нужно беспокоиться о INotifyPropertyChanged
,
dynamic obj = new ExpandoObject();
obj.Name = "John";
если имя привязано к какому-либо элементу управления, оно просто отлично работает.
Использовать этот
using System;
using System.ComponentModel;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Proxies;
public static class ObservableFactory
{
public static T Create<T>(T target)
{
if (!typeof(T).IsInterface)
throw new ArgumentException("Target should be an interface", "target");
var proxy = new Observable<T>(target);
return (T)proxy.GetTransparentProxy();
}
}
internal class Observable<T> : RealProxy, INotifyPropertyChanged, INotifyPropertyChanging
{
private readonly T target;
internal Observable(T target)
: base(ImplementINotify(typeof(T)))
{
this.target = target;
}
public override IMessage Invoke(IMessage msg)
{
var methodCall = msg as IMethodCallMessage;
if (methodCall != null)
{
return HandleMethodCall(methodCall);
}
return null;
}
public event PropertyChangingEventHandler PropertyChanging;
public event PropertyChangedEventHandler PropertyChanged;
IMessage HandleMethodCall(IMethodCallMessage methodCall)
{
var isPropertySetterCall = methodCall.MethodName.StartsWith("set_");
var propertyName = isPropertySetterCall ? methodCall.MethodName.Substring(4) : null;
if (isPropertySetterCall)
{
OnPropertyChanging(propertyName);
}
try
{
object methodCalltarget = target;
if (methodCall.MethodName == "add_PropertyChanged" || methodCall.MethodName == "remove_PropertyChanged"||
methodCall.MethodName == "add_PropertyChanging" || methodCall.MethodName == "remove_PropertyChanging")
{
methodCalltarget = this;
}
var result = methodCall.MethodBase.Invoke(methodCalltarget, methodCall.InArgs);
if (isPropertySetterCall)
{
OnPropertyChanged(methodCall.MethodName.Substring(4));
}
return new ReturnMessage(result, null, 0, methodCall.LogicalCallContext, methodCall);
}
catch (TargetInvocationException invocationException)
{
var exception = invocationException.InnerException;
return new ReturnMessage(exception, methodCall);
}
}
protected virtual void OnPropertyChanged(string propertyName)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual void OnPropertyChanging(string propertyName)
{
var handler = PropertyChanging;
if (handler != null) handler(this, new PropertyChangingEventArgs(propertyName));
}
public static Type ImplementINotify(Type objectType)
{
var tempAssemblyName = new AssemblyName(Guid.NewGuid().ToString());
var dynamicAssembly = AppDomain.CurrentDomain.DefineDynamicAssembly(
tempAssemblyName, AssemblyBuilderAccess.RunAndCollect);
var moduleBuilder = dynamicAssembly.DefineDynamicModule(
tempAssemblyName.Name,
tempAssemblyName + ".dll");
var typeBuilder = moduleBuilder.DefineType(
objectType.FullName, TypeAttributes.Public | TypeAttributes.Interface | TypeAttributes.Abstract);
typeBuilder.AddInterfaceImplementation(objectType);
typeBuilder.AddInterfaceImplementation(typeof(INotifyPropertyChanged));
typeBuilder.AddInterfaceImplementation(typeof(INotifyPropertyChanging));
var newType = typeBuilder.CreateType();
return newType;
}
}
}
Я предлагаю использовать ReactiveProperty. Это самый короткий метод, кроме Fody.
public class Data : INotifyPropertyChanged
{
// boiler-plate
...
// props
private string name;
public string Name
{
get { return name; }
set { SetField(ref name, value, "Name"); }
}
}
вместо
public class Data
{
// Don't need boiler-plate and INotifyPropertyChanged
// props
public ReactiveProperty<string> Name { get; } = new ReactiveProperty<string>();
}
( Документы)
Я понимаю, что на этот вопрос уже есть миллиард ответов, но ни один из них не показался мне вполне подходящим. Моя проблема в том, что я не хочу никаких хитов производительности и готов мириться с небольшим многословием только по этой причине. Я также не слишком беспокоюсь об автоматических свойствах, что привело меня к следующему решению:
public abstract class AbstractObject : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected virtual bool SetValue<TKind>(ref TKind Source, TKind NewValue, params string[] Notify)
{
//Set value if the new value is different from the old
if (!Source.Equals(NewValue))
{
Source = NewValue;
//Notify all applicable properties
foreach (var i in Notify)
OnPropertyChanged(i);
return true;
}
return false;
}
public AbstractObject()
{
}
}
Другими словами, вышеприведенное решение удобно, если вы не возражаете против этого:
public class SomeObject : AbstractObject
{
public string AnotherProperty
{
get
{
return someProperty ? "Car" : "Plane";
}
}
bool someProperty = false;
public bool SomeProperty
{
get
{
return someProperty;
}
set
{
SetValue(ref someProperty, value, "SomeProperty", "AnotherProperty");
}
}
public SomeObject() : base()
{
}
}
Pros
- Нет отражения
- Уведомляет только если старое значение!= Новое значение
- Уведомить несколько свойств одновременно
Cons
- Нет авто свойств (вы можете добавить поддержку для обоих, хотя!)
- Немного многословия
- Бокс (небольшой удар по производительности?)
Увы, это все же лучше, чем делать это,
set
{
if (!someProperty.Equals(value))
{
someProperty = value;
OnPropertyChanged("SomeProperty");
OnPropertyChanged("AnotherProperty");
}
}
Для каждого свойства, которое становится кошмаром с дополнительным многословием;-(
Обратите внимание, я не утверждаю, что это решение лучше с точки зрения производительности по сравнению с другими, просто это жизнеспособное решение для тех, кому не нравятся другие представленные решения.
Я использую следующий метод расширения (используя C# 6.0), чтобы сделать реализацию INPC максимально простой:
public static bool ChangeProperty<T>(this PropertyChangedEventHandler propertyChanged, ref T field, T value, object sender,
IEqualityComparer<T> comparer = null, [CallerMemberName] string propertyName = null)
{
if (comparer == null)
comparer = EqualityComparer<T>.Default;
if (comparer.Equals(field, value))
{
return false;
}
else
{
field = value;
propertyChanged?.Invoke(sender, new PropertyChangedEventArgs(propertyName));
return true;
}
}
Реализация INPC сводится к (вы можете реализовать это каждый раз или создать базовый класс):
public class INPCBaseClass: INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected bool changeProperty<T>(ref T field, T value,
IEqualityComparer<T> comparer = null, [CallerMemberName] string propertyName = null)
{
return PropertyChanged.ChangeProperty(ref field, value, this, comparer, propertyName);
}
}
Затем напишите ваши свойства, как это:
private string testProperty;
public string TestProperty
{
get { return testProperty; }
set { changeProperty(ref testProperty, value); }
}
ПРИМЕЧАНИЕ. Вы можете опустить [CallerMemberName]
объявление в методе расширения, если хотите, но я хотел сохранить его гибкость.
Если у вас есть свойства без вспомогательного поля, вы можете перегружать changeProperty
:
protected bool changeProperty<T>(T property, Action<T> set, T value,
IEqualityComparer<T> comparer = null, [CallerMemberName] string propertyName = null)
{
bool ret = changeProperty(ref property, value, comparer, propertyName);
if (ret)
set(property);
return ret;
}
Пример использования будет:
public string MyTestProperty
{
get { return base.TestProperty; }
set { changeProperty(base.TestProperty, (x) => { base.TestProperty = x; }, value); }
}
Я пишу библиотеку, которая имеет дело с INotifyPropertyChanged, и основная идея заключается в использовании динамического прокси для уведомления об изменениях.
репо здесь: https://github.com/CaulyKan/NoMorePropertyChanged
с помощью этой библиотеки вы можете написать:
public dynamic Test1Binding { get; set; }
public TestDTO Test1
{
get { return (TestDTO)Test1Binding; }
set { SetBinding(nameof(Test1Binding), value); }
}
Затем сделайте все привязки и модификации перейдите к Test1Binding, который автоматически уведомит PropertyChange и CollectionChanged независимо от сложности TestDTO.
он также может обрабатывать зависимости.
[DependsOn("Test1Binding.TestString")]
public string Test2
{
get { return Test1Binding.TestString; }
}
Пожалуйста, дайте мне несколько предложений.
Другие вещи, которые вы, возможно, захотите учесть при реализации этих видов свойств, это тот факт, что INotifyPropertyChang *ed *ing оба используют классы аргументов события.
Если у вас установлено большое количество свойств, то количество экземпляров класса аргумента события может быть огромным, вам следует рассмотреть возможность их кэширования, поскольку они являются одной из областей, в которых может произойти взрыв строки.
Взгляните на эту реализацию и объясните, почему она была задумана.