Microsoft должна была внедрить что-то мгновенное для INotifyPropertyChanged
, как в автоматических свойствах, просто укажите {get; set; notify;}
Я думаю, что это имеет смысл сделать это. Или есть какие-то осложнения?
Можем ли мы сами реализовать что-то вроде "уведомлять" в наших свойствах. Есть ли изящное решение для реализации INotifyPropertyChanged
в вашем классе или единственный способ сделать это - это создать событие PropertyChanged
в каждом свойстве.
Если мы не можем написать что-то, чтобы автоматически генерировать фрагмент кода, чтобы поднять событие PropertyChanged
?
Не используя что-то вроде 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
указывает, был ли он нерабочим, если вы хотите применить другую логику.
или даже проще с С# 5:
protected bool SetField<T>(ref T field, T value,
[CallerMemberName] string propertyName = null)
{...}
который можно вызвать следующим образом:
set { SetField(ref name, value); }
с помощью которого компилятор автоматически добавит "Name"
.
С# 6.0 упрощает реализацию:
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
... и теперь с С# 7:
private string name;
public string Name
{
get => name;
set => SetField(ref name, value);
}
.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
Дополнительные примеры здесь и здесь
Также см. Информация о вызывающем абоненте (С# и Visual Basic)
Мне очень нравится решение Marc, но я думаю, что его можно немного улучшить, чтобы избежать использования "волшебной строки" (которая не поддерживает рефакторинг). Вместо использования имени свойства в виде строки легко сделать это лямбда-выражением:
private string name;
public string Name
{
get { return name; }
set { SetField(ref name, value, () => Name); }
}
Просто добавьте следующие методы в код Marc, он сделает трюк:
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;
}
BTW, это было вдохновлено этот пост в блоге обновленный 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
Установочный пакет 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>();
Внимание!!! Все ограниченные свойства должны быть украшены ключевое слово virtual, потому что они используются прокси-сервером замка для переопределения.
type
, interfaces to apply
, interceptors
.
Посмотрите здесь: http://dotnet-forum.de/blogs/thearchitect/archive/2012/11/01/die-optimale-implementierung-des-inotifypropertychanged-interfaces.aspx
Это написано на немецком языке, но вы можете загрузить ViewModelBase.cs. Все комментарии в cs файле написаны на английском языке.
С помощью этого ViewModelBase-класса можно реализовать свойства связывания, аналогичные хорошо известным свойствам зависимостей:
public string SomeProperty
{
get { return GetValue( () => SomeProperty ); }
set { SetValue( () => SomeProperty, value ); }
}
Очень похожий на AOP подход заключается в том, чтобы вводить материал INotifyPropertyChanged на уже созданный объект на лету. Вы можете сделать это с помощью чего-то вроде Castle DynamicProxy. Вот статья, которая объясняет технику:
Основываясь на ответе Томаса, который был адаптирован из ответа Marc, я превратил преобразованное свойство измененного кода в базовый класс:
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() в сетке.
Позвольте мне представить свой собственный подход под названием 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 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 protected method.
pthis.OnPropertyChanged(eventArgs);
};
}
}
}
Он полностью безопасен для рефакторинга, не использует отражения после построения типа и достаточно быстро.
TDeclaration
в PropertyImplementation
? Конечно, вы можете найти подходящий тип для вызова (не callvirt) геттер / сеттер только с TImplementation
?
Все эти ответы очень приятные.
Мое решение использует фрагменты кода для выполнения задания.
Здесь используется простейший вызов события 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>
Вы можете изменить вызов по своему усмотрению (использовать приведенные выше решения)
Я написал статью, которая помогает с этим (https://msdn.microsoft.com/magazine/mt736453). Вы можете использовать пакет SolSoft.DataBinding NuGet. Затем вы можете написать код следующим образом:
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)
}
Преимущества:
Вот версия Unity3D или Non-CallerMemberName NotifyPropertyChanged
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 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 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); }
}
Кроме того, если вы создаете фрагмент шаблона/поиска, вы также можете автоматизировать свой рабочий процесс, преобразовывая простые поля прокрутки в вышеупомянутую поддержку.
Шаблон поиска:
public $type$ $fname$ { get; set; }
Заменить шаблон:
public $type$ $fname$
{
get { return Get<$type$>(); }
set { Set(value); }
}
Я держу это как фрагмент. С# 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);
}
}
}
Если вы используете динамику в .NET 4.5, вам не нужно беспокоиться о INotifyPropertyChanged
.
dynamic obj = new ExpandoObject();
obj.Name = "John";
Если имя связано с каким-то элементом управления, оно просто отлично работает.
Я придумал этот базовый класс для реализации наблюдаемого шаблона, в значительной степени делает то, что вам нужно ( "автоматически" реализует набор и 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); }
}
}
Я понимаю, что этот вопрос уже имеет ответы, но никто из них не чувствовал себя вполне подходящим для меня. Моя проблема в том, что я не хочу никаких хитов производительности, и я готов смириться с небольшой подробностью только по этой причине. Я также не слишком забочусь о свойствах авто, что привело меня к следующему решению:
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
против
Увы, это все же лучше, чем делать это,
set
{
if (!someProperty.Equals(value))
{
someProperty = value;
OnPropertyChanged("SomeProperty");
OnPropertyChanged("AnotherProperty");
}
}
Для каждого отдельного свойства, которое становится кошмаром с дополнительной детализацией; - (
Заметьте, я не утверждаю, что это решение лучше по производительности по сравнению с остальными, просто это жизнеспособное решение для тех, кому не нравятся другие представленные решения.
Еще одно комбинированное решение использует 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); }
}
}
Идея, использующая отражение:
class ViewModelBase : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged;
bool Notify<T>(MethodBase mb, ref T oldValue, T newValue) {
// Get Name of Property
string name = mb.Name.Substring(4);
// Detect Change
bool changed = EqualityComparer<T>.Default.Equals(oldValue, newValue);
// Return if no change
if (!changed) return false;
// Update value
oldValue = newValue;
// Raise Event
if (PropertyChanged != null) {
PropertyChanged(this, new PropertyChangedEventArgs(name));
}//if
// Notify caller of change
return true;
}//method
string name;
public string Name {
get { return name; }
set {
Notify(MethodInfo.GetCurrentMethod(), ref this.name, value);
}
}//method
}//class
Другими вещами, которые вы, возможно, захотите рассмотреть при реализации этих свойств, является тот факт, что INotifyPropertyChang * ed * ing используют классы аргументов аргументов.
Если у вас установлено большое количество свойств, количество экземпляров класса аргументов событий может быть огромным, вы должны рассмотреть их кеширование, поскольку они являются одной из областей, в которых может произойти взрыв строки.
Взгляните на эту реализацию и объяснение того, почему она была задумана.
Я предлагаю использовать 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>();
}
(DOCS)
Призма 5:
public abstract class BindableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual bool SetProperty<T>(ref T storage,
T value,
[CallerMemberName] string propertyName = null)
{
if (object.Equals(storage, value)) return false;
storage = value;
this.OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged(string propertyName)
{
var eventHandler = this.PropertyChanged;
if (eventHandler != null)
{
eventHandler(this, new PropertyChangedEventArgs(propertyName));
}
}
protected void OnPropertyChanged<T>(Expression<Func<T>> propertyExpression)
{
var propertyName = PropertySupport.ExtractPropertyName(propertyExpression);
this.OnPropertyChanged(propertyName);
}
}
public static class PropertySupport
{
public static string ExtractPropertyName<T>(Expression<Func<T>> propertyExpression)
{
if (propertyExpression == null)
{
throw new ArgumentNullException("propertyExpression");
}
var memberExpression = propertyExpression.Body as MemberExpression;
if (memberExpression == null)
{
throw new ArgumentException("The expression is not a member access expression.", "propertyExpression");
}
var property = memberExpression.Member as PropertyInfo;
if (property == null)
{
throw new ArgumentException("The member access expression does not access a property.", "propertyExpression");
}
var getMethod = property.GetMethod;
if (getMethod.IsStatic)
{
throw new ArgumentException("The referenced property is a static property.", "propertyExpression");
}
return memberExpression.Member.Name;
}
}
Я использую следующий метод расширения (используя С# 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); }
}
Я решил на этом пути (это немного рабочая, но это, безусловно, быстрее во время выполнения).
В VB (извините, но я думаю, что это не сложно перевести его на С#), я делаю эту замену 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-код установленного метода, но я не могу написать много скомпилированного кода в IL... Если в день я его напишу, я скажу вам!
Используйте этот
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;
}
}
}
= > здесь мое решение со следующими функциями
public ResourceStatus Status
{
get { return _status; }
set
{
_status = value;
Notify(Npcea.Status,Npcea.Comments);
}
}
Другая идея...
public class ViewModelBase : INotifyPropertyChanged
{
private Dictionary<string, object> _propertyStore = new Dictionary<string, object>();
protected virtual void SetValue<T>(T value, [CallerMemberName] string propertyName="") {
_propertyStore[propertyName] = value;
OnPropertyChanged(propertyName);
}
protected virtual T GetValue<T>([CallerMemberName] string propertyName = "")
{
object ret;
if (_propertyStore.TryGetValue(propertyName, out ret))
{
return (T)ret;
}
else
{
return default(T);
}
}
//Usage
//public string SomeProperty {
// get { return GetValue<string>(); }
// set { SetValue(value); }
//}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
var temp = PropertyChanged;
if (temp != null)
temp.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Я только что нашел ActiveSharp - Automatic INotifyPropertyChanged, я еще не использовал его, но он выглядит хорошо.
Процитировать с веб-сайта...
Отправить уведомления об изменении свойств без указания имени свойства как строка.
Вместо этого напишите свойства следующим образом:
public int Foo
{
get { return _foo; }
set { SetValue(ref _foo, value); } // <-- no property name here
}
Обратите внимание, что нет необходимости включать имя свойства в виде строки. ActiveSharp надежно и правильно определяет это для себя. Он работает на основе того факта, что ваша реализация свойства передает поле поддержки (_foo) по ссылке. (ActiveSharp использует вызов "по ref" для определения того, какое поле поддержки было передано, и из поля оно идентифицирует свойство).
Разговор о массовом перестройке. Это значительно сложнее, чем просто правильно это делает и дает мало пользы. Если ваша среда IDE поддерживает фрагменты кода (Visual Studio/MonoDevelop do), вы можете сделать реализацию этой смехотворно простой. Все, что вам нужно набрать, это тип свойства и имя свойства. Дополнительные три строки кода будут автогенерированы.