抱歉,您的瀏覽器無法訪問本站
本頁面需要瀏覽器支持(啟用)JavaScript
了解詳情 >

在物件導向開發中,觀察者模式經常應用於模組化與封裝的設計中。它能讓物件以彈性的方式傳遞訊息,是實現鬆耦合的一種常見做法。

而在 C# 中,常用 event、Action/ Func、RX 等方式實作觀察者模式,看似達成了解耦。但實際上,訂閱者仍需知道事件來源,這造成一種隱性耦合,讓物件之間仍有依賴。

CommunityToolkit.Mvvm 套件中,除了提供 MVVM 架構下常用的 ObservableObject 與 ObservableValidator 外,還支援基於中介者模式的訊息傳遞機制 Messenger。

該機制讓訊息的發送者與觀察者無需彼此參照,只需註冊並透過 Messenger 發送訊息,就能由框架轉發給所有訂閱者。為了整合這套機制,套件也提供了 ObservableRecipient 類別,讓 ViewModel 更容易註冊與接收訊息。

這邊要注意的是,使用 Messenger 並不代表完全沒有耦合,而是將依賴轉向了中介物件 ( Messenger 本身),達成相對鬆耦合的設計。

本次將使用 WPF 並搭配 CommunityToolkit.Mvvm 套件的ObservableRecipient 物件與 Messenger 機制,實作訊息傳遞的示範。

本篇文章為 CommunityToolkit.Mvvm 套件系列的第三部分,若想了解關於該套件的基礎屬性通知或資料驗證的使用,可從下列網址前往。

使用ToolKit.MVVM實現ViewModel – Part 1 (基礎 MVVM 架構)

使用ToolKit.MVVM實現ViewModel – Part 2 (資料驗證)

本次的開發環境,如下

  • IDE : Visual Studio 2022
  • .NET版本 : . NET 6.0
  • Nuget套件: CommunityToolkit.Mvvm 8.2.2

常見的發布/訂閱機制

在正式使用 Messenger 之前,首先回顧一下常用的訊息傳遞機制。

如最開頭所提,在 C# 開發中,無論是自行實作觀察者模式,或是使用現成機制,如 event、Action/ Func 或是 RX 等,這些方式都能在物件之間建立彈性的通知機制。

實作時,通常是讓觀察者主動訂閱發送者的訊息,建立起一種動態但仍有方向性的聯繫關係。

以下將透過一個簡單範例,說明這類機制的基本運作方式。

雖然上述機制可廣泛應用於各種情境中,本範例則是模擬 ViewModel 之間的訊息溝通。

首先來看扮演發送者角色的 ViewModel ,

此 ViewModel 中包含:

  • Msg 屬性,供 UI Binding 使用的 。
  • OnMessageSend,用來通知訂閱者的委派。
  • SendStrClick(),透過 Command 觸發通知的函式 。

Window0_SubSendViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public partial class Window0_SubSendViewModel : ObservableObject
{
[ObservableProperty]
protected string _msg;

public Action<string>? OnMessageSend { get; set; }

public Window0_SubSendViewModel()
{
_msg = "Message Text";
}

[RelayCommand]
protected void SendStrClick()
{
OnMessageSend?.Invoke(Msg);
}
}

接下來是扮演觀察者角色的 ViewModel ,

  • 提供公開方法 Receive(string message),用來接收訊息。
  • Receive 方法直接將訊息輸出到 Console。

Window0_SubReceiveViewModel

1
2
3
4
5
6
7
public partial class Window0_SubReceiveViewModel : ObservableObject
{
public void Receive(string message)
{
Console.WriteLine(message);
}
}

此範例中,為求精簡只對應一個 View ,

  • 為簡化示範,集中管理發送者與觀察者兩個 ViewModel 。
  • 在建構函式中,將發送者的 OnMessageSend 委派指向觀察者的 Receive 方法,建立訂閱關係。

Window0_MainViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
public partial class Window0_MainViewModel : ObservableObject
{
public Window0_SubSendViewModel SendViewModel { get; protected set; }
public Window0_SubReceiveViewModel RecViewModel { get; protected set; }

public Window0_MainViewModel()
{
SendViewModel = new();
RecViewModel = new();

SendViewModel.OnMessageSend = RecViewModel.Receive;
}
}

最後是 View 的部分,

  • 直接在 Window.DataContext 中建立 MainViewModel 實例。
  • 使用 StackPanel 並將資料來源綁定到發送者的 SendViewModel。
  • 畫面包含一個文字輸入框與一個按鈕,分別用於輸入訊息和觸發發送命令。
  • 觀察者 ViewModel 僅在 Console 輸出訊息,未綁定任何 UI 元件。

Window0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<Window.DataContext>
<local:Window0_MainViewModel/>
</Window.DataContext>

<StackPanel Margin="30"
DataContext="{Binding Path=SendViewModel}">

<TextBox Text="{Binding Path=Msg, UpdateSourceTrigger=PropertyChanged}"/>

<Button Margin="0,10,0,0"
Height="30"
Content="Send Str"
Command="{Binding Path=SendStrClickCommand}"/>

</StackPanel>

執行的畫面與結果,如下,

在 UI 中輸入要傳送的字串,

一般發布訂閱機制小程式畫面


觀察者將接收到的訊息,顯示在 Console 上,

一般發布訂閱機制小程式 Console 結果


實務上,ViewModel(或一般物件)的建立方式可能相當複雜,可能透過不同的流程、由 DI 容器產生,或分散在不同模組中。

然而,不論它們是如何建立、位於何處,只要有觀察需求,就必須在某處讓觀察者直接訂閱發送者的訊息。

開始使用 ObservableRecipient

在 CommunityToolkit.Mvvm 套件中,撰寫 ViewModel 通常會繼承 ObservableObject,以支援屬性變更通知等基本功能。

若在此基礎上,還需要實作 ViewModel 之間的訊息傳遞,套件也提供了 ObservableRecipient 類別。該類別不僅繼承自 ObservableObject,還封裝了與 Messenger 搭配使用的相關機制,能更方便地註冊與接收訊息。

以下將透過一個簡單範例,展示 ObservableRecipient 的基本使用方式。

首先是發送者的 ViewModel ,

  • 繼承 ObservableRecipient 讓 ViewModel 具備 Messenger 的功能。
  • Msg 屬性,提供 UI Binding 並作為要傳送的資料內容。
  • SendStrClick() 方法,供 UI Binding 的 Command,觸發後透過 Messenger.Send() 廣播 Msg 給所有訂閱者。

Window1_SubSendViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public partial class Window1_SubSendViewModel : ObservableRecipient
{
[ObservableProperty]
protected string _msg;

public Window1_SubSendViewModel()
{
_msg = "Message Text";
}

[RelayCommand]
protected void SendStrClick()
{
Messenger.Send(Msg);
}
}

在這個範例中,發送者是繼承自 ObservableRecipient,這裡做個部份說明:

  • protected IMessenger Messenger { get; }

    這個屬性就是發送者在呼叫 Messenger.Send() 時所使用的 Messenger 實例。

  • ObservableRecipient(IMessenger messenger)

    這個建構式會要求傳入一個 IMessenger 介面的物件,並將它指定給上面的 Messenger 屬性。

  • ObservableRecipient()

    這個無參數建構式會自動使用 WeakReferenceMessenger.Default 作為 Messenger 實例。

ObservableRecipient 類別定義


接著是觀察者的 ViewModel,

  • 繼承 ObservableRecipient 讓 ViewModel 具備 Messenger 的功能。
  • 實作 IRecipient<string>,用於接收發送端傳來的 string 訊息,只要設為 IsActive = true , Messenger 就會自動把它註冊為該型別的觀察者。
  • Receive(string message) 方法,介面實作,接收到訊息後,將其輸出至 Console 。

Window1_SubReceiveViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
public partial class Window1_SubReceiveViewModel
: ObservableRecipient, IRecipient<string>
{
public Window1_SubReceiveViewModel()
{
IsActive = true;
}

public void Receive(string message)
{
Console.WriteLine(message);
}
}

上面的程式簡潔得幾乎不可思議,初看之下可能會讓人摸不著頭緒。以下針對「觀察者」部分的關鍵機制做一些補充說明:

  • IsActive 屬性

    當將 IsActive 設為 true 時,會觸發 OnActivated() 方法;反之,設為 false 則會呼叫 OnDeactivated()。

    簡單來說,這個屬性決定了物件是否會去接收來自 Messenger 的訊息。

IsActive 屬性定義


  • OnActivated() 與 OnDeactivated() 方法

    這兩個方法分別負責呼叫 Messenger 的 RegisterAll 與 UnregisterAll 方法。顧名思義,前者會將物件註冊到 Messenger 以便接收訊息,後者則會取消註冊。

OnActivated() 與 OnDeactivated() 方法定義


  • IRecipient<T> 的作用

    從 OnActivated() 方法的 API 註解可以看到,只要類別實作了 IRecipient<T> 介面,就能在 IsActive 設為 true 時自動完成註冊,無需額外手動呼叫 Messenger 的註冊方法。

IRecipient 的作用文件註解


與上個範例相同,

使用 MainViewModel 集中管理發送者與觀察者兩個 ViewModel 實例。

Window1_MainViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public partial class Window1_MainViewModel : ObservableObject
{
public Window1_SubSendViewModel SendViewModel
{ get; protected set; }

public Window1_SubReceiveViewModel RecViewModel
{ get; protected set; }

public Window1_MainViewModel()
{
SendViewModel = new();
RecViewModel = new();
}
}

最後是 View 的部分,

這部分與上一個範例相同,因此不再重複說明。

Window1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<Window.DataContext>
<local:Window1_MainViewModel/>
</Window.DataContext>

<StackPanel Margin="30"
DataContext="{Binding Path=SendViewModel}">

<TextBox Text="{Binding Path=Msg, UpdateSourceTrigger=PropertyChanged}"/>

<Button Margin="0,10,0,0"
Height="30"
Content="Send Str"
Command="{Binding Path=SendStrClickCommand}"/>

</StackPanel>

執行的畫面與結果,如下,

基本上與前個範例相同,

繼承 ObservableRecipient 類別,WPF 程式執行結果

繼承 ObservableRecipient 類別,程式執行結果


到這邊,已經完成 ObservableRecipient 的基礎使用範例,示範了具備發送者與觀察者的 ViewModel 實作。

與一般的訂閱機制相比,這種做法具備以下優勢:

  • 利用 Messenger 作為中介者,無需手動建立訂閱關係。
  • 發送者與觀察者的 ViewModel 不需存在於相同範疇,即使分屬不同模組或畫面,也能順利完成訊息傳遞。

訊息類型與註冊控制

在前一個範例中,已經了解如何使用 ObservableRecipient 傳送與接收 string 型別的訊息,並體驗到自動註冊 Messenger 的功能。

接下來將進入進階應用,包含:

  • 傳送不同型別的訊息
    • 內建的 ValueChangedMessage<T> 傳遞任意型別資料。
    • 自定義類別。
  • 手動註冊與取消註冊 Messenger
    • 讓物件可以訂閱多種不同型別的訊息。
    • 手動管理註冊與取消註冊。

這些功能不僅能讓 Messenger 應對更複雜的情況,也能對底層運作有更深一層的認識。

首先,定義了一個自訂類別 CustomizeClass 作為傳送的物件。

這就像在一般的發布/訂閱模式中,會傳遞某個物件給觀察者一樣。

CustomizeClass

1
2
3
4
public class CustomizeClass
{
public int Data { get; set; }
}

發送者 ViewModel 的部分,示範三個不同的 Command,傳送三種類型的訊息。

分別說明如下:

  • SendStrClick():傳送 string 的訊息。
  • SendCount1Click():傳送 ValueChangedMessage 訊息。
    • ValueChangedMessage<T> 是 Toolkit 提供的內建訊息類別,用來包裝單一資料。
      因為 Messenger.Send() 只能傳送類別,若要傳遞像 int 或 double 這類結構型別,就需要用 ValueChangedMessage<T> 來包起來。
  • SendCount2Click:傳送自定義類別 CustomizeClass 訊息。

Window2_SubSendViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public partial class Window2_SubSendViewModel : ObservableRecipient
{
protected int count;

[ObservableProperty]
protected string _msg;

public Window2_SubSendViewModel()
{
count = 0;
_msg = "Message Text";
}

[RelayCommand]
protected void SendStrClick()
{
Messenger.Send(Msg);
}

[RelayCommand]
protected void SendCount1Click()
{
count++;
Messenger.Send(
new ValueChangedMessage<int>(count));
}

[RelayCommand]
protected void SendCount2Click()
{
count += 2;
Messenger.Send(
new CustomizeClass
{
Data = count
});
}
}

此範例有兩個觀察者 ViewModel,

首先第一個觀察者 ViewModel 的部分:

  • 覆寫 OnActivated()
    • 手動註冊(訂閱) string 與 ValueChangedMessage 兩種型別的訊息。
    • 訊息接收後,便會呼叫對應的 Receive 方法,並將內容輸出到 Console 。
  • 覆寫 OnDeactivated(),用來取消對 Messenger 的手動註冊。

實務上,Messenger 的註冊與取消註冊可以自行控制,

比方說,可以直接在建構子裡註冊,或是在需要的時候再註冊,

不過覆寫 OnActivated / OnDeactivated 搭配 IsActive 屬性的好處是,能用一個布林值一次性管理整個註冊與取消註冊的流程。

Window2_SubReceiveViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public partial class Window2_SubReceiveViewModel : ObservableRecipient
{
public Window2_SubReceiveViewModel()
{
IsActive = true;
}

protected override void OnActivated()
{
Messenger.Register<string>(
this, (r, m) => Receive(m));

Messenger.Register<ValueChangedMessage<int>>(
this, (r, m) => Receive(m));
}

protected override void OnDeactivated()
{
Messenger.Unregister<string>(this);
Messenger.Unregister<ValueChangedMessage<int>>(this);
}

protected void Receive(string message)
{
Console.WriteLine($"RecVm String: {message}");
}

protected void Receive(ValueChangedMessage<int> message)
{
Console.WriteLine($"RecVm ValueChangedMessage: {message.Value}");
}
}

第二個觀察者 ViewModel 的部分:

  • 覆寫 OnActivated()
    • 手動註冊(訂閱) string 與 CustomizeClass 兩種型別的訊息。
    • 訊息接收後,便會呼叫對應的 Receive 方法,並將內容輸出到 Console,為了與第一個觀察者 ViewModel 區分,這裡使用 ConsoleWriteStyled 方法,讓輸出文字呈現不同顏色。
  • 覆寫 OnDeactivated(),用來取消對 Messenger 的手動註冊。

Window2_SubReceiveAnotherViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public partial class Window2_SubReceiveAnotherViewModel : ObservableRecipient
{
public Window2_SubReceiveAnotherViewModel()
{
IsActive = true;
}

protected override void OnActivated()
{
Messenger.Register<string>(
this, (r, m) => Receive(m));

Messenger.Register<CustomizeClass>(
this, (r, m) => Receive(m));
}

protected override void OnDeactivated()
{
Messenger.Unregister<string>(this);
Messenger.Unregister<CustomizeClass>(this);
}

protected void Receive(string message)
{
ConsoleWriteStyled($"RecAnotherVm String: {message}");
}

protected void Receive(CustomizeClass message)
{
ConsoleWriteStyled($"RecAnotherVm CustomizeClass: {message.Data}");
}

protected void ConsoleWriteStyled(
string value,
ConsoleColor color = ConsoleColor.Green)
{
Console.ForegroundColor = color;
Console.WriteLine(value);
Console.ResetColor();
}
}

MainViewModel 部分,集中管理發送者與兩個觀察者的 ViewModel 實例。

Window2_MainViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public partial class Window2_MainViewModel : ObservableObject
{
public Window2_SubSendViewModel SendViewModel
{ get; protected set; }

public Window2_SubReceiveViewModel RecViewModel
{ get; protected set; }

public Window2_SubReceiveAnotherViewModel RecAnotherViewModel
{ get; protected set; }

public Window2_MainViewModel()
{
SendViewModel = new();
RecViewModel = new();
RecAnotherViewModel = new();
}
}

View 的部分,為配合發送者 ViewModel 的接口,撰寫相應的 UI 元件。

Window2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<Window.DataContext>
<local:Window2_MainViewModel/>
</Window.DataContext>

<StackPanel Margin="30"
DataContext="{Binding Path=SendViewModel}">

<TextBox Text="{Binding Path=Msg, UpdateSourceTrigger=PropertyChanged}"/>

<Button Margin="0,10,0,0"
Height="30"
Content="Send Str"
Command="{Binding Path=SendStrClickCommand}"/>

<Button Margin="0,10,0,0"
Height="30"
Content="Send Count 1"
Command="{Binding Path=SendCount1ClickCommand}"/>

<Button Margin="0,10,0,0"
Height="30"
Content="Send Count 2"
Command="{Binding Path=SendCount2ClickCommand}"/>

</StackPanel>

執行的畫面與結果,如下,

依序按下 View 中從上到下的按鈕,

繼承 ObservableRecipient 類別,並使用不同型態訊息的 WPF 程式執行結果


從 Console 輸出的內容可以觀察到,兩個觀察者都成功接收到對應型別的訊息,並顯示出來。

繼承 ObservableRecipient 類別,並使用不同型態訊息的程式執行結果


繼承 ObservableRecipient 類別,並使用不同型態訊息的程式執行結果


繼承 ObservableRecipient 類別,並使用不同型態訊息的程式執行結果


到此為止,已經學會如何使用 ObservableRecipient 傳送不同型別的訊息,以及手動註冊與取消註冊。同時也看到,Messenger 會自動將訊息傳遞給對應型別的觀察者。

使用 Token 機制區隔同類別訊息

Messenger 會依照訊息的型別自動配對發送與接收方。但若有多個來源傳送相同型別的訊息,且希望由不同的接收者各自處理,這時就可能發生混淆或誤收的問題。

為此,Messenger 提供了 Token 機制,可將相同型別的訊息依據不同的情況加以區隔,讓訊息傳遞更精確。

本節將針對此情況 Token 的部分進行演示。

發送者 ViewModel 的部分說明如下:

  • SendStrClick():傳送 string 訊息,未指定 Token,實際上會使用預設 Token ( Unit.Default )。
  • SendToken01Click():傳送 string 訊息,並指定 Token 為 string 型別的 “Token01”。
  • SendToken02Click():傳送 string 訊息,並指定 Token 為 “Token02”,與其他 string 訊息做區隔。

Window3_SubSendViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public partial class Window3_SubSendViewModel : ObservableRecipient
{
[ObservableProperty]
protected string _msg;

public Window3_SubSendViewModel()
{
_msg = "Message Text";
}

[RelayCommand]
protected void SendStrClick()
{
Messenger.Send(Msg);
}

// Token01
[RelayCommand]
protected void SendToken01Click()
{
Messenger.Send(
$"{Msg} use Token01",
"Token01");
}

// Token02
[RelayCommand]
protected void SendToken02Click()
{
Messenger.Send(
$"{Msg} use Token02",
"Token02");
}
}

第一個觀察者 ViewModel 的部分:

  • 接收未指定 Token (即預設 Unit.Default) 的 string 訊息。
  • 接收指定 Token 為 “Token01” 的 string 訊息。

Window3_SubReceiveViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public partial class Window3_SubReceiveViewModel : ObservableRecipient
{
public Window3_SubReceiveViewModel()
{
IsActive = true;
}

protected override void OnActivated()
{
Messenger.Register<string>(
this, (r, m) => Receive(m));

Messenger.Register<string, string>(
this, "Token01", (r, m) => ReceiveWithToken01(m));
}

protected void Receive(string message)
{
Console.WriteLine($"RecVm msg: {message}");
}

// Token01
protected void ReceiveWithToken01(string message)
{
Console.WriteLine($"RecVm Token01 msg: {message}");
}
}

第二個觀察者 ViewModel 的部分:

  • 接收未指定 Token (即預設 Unit.Default) 的 string 訊息。
  • 接收指定 Token 為 “Token02” 的 string 訊息。
  • 使用 ConsoleWriteStyled 方法,讓輸出文字呈現不同顏色。

Window3_SubReceiveAnotherViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public partial class Window3_SubReceiveAnotherViewModel : ObservableRecipient
{
public Window3_SubReceiveAnotherViewModel()
{
IsActive = true;
}

protected override void OnActivated()
{
Messenger.Register<string>(
this, (r, m) => Receive(m));

Messenger.Register<string, string>(
this, "Token02", (r, m) => ReceiveWithToken02(m));
}

protected void Receive(string message)
{
ConsoleWriteStyled($"RecAnotherVm msg: {message}");
}

// Token02
protected void ReceiveWithToken02(string message)
{
ConsoleWriteStyled($"RecAnotherVm Token02 msg: {message}");
}

protected void ConsoleWriteStyled(
string value,
ConsoleColor color = ConsoleColor.Green)
{
Console.ForegroundColor = color;
Console.WriteLine(value);
Console.ResetColor();
}
}

MainViewModel 部分,集中管理發送者與兩個觀察者的 ViewModel 實例。

Window3_MainViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public partial class Window3_MainViewModel : ObservableObject
{
public Window3_SubSendViewModel SendViewModel
{ get; protected set; }

public Window3_SubReceiveViewModel RecViewModel
{ get; protected set; }

public Window3_SubReceiveAnotherViewModel RecAnotherViewModel
{ get; protected set; }

public Window3_MainViewModel()
{
SendViewModel = new();
RecViewModel = new();
RecAnotherViewModel = new();
}
}

View 的部分,為配合發送者 ViewModel 的接口,撰寫相應的 UI 元件。

Window3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<Window.DataContext>
<local:Window3_MainViewModel/>
</Window.DataContext>

<StackPanel Margin="30"
DataContext="{Binding Path=SendViewModel}">

<TextBox Text="{Binding Path=Msg, UpdateSourceTrigger=PropertyChanged}"/>

<Button Margin="0,10,0,0"
Height="30"
Content="Send Str"
Command="{Binding Path=SendStrClickCommand}"/>

<Button Margin="0,10,0,0"
Height="30"
Content="Send Token 1"
Command="{Binding Path=SendToken01ClickCommand}"/>

<Button Margin="0,10,0,0"
Height="30"
Content="Send Token 2"
Command="{Binding Path=SendToken02ClickCommand}"/>

</StackPanel>

執行的畫面與結果,如下,

依序按下 View 中從上到下的按鈕,

繼承 ObservableRecipient 類別,並使用 Token 的 WPF 程式執行結果


從 Console 輸出的內容可以觀察到,兩個觀察者都成功接收到對應 Token 的訊息,並顯示出來。

繼承 ObservableRecipient 類別,並使用 Token 的程式執行結果


繼承 ObservableRecipient 類別,並使用 Token 的程式執行結果


繼承 ObservableRecipient 類別,並使用 Token 的程式執行結果


在 Messenger 應用中,可以透過兩種方式來避免訊息混淆:

  • 為每種訊息定義不同的傳送類型。
  • 使用 Token 機制進行區別。

其中 Token 並不限使用 string,實際上它是以泛型 T (Class) 實作,因此也可以使用自訂物件作為 Token,只是該類別需實作 IEquatable 介面。使用 string 作為 Token 是最簡單直接的做法。

屬性變更時的通知

就像在 ObservableObject 中,可以透過 NotifyCanExecuteChangedFor Attribute 來通知指令狀態改變
在 ObservableRecipient 中,也提供了一個專屬 Attribute :NotifyPropertyChangedRecipients ,這個 Attribute 可在屬性變更時,自動透過 Messenger 通知其他註冊的觀察者。

發送者 ViewModel 的部分說明如下:

  • RealtimeStr 使用 [ObservableProperty] 可自動通知 Viwe 屬性已變更,同時套用 [NotifyPropertyChangedRecipients],使該屬性在變動時,也會自動傳送訊息給已註冊的觀察者。

Window4_SubSendViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public partial class Window4_SubSendViewModel : ObservableRecipient
{
[ObservableProperty]
protected string _msg;

[ObservableProperty]
[NotifyPropertyChangedRecipients]
protected string _realtimeStr;

public Window4_SubSendViewModel()
{
_msg = "Message Text";
_realtimeStr = "";
}

[RelayCommand]
protected void SendStrClick()
{
Messenger.Send(Msg);
}
}

首先前往 RealtimeStr 屬性在變更時所觸發的程式碼,大部分內容與單純使用 [ObservableProperty] 時相同,都是 MVVM 中用來通知 View 屬性已更新的機制。

比較特別的是,在程式碼的最後,多了 Broadcast 方法的呼叫。

使用 [NotifyPropertyChangedRecipients] Attribute 後,自動產生的屬性程式碼


接著查看 Broadcast 方法的定義,可以看到它會將屬性變更的相關資訊封裝進 PropertyChangedMessage<T> 物件中,並透過 Messenger.Send() 將訊息發送出去。

Broadcast 方法的定義


簡單來說,其運作原理是:當屬性發生變更時,系統會自動透過 Messenger.Send(new PropertyChangedMessage<T>())

發送通知。若有需要接收該通知的觀察者,只要註冊 PropertyChangedMessage<T> 即可取得更新資訊。

觀察者 ViewModel 的部分說明如下:

  • 為了對應 [NotifyPropertyChangedRecipients] 所發送的訊息,觀察者需要註冊 PropertyChangedMessage<T> 類型的監聽。
  • 從 PropertyChangedMessage<T> 中,可以取得變更的屬性名稱、舊值、新值,以及變更來源的物件。若有需要,便可依這些資訊進行對應處理,這邊僅將它們簡單地輸出至 Console。

Window4_SubReceiveViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public partial class Window4_SubReceiveViewModel : ObservableRecipient
{
public Window4_SubReceiveViewModel()
{
IsActive = true;
}

protected override void OnActivated()
{
Messenger.Register<string>(
this, (r, m) => Receive(m));

Messenger.Register<PropertyChangedMessage<string>>(
this, (r, m) => Receive(m));
}

protected void Receive(string message)
{
Console.WriteLine($"RecVm msg: {message}");
}

protected void Receive(PropertyChangedMessage<string> message)
{
Console.WriteLine($"RecVm receive PropertyChangedMessage");
Console.WriteLine($"Sender: {message.Sender}, PropertyName: {message.PropertyName}");
Console.WriteLine($"OldValue: {message.OldValue}, NewValue: {message.NewValue}");
Console.WriteLine("");
}
}

MainViewModel 部分,集中管理發送者與觀察者的 ViewModel 實例。

Window4_MainViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public partial class Window4_MainViewModel : ObservableObject
{
public Window4_SubSendViewModel SendViewModel
{ get; protected set; }

public Window4_SubReceiveViewModel RecViewModel
{ get; protected set; }

public Window4_MainViewModel()
{
SendViewModel = new();
RecViewModel = new();
}
}

View 的部分,為配合發送者 ViewModel 的接口,撰寫相應的 UI 元件。

其中,RealtimeStr 的 Binding 使用了 UpdateSourceTrigger=PropertyChanged,這樣可以即時觸發 [NotifyPropertyChangedRecipients] 屬性,觀察其通知效果。

Window4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<Window.DataContext>
<local:Window4_MainViewModel/>
</Window.DataContext>

<StackPanel Margin="30"
DataContext="{Binding Path=SendViewModel}">

<TextBox Text="{Binding Path=Msg, UpdateSourceTrigger=PropertyChanged}"/>

<Button Margin="0,10,0,0"
Height="30"
Content="Send Str"
Command="{Binding Path=SendStrClickCommand}"/>

<TextBlock Margin="0,10,0,0"
Text="RealtimeStr:"/>
<TextBox Text="{Binding Path=RealtimeStr, UpdateSourceTrigger=PropertyChanged}"/>

</StackPanel>

執行的畫面與結果,如下,
最下方的 TextBox 在輸入文字時,會即時變更 RealtimeStr 屬性。

繼承 ObservableRecipient 類別,並使用 NotifyPropertyChangedRecipients 的 WPF 程式執行結果


從 Console 輸出可觀察到,當 RealtimeStr 屬性發生變化時,觀察者所接收到的訊息內容與細節。

繼承 ObservableRecipient 類別,並使用 NotifyPropertyChangedRecipients 的程式執行結果


無法繼承 ObservableRecipient 時的替代方案

在物件導向程式設計中,繼承是一個重要的設計手段。

在前面的範例中,ViewModel 都透過繼承 ObservableRecipient,不僅實作了 INotifyPropertyChanged,滿足 WPF MVVM 所需的資料綁定需求,同時也內建支援 Messenger 的訊息傳遞功能。

不過,由於 C# 不支援多重繼承,當 ViewModel 已經繼承了其他基底類別時,就無法再繼承 ObservableRecipient,進而無法直接使用其封裝好的 Messenger 功能。

為此可以使用 CommunityToolkit.Mvvm 提供的其他方案:

  • [ObservableRecipient] Attribute ,標註在類別上後,即可獲得與繼承 ObservableRecipient 類似的行為。
  • IRecipient<T>,適用於簡單場景,可讓物件訂閱並接收特定型別的訊息。

發送者 ViewModel 的部分說明如下:

  • 類別上標註了 [ObservableRecipient] Attribute ,使其具備與繼承 ObservableRecipient 類別相同的功能。
  • 繼承 ObservableValidator,該類別與 ObservableRecipient 同樣繼承自 ObservableObject,但兩者彼此無關,無法同時繼承。若需要同時具備驗證與 Messenger 功能,就必須透過 [ObservableRecipient] Attribute 來補足。
  • 本範例雖然是以 ObservableValidator 為基底類別,但實務上可以替換為任何其他類別,並不限定於此狀況。
  • 當使用 [ObservableRecipient] 時,必須手動指定 Messenger 實例( Messenger = WeakReferenceMessenger.Default )。相較之下,若是直接繼承 ObservableRecipient,則建構函式會自動初始化 Messenger。

Window5_SubSendViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
[ObservableRecipient]
public partial class Window5_SubSendViewModel : ObservableValidator
{
protected int count;

[ObservableProperty]
protected string _msg;

public Window5_SubSendViewModel()
{
Messenger = WeakReferenceMessenger.Default;
count = 0;
_msg = "Message Text";
}

[RelayCommand]
protected void SendStrClick()
{
Messenger.Send(Msg);
}

[RelayCommand]
protected void SendCountClick()
{
count++;
Messenger.Send(
new ValueChangedMessage<int>(count));
}
}

第一個觀察者 ViewModel 的部分說明如下:

  • 類別上同樣標註了 [ObservableRecipient] Attribute 。
  • 依然需要手動指定 Messenger 實例。
  • OnActivated() 的部分,需要改用 partial 形式來擴充方法定義,與繼承 ObservableRecipient 時直接覆寫 OnActivated() 方法的情況不同。

Window5_SubReceiveViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[ObservableRecipient]
public partial class Window5_SubReceiveViewModel : ObservableValidator
{
public Window5_SubReceiveViewModel()
{
Messenger = WeakReferenceMessenger.Default;
IsActive = true;
}

protected virtual partial void OnActivated()
{
Messenger.Register<string>(
this, (r, m) => Receive(m));
}

protected void Receive(string message)
{
Console.WriteLine($"RecVm msg: {message}");
}
}

第二個觀察者 ViewModel 的部分說明如下:

  • 繼承了 IRecipient<T> 介面,因為沒有繼承 ObservableRecipient,所以需要自行使用 Messenger 進行註冊。
  • 由於實作了 IRecipient<T>,在註冊時不需額外指定接收方法,Messenger 會自動呼叫介面中定義的 Receive 方法。

Window5_SubReceiveAnotherViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public partial class Window5_SubReceiveAnotherViewModel
: ObservableValidator, IRecipient<ValueChangedMessage<int>>
{
public Window5_SubReceiveAnotherViewModel()
{
WeakReferenceMessenger.Default.Register(this);
}

public void Receive(ValueChangedMessage<int> message)
{
ConsoleWriteStyled($"RecAnotherVm ValueChangedMessage: {message.Value}");
}

protected void ConsoleWriteStyled(
string value,
ConsoleColor color = ConsoleColor.Green)
{
Console.ForegroundColor = color;
Console.WriteLine(value);
Console.ResetColor();
}
}

MainViewModel 部分,集中管理發送者與兩個觀察者的 ViewModel 實例。

Window5_MainViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public partial class Window5_MainViewModel : ObservableObject
{
public Window5_SubSendViewModel SendViewModel
{ get; protected set; }

public Window5_SubReceiveViewModel RecViewModel
{ get; protected set; }

public Window5_SubReceiveAnotherViewModel RecAnotherViewModel
{ get; protected set; }

public Window5_MainViewModel()
{
SendViewModel = new();
RecViewModel = new();
RecAnotherViewModel = new();
}
}

View 的部分,為配合發送者 ViewModel 的接口,撰寫相應的 UI 元件。

Window5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<Window.DataContext>
<local:Window5_MainViewModel/>
</Window.DataContext>

<StackPanel Margin="30"
DataContext="{Binding Path=SendViewModel}">

<TextBox Text="{Binding Path=Msg, UpdateSourceTrigger=PropertyChanged}"/>

<Button Margin="0,10,0,0"
Height="30"
Content="Send Str"
Command="{Binding Path=SendStrClickCommand}"/>

<Button Margin="0,10,0,0"
Height="30"
Content="Send Count"
Command="{Binding Path=SendCountClickCommand}"/>

</StackPanel>

執行的畫面與結果,如下,
發送者的部分,

使用 ObservableRecipient Attribute 的 WPF 程式執行結果


觀察者接收到的訊息部分,

其行為與繼承 ObservableRecipient 時的情況基本相同。

使用 ObservableRecipient Attribute 的程式執行結果


直接使用 Messenger 機制

到目前為止的範例,主要都是在 ViewModel 中使用 Messenger,並對不同情境提供相應的處理,那麼一般的類別是否也能使用呢?

答案是可以的,Messenger 提供了靜態的功能,能讓任何物件使用,而 ObservableRecipient 則是對此功能的封裝與簡化。

接下來,將直接使用 Messenger,其中也包含讓 View 直接使用的範例。

首先,定義了一個自訂類別 ScrollIdxMessage 作為傳送的物件。

ScrollIdxMessage

1
2
3
4
public class ScrollIdxMessage
{
public int Index { get; set; } = 0;
}

發送者 ViewModel 的部分說明如下:

  • StringCollection,供 UI 綁定的字串集合。
  • SendStrClick(),與前面範例類似,傳送字串訊息,但改為使用 WeakReferenceMessenger 進行發送。
  • SendScrollClick(),根據 StringCollection 的數量,隨機產生一個索引,並以 ScrollIdxMessage 封裝後,透過 WeakReferenceMessenger 傳送該訊息。

Window6_SubSendViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public partial class Window6_SubSendViewModel : ObservableObject
{
[ObservableProperty]
protected string _msg;

public ObservableCollection<string> StringCollection
{ get; set; }

public Window6_SubSendViewModel()
{
_msg = "Message Text";

StringCollection = new ObservableCollection<string>(
Enumerable.Range(0, 21).Select(i => $"Item {i:00}")
);
}

[RelayCommand]
protected void SendStrClick()
{
WeakReferenceMessenger.Default.Send(Msg);
}

[RelayCommand]
protected void SendScrollClick()
{
var random = new Random();
var count = StringCollection.Count;
int index = random.Next(0, count);

WeakReferenceMessenger.Default.Send(
new ScrollIdxMessage
{
Index = index,
});
}
}

觀察者普通類別的部分說明如下:

  • 在建構函式中,使用 WeakReferenceMessenger 註冊接收 string 類型的訊息。

Window6_SubReceiveClass

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Window6_SubReceiveClass
{
public Window6_SubReceiveClass()
{
WeakReferenceMessenger.Default.Register<string>(
this, (r, m) => Receive(m));
}

protected void Receive(string message)
{
Console.WriteLine($"RecClass msg: {message}");
}
}

MainViewModel 部分,集中管理發送者與觀察者的實例。

Window6_MainViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
public partial class Window6_MainViewModel : ObservableObject
{
public Window6_SubSendViewModel SendViewModel
{ get; protected set; }

private Window6_SubReceiveClass recViewClass;

public Window6_MainViewModel()
{
SendViewModel = new();
recViewClass = new();
}
}

View 的 XAML 部分,為配合發送者 ViewModel 的接口,撰寫相應的 UI 元件。

其中包含一個名為 listBox 的 ListBox 控制項,用來顯示發送者 ViewModel 中的字串集合 StringCollection。

Window6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<Window.DataContext>
<local:Window6_MainViewModel/>
</Window.DataContext>

<Grid Margin="30"
DataContext="{Binding Path=SendViewModel}">

<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>

<StackPanel Grid.Row="0">

<TextBox Text="{Binding Path=Msg, UpdateSourceTrigger=PropertyChanged}"/>

<Button Margin="0,10,0,0"
Height="30"
Content="Send Str"
Command="{Binding Path=SendStrClickCommand}"/>

</StackPanel>

<ListBox x:Name="listBox"
Grid.Row="1"
Margin="0,10"
ItemsSource="{Binding Path=StringCollection}">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Height" Value="30"/>
</Style>
</ListBox.ItemContainerStyle>
</ListBox>

<Button Grid.Row="2"
Height="30"
Content="Send Scroll"
Command="{Binding Path=SendScrollClickCommand}"/>

</Grid>

習慣使用 MVVM 架構後,大多會在 XAML 中處理各種互動行為。

除了 Button 支援 Command 綁定外,其他控制項通常會透過 EventTrigger 將事件轉換為 Command,或在較複雜的情況下,使用 Behavior 來實現對應的功能。

不過,有些控制項的方法並不是 event,因此無法用 EventTrigger 對應,例如 ListBox 中的 ScrollIntoView() 方法,可用來將視圖捲動至特定項目,但它並不屬於事件,也不是命令,因此在 MVVM 架構下較難直接使用。

雖然可以透過 Behavior 來解決,但實作起來可能較為繁瑣,另一種作法是讓 ViewModel 與 View 在初始化時透過事件或委派建立直接關聯。

現在既然已經熟悉了 Messenger 機制,就多了一種新的方式來實現這類需求,讓 View 也成為訊息的接收者,執行對應的操作。

View 的 CS 部分說明如下:

  • 在建構函式中,使用 WeakReferenceMessenger 註冊接收自定義的 ScrollIdxMessage 類型的訊息。
  • 當接收到訊息時,則呼叫 ScrollItemToIdx() 方法,進而觸發 listBox.ScrollIntoView(),使畫面自動捲動至指定項目。

Window6

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public partial class Window6 : Window
{
public Window6()
{
InitializeComponent();

WeakReferenceMessenger.Default.Register<ScrollIdxMessage>(
this, (r, m) => ScrollItemToIdx(m));
}

private void ScrollItemToIdx(ScrollIdxMessage msg)
{
Application.Current.Dispatcher.Invoke(() =>
{
var listBox = this.listBox;
var count = listBox.Items.Count;
var idx = msg.Index;

Console.WriteLine($"ScrollItemToIdx: {idx}");

if (count > 0
&& idx >= 0
&& idx < count)
{
var item = listBox.Items[idx];
listBox.ScrollIntoView(item);
}
});
}
}

稍微看看 View 中的普通方法:

  • 例如 ListBox,有 ScrollIntoView、SelectAll 等方法。

WPF ListBox 普通方法定義


  • 又例如 TextBox,有 ScrollToLine、Select 等方法。

WPF TextBox 普通方法定義


這些方法本身不是 command,也不是 event,因此在 MVVM 架構下無法直接從 ViewModel 呼叫;那麼可以使用 Messenger 機制,就可以讓 ViewModel 發送訊息,通知 View 執行這些方法。

執行的畫面與結果,如下,

首先是普通類別作為觀察者的情況,與先前的範例都相同。

直接使用 Messenger 機制的程式執行結果


接著在範例中產生一個隨機數字,讓 ListBox 對應索引的項目自動捲動到畫面中。下圖為索引 11 的結果。

直接使用 Messenger 機制的程式執行結果


再次產生隨機數字,這次讓 ListBox 捲動到索引 14 的結果。

直接使用 Messenger 機制的程式執行結果


額外補充:可接收回應的 Messenger

基本上,CommunityToolkit.Mvvm 套件中的 Messenger 機制,類似一般的訂閱機制 ,發送者傳遞一份訊息物件,觀察者接收後進行處理,整個過程就結束了。

不過,套件中也提供了一種特殊的訊息類型,讓觀察者在接收到訊息後,能夠回傳一個結果給發送者,這種「請求並取得回應」的場景,就可以透過 RequestMessage<T> 來實現。

以下範例將說明其用法。

發送者 ViewModel 的部分說明如下:

  • SendMegClick() ,建立一個 RequestMessage<double> 實例,這與一般 Messenger 傳送資料的情況不同,這邊是主動向觀察者「請求」一筆 double 型別的資料。
  • 將 RequestMessage 透過 Messenger.Send() 傳送出去後,接著透過 HasReceivedResponse 屬性檢查是否有收到觀察者的回應。
  • 若有回應,則可使用 Response 屬性取得觀察者所回傳的資料,並在 Console 中印出對應結果,若未收到回應,也會顯示相應訊息。

Window7_SubSendViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public partial class Window7_SubSendViewModel : ObservableRecipient
{
public Window7_SubSendViewModel()
{

}

[RelayCommand]
protected void SendMegClick()
{
var req = new RequestMessage<double>();

Console.WriteLine($"SendMeg");
Messenger.Send(req);

if (req.HasReceivedResponse is true)
{
Console.WriteLine($"HasReceivedResponse: true");
Console.WriteLine($"Response: {req.Response}");
}
else
{
Console.WriteLine($"HasReceivedResponse: false");
}

Console.WriteLine("");
}
}

觀察者 ViewModel 的部分說明如下:

  • 註冊接收 RequestMessage<double> 類型的訊息,當接收到請求時,代表有其他物件主動要求一筆 double 資料。
  • 在 Receive 方法中,隨機產生一個 double 數值,並透過 Reply() 將此值回傳給請求者,完成一次有回應的訊息傳遞。

Window7_SubReceiveViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public partial class Window7_SubReceiveViewModel : ObservableRecipient
{
public Window7_SubReceiveViewModel()
{
IsActive = true;
}

protected override void OnActivated()
{
Messenger.Register<RequestMessage<double>>(
this, (r, m) => Receive(m));
}

protected void Receive(RequestMessage<double> m)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.WriteLine($"Receive a RequestMessage");

var random = new Random();
double randomNum = Math.Round(random.NextDouble() * 100, 2);

Console.WriteLine($"Prepare Reply: {randomNum}");
Console.ResetColor();
m.Reply(randomNum);
}
}

MainViewModel 部分,集中管理發送者與觀察者的實例。

Window7_MainViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public partial class Window7_MainViewModel : ObservableObject
{
public Window7_SubSendViewModel SendViewModel
{ get; protected set; }

public Window7_SubReceiveViewModel RecViewModel
{ get; protected set; }

public Window7_MainViewModel()
{
SendViewModel = new();
RecViewModel = new();
}
}

View 的部分,為配合發送者 ViewModel 的接口,撰寫相應的 UI 元件。

Window7

1
2
3
4
5
6
7
8
9
10
11
12
13
<Window.DataContext>
<local:Window7_MainViewModel/>
</Window.DataContext>

<StackPanel Margin="30"
DataContext="{Binding Path=SendViewModel}">

<Button Margin="0,10,0,0"
Height="30"
Content="Send Str"
Command="{Binding Path=SendMegClickCommand}"/>

</StackPanel>

執行的畫面與結果如下,

按下按鈕後,可在 Console 中看到完整的訊息流程:

  • 白字部分:代表發送者,顯示了訊息的發送過程與收到回應後的結果。
  • 綠字部分:代表觀察者,顯示接收到的請求訊息,以及隨機產生並回覆的數值。

使用 RequestMessage 機制的程式執行結果


總結

到此,已經對 CommunityToolkit.Mvvm 套件中 Messenger 的使用方式,以及在一些特殊情境下的應對方式,已有相當的了解。

不論是在 ViewModel、一般類別,甚至直接在 View 中,都能靈活運用 Messenger 進行訊息傳遞。

該套件中,關於 Messenger 的部分還有其他物件。

而由於篇幅限制,加上這些功能較為進階,所以未深入探討,例如:

Messaging 相關類別:

  • StrongReferenceMessenger
  • MessageHandler<TRecipient, TMessage>

Messages 相關類別:

  • AsyncRequestMessage<T>
  • CollectionRequestMessage<T>
  • AsyncCollectionRequestMessage<T>

這些進階功能可用於非同步請求、集合型訊息,或需要 Strong 參考與自訂訊息處理的情境。

整體而言,Messenger 相較於一般的發布/訂閱機制,有其特別之處,例如自動型別匹配、支援回應訊息以及輕鬆與 ViewModel 或 View 整合等功能。但這並不代表它是適用於所有場合的最佳方案,建議仍依照專案架構與實際需求來選擇使用方式。

自言自語543

回想剛接觸 WPF 的 MVVM 架構時,除了要實作 INotifyPropertyChanged、ICommand 這兩個介面外,ViewModel 與 View 的 Binding 屬性還要寫一堆重複度極高的程式碼。

在使用 CommunityToolkit.Mvvm 套件之後,不僅免去了自己尋找介面實作的麻煩,還有一些特別的 Attribute 可以減少大量重複的程式。

用了套件後,考試都考 100 分呢 (不要瞎掰好嗎?)

套件裡除了必用的 ObservableObject,還有依情況會用到的 ObservableValidator。其實也曾注意過 ObservableRecipient,但一開始以為它只是普通的發布/訂閱機制,所以沒有特別去深入了解。直到某次需要在 View 裡呼叫一般方法時,才發現它其實有特別之處。

接著開始做相關測試,沒想到居然是坑最大的一個……,結果篇幅寫得有點長,也因此有點亂,未來應該會稍微控制一下。

評論




本站使用 Volantis 作為主題,總訪問量為 次