۱۳۹۴ فروردین ۷, جمعه

پیاده سازی الگوی NotifyPropertyChanged به کمک Attribute در Unity DI

پیشنیاز این مطلب، آشنایی با الگوی طراحی MVVM و آشنایی با مفاهیم تزریق وابستگی به کمک Unity است.

اگر از طرفداران الگوی طراحی MVVM در توسعه برنامه های مبتنی بر WPF باشید حتما مجبور شده اید برای کلاس های View Mode خود واسط INotifyPropertyChanged را پیاده سازی کنید. یکی از راه های معمول و ساده، پیاده سازی کلاسی پایه و پیاده سازی تابعی برای فراخوانی رویداد ProperyChanged است که نمونه ی آن را در زیر می بینید:

public abstract class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged(string propertyName)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
}

public class PersonViewModel : BaseViewModel
{
private string _fullName;
public string FullName
{
get
return _fullName; 
}
set
{
if(_fullName != value)
{
_fullName = value;
OnPropertyChanged("FullName");
}
}
}
}

ولی بهتر نمی شد اگر کلاس PersonViewModel ما به این شکل می شد؟:

public class MainViewModel : BaseViewModel
{
public string FullName { get; [Notify]set; }
}
و کلاس BaseViewModel به این شکل:

public class BaseViewModel : MarshalByRefObject, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
}

اگر با من موافق هستید دست به کار شوید. Visual Studio را باز کنید و یک پروژه MS Unit Test بسازید. سپس در Nuget Package Manager Console دستور زیر را وارد کنید تا همه آنچه لازم داریم را نصب کنیم:

Install-Package Unity.Interception

این دستور بسته Unity.Interception را نصب می کند. از آنجایی که این بسته نیازمند Unity است پس بسته Unity هم نصب می شود.
ابتدا کلاس BaseViewModel.cs را به شکل زیر اضافه کنید:

public class BaseViewModel : MarshalByRefObject, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
}
دقت کنید این کلاس حتما باید از MarshalByRefObject ارث بری کند. کاربرد اصلی این کلاس در Remoting است. هنگامی که یک Process درخواست فراخوانی تابعی از یک شیء در Process دیگر را دارد، در صورتی که آن نوع آن شیء از MarshalByRefObject ارث بری کرده باشد، به جای ارسال کپی این شیء، یک Proxy ایجاد شده و به Process فراخوان ارسال می شود. در ادامه متوجه خواهیم شد چرا به Proxy نیاز است.

اکنون نوبت به پیاده سازی ICallHandler می رسد. کلاسی با نام NotifyPropertyChangedCallHandler بسازید و کد زیر را در آن قرار دهید:

public class NotifyPropertyChangedCallHandler : ICallHandler
{
private readonly string PropertyName = null;
public NotifyPropertyChangedCallHandler(string propertyName, int order)
{
this.PropertyName = propertyName;
this.Order = order;
}

public NotifyPropertyChangedCallHandler()
: this(null, 0)
{
}

#region ICallHandler Members

public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
bool shouldRaise = ShouldRaiseEvent(input);
IMethodReturn result = getNext()(input, getNext);

if (result.Exception == null && shouldRaise)
{
if (String.IsNullOrWhiteSpace(PropertyName))
RaiseEvent(input);
else
RaiseEvent(PropertyName, input.MethodBase.DeclaringType, input.Target);
}

return result;
}

private bool ShouldRaiseEvent(IMethodInvocation input)
{
MethodBase methodBase = input.MethodBase;

//Is the method a property setter?
if (!methodBase.IsSpecialName || !methodBase.Name.StartsWith("set_"))
{
return false;
}

//Get the name of the property out so we can use it to raise a 
//property changed event
string propertyName = this.PropertyName ?? methodBase.Name.Substring(4);

//Retrieve the property getter
PropertyInfo property = methodBase.ReflectedType.GetProperty(propertyName);
MethodInfo getMethod = property.GetGetMethod();

//IF the property has no get method, we don't care
if (getMethod == null)
{
return false;
}

//Get the current value out
object oldValue = getMethod.Invoke(input.Target, null);

//Get the updated value
object value = input.Arguments[0];

//Is the new value null?
if (value != null)
{
//Is the new value different from the old value?
if (value.Equals(oldValue) == false)
{
return true;
}
}
else
{
//Is the new value (null) different from the 
//old value (non-null)?
if (value != oldValue)
{
return true;
}
}

return false;
}

private void RaiseEvent(IMethodInvocation input)
{
FieldInfo field = null;

//Get a reference to the PropertyChanged event out of the current 
//type or one of the base types
var type = input.MethodBase.ReflectedType;
while (field == null && type != null)
{
field = type.GetField("PropertyChanged", BindingFlags.Instance | BindingFlags.NonPublic);
type = type.BaseType;
}

//If we found the PropertyChanged event
if (field != null)
{
//Get the event handler if there is one
var evt = field.GetValue(input.Target) as MulticastDelegate;
if (evt != null)
{
//Get the property name out
string propertyName = input.MethodBase.Name.Substring(4);
//Invoke the property changed event handlers
evt.DynamicInvoke(input.Target, new PropertyChangedEventArgs(propertyName));
}
}
}

private void RaiseEvent(string propertyName, Type targetType, object targetInstance)
{
FieldInfo field = null;

//Get a reference to the PropertyChanged event out of the current 
//type or one of the base types
var type = targetType;
while (field == null && type != null)
{
field = type.GetField("PropertyChanged", BindingFlags.Instance | BindingFlags.NonPublic);
type = type.BaseType;
}

//If we found the PropertyChanged event
if (field != null)
{
//Get the event handler if there is one
var evt = field.GetValue(targetInstance) as MulticastDelegate;
if (evt != null)
{
//Invoke the property changed event handlers
evt.DynamicInvoke(targetInstance, new PropertyChangedEventArgs(propertyName));
}
}
}

public int Order
{
get;
set;
}

#endregion
}

مهمترین تابع این کلاس Invoke است که توسط Unity فراخوانی می شود. پارامتر input از نوع InvocationMethod تابعی از کد کاربر که قرار است فراخوانی شود را مشخص می کند. در مثال ما این تابع set_FullName خواهد بود. چرا؟ چون تابع set از مشخصه (Property) با نام FullName کلاس PersonViewModel را به NotifyAttribute مزین کرده ایم. در ادامه درباره NotifyAttribute خواهیم خواند. تا اینجا متوجه شده اید که قرار است تغییراتی در تابع set_FullName اتفاق بیوفتد. اجازه بدهید کد این تابع را مرور کنیم. در ابتدا تابع ShouldRaiseEvent فراخوانی می شود. اگر این تابع را برسی کنید متوجه می شوید ابتدا چک می کند که آیا واقعا تابع مورد نظر یک set هست یا خیر. در ادامه برسی می کند که آیا مقدار ارسالی به این مشخصه (Property) با مقدار موجود در آن متفاوت است یا خیر. دقیقا همان کاری که در حالت کلاسیک چک می کردیم. در صورتی که مقادیر متفاوت باشد مقدار true و در غیر این صورت مقدار false را برمی گرداند. بنابراین همانطور که از نامش مشخص بود می فهمیم آیا باید رویداد PropertyChanged را فراخوانی کنیم یا خیر. در ادامه خود تابع set فراخوانی خواهد شد:
IMethodReturn result = getNext()(input, getNext);

 یعنی همان عمل عادی قرار دادن value در مشخصه FullName. ولی چرا به این شکل فراخوانی شده؟ به کمک getNext در واقع Unity به دنبال یک ICallHandler احتمالی دیگر می رود و در صورت وجود آن را اجرا می کند. از انجایی که در مثال ما فقط یک ICallHandler وجود دارد (همان NotifyPropertyChangedCallHandler) پس بعد از آن خود تابع set اجرا می شود و مقدار جدید (فارغ از تکراری بودن یا نبودن) در مشخصه FullName قرار می گیرد.
در ادامه اگر خطایی رخ نداده بود، با برسی shouldRaise رویداد PropertyChanged را فراخوانی می کنیم. دقت کنید که دو نوع تابع RaiseEvent وجود دارد که نوع اول آن، رویداد PropertyChanged را با نام مشخصه ای که از پارامتر input گرفته فراخوانی می کند و نوع دوم از نامی که در تابع سازنده مشخص شده استفاده می کند.

حالا باید NotifyAttribute را تعریف کنیم. از این کلاس برای مزین کردن مشخصه هایی که می خواهیم PropertyChanged برای آن ها فراخوانی شود استفاده می کنیم. کد زیر را در این کلاس وارد کنید:

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Method, AllowMultiple = true)]
public class NotifyAttribute : HandlerAttribute
{
private readonly ICallHandler handler;
public string PropertyName { get; set; }
public NotifyAttribute()
{
}

public NotifyAttribute(string propertyName)
{
this.PropertyName = propertyName;
}

public NotifyAttribute(string propertyName, int order)
{
this.PropertyName = propertyName;
this.Order = order;
}

public override ICallHandler CreateHandler(IUnityContainer container)
{
if (string.IsNullOrWhiteSpace(PropertyName))
return new NotifyPropertyChangedCallHandler();
return new NotifyPropertyChangedCallHandler(PropertyName, Order);
}
}

چیز زیادی برای توضیح نیست. نام مشخصه مورد نظر توسط propertyName و ترتیب اجرا توسط order مشخص می شود. در صورتی که این پارامترها را وارد نکنیم، propertyName توسط پارامتر input در تابع Invoke بدست می آید و order نیز به طور پیش فرض مقدار 0 خواهد داشت. با اجرای CreateHandler توسط Unity در واقع ICallHandler مورد نظر را برای Unity مشخص می کنیم.

حالا به سراغ کلاس PersonViewModel بروید:

public class PersonViewModel : BaseViewModel
{
public string FullName { get; [Notify]set; }
public DateTime Birthday { get; [Notify(Order = 1)][Notify(PropertyName = "Age", Order = 2)]set; }
public int Age { get { return DateTime.Now.Year - Birthday.Year; } }
}

به مشخصه Birthday دقت کنید. با set شدن مقدار جدید به آن رویداد PropertyChanged برای مشخصه Age هم فراخوانی می شود. اکنون متوجه می شوید چرا در کلاس NotifyAttribute چند تابع سازنده داشتیم یا چرا در کلاس NotifyPropertyChangedCallHandler دو تابع RaiseEvent داشتیم. اکنون می توانیم با فرستادن نام و ترتیب اجرای مشخصه مورد نظر، نحوه فراخوانی رویداد PropertyChanged را کنترل کنیم.
اکنون وقت خروجی گرفتن است. کد زیر را در UnitTest بنویسید:

[TestClass]
public class PropertyChangesUnitTest
{
[TestMethod]
public void CheckPropertyChangedGetsCalledForPropertiesInAttributeWay()
{
using (var container = new UnityContainer())
{
container.AddNewExtension<Interception>();
container.RegisterType<PersonViewModel>(
new Interceptor<TransparentProxyInterceptor>(),
new InterceptionBehavior<PolicyInjectionBehavior>());

var propertyChanged = false;
var propertyNames = new List<string>();
var viewModel = container.Resolve<PersonViewModel>();

viewModel.PropertyChanged += (sender, e) =>
{
propertyChanged = true;
propertyNames.Add(e.PropertyName);
};

viewModel.FullName = "new value!";
Assert.IsTrue(propertyChanged, "PropertyChanged not called!");
Assert.IsTrue(propertyNames.Contains("FullName"));
propertyNames.Clear();

viewModel.Birthday = new DateTime(1990, 1, 1);
Assert.IsTrue(propertyChanged, "PropertyChanged not called!");
Assert.IsTrue(propertyNames.Contains("Birthday"));
Assert.IsTrue(propertyNames.Contains("Age"));
}
}
}

پس از نمونه سازی از UnityContainer، افزونه Interception را به آن اضافه می کنیم. درواقع تمام این عملیات به کمک Interception ها انجام می شود. به کمک Interception می توان کد کاربر را احاطه کرد و از بیرون بر روی نحوه اجرای قسمت های مختلف آن (همانند مثال ما) اعمال کنترل نمود. در ادامه هنگام ثبت کلاس PersonViewModel از دو کلاس TransparentProxyInterceptor و رفتار PolicyInjectionBehavior استفاده می کنیم. به کمک TransparentProxyInterceptor مشخص می کنیم که عملیات احاطه کردن کد (Interception) باید به کمک ساخت Proxy از کلاس کاربر انجام شود. حالا می توان دلیل استفاده از MarshalByRefObject را متوجه شد. در ادامه توسط PolicyInjectionBehavior مشخص می کنیم که Policy های ما به کمک attribute هایی در کلاس ثبت شده (در اینجا PersonViewModel) اعمال شده اند.

برای آشنایی بیشتر با Unity و Interception ها از اینجا شروع کنید.

هیچ نظری موجود نیست: