2018 年 5 月

第 33 卷,第 5 期

通用 Windows 平台 - 使用 UWP 和 Project Rome 构建连接的应用

通过Tony 冠军

在当今世界中,生成成功的应用程序表示过渡超出单个设备。用户需要应用跨所有设备和甚至连接与其他用户。提供这种体验可以是一个巨大的挑战,则还可以显示为最少。为了帮助解决这一不断增长需要生态系统,Microsoft 中的,引入了项目罗马。项目罗马旨在创建跨越应用、 设备和用户更具个性化 OS。当项目罗马具有可用于最主要的平台的 Sdk 时,此文章中我将尝试使用项目罗马创建消息传送的通用 Windows 平台 (UWP) 应用程序团队。

说到项目罗马 Hello

Project Rome 是帮助推动用户跨应用和设备进行参与的方案。它是 Api,是 Microsoft 图形的一部分,并且可以分为两个区域的集合: 现在继续并稍后继续。

远程系统 Api 启用的应用程序的用户的当前设备,外部中断启用继续-现在体验。是否允许用户与助理或远程控制应用程序,用于单一体验,两个设备或允许多个用户连接并共享单一体验,这些 Api 提供用户的当前用户参与策略的扩展的的视图。消息传送这篇文章中生成的应用团队将创建共享的用户体验。

项目罗马,活动 Api 中,另一半重点介绍继续在更高版本时的用户的体验。这些 Api 允许你以记录并从任何设备中检索用户就可以继续你应用中的特定操作。我不在本文中讨论它们,但这些是肯定值得到。

开始使用

在深入分析生成的应用程序之前, 我首先需要设置我的环境。虽然项目罗马的第一个版本已经出一段时间少,本文中使用的功能的一些已刚刚发布最近秋季创建者更新的过程中。因此,您的计算机必须运行生成号 16299 或更高版本。在这种情况下,此版本可用于更新慢速环和大多数计算机应该会正确更新。

使用与其他用户远程系统 Api 运行应用程序需要在计算机上启用的共享的体验。这可以在设置中的系统设置 |系统 |共享的体验。对于团队消息传递方案,你需要启用不同的用户可以启用这意味着你需确保共享体验,并确保可以共享或中所示,从"所有人附近,"接收与你的设备,通信图 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 包的一部分。有,但是,必须添加到的应用程序中远程会话 Api 才能正常工作的几个功能。这可以通过打开 package.appxmanifest 文件,然后选择功能选项卡。在可用功能列表中,确保选中了以下各:蓝牙、 Internet (客户端和服务器) 和远程系统。

生成的会话连接

此应用将包含两个页,负责创建或加入远程会话使用远程系统 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 之后, 在应用中,创建一个新的 Viewmodel 文件夹,然后在调用 MainViewModel.cs 该文件夹中添加新的公共类。此视图模型将连接到要处理功能的视图。

视图模型的第一个部分将处理管理确定是否用户为其创建新的会话的单选按钮的状态或加入现有。在调用 IsNewSession bool 中保留状态。两种方法用于切换此 bool CreateSession 和 JoinSession 的状态:

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

每个单选按钮的选中的事件绑定到这些方法之一。

其余的 UI 元素将跟踪与简单属性。为 SessionName 和 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: 绑定生成的编译时绑定。此外,它需要在视图模型中创建的两个事件订阅。如果成功建立连接,将导航到新页,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 的新公共类。这样可将静态属性添加到 App 类,在 App.xaml.cs 中,应用将使用 RemoteSessionManager,单个共享的的实例:

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 线程上调用此方法,因此它可以成功提示用户。一旦用户已授予或拒绝了对应用程序权限,任何后续调用将自动返回用户的首选项。对于此应用中,将此权限添加到会话发现这是因为工作流中首次调用。

请注意可以 Windows.System.RemoteSystem 命名空间中找到所有远程系统 Api。

第一种方法将添加到 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 后用户已将标识的会话他们想要将加入和提供一个名称,必须能够将其连接到选定的会话。这将由 RemoteSessionManager,将选定的会话和输入的显示名称作为参数上的新 JoinSession 方法处理。

通过在提供的会话上调用 JoinAsync 方法开始 JoinSession 方法。反过来,这将在主机会话上触发 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 用于 rehydrate DeserializeMessage 类中的邮件类。我无法告知哪种类型的消息序列化,因为应用程序将发送给每种类型的消息 ValueSet 值都不同。DeserializeMessage 类将确定使用哪个密钥,并返回正确的类。

Message 类准备就绪后,具体取决于其类型的消息将操作管理器类。如你所见,参与者将本身到主机通过发送公告其 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,发送消息,此最终方法图 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,添加到 Viewmodel 文件夹。此视图模型需要支持 INotifyPropertyChanged 以允许这种双向绑定才能正常工作。此视图模型将包含三个属性:用户、 消息和 NewMessage。用户只需将公开到视图 RemoteSessionManager 的用户集合。消息将 ObservableCollection UserMessage 后收到的对象和包含文本的 NewMessage 字符串以一条新消息的形式发送。此外还有一个事件,MessageAdded,将使用通过 MessagePage 中的代码隐藏。在视图模型的构造函数,我需要的用户属性映射,则调用 MessageReceived 事件中,在 RemoteSessionManager,并注册的 StartReceivingMessages 方法中所示图 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);
}

最后一段视图模型是从用户广播新消息。SubmitMessage 方法构造新 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,我需要做两件事情: 创建的 xaml 以引用,并处理 MessageAdded 事件 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


Tony 冠军是具有多个 20 多年经验开发与 Microsoft 技术的软件架构师。作为冠军 DS 和其主管软件架构师总裁他中保持活动状态的最新的趋势和技术,在 Microsoft 平台上创建自定义解决方案。他的客户端列表跨多个行业,并包括公司,如:Schlumberger、 Microsoft、 Boeing、 大中企业和 v 形图标/Philips 中。Champion 就六年 Microsoft MVP、 国际扬声器、 已发布的作者和博客作者的积极社区参与。

衷心感谢以下 Microsoft 技术专家对本文的审阅:Shawn Henry


在 MSDN 杂志论坛讨论这篇文章