2018 年 5 月

第 33 卷,第 5 期

本文章是由機器翻譯。

通用 Windows 平台 - 透過 UWP 與 Project Rome 建置連線應用程式

Tong 捍衛

在現今的世界中,建置成功的應用程式表示移動超過單一裝置。使用者想要擴展他們的裝置,且即使連接與其他使用者的應用程式。提供這種類型,可以是體驗的鉅的挑戰,說出最少。為了協助解決生態系統,Microsoft 在這個成長需要引進了專案羅馬。專案羅馬以外的工具來建立個人的 OS 跨越應用程式、 裝置和使用者。專案羅馬具有適用於大部分主要的平台 Sdk,而本文章中我們即將內容探索如何使用專案羅馬建立小組傳訊通用 Windows 平台 (UWP) 應用程式。

看看專案羅馬

Project Rome 是一個新計畫,能協助您在各應用程式和裝置推動使用者參與。它是集合的 Api,屬於 Microsoft Graph,但可以分成兩個區域: 現在繼續,並稍後再繼續。

遠端系統 Api 啟用中斷使用者的目前裝置界限之外的應用程式啟用繼續-現在體驗。是否允許使用者為單一體驗,使用兩個裝置,以小幫手或遠端控制的應用程式,或讓多位使用者連線及共用單一的體驗,這些 Api 會提供目前的使用者參與的展開的檢視。訊息本文中建置應用程式的小組將會建立共用的使用者經驗。

專案羅馬,活動的 Api,另一半著重於使用者的經驗,稍後再繼續。這些 Api 可讓您記錄並擷取從任何裝置的使用者可以繼續應用程式內的特定動作。我不在本文中討論它們,但這些都是肯定值得究竟。

入門

在探究之前建置的應用程式,我需要設定我的環境。專案羅馬的第一個版本已出一小段時間,而某些功能在本文中使用剛才發行最近改建立者更新期間。因此,您的電腦必須執行組建編號 16299 或更高。此時,此版本中使用中的更新緩慢的信號,應該正確更新大部分的機器。

與遠端系統 Api 與其他使用者執行應用程式需要電腦上的共用的經驗會啟用。作法是在 [系統設定,在 [設定 |系統 |共用的體驗。小組傳訊案例中,您必須啟用不同的使用者與您的裝置進行通訊,這表示您需要先確定共用經驗會啟用,而且您可以共用,或如中所示,從 「 Everyone 附近,「 接收圖 1.

啟用共用的體驗
圖 1 啟用共用的體驗

最終的需求是連線的您的裝置會成為可搜尋,某些層級。遠端系統 Api 將探索其他電腦上相同的網路,以及附近使用藍芽。可以在 [藍芽和其他裝置設定] 頁面中您的系統設定中啟用藍芽。

設定機器,讓我們先建立新 Visual C# 應用程式在 Visual Studio 2017 中使用空白的應用程式 (通用 Windows) 範本。呼叫應用程式"TeamMessenger。 」 如先前所述,此專案需要改建立者更新,因此,設定為 [組建 16299] 應用程式的目標和最小版本或更多,如下所示圖 2。如此可防止支援舊版的 Windows 10 應用程式,但需要的某些功能在本文觸及。

設定應用程式的目標版本
圖 2 設定目標版本的應用程式

請注意,如果您沒有在您的裝置上更新 Sdk 改建立者,以取得這些最簡單的方式更新至最新版本的 Visual Studio 2017。

專案羅馬 Api 是 Windows 10 SDK,表示沒有其他要下載 Sdk 或 NuGet 封裝,才能建立此應用程式安裝的一部分。有,不過,必須加入至應用程式開發介面的遠端工作階段才可正確運作的應用程式的一些功能。這可藉由開啟 package.appxmanifest 檔案,然後選取 [功能] 索引標籤。在可用功能的清單,請確定下列檢查:藍芽、 網際網路 (用戶端和伺服器) 和遠端系統。

建立工作階段連線

此應用程式將會包含兩個頁面,以負責建立或加入遠端工作階段,使用遠端系統 Api 的第一頁。為了簡單起見,我將建置此頁面,使用已建立與方案和已有線網路到應用程式載入的第一個頁面 MainPage.xaml。UI 有兩種模式: 建立或裝載的工作階段,並加入現有的工作階段。建立工作階段需要是公開給想要加入的使用者工作階段名稱。加入現有的工作階段需要顯示一份可用附近的工作階段。這兩種模式需要可向使用者顯示的名稱。圖 3顯示產生 UI 外觀應該為何 MainPage 和 XAML 建置此頁面可以在找到圖 4

MainPage UI
圖 3 MainPage UI

圖 4 MainPage XAML

<Page
  x:Class="TeamMessenger.MainPage"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:remotesystems="using:Windows.System.RemoteSystems"
  mc:Ignorable="d">
  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <StackPanel Width="400"
                HorizontalAlignment="Center"
                BorderBrush="Gray"
                BorderThickness="1"
                MaxHeight="600"
                VerticalAlignment="Center"
                Padding="10">
    <RadioButton x:Name="rbCreate"
                GroupName="options"
                IsChecked="True"
                Checked="{x:Bind ViewModel.CreateSession}"
                Content="Create a New Session"/>
    <StackPanel Orientation="Horizontal" Margin="30,10,20,30">
      <TextBlock VerticalAlignment="Center">Session Name :</TextBlock>
      <TextBox Text="{x:Bind ViewModel.SessionName, Mode=TwoWay}"
               Width="200"
               Margin="20,0,0,0"/>
    </StackPanel>
    <RadioButton x:Name="rbJoin"
                GroupName="options"
                Checked="{x:Bind ViewModel.JoinSession}"
                Content="Join Session"/>
    <ListView ItemsSource="{x:Bind ViewModel.Sessions}"
              SelectedItem="{x:Bind ViewModel.SelectedSession, Mode=TwoWay}"
              IsItemClickEnabled="True"
              Height="200"
              BorderBrush="LightGray"
              BorderThickness="1"
              Margin="30,10,20,30">
      <ListView.ItemTemplate>
        <DataTemplate x:DataType="remotesystems:RemoteSystemSessionInfo">
          <TextBlock Text="{x:Bind DisplayName}"/>
        </DataTemplate>
      </ListView.ItemTemplate>
    </ListView>
    <StackPanel Orientation="Horizontal">
      <TextBlock VerticalAlignment="Center">Name : </TextBlock>
      <TextBox Text="{x:Bind ViewModel.JoinName, Mode=TwoWay}"
               Width="200"
               Margin="20,0,0,0"/>
    </StackPanel>
    <Button Content="Start"
            Margin="0,30,0,0"
            Click="{x:Bind ViewModel.Start}"/>
    </StackPanel>
  </Grid>
</Page>

之後建立頁面的 XAML,應用程式中建立新的 ViewModels 資料夾稱為 MainViewModel.cs 該資料夾中加入新的公用類別。此檢視模型將有線到檢視,以處理功能。

檢視模型的第一個部分會處理管理選項按鈕,以決定使用者是否在建立新的工作階段狀態或加入現有。狀態保存在稱為 IsNewSession 的布林值。兩種方法用來切換此 bool CreateSession 和 JoinSession 狀態:

public bool IsNewSession { get; set; } = true;
public void CreateSession()
{
  IsNewSession = true;
}
public void JoinSession()
{
  IsNewSession = false;
}

每個選項按鈕的核取的事件繫結至其中一種方法。

其餘的 UI 項目會追蹤具有簡單內容。工作階段名稱與 JoinName 屬性會繫結工作階段名稱和使用者名稱。SelectedSession 屬性繫結至清單檢視中的 SelectedItem 屬性,其 ItemsSource 繫結至工作階段屬性:

public string JoinName { get; set; }
public string SessionName { get; set; }
public object SelectedSession { get; set; }
public ObservableCollection<
  RemoteSystemSessionInfo> Sessions { get; } =
  new —ObservableCollection<
  RemoteSystemSessionInfo>();

檢視模型有兩個將用於讓知道工作階段的連接是否成功或不檢視的事件:

public event EventHandler SessionConnected =
  delegate { };
public event EventHandler<SessionCreationResult> ErrorConnecting = delegate { };

最後,[開始] 按鈕會繫結至開始方法。這個方法可以保留空白目前。

檢視模型完成後,一旦 MainPage 程式碼後的置需要建立 MainViewModel 的執行個體的公用屬性。這就是允許 X:bind 建置的編譯時間繫結。此外,它必須在檢視模型中建立的兩個事件訂閱。如果已成功建立連線,我會導覽至新的頁面上,MessagePage。如果連接失敗,將會顯示 MessageDialog,通知使用者的連線失敗。圖 5 MainPage.xaml.cs 包含的程式碼。

圖 5 MainPage.xaml 程式碼後置

public sealed partial class MainPage : Page
{
  public MainPage()
  {
    this.InitializeComponent();
    ViewModel.SessionConnected += OnSessionConnected;
    ViewModel.ErrorConnecting += OnErrorConnecting;
  }
  private async void OnErrorConnecting(object sender, SessionCreationResult e)
  {
    var dialog = new MessageDialog("Error connecting to a session");
    await dialog.ShowAsync();
  }
  private void OnSessionConnected(object sender, EventArgs e)
  {
    Frame.Navigate(typeof(MessagePage));
  }
  public MainViewModel ViewModel { get; } = new MainViewModel();
}

定義資料模型

之前深入核心的應用程式,您必須在應用程式中定義幾個將用於資料模型。應用程式中建立 Models 資料夾,然後在其中建立兩個類別:使用者和 UserMessage。正如其名,使用者模型將會追蹤使用者連接到應用程式的相關資訊:

public class User
{
  public string Id { get; set; }
  public string DisplayName { get; set; }
}

UserMessage 類別將會包含訊息內容,內容和建立訊息時所建立的使用者:

public class UserMessage
{
  public User User { get; set; }
  public string Message { get; set; }
  public DateTime DateTimeStamp { get; set; }
}

建立工作階段

主頁面相當完整的程式碼後,我可以開始建置出 RemoteSessionManager,將會用來包裝遠端系統 API。加入新的公用類別,稱為 RemoteSessionManager 至應用程式的根目錄。應用程式會使用 RemoteSessionManager 的單一共用執行個體,因此對應用程式中的類別 App.xaml.cs 的靜態屬性:

public static RemoteSessionManager SessionManager { get; } = new RemoteSessionManager();

應用程式可以存取任何遠端系統 Api 之前,它必須先取得權限的使用者。此權限是藉由呼叫靜態方法 RemoteSystem.RequestAccessAsync 取得:

RemoteSystemAccessStatus accessStatus = 
  await RemoteSystem.RequestAccessAsync();
if (accessStatus != RemoteSystemAccessStatus.Allowed)
{
  // Access is denied, shortcut workflow
}

方法會傳回可用來判斷是否授與存取權的 RemoteSystemAccessStatus 列舉。必須從 UI 執行緒呼叫這個方法,以便成功,則可以提示使用者。一旦使用者已授與或拒絕應用程式的權限,任何後續呼叫會自動傳回使用者的喜好設定。此應用程式中,此權限將會加入至工作階段探索因為它會先呼叫工作流程中。

請注意,所有的遠端系統 Api 可以找到 Windows.System.RemoteSystem 命名空間中。

第一種方法可以新增到 RemoteSessionManager 類別是 CreateSession 方法。由於有數個可以從這個方法傳回的結果,我會包裝這些新的列舉中 — SessionCreationResult。SessionCreationResult 有四個可能的值: 成功和三個不同的失敗。工作階段無法建立,因為使用者未授與存取應用程式。應用程式目前有太多的工作階段執行。或者,系統錯誤而無法建立工作階段:

public enum SessionCreationResult
{
  Success,
  PermissionError,
  TooManySessions,
  Failure
}

遠端工作階段是由 RemoteSystemSessionController 管理。在建立新的 RemoteSystemSessionController 執行個體時,您必須傳遞在將裝置加入工作階段嘗試要顯示的名稱。

一旦要求控制器時,可以藉由呼叫 CreateSession 方法啟動工作階段。這個方法會傳回包含狀態和工作階段的新執行個體,如果已順利完成 RemoteSystemSessionCreationResult。RemoteSessionManager 會將新的控制站和儲存工作階段中的私用變數。

新的公用屬性,IsHost,應加入至管理員],以及對於判斷工作流程。期間 CreateSession 方法中,這個值設定為 true 時,用來識別此應用程式為主控件。另一個公用屬性,而 CurrentUser,在電腦上提供的使用者執行個體,而且會用於訊息。工作階段管理員也會以目前工作階段中的使用者 ObservableCollection。這個集合會使用新建立的使用者初始化。工作階段的主機,此執行個體取得 CreateSession 方法中建立。RemoteSessionManager 附加的結果如下所示圖 6

圖 6 CreateSession 方法

private RemoteSystemSessionController _controller;
private RemoteSystemSession _currentSession;
public bool IsHost { get; private set; }
public User CurrentUser { get; private set; }
public ObservableCollection<User> Users { get; } =
  new ObservableCollection<User>();
public async Task<SessionCreationResult> CreateSession(
  string sessionName, string displayName)
{
  SessionCreationResult status = SessionCreationResult.Success;
  RemoteSystemAccessStatus accessStatus = await RemoteSystem.RequestAccessAsync();
  if (accessStatus != RemoteSystemAccessStatus.Allowed)
  {
    return SessionCreationResult.PermissionError;
  }
  if (_controller == null)
  {
    _controller = new RemoteSystemSessionController(sessionName);
    _controller.JoinRequested += OnJoinRequested;
  }
  RemoteSystemSessionCreationResult createResult =
    await _controller.CreateSessionAsync();
  if (createResult.Status == RemoteSystemSessionCreationStatus.Success)
  {
    _currentSession = createResult.Session;
    InitParticipantWatcher();
    CurrentUser = new User() { Id = _currentSession.ControllerDisplayName,
      DisplayName = displayName };
    Users.Add(CurrentUser);
    IsHost = true;
  }
  else if(createResult.Status ==
    RemoteSystemSessionCreationStatus.SessionLimitsExceeded)
  {
    status = SessionCreationResult.TooManySessions;
  } else
  {
    status = SessionCreationResult.Failure;
  }
  return status;
}

有三個要加入至 RemoteSessionManager 完成 CreateSession 方法的多個項目。第一個是事件處理常式,當使用者嘗試加入的工作階段而 JoinRequested 引發工作階段上。OnJoinRequested 方法將會自動接受任何嘗試加入的使用者。這可延伸使用者加入至工作階段之前,提示核准的主機。包含在事件處理常式的 RemoteSystemSessionJoinRequestedEventArgs 參數 RemoteSystemSessionJoinRequest 提供要求資訊。叫用接受方法會將使用者加入至工作階段。下列程式碼包含要加入至 RemoteSessionManager,以及完成的 OnJoinRequested 方法的新事件:

private void OnJoinRequested(RemoteSystemSessionController sender,
  RemoteSystemSessionJoinRequestedEventArgs args)
{
  var deferral = args.GetDeferral();
  args.JoinRequest.Accept();
  deferral.Complete();
}

新增或移除目前工作階段,透過 RemoteSystemSessionParticipantWatcher 參與者時,可以監視工作階段管理員。這個類別會監控參與者,並產生在需要時加入 」 或 「 已移除事件。當使用者已經加入工作階段正在進行中時,每個參與者已在目前工作階段將會收到附加事件。應用程式會採用這一系列的事件,並判斷哪個參與者是藉由符合針對工作階段的 ControllerDisplayName DisplayName 的主機。這可讓參與者可直接與主機通訊。工作階段管理員維護參與者監看員為初始化 InitParticipantWatcher 中的私用變數。建立工作階段是否會呼叫這個方法,或將現有的工作階段。圖 7包含新的新增項目。您會發現此工作流程需要知道參與者已移除時只有當您是主機,而且是否參與者會加入,如果您正在連線工作階段。為主機,參與者會保留工作階段時,才被有關 RemoteSessionManager。主機會直接透過通知參與者在加入時,您會發現在本文稍後。若要判斷目前的主控件帳戶只需要參與者。

圖 7 InitParticipantWatcher

private RemoteSystemSessionParticipantWatcher _participantWatcher;
private void InitParticipantWatcher()
{
  _participantWatcher = _currentSession.CreateParticipantWatcher();
  if (IsHost)
  {
    _participantWatcher.Removed += OnParticipantRemoved;
  }
  else
  {
    _participantWatcher.Added += OnParticipantAdded;
  }
  _participantWatcher.Start();
}
private void OnParticipantAdded(RemoteSystemSessionParticipantWatcher watcher,
  RemoteSystemSessionParticipantAddedEventArgs args)
{
  if(args.Participant.RemoteSystem.DisplayName ==
    _currentSession.ControllerDisplayName)
  {
    Host = args.Participant;
  }
}
private async void OnParticipantRemoved(RemoteSystemSessionParticipantWatcher watcher,
  RemoteSystemSessionParticipantRemovedEventArgs args)
{
  var qry = Users.Where(u => u.Id == args.Participant.RemoteSystem.DisplayName);
  if (qry.Count() > 0)
  {
    var dispatcher = CoreApplication.MainView.CoreWindow.Dispatcher;
    await dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High,
      () => { Users.Remove(qry.First()); });
    await BroadCastMessage("users", Users);
  }
}

CreateSession 方法需要加入至類別的最後一件事是,讓取用者知道工作階段已中斷連接時的事件。新的 SessionDisconnected 事件定義如下:

public event EventHandler<RemoteSystemSessionDisconnectedEventArgs> SessionDisconnected =
  delegate { };

加入工作階段

應用程式成為能夠廣播新的遠端工作階段之後下, 一步要實作會加入該工作階段,從另一部電腦的能力。有兩個要加入遠端工作階段的步驟: 探索,然後連接到工作階段。

探索工作階段應用程式就可以找出附近的 RemoteSystemSessionWatcher,建立靜態 CreateWatcher 方法從透過遠端工作階段。這個類別會引發事件,每次加入或移除工作階段。將新方法加入 — DiscoverSessions — RemoteSessionManager 至。這個方法將做為類別的私用變數建立 RemoteSystemSessionWatcher,並處理加入和移除事件。這些事件將由兩個新事件加入至 RemoteSessionManager 包裝:SessionAdded 和 SessionRemoved。因為這會初始化遠端工作階段的使用者的另一個進入點,您必須確定將呼叫加入 RemoteSystem.RequestAccessAsync。圖 8包含私用變數、 兩個事件和完整 DiscoverSessions 方法。

圖 8 探索工作階段

private RemoteSystemSessionWatcher _watcher;
public event EventHandler<RemoteSystemSessionInfo> SessionAdded = delegate { };
public event EventHandler<RemoteSystemSessionInfo> SessionRemoved = delegate { };
public async Task<bool> DiscoverSessions()
{
  RemoteSystemAccessStatus status = await RemoteSystem.RequestAccessAsync();
  if (status != RemoteSystemAccessStatus.Allowed)
  {
    return false;
  }
  _watcher = RemoteSystemSession.CreateWatcher();
  _watcher.Added += (sender, args) =>
  {
    SessionAdded(sender, args.SessionInfo);
  };
  _watcher.Removed += (sender, args) =>
  {
    SessionRemoved(sender, args.SessionInfo);
  };
  _watcher.Start();
  return true;
}

現在很可能在網路中可用的本機工作階段以更新工作階段屬性 MainViewModel。DiscoverSessions 方法是非同步的因為 MainViewModel 的建構函式必須初始化來叫用它的工作。正在初始化的方法也應該登錄,並處理 SessionAdded 和 SessionRemoved 事件。因為不會在 UI 執行緒上引發這些事件,更新的工作階段屬性時,務必使用 CoreDispatcher。MainViewModel 更新位於圖 9

圖 9 新增工作階段探索

public MainViewModel()
{
  _initSessionManager = InitSessionManager();
}
private Task _initSessionManager;
private async Task InitSessionManager()
{
  App.SessionManager.SessionAdded += OnSessionAdded;
  App.SessionManager.SessionRemoved += OnSessionRemoved;
  await App.SessionManager.DiscoverSessions();
}
private async void OnSessionAdded(object sender, RemoteSystemSessionInfo e)
{
  var dispatcher = CoreApplication.MainView.CoreWindow.Dispatcher;
  await dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High,
    () => { Sessions.Add(e); });
}
private async void OnSessionRemoved(object sender, RemoteSystemSessionInfo e)
{
  if (Sessions.Contains(e))
  {
    var dispatcher = CoreApplication.MainView.CoreWindow.Dispatcher;
    await dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High,
      () => { Sessions.Remove(e); });
  }
}

連接到工作階段RemoteSessionManager 一旦使用者已經識別它們要加入,並提供名稱的工作階段,必須能夠將它們連接至所選工作階段。這將會處理新 JoinSession 方法上 RemoteSessionManager,接受選取的工作階段和輸入的顯示名稱做為參數。

JoinSession 方法一開始所提供的工作階段上呼叫 JoinAsync 方法。這將引發 JoinRequested 事件上主機工作階段。如果主應用程式核准要求,會傳回成功狀態,且 CurrentUser 設定使用的顯示名稱。為使用 CreateSession 方法時,InitParticipantWatcher 方法會叫用參與者會加入至工作階段時,註冊的事件處理常式。JoinSession 方法所示圖 10

圖 10 JoinSession 方法

public async Task<bool> JoinSession(RemoteSystemSessionInfo session, string name)
{
  bool status = true;
  RemoteSystemSessionJoinResult joinResult = await session.JoinAsync();
  if (joinResult.Status == RemoteSystemSessionJoinStatus.Success)
  {
    _currentSession = joinResult.Session;
    CurrentUser = new User() { DisplayName = name };
  }
  else
  {
    status = false;
  }
  InitParticipantWatcher();
  return status;
}

參與加入工作階段的最後一個步驟是建立或加入的工作階段使用 RemoteSessionManager 中建立的功能。圖 11中會繫結至 [開始] 按鈕,在 MainPage MainViewModel 顯示開始方法。方法的工作流程很簡單。根據 IsNewSession,它會呼叫 CreateSession 方法或 JoinSession 方法。藉由引發 SessionConnected 或 ErrorConnecting 事件會傳回結果。如果工作階段成功時,應用程式巡覽至 MessagePage,我會建置在下一節。

圖 11 啟動工作階段

public async void Start()
{
  if(IsNewSession)
  {
    var result = await App.SessionManager.CreateSession(SessionName, JoinName);
    if(result == SessionCreationResult.Success)
    {
      SessionConnected(this, null);
    } else
    {
      ErrorConnecting(this, result);
    }
  } else
  {
    if(SelectedSession != null)
    {
      var result = await App.SessionManager.JoinSession(
        SelectedSession as RemoteSystemSessionInfo, JoinName);
      if(result)
      {
        SessionConnected(this, null);
      } else
      {
        ErrorConnecting(this, SessionCreationResult.Failure);
      }
    }
  }
}

保留與對話的應用程式

此時,應用程式可以成功地建立或加入的工作階段並已準備好可供傳訊的 UI。唯一的其餘步驟啟用的裝置來與對方進行通訊。這被透過使用遠端系統 API 傳送 ValueSet 機器之間的執行個體。每個 ValueSet 是序列化裝載的索引鍵/值組集合。

接收訊息RemoteSystemSessionMessageChannel 透過在工作階段中傳輸訊息。工作階段可以有多個通道。不過,此應用程式必須單一通道。在 RemoteSessionManager,我要加入 StartReceivingMessages 方法。這個方法會建立新的訊息通道會儲存在私用變數,然後將處理常式加入 ValueSetReceived 事件。

訊息會以文字傳送,因為應用程式正在使用以訊息的類別,需要序列化的資料。從通道收到 ValueSet 時,DataContractJsonSerializer 用來解除凍結 DeserializeMessage 類別中的訊息類別。我無法通知訊息的何種型別會序列化,因為應用程式會傳送給每一種訊息集中不同的值。DeserializeMessage 類別會判斷要使用哪個金鑰,並傳回正確的類別。

準備訊息類別之後,將會根據其類型在訊息上做管理員類別。如您所見,參與者會宣布本身主應用程式傳送其 CurrentUser 執行個體。回應時,主機會將更新的使用者清單廣播給所有參與者。如果工作階段管理員接收到參與者的清單,它會使用更新的資料來更新使用者集合。最後一個選擇 UserMessage,將會引發新的 MessageReceived 事件所經歷的訊息和傳送訊息的參與者。RemoteSessionManager 這些新增項目位於圖 12

圖 12 接收訊息

private RemoteSystemSessionMessageChannel _messageChannel;
public event EventHandler<MessageReceivedEventArgs> MessageReceived = delegate { };
public void StartReceivingMessages()
{
  _messageChannel = new RemoteSystemSessionMessageChannel(_currentSession, "OpenChannel");
  _messageChannel.ValueSetReceived += OnValueSetReceived;
}
private object DeserializeMessage(ValueSet valueSet)
{
  Type serialType;
  object data;
   if(valueSet.ContainsKey("user"))
   {
    serialType = typeof(User);
    data = valueSet["user"];
  } else if (valueSet.ContainsKey("users"))
  {
    serialType = typeof(List<User>);
    data = valueSet["users"];
  } else
  {
    serialType = typeof(UserMessage);
    data = valueSet["message"];
  }
  object value;
  using (var stream = new MemoryStream((byte[])data))
  {
    value = new DataContractJsonSerializer(serialType).ReadObject(stream);
  }
  return value;
}
private async void OnValueSetReceived(RemoteSystemSessionMessageChannel sender,
  RemoteSystemSessionValueSetReceivedEventArgs args)
{
  var data = DeserializeMessage(args.Message);
  if (data is User)
  {
    var user = data as User;
    user.Id = args.Sender.RemoteSystem.DisplayName;
    if (!Users.Contains(user))
    {
      var dispatcher = CoreApplication.MainView.CoreWindow.Dispatcher;
      await dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.High,
        () => { Users.Add(user); });
    }
    await BroadcastMessage("users", Users.ToList());
  }
  else if (data is List<User>)
  {
    var users = data as List<User>;
    Users.Clear();
    foreach(var user in users)
    {
      Users.Add(user);
    }
  }
  else
  {
    MessageReceived(this, new MessageReceivedEventArgs()
    {
      Participant = args.Sender,
      Message = data
    });
  }
}

圖 12包含新的事件處理常式類別,MessageReceivedEventArgs,也必須建立。這個類別包含兩個屬性: 寄件者和訊息:

public class MessageReceivedEventArgs
{
  public RemoteSystemSessionParticipant Participant { get; set; }
  public object Message { get; set; }
}

傳送訊息遠端系統 API 提供兩種方法來傳遞訊息給其他使用者。第一個是廣播訊息到的所有使用者工作階段中。這個方法會用於兩個訊息類型: UserMessage 與的使用者清單。讓我們來建立新的方法,BroadcastMessage,RemoteSystemManager 中。這個方法會接受做為參數的金鑰和訊息。使用 DataContractJsonSerializer,我序列化的資料,並使用 BroadcastValueSetAsync 方法將訊息傳送至所有使用者,如中所示圖 13

圖 13,廣播一則訊息

public async Task<bool> BroadcastMessage(string key, object message)
{
  using (var stream = new MemoryStream())
  {
    new DataContractJsonSerializer(message.GetType()).WriteObject(stream, message);
    byte[] data = stream.ToArray();
    ValueSet msg = new ValueSet();
    msg.Add(key, data);
    await _messageChannel.BroadcastValueSetAsync(msg);
  }
  return true;
}

第二種方法是將訊息傳送至單一的參與者。這個方法很類似於廣播訊息時,除了它使用 SendValueSetAsync 方法,直接訊息參與者。這個最後的方法,以 RemoteSystemManager,SendMessage,位於圖 14

圖 14 傳送直接的訊息

public async Task<bool> SendMessage(string key, 
  object message, 
  RemoteSystemSessionParticipant participant)
{
  using (var stream = new MemoryStream())
  {
    new DataContractJsonSerializer(message.GetType()).WriteObject(stream, message);
    byte[] data = stream.ToArray();
    ValueSet msg = new ValueSet();
    msg.Add(key, data);
    await _messageChannel.SendValueSetAsync(msg, participant);
  }
  return true;
}

建置訊息的頁面

搭配訊息現在就地,就可以使其使用,並完成應用程式開始。應用程式中,MessagePage.xaml 加入新的空白頁面。此頁面將會包含一份使用者、 郵件視窗並輸入的欄位,以新增訊息。中可以找到完整的 XAML圖 15

圖 15 MessagePage XAML

<Page
  x:Class="TeamMessenger.MessagePage"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:local="using:TeamMessenger"
  xmlns:models="using:TeamMessenger.Models"
  xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
  xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
  xmlns:remotesystems="using:Windows.System.RemoteSystems"
  mc:Ignorable="d">
  <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.ColumnDefinitions>
      <ColumnDefinition MinWidth="200" Width="Auto"/>
      <ColumnDefinition Width="*"/>
    </Grid.ColumnDefinitions>
    <Grid VerticalAlignment="Stretch"
          BorderBrush="Gray" BorderThickness="0,0,1,0">
      <ListView ItemsSource="{x:Bind ViewModel.Users}">
        <ListView.ItemTemplate>
          <DataTemplate x:DataType="models:User">
            <TextBlock Height="25"
                       FontSize="16"
                       Text="{x:Bind DisplayName}"/>
          </DataTemplate>
        </ListView.ItemTemplate>
      </ListView>
    </Grid>
    <Grid Grid.Column="1" Margin="10,0,10,0">
      <Grid.RowDefinitions>
        <RowDefinition Height="*"/>
        <RowDefinition Height="Auto"/>
      </Grid.RowDefinitions>
      <ListView x:Name="lvMessages" ItemsSource="{x:Bind ViewModel.Messages}">
        <ListView.ItemTemplate>
          <DataTemplate x:DataType="models:UserMessage">
            <StackPanel Orientation="Vertical"
                        Margin="10,20,10,5">
              <TextBlock TextWrapping="WrapWholeWords"
                         Height="Auto"
                         Text="{x:Bind Message}"/>
              <StackPanel Orientation="Horizontal"
                          Margin="20,5,0,0">
                <TextBlock Text="{x:Bind User.DisplayName}"
                           FontSize="12"
                           Foreground="Gray"/>
                <TextBlock Text="{x:Bind DateTimeStamp}"
                           Margin="20,0,0,0"
                           FontSize="12"
                           Foreground="Gray"/>
              </StackPanel>
            </StackPanel>
          </DataTemplate>
        </ListView.ItemTemplate>
      </ListView>
      <Grid Grid.Row="1" Height="60"
            Background="LightGray">
        <Grid.ColumnDefinitions>
          <ColumnDefinition Width="*"/>
          <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <TextBox Text="{x:Bind ViewModel.NewMessage, Mode=TwoWay}"
                 Margin="10"/>
        <Button Grid.Column="1" Content="Send"
                Click="{x:Bind ViewModel.SubmitMessage}"
                Margin="10"/>
      </Grid>
    </Grid>
  </Grid>
</Page>

MainPage,像是 MessagePage 需要檢視模型。將新的類別,MessageViewModel,加入 ViewModels 資料夾。此檢視模型需要支援 INotifyPropertyChanged 才能正常運作的雙向繫結。此檢視的模型包含三個屬性:使用者、 訊息和 NewMessage。使用者會直接公開 RemoteSessionManager 檢視的使用者集合。將 ObservableCollection UserMessage 時收到的物件和 NewMessage 字串,包含文字,以新的訊息傳送訊息。另外還有一個 MessageAdded,供 MessagePage 中的程式碼後置的事件。檢視模型的建構函式,我需要對應的使用者屬性,請叫用 StartReceivingMessages RemoteSessionManager 和暫存器中 MessageReceived 事件方法,如中所示圖 16。建構函式也包含 INotifiyPropertyChanged 的實作。

圖 16 MessageViewModel 建構函式

public event PropertyChangedEventHandler PropertyChanged = delegate { };
public event EventHandler MessageAdded = delegate { };
public ObservableCollection<UserMessage> Messages { get; private set; }
public ObservableCollection<User> Users { get; private set; }
private string _newMessage;
public string NewMessage {
  get { return _newMessage; }
  set
  {
    _newMessage = value;
    PropertyChanged(this, new
    PropertyChangedEventArgs(nameof(NewMessage)));
  }
}
public MessageViewModel()
{
  Users = App.SessionManager.Users;
  Messages = new ObservableCollection<UserMessage>();
  App.SessionManager.StartReceivingMessages();
  App.SessionManager.MessageReceived += OnMessageRecieved;
  RegisterUser();
}

在建構函式沒有 RegisterUser 呼叫。這個方法會將傳送 CurrentUser 加入主機的工作階段時所建立。這向新使用者已加入的主機和顯示名稱。為了回應,主機會顯示應用程式中傳送出目前的使用者清單:

private async void RegisterUser()
{
  if(!App.SessionManager.IsHost)
    await App.SessionManager.SendMessage("user", App.SessionManager.CurrentUser,
                                                 App.SessionManager.Host);
}

檢視模型的最後一塊,就是使用者從廣播新訊息。Ibttransportbatch 方法建構新 UserMessage 和 RemoteSessionManager 上呼叫 BroadcastMessage 方法。接著清除 NewMessage 值,並引發 「 MessageAdded 事件,如中所示圖 17

圖 17 提交訊息

public async void SubmitMessage()
{
  var msg = new UserMessage()
  {
    User = App.SessionManager.CurrentUser,
    DateTimeStamp = DateTime.Now,
    Message = NewMessage
  };
  await App.SessionManager.BroadcastMessage("message", msg);
  Messages.Add(msg);
  NewMessage = "";
  MessageAdded(this, null);
}

在程式碼後置 MessagePage,以顯示圖 18,我需要做兩件事: 建立參考,並處理 MessageAdded 事件之 xaml MessageViewModel 的執行個體。在事件處理常式我會指示 ListView 來捲動到清單底部的位置會顯示最新的訊息。

圖 18 MessagePage 程式碼後置

public sealed partial class MessagePage : Page
{
  public MessagePage()
  {
    this.InitializeComponent();
    ViewModel.MessageAdded += OnMessageAdded;
  }
  private void OnMessageAdded(object sender, EventArgs e)
  {
    lvMessages.ScrollIntoView(ViewModel.Messages.Last());
  }
  public MessageViewModel ViewModel { get; } = new MessageViewModel();
}

小組傳訊應用程式現在應該準備好執行。在一部電腦上執行應用程式,並建立新的工作階段。然後啟動第二部電腦,這應會顯示新建立的訊息上的應用程式。一旦您加入工作階段,您會將他們帶往何處開始交談與其他人在工作階段中,新的訊息頁面中所示圖 19。您現在已建立的多使用者使用遠端系統 API 的應用程式。

多使用者訊息
圖 19 多使用者訊息

總結

建立應用程式內成功的使用者體驗,通常需要單一裝置或平台或甚至是使用者的因素。Microsoft 開發專案羅馬,可讓開發人員提供自己的應用程式內的體驗的這個層級。我可以在本文中建置 UWP 應用程式使用遠端系統 API;不過,藉由使用專案羅馬 Sdk 可用於其他平台,您可以擴充此應用程式,在多個平台上運作。當建立下一個絕佳的體驗,為您的使用者,請記得要考慮專案羅馬可以幫助您讓應用程式更個人。這份文件的原始程式碼,請參閱bit.ly/2FWtCc5


Tong 捍衛是與 20 多年的經驗與 Microsoft 技術所開發的軟體架構設計人員。為捍衛 DS 和其前置軟體架構設計人員的副總裁,他會保持作用中的最新的趨勢和技術,Microsoft 平台上建立自訂解決方案。他的用戶端清單跨越多個產業和包含公司,例如:Schlumberger、 Microsoft、 波音、 支和/Philips 的 > 形箭號。Champion 六年 Microsoft MVP、 以及國際喇叭、 已發行的作者部落客是社群的使用中參與者。

非常感謝下列 Microsoft 技術專家檢閱這篇文章:Shawn Henry


MSDN Magazine 論壇中的這篇文章的討論