آشنایی با Mediator در WPF
پنجشنبه 26 فروردین 1395Mediator کلاسی است که نمونه ای استاتیک و دیکشنری share شده (استاتیک ) ، را نگهداری میکند. در این مقاله از Mediator به عنوان رابطی بین ViewModel ها استفاده میشود.
ViewModel یک کلاس است که همه کارهای پر حجم را انجام می دهد. این همان قسمتی است که دیتابیس را فراخوانی می کند، یک observablecollection می سازد، و هر کد Back endای که نیاز به پیاده سازی آن است ،را دارد .
Mediator اساسا یک کلاس است که دارای نمونه ای به اشتراک گذاشته شده (استاتیک) و دیکشنری به اشتراک گذاشته (استاتیک) که دارای تمام آیتم های متصل شده است را نگهداری میکند . در Cinch شما می توانید با یک "رویداد" در یک پروپرتی، یک Delegate با void در ViewModel دیگر ارسال کنید. شکل زیر پیاده سازی آن را نشان می دهد
یک مشکل کوچک
رابطه ی موجود بین Property ها در WPF می تواند به راحتی و با استفاده از رویدادهای مناسب ، که به آن ها PropertyDescriptor می گوییم، حل شوند . مشکلی که در استفاده از آن ها وجود دارد این است که در داخل mediator هر بار که رویداد به property ای متصل می شود که شما می خواهید با آن کار کنید، یک رابطه ی قوی بین Mediator و ViewModel ای که property از آن آمده است، به وجود می آید، این عمل، دقیقا مشابه اتصالی است که بین ViewModel و View برقرار است. این اتفاق باعث می شود تا garbage collector (زباله جمع کن) تصور کند که کلاس موردنظر هنوز هم در حال استفاده است ، بنابراین نمی تواند آن را بردارد و فضای اشغال شده را کاهش بدهد.
مشکلی که در استفاده از ViewModel ها در MVVM وجود دارد این است که ViewModel می تواند ارتباط خودش با View را بدون این که اطلاعاتی در این زمینه به Mediator ارسال بشود، قطع کند. این عمل باعث می شود تا ViewModel بدون استفاده ، همراه با یک handle و یا یک Property در حافظه باقی بماند، و ارتباط قدرتمندی نیز در این ناحیه ایجاد شود. اگر شما چندین بار این عمل را با یک ViewModel یکسان تکرار کنید ، به زودی کلاس های بی شماری خواهید داشت که بدون استفاده هستند ولی بر روی حافظه کامپیوتر شما باقی مانده اند و در حال کاهش عمر آن هستند.
راه حل این مشکلات ، به کارگیری WeakReferance و WeakEvents در داخل Mediator است. این الگو اساسا اینطور رفتار می کند: به garbage collector اعلام می کند که من به این شی متصل هستم ، اما در صورت نیاز، می توانی آن را پاک کنی و حافظه را آزاد کنی. بنابراین اگر هیچ گونه weak event و یا weak references ای به کلاس مورد نظر متصل نباشد، garbage collector می تواند آن کلاس را از حافظه پاک کند.
به یاد داشته باشید که استفاده از WeakEvent ها به این معنی نیست که همه ی مشکلات برطرف شده اند، شما هنوز هم باید در استفاده از WeakEvent ها مثل رویدادهای عادی دقت داشته باشید .
WeakEvent یک ViewModel نرمال کاملا ساده برای پیاده سازی است، و به صورت عادی عمل میکند.
public class StudentViewModel:NotifierBase { public StudentViewModel() { StudentViewModel student = new StudentViewModel(); WeakEventManager<StudentViewModel, PropertyChangedEventArgs>.AddHandler(student, "PropertyChanged", newStudent_PropertyChanged); WeakEventManager<StudentViewModel, PropertyChangedEventArgs>.RemoveHandler(student, "PropertyChanged", newStudent_PropertyChanged); } void newStudent_PropertyChanged(object sender, PropertyChangedEventArgs e) { if (e.PropertyName.Equals("Name")) { } } private string m_Name = "Name"; public string Name { get { return m_Name; } set { SetProperty(ref m_Name, value); } } }
اما کار با reflection چندان ساده نیست.. اول از همه شما نیاز به ایجاد یک type mold برای مشخص کردن تعداد ورودی WeakEventManager دارید، و پس از آن نوع آرگومان خواسته شده را برمیگرداند
Type unboundWEMType = typeof(WeakEventManager<,>); Type[] typeArgs = { _sender.Target.GetType(), typeof(EventArgs) }; Type constructedWEMType = unboundWEMType.MakeGenericType(typeArgs);
استفاده از نوع کلی (generic type) :
WeakEventManager<SenderType, EventArgs>
در این قسمت با مشکلاتی روبرو میشویم که احتمالا مربوط به استفاده از چند نوع generic که با هم در EventArgs تداخل دارند ، ناشی میشود. اگر بخواهیم از PropertyChangedEventArgs استفاده کنیم ،با خطایی روبرو میشویم ، که میگوید قادر به cast کردن آن نیست.این بدان معنی است که مجبور به تغییرات، EventArgs در PropertyChangedEventArgs هستیم
EventHandler<EventArgs> handler = new EventHandler<EventArgs>(WeakPropertyDescriptor_PropertyChanged); private void WeakPropertyDescriptor_PropertyChanged(object sender, EventArgs e) { PropertyChangedEventArgs arg = (PropertyChangedEventArgs)e; if (arg.PropertyName == _PropertyName) { ... } }
با استفاده از MethodInfo متدهای AddHandler و RemoveHandler را ایجاد میکنیم.
MethodInfo addHandlerMethod = constructedWEMType.GetMethod("AddHandler"); addHandlerMethod.Invoke(null, new object[] { _sender.Target, "PropertyChanged", handler });
پیاده سازیWeakReferance بسیار آسان است:
WeakReference WeakSender = WeakReferance(Sender);
همچنین object یی که ارسال کننده WeakSender.Target
را نگهداری میکرد ، یک property WeakSender.IsAlive در صورتی که شی اصلی ، در شی دیگر bound شود ، را نشان می دهد.
چگونه از آن در ViewModel استفاده کنیم
Mediator را در کلاسی که می خواهید از آن استفاده کنید ، ریجستر کنید:
public class A : NotifierBase { public A() { Mediator.Instance.Register(this); } ... }
این Mediator ، برحسب حلقه از طریق تمام متدها پروپرتی در کلاس و برای دیدن هر آیتم مشخص شده توسط ویژگی MediatorConnection قادر میسازد
این عمل Mediator را قادر میسازد که همه پروپرتی ها و متدها را وارد یک حلقه (loop ) کند و دنبال آیتم هایی که بوسیله ی MediatorConnection مشخص شده اند، میگردد.
[AttributeUsage(AttributeTargets.Method|AttributeTargets.Property)] public sealed class MediatorConnection : Attribute { /// <summary> /// Message key /// </summary> public string MessageKey { get; private set; } /// <summary> /// Update Async /// </summary> public bool SendAsync { get; private set; } /// <summary> /// Default constructor /// </summary> public MediatorConnection() { MessageKey = null; SendAsync = false; } /// <summary> /// Constructor that takes a message key /// </summary> /// <param name="messageKey">Message Key</param> public MediatorConnection(string messageKey,bool sendAsync = true) { MessageKey = messageKey; SendAsync = sendAsync; } }
همانطور که می بینیدبه آن property با یک رشته شناسه منحصر به فرد به نام MessageKey میدهیم، و برای اتصال پروپرتی و یا متدهای های مختلف در داخل Mediator از آن استفاده کنید. آن نیز دارای یک آرگومان اختیاری برای اجازه دادن پروپرتی به ارسال async است، که آن به طور پیش فرض با true تنظیم شده است.
property در ClassA به شرح زیر اجرا میشود (با به روز رسانی async از propertie دیگرفعال شده است)
private string m_Name = "Class A"; [MediatorConnection("NameMessenge")] public string Name { get { return m_Name; } set { SetProperty(ref m_Name, value); } }
توجه داشته باشید که property به روز رسانی شده ، نیاز به پیاده سازی INotifyPropertyChanged برای واکنش به تغییرات Mediator ،و پیاده سازی SetProperty برای یک زیرروال کمکی دارد. شما همچنین باید مطمئن شوید که رویداد INotifyPropertyChanged ، ء firedنباشد .اگر مقداربا مقدار فعلی برابر باشد، از آن برای جلوگیری از خصوصیت ارسال مجدداستفاده میشود.
آخرین چیزی که شما می توانید انجام دهید، لغو اشتراک Mediator در Dispose است:
~A() { Mediator.Instance.Unregister(this); }
این Mediator فقط برای حذف ،بلافاصله بعد از تماشای لیست است .
در داخل Mediator
Mediator به شدت متکی بر استفاده از reflection با مقداردهی اولیه weak handler ها است. اول از همه از طریق حلقه، تمام خواص و متدهای کلاس ریجستر میشوند ، و اگر آنها با یک صفت MediatorConnection
ساخته شده باشند ،باید تمام اشیاء لازم از این مثال در دیکشنری ذخیره شوند، مهم این است که صفات MessageKey برابر key است
public void Register(object view) { // Look at all instance/static properties on this object type. foreach (var mi in view.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)) { // var test = mi.GetCustomAttributes(typeof(PropertyMediatorAttribute)); // See if we have a target attribute - if so, register the method as a handler. foreach (var att in mi.GetCustomAttributes(typeof(PropertyMediatorAttribute))) { var mha = (PropertyMediatorAttribute)att; RegisterProperty(mha.MessageKey, view, mi.Name, mha.SendAsync); } } }
دیکشنری که در آن متغیرها ذخیره می شود استاتیک است، و مقادیر یک لیست، از کلاس داخلی به نام WeakMediatorHelper هستند. همه آیتم ها با همان MessageKey در لیست WeakMediatorHelper اضافه میشوند
private readonly Dictionary<string, List<WeakPropertyDescriptor>> _registeredListners = new Dictionary<string, List<WeakPropertyDescriptor>>();
از آنجایی که این دیکشنری بین تمام PropertyMediator مشترک است، قفل کردن دیکشنری در زمان انجام کاری مهم است:
lock (_registeredListners)
این کار از انجام هر تغییری در نمونه های دیگر ، تا زمانی که قفل را آزاد کنید، جلوگیری می کند .
کلاس زیر فقط پیاده سازی بخشی از پروپرتی که به عنوان Mediator اصلی دارای کد برای متد Delegate است ،را نشان می دهد
/// <summary> /// Internal class that is able to hold referances to a method or a property. /// </summary> internal class WeakMediatorHelper { // Varibales used by either Property or Method calls private Mediator _MediatorInstance; private WeakReference _WeakBoundObject; private Type SendOrListenType; private string _MessageKey; private bool _SendAsync; private bool _IsProperty; // Property variables private string _PropertyName; private EventHandler<EventArgs> handler; // Method variables private Type _MessengerType; private string _methodName; /// <summary> /// From a property call /// </summary> /// <param name="MethodOwner">This is a strong referance to the Mediator</param> /// <param name="sender">The class that owns the property</param> /// <param name="PropertyName"></param> /// <param name="PropertyType"></param> /// <param name="MessageKey"></param> /// <param name="SendAsync"></param> public WeakMediatorHelper(Mediator MethodOwner, Object sender, string PropertyName, Type PropertyType, string MessageKey, bool SendAsync) { _MediatorInstance = MethodOwner; _WeakBoundObject = new WeakReference(sender); _PropertyName = PropertyName; _MessageKey = MessageKey; _SendAsync = SendAsync; _IsProperty = true; _MessengerType = PropertyType; handler = new EventHandler<EventArgs>(WeakMediatorHelper_PropertyChanged); Type unboundWEMType = typeof(WeakEventManager<,>); Type[] typeArgs = { _WeakBoundObject.Target.GetType(), typeof(EventArgs) }; SendOrListenType = unboundWEMType.MakeGenericType(typeArgs); this.AddHandler(); } /// <summary> /// From a method call /// </summary> /// <param name="target"></param> /// <param name="actionType"></param> /// <param name="mi"></param> public WeakMediatorHelper(object target, Type actionType, MethodBase mi) { _IsProperty = false; if (target == null) { Debug.Assert(mi.IsStatic); SendOrListenType = mi.DeclaringType; } _WeakBoundObject = new WeakReference(target); _methodName = mi.Name; _MessengerType = actionType; LastMessage = new object(); } public object LastMessage { get; set; } public bool IsProperty { get { return _IsProperty; } } public Type MessengerType { get { return _MessengerType; } } public string PropertyName { get { return _PropertyName; } } public WeakReference WeakBoundObject { get { return _WeakBoundObject; } } public bool HasBeenCollected { get { if (IsProperty) return (_WeakBoundObject == null || !_WeakBoundObject.IsAlive); else return (SendOrListenType == null && (_WeakBoundObject == null || !_WeakBoundObject.IsAlive)); } } private void WeakMediatorHelper_PropertyChanged(object sender, EventArgs e) { PropertyChangedEventArgs arg = (PropertyChangedEventArgs)e; if (arg.PropertyName == _PropertyName) { if (_SendAsync) _MediatorInstance.NotifyColleaguesOfVauleChangedAsync(_WeakBoundObject.Target, e, _MessageKey, _PropertyName); else _MediatorInstance.NotifyColleaguesOfValueChanged(_WeakBoundObject.Target, e, _MessageKey, _PropertyName); } } public void AddHandler() { MethodInfo addHandlerMethod = SendOrListenType.GetMethod("AddHandler"); addHandlerMethod.Invoke(null, new object[] { _WeakBoundObject.Target, "PropertyChanged", handler }); } public void RemoveHandler() { MethodInfo removeHandlerMethod = SendOrListenType.GetMethod("RemoveHandler"); removeHandlerMethod.Invoke(null, new object[] { _WeakBoundObject.Target, "PropertyChanged", handler }); } public Delegate GetMethod() { if (SendOrListenType != null) { if (_methodName == null) return null; else return Delegate.CreateDelegate(_MessengerType, SendOrListenType, _methodName); } if (_WeakBoundObject != null && _WeakBoundObject.IsAlive) { object target = _WeakBoundObject.Target; if (target != null) return Delegate.CreateDelegate(_MessengerType, target, _methodName); } return null; } }
کد فقط در Mediator ، برای به روز رسانی دیکشنری توسط افزودن و حذف المنتها یا کلیدها، و همچنین به روز رسانی متدهای متصل شده و propertyها وجود دارد.
چند قابلیت برای تست و راهنمایی
کد در MainWindow یک سناریو اتصال اساسی را بین ViewModel و View را شبیه سازی میکند
public partial class MainWindow : Window { ViewModelA A = new ViewModelA(); ViewModelB B = new ViewModelB(); public MainWindow() { InitializeComponent(); // Bind the class to a TextBox // This simulates a ViewModel binded to a View, // and will keep the WeakReferance.IsAlive to true View1.DataContext = A; View2.DataContext = B; // This object will be removed from the Mediator // when a property changed event is fired // WeakReferance.IsAlive property will be false on this instance ViewModelB C = new ViewModelB(); } private void btn_Click(object sender, RoutedEventArgs e) { // Force a garbage collecter to clean up GC.Collect(); } }
برای ورودی هرکاربر،از پروپرتی connection ،که میتواند در ViewModel های مختلف رخ دهد، استفاده می شود،و پس از آن، از made sense برای به روز رسانی و یا اتصال دو پروپرتی با هم استفاده میشود. . شما باید هنگام استفاده از این در برنامه، به راحتی بتوانید دو ویژگی competing در کد که به هم در ارتباط هستند، راه اندازی کنید.
- WPF
- 2k بازدید
- 1 تشکر