INotifyPropertyChanged 구현-더 나은 방법이 있습니까?
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
다른 논리를 적용하려는 경우 작동하지 않는지 여부 를 알려줍니다.
또는 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 :
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
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) 도 참조하세요.
저는 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 에서 영감을 얻었습니다.
다음과 같이 작성할 수있는 PropertyChanged 추가 기능 이있는 Fody 도 있습니다.
[ImplementPropertyChanged]
public class Person
{
public string GivenNames { get; set; }
public string FamilyName { get; set; }
}
... 그리고 컴파일 타임에 속성 변경 알림을 삽입합니다.
사람들은 성능에 조금 더주의를 기울여야한다고 생각합니다. 바인딩 할 개체가 많을 때 (1 만 개 이상의 행이있는 그리드를 생각하면) 또는 개체의 값이 자주 변경되는 경우 (실시간 모니터링 앱) UI에 실제로 영향을 미칩니다. .
여기와 다른 곳에서 발견 된 다양한 구현을 가져와 비교를 했고 INotifyPropertyChanged 구현의 성능 비교를 확인했습니다 .
다음은 결과를 살짝 보여줍니다.
내 블로그 http://timoch.com/blog/2013/08/annoyed-with-inotifypropertychange/ 에서 Bindable 클래스를 소개합니다. 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 검색 (내가이 글을 쓰기 전에 했어야했던)은 적어도 한 사람이 여기 전에 이와 같은 일을했음을 보여줍니다 . 내가 염두에 둔 것이 아니라 이론이 훌륭하다는 것을 보여줄만큼 가깝습니다.
예, 더 나은 방법은 확실히 존재합니다. 여기있어:
이 유용한 기사를 기반으로 단계별 자습서가 축소되었습니다 .
- 새 프로젝트 만들기
- 프로젝트에 캐슬 코어 패키지 설치
설치 패키지 Castle.Core
- mvvm light 라이브러리 만 설치
설치 패키지 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>();
- 즐겨.
주의!!! 모든 경계 속성은 캐슬 프록시에서 재정의를 위해 사용하기 때문에 가상 키워드로 장식되어야합니다.
매우 AOP와 유사한 접근 방식은 INotifyPropertyChanged 항목을 이미 인스턴스화 된 개체에 즉시 주입하는 것입니다. Castle DynamicProxy와 같은 방식으로이를 수행 할 수 있습니다. 다음은 기술을 설명하는 기사입니다.
기존 개체에 INotifyPropertyChanged 추가
독일어로 작성되었지만 ViewModelBase.cs를 다운로드 할 수 있습니다. cs-File의 모든 주석은 영어로 작성되었습니다.
이 ViewModelBase-Class를 사용하면 잘 알려진 Dependency Properties와 유사한 바인딩 가능한 속성을 구현할 수 있습니다.
public string SomeProperty
{
get { return GetValue( () => SomeProperty ); }
set { SetValue( () => SomeProperty, value ); }
}
Marc의 답변에서 채택 된 Thomas의 답변을 기반으로 반영 속성 변경 코드를 기본 클래스로 변경했습니다.
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);
}
}
사용법은 통지 할 추가 속성을 전달할 수 있다는 점을 제외하면 Thomas의 답변과 동일합니다. 그리드에서 새로 고쳐야하는 계산 된 열을 처리하는 데 필요했습니다.
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; } }
DataGridView를 통해 노출 된 BindingList에 저장된 항목 컬렉션을 구동합니다. 그리드에 대한 수동 Refresh () 호출을 수행 할 필요가 없습니다.
Yappi 라는 저만 의 접근 방식을 소개하겠습니다 . 런타임 프록시 | 파생 클래스 생성기에 속하며 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);
};
}
}
}
리팩토링에 완전히 안전하고 유형 생성 후 반사를 사용하지 않으며 충분히 빠릅니다.
이 모든 대답은 매우 좋습니다.
내 솔루션은 코드 조각을 사용하여 작업을 수행하는 것입니다.
이것은 PropertyChanged 이벤트에 대한 가장 간단한 호출을 사용합니다.
이 스 니펫을 저장하고 'fullprop'스 니펫을 사용할 때 사용하십시오.
위치는 Visual Studio의 'Tools \ Code Snippet Manager ...'메뉴에서 찾을 수 있습니다.
<?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>
원하는대로 통화를 수정할 수 있습니다 (위의 솔루션을 사용하기 위해).
.NET 4.5에서 역학을 사용하는 경우 .NET Framework에 대해 걱정할 필요가 없습니다 INotifyPropertyChanged
.
dynamic obj = new ExpandoObject();
obj.Name = "John";
Name이 일부 컨트롤에 바인딩되어 있으면 제대로 작동합니다.
또 다른 결합 솔루션은 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); }
}
}
재사용을 위해 기본 라이브러리에 확장 메서드를 만들었습니다.
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;
}
}
이것은 CallerMemberNameAttribute 때문에 .Net 4.5에서 작동합니다 . 당신은 이전 닷넷 버전으로 사용하려는 경우 당신은에서 메소드 선언을 변경해야합니다 : ...,[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);
}
}
}
나는 이것을 스 니펫으로 유지합니다. 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));
}
}
다음은 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에서 패턴 / 검색 스 니펫을 생성하면 간단한 prop 필드를 위의 지원으로 변환하여 워크 플로를 자동화 할 수도 있습니다.
검색 패턴 :
public $type$ $fname$ { get; set; }
패턴 교체 :
public $type$ $fname$
{
get { return Get<$type$>(); }
set { Set(value); }
}
이에 도움이되는 기사를 작성했습니다 ( 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)
}
혜택:
- 기본 클래스는 선택 사항입니다.
- 모든 '설정된 값'에 대한 반영 없음
- 다른 속성에 의존하는 속성을 가질 수 있으며 모두 자동으로 적절한 이벤트를 발생시킵니다 (문서에 이에 대한 예가 있음).
이러한 종류의 속성을 구현할 때 고려해야 할 다른 사항은 INotifyPropertyChang * ed * ing 둘 다 이벤트 인수 클래스를 사용한다는 사실입니다.
설정되는 속성이 많으면 이벤트 인수 클래스 인스턴스의 수가 엄청날 수 있습니다. 문자열 폭발이 발생할 수있는 영역 중 하나이므로 캐싱을 고려해야합니다.
이 구현과 그것이 왜 고안되었는지에 대한 설명을 살펴보십시오.
방금 ActiveSharp-Automatic INotifyPropertyChanged를 찾았 지만 아직 사용하지 않았지만 괜찮아 보입니다.
웹 사이트에서 인용하려면 ...
속성 이름을 문자열로 지정하지 않고 속성 변경 알림을 보냅니다.
대신 다음과 같은 속성을 작성하십시오.
public int Foo
{
get { return _foo; }
set { SetValue(ref _foo, value); } // <-- no property name here
}
속성 이름을 문자열로 포함 할 필요가 없습니다. ActiveSharp는이를 안정적이고 정확하게 파악합니다. 속성 구현이 ref에 의해 지원 필드 (_foo)를 전달한다는 사실을 기반으로 작동합니다. (ActiveSharp는 "by ref"호출을 사용하여 전달 된 지원 필드를 식별하고 필드에서 속성을 식별합니다.)
반사를 사용한 아이디어 :
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
나는 This Way로 해결했습니다 (조금 힘들지만 런타임에서는 확실히 빠릅니다).
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
이 transofrm 모든 코드는 다음과 같습니다.
<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}
나는 set 메소드의 IL 코드를 교체하려고 던졌지 만 IL에서 컴파일 된 코드를 많이 쓸 수는 없다. 하루 만에 작성하면 말하겠다!
나는이 질문에 이미 무궁무진 한 답이 있다는 것을 알고 있지만 그들 중 어느 것도 나에게 옳다고 생각하지 않았습니다. 내 문제는 성능 히트를 원하지 않으며 그 이유만으로도 약간의 자세한 내용을 기꺼이 참을 수 있다는 것입니다. 나는 또한 자동 속성에 대해 너무 신경 쓰지 않아서 다음과 같은 해결책을 찾았습니다.
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()
{
}
}
장점
- 반사 없음
- 이전 값! = 새 값인 경우에만 알립니다.
- 한 번에 여러 속성에 알림
단점
- 자동 속성 없음 (두 가지 모두에 대한 지원을 추가 할 수 있습니다!)
- 약간의 장황함
- 권투 (작은 성능 히트?)
아아,이 일보다 여전히 낫습니다.
set
{
if (!someProperty.Equals(value))
{
someProperty = value;
OnPropertyChanged("SomeProperty");
OnPropertyChanged("AnotherProperty");
}
}
모든 단일 속성에 대해 추가 장황함으로 악몽이됩니다 ;-(
이 솔루션이 다른 솔루션에 비해 성능면에서 더 우수하다고 주장하지는 않으며 제시된 다른 솔루션을 좋아하지 않는 사람들에게 실행 가능한 솔루션이라는 점에 유의하십시오.
관찰 가능한 패턴을 구현하기 위해이 기본 클래스를 생각해 냈습니다. 필요한 작업을 거의 수행합니다 (세트를 "자동으로" 구현하고 가져옵니다). 프로토 타입으로 한 시간을 보냈기 때문에 단위 테스트는 많지 않지만 개념을 증명합니다. 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); }
}
}
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>();
}
( 문서 )
AOP 매직 답변을 제외하고는 분명히 많은 방법이 있지만 참조 할 로컬 필드가없는 뷰 모델에서 직접 모델의 속성을 설정하는 방법은 없습니다.
문제는 속성을 참조 할 수 없다는 것입니다. 그러나 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 repo 에서 메서드의 전체 구현과 LINQ를 사용하는 메서드와 리플렉션을 사용하는 메서드를 포함하여 동일한 결과를 얻는 몇 가지 다른 방법을 확인하세요. 이러한 방법은 성능 측면에서 더 느립니다.
또 다른 아이디어 ...
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));
}
}
=> 여기 다음과 같은 기능을 가진 내 솔루션
public ResourceStatus Status
{
get { return _status; }
set
{
_status = value;
Notify(Npcea.Status,Npcea.Comments);
}
}
- 반사 없음
- 짧은 표기법
- 비즈니스 코드에 마술 문자열이 없습니다.
- 응용 프로그램에서 PropertyChangedEventArgs의 재사용 가능성
- 하나의 명령문에서 여러 속성을 알리는 가능성
이것을 사용하십시오
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;
}
}
}
'Program Tip' 카테고리의 다른 글
현재 변경 사항을 git [duplicate]의 다른 지점에 커밋하는 방법 (0) | 2020.10.02 |
---|---|
jQuery 각 루프에서 벗어나는 방법 (0) | 2020.10.02 |
Java에서 폴더의 모든 파일을 읽는 방법은 무엇입니까? (0) | 2020.10.02 |
iOS7에서 UITableViewStyleGrouped 스타일로 UITableView 상단에 추가 패딩이있는 이유는 무엇입니까? (0) | 2020.10.02 |
분기는 어떻게 만듭니 까? (0) | 2020.10.02 |