UWP - 介紹 Project Rome - 2

承接 UWP - 介紹 Project Rome - 1 介紹 RemoteSystem 的應用,這篇介紹利用 RemoteSystemSessionInfo 做到多個設備互相交換訊息。

利用 RemoteSystemSessionInfo 可以讓多個設備進入同一個 Session (如同進到包廂),並在裏面方便的交換訊息。

因此,下面範例分別用建立 Session 與加入 Session,最後再説明怎麽交換訊息。

在使用這些功能前,要記得呼叫 RemoteSystem.RequestAccessAsync,讓用戶允許 App 有權限。

 

* 利用 RemoteSystemSessionController 建立 Session,並處理相關事件

public async void OnCreateRemoteSystemSessionClick(object sender, RoutedEventArgs e)
{
    // 加入 option 限制只有被邀請的人才可以加入
    //RemoteSystemSessionOptions options = new RemoteSystemSessionOptions()
    //{
    //    IsInviteOnly = true
    //};

    sessionController = new RemoteSystemSessionController("today is happy day");

    sessionController.JoinRequested += SessionController_JoinRequested;

    // 建立一個 Remote Session
    RemoteSystemSessionCreationResult result = await sessionController.CreateSessionAsync();

    if (result.Status == RemoteSystemSessionCreationStatus.Success)
    {
        currentSession = result.Session;
        currentSession.Disconnected += (obj, args) =>
        {
             // 代表從該 Session 離線了
             Debug.WriteLine($"session_disconnected: {args.Reason.ToString()}");
        };

        // 注冊訊息通道做資料傳遞
        RegistMessageChannel(currentSession, currentSession.DisplayName);
        // 註冊有哪些參與者加入或離開
        SubscribeParticipantWatcher(currentSession);

        // 假設有選到特定的設備,也可以直接發邀請給對放        
        if (currentRemoteSystem != null)
        {
             var inviationResult = await currentSession.SendInvitationAsync(currentRemoteSystem);
        }
    }
}

private async void SessionController_JoinRequested(RemoteSystemSessionController sender, RemoteSystemSessionJoinRequestedEventArgs args)
{
    var deferral = args.GetDeferral();
    var remoteSystem = args.JoinRequest.Participant.RemoteSystem;

    await dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
    {
        var dialog = new MessageDialog($"do you access {remoteSystem.DisplayName} to join the session?");
        dialog.Commands.Add(new UICommand("Accept", (cmd) =>
        {
            args.JoinRequest.Accept();
        }));
        dialog.Commands.Add(new UICommand("Abort"));
        dialog.DefaultCommandIndex = 0;
        await dialog.ShowAsync();
    });

deferral.Complete();
}

如果是被邀請的參與者,需要註冊 RemoteSystemSessionInvitationListener 來處理被邀請的事件通知,如下範例:

private RemoteSystemSessionInvitationListener invitationListener;

public async void OnSubscribeAndHandleInvoke()
{
    invitationListener = new RemoteSystemSessionInvitationListener();
    // 註冊處理來自其他 RemoteSystem 發出的 RemoteSession 邀請
    invitationListener.InvitationReceived += async (sender, args) =>
    {
        // 未加入前,是利用 RemoteSystemInfo 做 JoinAsync()
        RemoteSystemSessionJoinResult joinResult = await args.Invitation.SessionInfo.JoinAsync();

        if (joinResult.Status == RemoteSystemSessionJoinStatus.Success)
        {
            // 注冊訊息通道做資料傳遞
            RegistMessageChannel(currentSession, currentSession.DisplayName);    
            // 註冊有哪些參與者加入或離開
            SubscribeParticipantWatcher(currentSession);                
        }
    };
}

* 利用 RemoteSystemSessionWatcher 找到可以加入的 Sessions,並請求加入

public void OnDescoverSessionAsync(object sender, RoutedEventArgs e)
{
    // 建立 Watcher 來查看有哪些 Sessions 被建立或是刪除
    sessionWatcher = RemoteSystemSession.CreateWatcher();

    sessionWatcher.Added += (s, a) => {                
        // 將找到的 RemoteSystemInfo 加入 UI 顯示
        RemoteSystemSessionInfo sessionInfo = a.SessionInfo;

        var addedTask = dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
        {
     sessionList.Add(sessionInfo);
        });
    };

    sessionWatcher.Removed += (s, a) => {
 // 將已經結束的 session 從 UI 移除
 var removedSession = a.SessionInfo;
 var exist = sessionList.Where(x => x.ControllerDisplayName == removedSession.ControllerDisplayName && x.DisplayName == removedSession.DisplayName).FirstOrDefault();

 if (exist!= null)
 {
     var removedTask = dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
            {
  sessionList.Remove(exist);
            });
        }
    };

    sessionWatcher.Updated += (s, a) => {
 // 講更新的 RemoteSystemInfo 加入到 UI
 var updatedSession = a.SessionInfo;
 var exist = sessionList.Where(x => x.ControllerDisplayName == updatedSession.ControllerDisplayName && x.DisplayName == updatedSession.DisplayName).FirstOrDefault();

 if (exist != null)
 {
     var updatedTask = dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
            {
  sessionList.Remove(exist);
  sessionList.Add(updatedSession);
            });
        }
    };

    sessionWatcher.Start();
}

選擇找到的 RemoteSystemInfo 來請求加入:

public async void OnJoinSessionSelectChangedAsync(object sender, SelectionChangedEventArgs e)
{
    var listView = sender as ListView;
    var session = listView.SelectedItem as RemoteSystemSessionInfo;
 
    if (session!= null)
    {
        // 請求加入 session
        var result = await session.JoinAsync();
        Debug.WriteLine($"join {session.DisplayName}: {result.Status.ToString()}");
  
 if (result.Status == RemoteSystemSessionJoinStatus.Success)
 {
     this.currentSession = result.Session;
            // 注冊訊息通道做資料傳遞
            RegistMessageChannel(currentSession, currentSession.DisplayName);
            // 註冊有哪些參與者加入或離開
            SubscribeParticipantWatcher(currentSession);
        }
    }
}

* 註冊 Session 的交易資料通道來發送或接收訊息

private RemoteSystemSessionMessageChannel appMessageChannel;

private void RegistMessageChannel(RemoteSystemSession session, string channelName)
{
    if (appMessageChannel != null)
    {
        appMessageChannel.ValueSetReceived -= AppMessageChannel_ValueSetReceived;
        appMessageChannel = null;
    }

    // 利用 RemoteSystemSession 注冊訊息通道
    appMessageChannel = new RemoteSystemSessionMessageChannel(session, channelName);
    appMessageChannel.ValueSetReceived += AppMessageChannel_ValueSetReceived;
}

private void AppMessageChannel_ValueSetReceived(RemoteSystemSessionMessageChannel sender, RemoteSystemSessionValueSetReceivedEventArgs args)
{
    // 處理收到的訊息
    ValueSet receivedMessage = args.Message;

    if (receivedMessage != null)
    {
        // 建立一個假的 MessageData 類別來做為交易訊息的内容
        MessageData msgData = new MessageData();
        byte[] data = receivedMessage["Key"] as byte[];
        using (MemoryStream ms = new MemoryStream(data))
        {
            DataContractJsonSerializer ser = new DataContractJsonSerializer(msgData.GetType());
            msgData = ser.ReadObject(ms) as MessageData;
        }
    }
}

要在 session 裏面傳遞訊息要記得註冊 RemoteSystemSessionMessageChannel

發送訊息的範例程式如下:

public void OnSendMessageToSessionClick(object sender, RoutedEventArgs e)
{
    // 準備要發送的訊息内容
    var msg = new MessageData
    {
        Content = "test"
    };

    using (var stream = new MemoryStream())
    {
        new DataContractJsonSerializer(message.GetType()).WriteObject(stream, message);
        byte[] data = stream.ToArray();
        
        // 將内容包裝到 ValueSet 
        ValueSet sentMessage = new ValueSet { ["Key"] = data };
        
        // 可以選擇發給全部的參與者或是特定的參與者們
        await appMessageChannel.BroadcastValueSetAsync(sentMessage);
    }
}

public class MessageData
{
    public string Content { get; set; }
}

如果想要發訊息給特定的參與者,可以參考:

private RemoteSystemSessionParticipantWatcher participantWatcher;

public ObservableCollection Participants => watchedParticipants;
private ObservableCollection watchedParticipants;

private void SubscribeParticipantWatcher(RemoteSystemSession session)
{
    if (participantWatcher != null)
    {
        participantWatcher.Stop();
    }

    participantWatcher = null;

    // 當加入或建立 RemoteSystemSession 之後,利用 ParticipantWatcher 來看有那些參與者
    participantWatcher = session.CreateParticipantWatcher();

    participantWatcher.Added += (s, a) => {
        watchedParticipants.Add(a.Participant);
    };

    participantWatcher.Removed += (s, a) => {
        watchedParticipants.Remove(a.Participant);
    };

    participantWatcher.Start();
}

上面的範例可以到 26-RemoteSystemSample 下載來使用。

幾個重要的元素:

  1. RemoteSystemSessionController
    負責建立與管理 new remote session 讓其他設備可以加入。每一個 session 都有一位參與者扮演管理者角色,只有管理者才能設定 session 的特性,允許誰可以加入,或是把人踢出。
    Constructors RemoteSystemSessionController(String, RemoteSystemSessionOptions) 給與 remote session 的公開名稱, 並設定相關特性 RemoteSystemSessionOptions
    Methods CreateSessionAsync() 非同步建立 RemoteSystemSession
      RemoveParticipantAsync( RemoteSystemSessionParticipant) 從 Session 移除特定參與者(RemoteSystemSessionParticipant)。
    Events JoinRequested 當有另一個設備找到該 Session 並請求加入時會觸發。
  2. RemoteSystemSessionWatcher
    監視與發現遠端會話相關的活動並引發相應的事件。
    Status 取得 RemoteSystemSessionWatcher 的運作狀態
    Added 當發現有新的 Session 被找到時會觸發
    Removed 當之前被找到的 Session 消失時會觸發
    Updated 當先前被找到的 Session 有部分資訊被更新時會觸發
  3. RemoteSystemSession
    表示和處理可在兩個或多個連接的設備之間共用的遠端會話。 它是更廣泛的遠端系統功能集的一部分,它被建立之後可以讓多個設備加入並裏面交換訊息,實現跨裝置的資訊交換。
    CreateParticipantWatcher() 初始化 RemoteSystemSessionParticipantWatcher 來監看裏面的參與者
    CreateWatcher() 實例化取得 RemoteSystemSessionWatcher 來監控可能的 session
    SendInvitationAsync(RemoteSystem) 邀請某個 RemoteSystem 加入 Session。 接受邀請的設備需要搭配 RemoteSystemSessionInvitationListener 來處理。
    Disconnected 當這個設備從這個 remote session 斷線時被觸發
  4. RemoteSystemSessionInfo
    内容包含 Remote Session 的相關資訊。它與 RemoteSystemSession 最大差別在於,已經加入的 session 用 RemoteSystemSession 代表,未加入的 session 則是 RemoteSystemSessionInfo (因爲它才有 JoinAsync 的 method 可以用)。
  5. RemoteSystemSessionParticipantWatcher
    負責監控 remote session 有哪些參與者加入/離開。
  6. RemoteSystemSessionMessageChannel
    在 remote session 中處理訊息專用的資料傳輸通道,包括:傳送與接收。
    BroadcastValueSetAsync(ValueSet) 傳送訊息給所有的參與者
    SendValueSetAsync(ValueSet, RemoteSystemSessionParticipant) 傳送訊息給特定的一位參與者
    SendValueSetToParticipantsAsync(ValueSet, IIterable) 傳訊訊息給特定一群參與者
    ValueSetReceived 當收到訊息時會觸發該事件

======

另外 Windows Holographic 利用 SpatialEntityStore 建立多個設備之間訊息的傳遞與資料管理,更多細範例可參考 Quiz Game sample app

這篇内容參考 Connect devices through remote sessions 來加以説明,希望對大家有所幫助。

References