آشنایی با Mediator در WPF

Mediator کلاسی است که نمونه ای استاتیک و دیکشنری share شده (استاتیک ) ، را نگهداری میکند. در این مقاله از Mediator به عنوان رابطی بین ViewModel ها استفاده میشود.

آشنایی با Mediator در WPF

آکادمی برنامه نویسان ، برگزار کننده دوره های آموزش برنامه نویسی با استفاده از اساتید مجرب و حرفه ای در سراسر ایران .

[ جهت مشاهده دوره های درحال ثبت نام کلیک کنید ]

ارائه مدارک معتبر آموزشی و ورود به بازار کار .

 

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 در کد  که به هم در ارتباط هستند، راه اندازی کنید.

 

 

 

فایل های ضمیمه
دانلود نسخه ی PDF این مطلب