MVVM
MVVM Light Messenger 深入剖析
這一系列的模型-視圖-ViewModel (MVVM) 模式和使用 MVVM 光工具組已覆蓋很多地面,自從我開始差不多一年前,從使用 IOC 容器使用 MVVM 在中應用的方法來處理跨執行緒訪問和使用 MVVM 光的 DispatcherHelper 元件。 我也談到指揮 (與 RelayCommand 和 EventToCommand),查看服務,例如導航和對話方塊的服務,並簡要地討論了信使元件。
信使元件是實際上相當強大的元素的使用 MVVM 光工具組,其中一個經常引誘開發商由於其易用性,但也因為它可以創建如果它被濫用的風險引發了一些爭議。 此元件應該得到它自己的文章解釋它是如何工作、 什麼是風險,為其它最有意義的方案。
在本文中,我會討論背後的使者執行的一般原則,看看為什麼這種實現是較傳統的辦法比便於使用。 我還會探討如何這種方法可能會影響記憶體如果不採取某些預防措施。 最後,我將討論使用 MVVM 光明使者本身更詳細,特別是一些內置的郵件和它們的使用。
事件聚合和信使的簡化
像信使系統有時被命名事件巴士或事件聚合器。 此類元件連線寄件者和接收器 (有時稱為"發佈伺服器"和"訂閱伺服器,"分別)。 使用 MVVM 光被創建時,多個郵件系統所需收件者或寄件者,執行具體的方法。 例如,可能有一個 IReceiver 介面,指定一個接收的方法,和要註冊的消息傳遞系統,物件就必須實現此介面。 這種約束是令人討厭,因為它限制誰可以實際使用的郵件系統。 例如,如果您在使用一個協力廠商程式集,不能註冊實例從這個圖書館與消息傳遞系統,因為您沒有存取碼,不能修改協力廠商類來實現 IReceiver。
使用 MVVM 光明使者被創建以精簡這種情況下用一個簡單的前提:任何物件可以是一個接收器 ; 任何物件可以將寄件者 ; 任何物件可以是一條消息。
詞彙,也被簡化。 而不是使用字眼"事件聚合",很難去定義,說是有關消息,這是很容易理解。 訂閱伺服器上成為接收器和發佈伺服器成為寄件者。 而不是事件,沒有消息。 這些簡化的語言和簡化的實施,使易於入門信使和理解它是如何工作。
例如,請考慮中的代碼圖 1。 正如您所看到的在兩個單獨的物件正在使用 MVVM 光明使者。 註冊物件將消息發送到 RegisteredUser 的所有實例。 這種情況可以以多種方式實現和使者,不一定總是最好的解決辦法。 但是,根據您的體系結構,它可能是一個很好的工具來實現此功能,特別是如果發送方和接收方的應用程式應保持分離的部分。 請注意如何註冊實例不會顯式發送到 RegisteredUser 實例。 相反,它廣播通過信使消息。 任何實例可以註冊此類型的消息,併發送時通知。 在此示例中,發送的消息是一個 RegistrationInfo 實例。 但是,可以發送任何類型的消息,從簡單的值 (int、 bool 等等) 的專用的消息物件。 稍後我會討論使用消息和審查中的某些內置訊息類型使用 MVVM 光。
圖 1 發送和接收消息
public class Registration
{
public void SendUpdate()
{
var info = new RegistrationInfo
{
// ...
Some properties
};
Messenger.Default.Send(info);
}
}
public class RegisteredUser
{
public RegisteredUser()
{
Messenger.Default.Register<RegistrationInfo>(
this,
HandleRegistrationInfo);
}
private void HandleRegistrationInfo(RegistrationInfo info)
{
// Update registered user info
}
}
public class RegistrationInfo
{
// ...
Some properties
}
中的代碼圖 1 顯示該註冊為一種訊息類型 (RegistrationInfo) 通過一個委託 (HandleRegistrationInfo)。 這是 Microsoft.NET 框架中的共同機制。 例如,在 C# 中註冊事件處理常式也是通過將委託傳遞給事件,命名的方法或匿名 lambda 運算式。 同樣,您可以使用命名的方法或匿名 lambda 註冊一個接收器的使者,如中所示圖 2。
圖 2 註冊與命名的方法或 Lambda
public UserControl()
{
InitializeComponent();
// Registering with named methods ----
Loaded += Figure2ControlLoaded;
Messenger.Default.Register<AnyMessage>(
this,
HandleAnyMessage);
// Registering with anonymous lambdas ----
Loaded += (s, e) =>
{
// Do something
};
Messenger.Default.Register<AnyMessage>(
this,
message =>
{
// Do something
});
}
private void HandleAnyMessage(AnyMessage message)
{
// Do something
}
private void Figure2ControlLoaded (object sender, RoutedEventArgs e)
{
// Do something
}
跨執行緒訪問
使者,不做一件事是的手錶哪個執行緒發送一條消息。 如果你讀了我以前的文章,"多執行緒處理和調度中使用 MVVM 應用程式"(bit.ly/1mgZ0Cb),你知道一些預防措施需要採取在一個執行緒上運行的物件試圖訪問屬於另一個執行緒的物件時。 一個後臺執行緒和 UI 執行緒所擁有的控制項之間經常會出現這個問題。 在上一篇文章中,您看到了如何使用 MVVM 光 DispatcherHelper 可用於"派遣"在 UI 執行緒上的運行,並避免跨執行緒訪問的異常。
一些事件聚合器,讓你自動調度到 UI 執行緒發送消息。 使用 MVVM 光明使者從來沒有這樣做,但是,由於簡化的信使 API 的願望。 添加一個選項來自動調度到 UI 執行緒消息會將更多的參數添加到註冊方法。 此外,它將使調度不太明確,經驗較淺的開發人員理解正在發生被子可能更難。
相反,你應該明確調度到 UI 執行緒的消息,如果需要。 這樣做的最佳方法是使用 MVVM 光 DispatcherHelper。 前一篇文章中所示,CheckBeginInvokeOnUI 方法將派遣操作,只有在必要時。 如果使者,已經在 UI 執行緒上運行的可以不帶調度立即分發郵件:
public void RunOnBackgroundThread()
{
// Do some background operation
DispatcherHelper.CheckBeginInvokeOnUI(
() =>
{
Messenger.Default.Send(new ConfirmationMessage());
});
}
記憶體處理
每個系統,它允許物件進行通信無需知道對方的面孔不必保存對該郵件的收件者的引用的艱巨任務。 例如,請考慮的.NET 事件處理系統可以創建之間引發事件的物件和訂閱事件的物件的強引用。 中的代碼圖 3 強之間創建連結 _first 和 _second。 這是什麼意思是,如果清除方法被調用,並且 _second 設置為 null,垃圾回收器不能它從記憶體中刪除,因為 _first 仍有對它的引用。 要知道,如果它可以刪除從記憶體,這不會發生的第二個實例,因此,記憶體洩漏創建的物件的引用計數依賴垃圾回收器。 隨著時間的推移,這可能會導致很多問題 ; 應用程式可能會顯著減慢,最終它可以甚至崩潰。
圖 3Strong引用實例之間
public class Setup
{
private First _first = new First();
private Second _second = new Second();
public void InitializeObjects()
{
_first.AddRelationTo(_second);
}
public void Cleanup()
{
_second = null;
// Even though this is set to null, the Second instance is
// still kept in memory because the reference count isn't
// zero (there's still a reference in _first).
}
}
public class First
{
private object _another;
public void AddRelationTo(object another)
{
_another = another;
}
}
public class Second
{
}
為了減輕這,.NET 開發人員出了 WeakReference 物件。 此類允許對要以"弱"的方式存儲的物件的引用。 如果對該物件的所有其他引用都設置為 null,垃圾回收器可以仍然收集該物件,即使有 WeakReference 使用它。 這是非常方便,而且時明智地使用,它可以減輕記憶體洩漏的問題,儘管它始終不能解決所有的問題。 為了說明這一點, 圖 4 顯示了一個簡單的通信系統在其中的 SimpleMessenger 物件存儲對接收器在 WeakReference 的引用。 在處理消息之前,請注意到的 IsAlive 屬性複選。 如果接收器已刪除和垃圾收集之前,IsAlive 屬性將為 false。 這是一個標誌 WeakReference 不再有效,應予刪除。
圖 4 使用 WeakReference 實例
public class SuperSimpleMessenger
{
private readonly List<WeakReference> _receivers
= new List<WeakReference>();
public void Register(IReceiver receiver)
{
_receivers.Add(new WeakReference(receiver));
}
public void Send(object message)
{
// Locking the receivers to avoid multithreaded issues.
lock (_receivers)
{
var toRemove = new List<WeakReference>();
foreach (var reference in _receivers.ToList())
{
if (reference.IsAlive)
{
((IReceiver)reference.Target).Receive(message);
}
else
{
toRemove.Add(reference);
}
}
// Prune dead references.
// Do this in another loop to avoid an exception
// when modifying a collection currently iterated.
foreach (var dead in toRemove)
{
_receivers.Remove(dead);
}
}
}
}
使用 MVVM 光明使者是建立在大致相同的原則,雖然它是,當然,很多更複雜 ! 值得注意的是,因為使者,不要求接收器來實現任何給定的介面,它需要存儲到將用於傳輸消息的方法 (回檔) 的引用。 在Windows Presentation Foundation(WPF) 和 Windows 運行時,這不是問題。 在 Silverlight 和 Windows Phone,然而,該框架是更安全,Api 來防止某些操作的發生。 這些限制之一命中信使系統在某些情況下。
要理解這一點,你需要知道可以註冊什麼樣的方法來處理消息。 若要匯總,接收的一種方法可以是靜態,永遠不是一個問題 ; 或者它可以是一個實例方法,在這種情況下您區分公共、 內部和私有。 在許多情況下,接收的一種方法是一個匿名 lambda 運算式,它是一個私有方法相同。
當方法是靜態的或公共的時沒有危險,造成記憶體洩漏。 時的處理方法是內部的還是私有 (或匿名 lambda),它可以在 Silverlight 和 Windows Phone 的風險。 不幸的是,在這些情況下有沒有辦法為信使使用 WeakReference。 再次,這不是 WPF 或 Windows 運行時的一個問題。 圖 5 總結了這一資訊。
圖 5 的不登出的情況下記憶體洩漏的風險
Visibility | WPF | Silverlight | Windows Phone 8 | Windows 執行階段 |
靜態 | 無風險 | 無風險 | 無風險 | 無風險 |
Public | 無風險 | 無風險 | 無風險 | 無風險 |
內部 | 無風險 | Risk - 風險 | Risk - 風險 | 無風險 |
不公開 | 無風險 | Risk - 風險 | Risk - 風險 | 無風險 |
匿名 Lambda | 無風險 | Risk - 風險 | Risk - 風險 | 無風險 |
它是重要的是要注意到,即使有風險所示圖 5,未能登出並不總是帶來的記憶體洩漏。 那說,以確保沒有記憶體洩漏引起,它是好的做法,要當他們是不不再需要顯式登出從信使接收器。 這可以通過使用取消註冊的方法。 請注意有多個重載的登出。 接收方就可以完全未註冊的使者,或您可以選擇要登出只有一個給定的方法,但要保持他人處於活動狀態。
其他風險時使用信使
正如我指出的雖然 MVVM 光明使者就是一個非常強大和靈活的元件,它是重要的是要記住在使用它有一些風險。 我已提到在 Silverlight 和 Windows Phone 的潛在的記憶體洩漏。 另一個風險是不少技術:使用信使將分離的太多它可能很難明白到底怎麼回時發送和接收消息的物件。 對於經驗不足的開發人員從未使用過事件匯流排之前,它可能很難遵循業務流程。 例如,如果你踩到一個方法調用,並且此方法調用 Messenger.Send 方法,調試的流量會丟失,除非你知道若要搜索相應的 Messenger.Receive 方法,有放置中斷點。 儘管如此,信使操作是同步的和你瞭解使者如何工作,它是否仍有可能進行調試此流。
我傾向于使用信使作為"最後手段",當更傳統的程式設計技術或者是不可能或造成太多的依賴關係,我想要保持作為解耦的應用程式部件之間盡可能。 有時,然而,它是最好使用其他工具例如 IOC 容器和服務,以更明確的方式實現類似的結果。 談到國際奧會和視圖服務在本系列的第一條 (bit.ly/1m9HTBX)。
一個或多個信使
消息傳遞系統,如使用 MVVM 光明使者就是他們可以使用甚至跨程式集的優點之一 — — 在外掛程式的情況下,例如。 這是一個通用的體系結構為構建大型應用程式,尤其是在 WPF 中。 但一個外掛程式系統也可用於較小的應用程式,可以輕鬆地添加新的功能,而不必重新編譯的主要部分,例如。 儘快在該應用程式的應用程式域中載入 DLL,它包含的類可以使用 MVVM 光明使者與同一應用程式中的任何其他元件進行通信。 這是非常強大,尤其是當主應用程式並不知道多少分元件的載入,通常是基於外掛程式的應用程式的情況。
通常情況下,一個應用程式需要只有單個信使實例,涵蓋的所有通信。 存儲在 Messenger.Default 屬性中的靜態實例可能是你所需要的。 但是,您可以創建新信使實例,如果需要。 在這種情況下,每個信使行為作為一個獨立的通信通道。 這可以是有用的如果你想要確保給定的物件從未收到一條消息,為它不打算。 在代碼中的圖 6,例如,兩個類註冊為相同的訊息類型。 當收到郵件時,需要執行一些檢查,看到什麼消息這兩個實例。
圖 6 使用預設信使和檢查寄件者
public class FirstViewModel
{
public FirstViewModel()
{
Messenger.Default.Register<NotificationMessage>(
this,
message =>
{
if (message.Sender is MainViewModel)
{
// This message is for me.
}
});
}
}
public class SecondViewModel
{
public SecondViewModel()
{
Messenger.Default.Register<NotificationMessage>(
this,
message =>
{
if (message.Sender is SettingsViewModel)
{
// This message is for me
}
});
}
}
圖 7 演示如何實現與私人信使實例。 在這種情況下,SecondViewModel 將從未收到消息,因為它贊同的信使的不同實例,並偵聽到不同的頻道。
圖 7 使用私人信使
public class MainViewModel
{
private Messenger _privateMessenger;
public MainViewModel()
{
_privateMessenger = new Messenger();
SimpleIoc.Default.Register(() => _privateMessenger,
"PrivateMessenger");
}
public void Update()
{
_privateMessenger.Send(new NotificationMessage("DoSomething"));
}
}
public class FirstViewModel
{
public FirstViewModel()
{
var messenger
= SimpleIoc.Default.GetInstance<Messenger>("PrivateMessenger");
messenger.Register<NotificationMessage>(
this,
message =>
{
// This message is for me.
});
}
}
另一種方法,以避免將給定的消息發送到一個特定的接收器是可以使用權杖,如中所示圖 8。 這是合同的一種的發送者和接收者之間。 通常,一個權杖是唯一的識別碼例如 GUID,但它可以是任何物件。 如果發送者和接收者都使用相同的權杖,一個私人通信通道打開兩個物件之間。 在此方案中,不使用該標記的 SecondViewModel,不會通知發送一條消息。 主要優點是接收器不需要編寫邏輯,以確保該消息真的打算供它。 相反,信使篩選出基於標記的郵件。
圖 8 不同的溝通管道與權杖
public class MainViewModel
{
public static readonly Guid Token = Guid.NewGuid();
public void Update()
{
Messenger.Default.Send(new NotificationMessage("DoSomething"),
Token);
}
}
public class FirstViewModel
{
public FirstViewModel()
{
Messenger.Default.Register<NotificationMessage>(
this,
MainViewModel.Token,
message =>
{
// This message is for me.
});
}
}
使用消息
標記是一個不錯的方式到過濾消息,但這不會更改消息應攜帶一些背景,以理解的事實。 例如,您可以使用發送和接收具有布林值內容的方法,如中所示圖 9。 但如果多個寄件者發送布林的消息,一個接收器怎麼知道誰該消息的目的是為和與它做什麼? 這就是為什麼它是更好地使用專用的訊息類型為了使範圍內清除。
圖 9 使用的訊息類型來定義上下文
public class Sender
{
public void SendBoolean()
{
Messenger.Default.Send(true);
}
public void SendNotification()
{
Messenger.Default.Send(
new NotificationMessage<bool>(true, Notifications.PlayPause));
}
}
public class Receiver
{
public Receiver()
{
Messenger.Default.Register<bool>(
this,
b =>
{
// Not quite sure what to do with this boolean.
});
Messenger.Default.Register<NotificationMessage<bool>>(
this,
message =>
{
if (message.Notification == Notifications.PlayPause)
{
// Do something with message.Content.
Debug.WriteLine(message.Notification + ":" +
message.Content);
}
});
}
}
圖 9 也顯示正在使用的特定的訊息類型。 NotificationMessage < T > 最常使用的訊息類型之一,內置於 MVVM 光工具組,,它允許任何內容 (在本例中,一個布林值) 通知的字串一起寄送。 通常情況下,通知是在調用通知靜態類中定義一個唯一的字串。 這將允許發送指令和郵件一同發送。
當然,它也是可能派生從 NotificationMessage < T > ; 使用內置的不同消息的類型 ; 或執行您自己的訊息類型。 使用 MVVM 光工具組中包含一個 MessageBase 類,可以派生為此目的,但這絕對不是強制性要在您的代碼中使用這。
另一種內置的訊息類型是 PropertyChanged< T > 的消息。 這是特別有用就可測的通常用作基類物件的綁定操作所涉及的 ViewModelBase 類和物件。 這些類是實現 INotifyPropertyChanged 介面,在使用 MVVM 具有資料繫結的應用程式是至關重要。 例如,在代碼中圖 10,BankAccountViewModel 定義可觀察到的屬性命名為平衡。 當此屬性更改時,該 RaisePropertyChanged 方法採用一個布林型參數,導致要"廣播"與此屬性,如其名稱、 舊值和新值的資訊的 PropertyChangedMessage 的 ViewModelBase 類。 另一個物件可以訂閱這種訊息類型,並做出相應的反應。
圖 10 發送 PropertyChangedMessage
public class BankViewModel : ViewModelBase
{
public const string BalancePropertyName = "Balance";
private double _balance;
public double Balance
{
get
{
return _balance;
}
set
{
if (Math.Abs(_balance - value) < 0.001)
{
return;
}
var oldValue = _balance;
_balance = value;
RaisePropertyChanged(BalancePropertyName, oldValue, value, true);
}
}
}
public class Receiver
{
public Receiver()
{
Messenger.Default.Register<PropertyChangedMessage<double>>(
this,
message =>
{
if (message.PropertyName == BankViewModel.BalancePropertyName)
{
Debug.WriteLine(
message.OldValue + " --> " + message.NewValue);
}
});
}
}
有其他內置的郵件中使用 MVVM 光在各種方案中很有用。 另外,基礎結構來構建您自己的自訂消息是可用的。 本質上,這個想法是使接收器的生活更輕鬆通過提供足夠多的上下文,他們知道如何處理消息的內容。
總結
使者已證明在將很難實現,而一個完全解耦的消息傳遞解決方案的許多方案中很有用。 然而,它是一種高級的工具和應謹慎使用,以免造成可能會難以調試和維護以後的代碼混亂。
這篇文章出來使用 MVVM 光工具組元件的演示文稿進行舍入。 它是一個激動人心的時刻,對於.NET 開發人員,與基於 XAML 的多個平臺上使用相同的工具和技術的能力。 通過使用 MVVM 光,您可以共用代碼之間 WPF,Windows 運行時,Windows Phone,Silverlight — — 甚至連 Xamarin 平臺 Android 和 iOS。 我希望你找到這個系列的文章很有用對於理解如何使用 MVVM 光可以説明您高效地開發應用程式,同時使其易於設計、 測試和維護這些應用程式。
Laurent Bugnion IdentityMine inc.,Microsoft 合作夥伴與Windows Presentation Foundation、 Silverlight、 Pixelsense、 Kinect,Windows 8、 Windows Phone 和使用者體驗等技術工作的高級主任他設在瑞士蘇黎世的。他也是微軟最有價值球員和 Microsoft 區域主任。
感謝以下 Microsoft 技術專家對本文的審閱:傑佛瑞 · 弗曼
傑佛瑞 · 弗曼現任上Visual Studio程式管理器。 超過四年Jeff一直著重于 XAML 工裝Visual Studio和混合。 他喜歡建築業務線應用程式,並且嘗試使用不同的設計模式和做法。 他還擁有激情的可擴充性和與客戶要生成的控制項的設計時體驗工作的。