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

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

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

CommunityToolkit.Mvvm 套件中,除了提供 MVVM 架構下常用的 ObservableObjectObservableValidator 外,還支援基於中介者模式的訊息傳遞機制 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# 開發中,無論是自行實作觀察者模式,或是使用現成機制,如 eventAction/ 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 的 RegisterAllUnregisterAll 方法。顧名思義,前者會將物件註冊到 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,有 ScrollIntoViewSelectAll 等方法。

WPF ListBox 普通方法定義


  • 又例如 TextBox,有 ScrollToLineSelect 等方法。

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 作為主題,總訪問量為