Maio de 2018

Volume 33, Número 5

Plataforma Universal do Windows - Compilando Aplicativos Conectados com UWP e Project Rome

Por especialista Tony

No mundo, a criação de um aplicativo com êxito significa mudar além de um único dispositivo. Os usuários desejam aplicativos que abrangem todos os seus dispositivos e até mesmo se conectar com outros usuários. Fornecer esse tipo de experiência pode ser um desafio desanimadora, no mínimo. Para ajudar a resolver essa necessidade crescente ecossistema, Microsoft introduziu Roma do projeto. Visa Roma de projeto para criar um sistema operacional mais pessoal que abrange os aplicativos, dispositivos e usuários. Enquanto o projeto Roma tem disponível para a maioria dos principais plataformas de SDKs, neste artigo, vou explorar usando Roma de projeto para criar um aplicativo do Windows UWP (plataforma Universal) do sistema de mensagens.

Diga Olá para projeto Roma

Project Rome é uma iniciativa para ajudar a orientar o envolvimento dos usuários em aplicativos e dispositivos. É um conjunto de APIs que fazem parte do Microsoft Graph e podem ser categorizados em duas áreas: continuar agora e continuar mais tarde.

As APIs do sistema remoto habilitar um aplicativo quebrar ultrapassa os limites do dispositivo do usuário atual, habilitando um continuar-agora, experimente. Se permitir que o usuário usar dois dispositivos de uma única vez, com um aplicativo de controle remoto ou complementar ou permitindo que vários usuários se conectem e compartilhem uma experiência de único, essas APIs fornecem uma exibição expandida de contrato atual do usuário. A equipe de mensagens do aplicativo compilado neste artigo criará uma experiência de usuário compartilhada.

A outra metade do projeto Roma, as APIs de atividades, se concentra em continuar a experiência do usuário em um momento posterior. Essas APIs permitem que você registre e recuperar ações específicas dentro de seu aplicativo que o usuário pode continuar de qualquer dispositivo. Embora eles não discutir neste artigo, são definitivamente vale a pena examinar.

Introdução

Antes de mergulhar compilar o aplicativo, primeiro preciso configurar meu ambiente. Enquanto a primeira versão do projeto Roma foi criado por algum tempo, alguns dos recursos usados neste artigo foram lançadas apenas durante a atualização de criadores de queda recentes. Portanto, seu computador deve estar executando o número de compilação 16299 ou superior. Neste ponto, esta versão está disponível no anel lento para atualizações e maioria dos computadores devem ser atualizados corretamente.

Aplicativos em execução com as APIs do sistema remoto com outros usuários requer que as experiências compartilhadas estão habilitadas no computador. Isso pode ser feito nas configurações do sistema, em configurações | Sistema | Experiências compartilhadas. Para o cenário de mensagens de equipe, você precisa habilitar usuários diferentes para se comunicar com seu dispositivo, que significa que você precisa certificar-se compartilhados experiências está habilitado e você pode compartilhar ou receber de "Todos perto,", conforme mostrado no Figura 1.

Habilitar experiências compartilhadas
Figura 1 habilitando compartilhado experiências

O requisito de final é que o dispositivo seja detectável tendo algum nível de conectividade. As APIs do sistema remoto vai descobrir outros computadores na mesma rede, bem como os próximos usando Bluetooth. Bluetooth pode ser habilitada na página "Bluetooth e outras configurações de dispositivo" nas configurações do sistema.

Com o computador configurado, vamos começar criando um novo Visual C# aplicativo usando o modelo de aplicativo em branco (Universal do Windows) no Visual Studio de 2017. Chamar o aplicativo "TeamMessenger". Como mencionado anteriormente, este projeto requer a atualização de criadores de queda, portanto, definir as versões de destino e mínimo do aplicativo para "Build 16299" ou superior, conforme mostrado no Figura 2. Isso impedirá que o aplicativo de oferecer suporte a versões mais antigas do Windows 10, mas é necessário para alguns dos recursos abordados neste artigo.

Versões de destino de configuração para o aplicativo
Figura 2 versões de destino de configuração para o aplicativo

Observe que, se você não tiver os criadores de queda atualizar SDKs em seu dispositivo, é a maneira mais fácil para obtê-las atualizar o Visual Studio de 2017 para a versão mais recente.

As APIs de Roma projeto fazem parte do SDK do Windows 10, que significa que não há nenhum SDK adicional para baixar ou pacotes do NuGet para instalar para criar esse aplicativo. No entanto, há alguns recursos que devem ser adicionados ao aplicativo em ordem para a sessão remota APIs funcione corretamente. Isso pode ser feito abrindo o arquivo de manifesto e selecionando a guia de recursos. Na lista de recursos disponíveis, verifique se que os seguintes são selecionados: Bluetooth, Internet (cliente e servidor) e o sistema remoto.

Criar a Conexão da sessão

Este aplicativo consistirá em duas páginas, com a primeira página responsável por criar ou ingressar em uma sessão remota usando as APIs do sistema remoto. Para simplificar, vou criar essa página usando a MainPage. XAML que foi criado com a solução e já está conectado no aplicativo para ser a primeira página carregada. A interface do usuário tem dois modos: criação ou uma sessão de hospedagem e ingressar em uma sessão existente. Criando uma sessão requer um nome de sessão serão público para usuários que desejam para ingressar. Ingressar em uma sessão existente deve mostrar uma lista de disponíveis próximas sessões. Ambos os modos precisam de um nome a ser exibido para o usuário. Figura 3 mostra que a interface do usuário resultante deve ter aparência para MainPage e o XAML para compilar essa página podem ser encontrado no Figura 4.

A interface do usuário MainPage
Figura 3 MainPage interface do usuário

Figura 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>

Depois de criar o XAML para a página, crie uma nova pasta de ViewModels no aplicativo, em seguida, adicionar uma nova classe pública na pasta chamada MainViewModel.cs. Esse modelo de exibição será conectado no modo de exibição para lidar com a funcionalidade.

A primeira parte do modelo de exibição tratará o gerenciamento de estado para os botões de opção que determinam se o usuário está criando uma nova sessão ou ingressar em um existente. O estado é mantido em um bool chamado IsNewSession. Dois métodos são usados para alternar o status deste bool, CreateSession e JoinSession:

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

O evento de ativação para cada botão de opção é associado a um desses métodos.

Os elementos restantes da interface do usuário são controlados com propriedades simples. O nome da sessão e o nome de usuário associadas às propriedades SessionName e JoinName. A propriedade SelectedSession associa à propriedade SelectedItem em ListView e sua ItemsSource está associado à propriedade de sessões:

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

O modelo de exibição tem dois eventos que serão usados para informar o modo de exibição se uma conexão a uma sessão foi bem-sucedida ou não:

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

Por fim, o botão Iniciar é associado a um método de início. Esse método pode ser deixado vazio para o momento.

Depois que o modelo de exibição foi concluído, o code-behind para MainPage precisa criar uma propriedade pública que é uma instância do MainViewModel. Este é o que permite que X:Bind criar as associações de tempo de compilação. Além disso, ele precisa se inscrever para os dois eventos criados no modelo de exibição. Se uma conexão é estabelecida com êxito, será navegar para uma nova página, MessagePage. Se a conexão falhar, um MessageDialog será exibida, informando o usuário sobre a conexão com falha. Figura 5 contém o código para o MainPage.xaml.cs.

Figure 5 MainPage.xaml Codebehind

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();
}

Definindo os modelos de dados

Antes de se aprofundando na essência do aplicativo, você precisa definir dois modelos de dados que será usado no aplicativo. Crie uma pasta de modelos no aplicativo e, em seguida, crie duas classes nele: O usuário e UserMessage. Como o nome sugere, o modelo de usuário controlar informações sobre os usuários conectados ao aplicativo:

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

A classe UserMessage conterá a mensagem de conteúdo, o usuário que está criando o conteúdo e quando a mensagem foi criada:

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

Criação de uma sessão

Com o código para a página principal completo, é possível iniciar a criação de RemoteSessionManager, que será usada para encapsular a API de sistemas remotos. Adicione uma nova classe pública chamada RemoteSessionManager para o diretório raiz do aplicativo. O aplicativo usará uma única instância compartilhada do RemoteSessionManager, assim você pode adicionar uma propriedade estática para a classe App em App.xaml.cs:

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

Antes do aplicativo pode acessar qualquer uma das APIs de sistemas remotos, ele deve primeiro obter permissão do usuário. Essa permissão é obtida chamando o método estático RemoteSystem.RequestAccessAsync:

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

O método retornará um enum RemoteSystemAccessStatus que pode ser usado para determinar se o acesso foi concedido. Esse método deve ser chamado do thread da interface do usuário para que ele pode solicitar que o usuário com êxito. Depois que o usuário tem permissão concedida ou negada para o aplicativo, todas as chamadas subsequentes retornará automaticamente a preferência do usuário. Para este aplicativo, essa permissão será adicionada para a descoberta de sessão porque ele é chamado pela primeira vez no fluxo de trabalho.

Observe que todas as APIs do sistema remoto podem ser encontradas no namespace Windows.System.RemoteSystem.

O primeiro método para adicionar a classe RemoteSessionManager é o método CreateSession. Como há vários resultados que podem ser retornados deste método, mostrarei aqueles em uma nova enum — SessionCreationResult. SessionCreationResult tem quatro valores possíveis: êxito e três falhas diferentes. Uma sessão pode não conseguir criar porque o usuário não conceder acesso ao aplicativo; o aplicativo tem muitas sessões em execução no momento ou um erro de sistema não conseguiu criar a sessão:

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

Sessões remotas são gerenciadas por um RemoteSystemSessionController. Ao criar uma nova instância de RemoteSystemSessionController, você deve passar um nome que será exibido aos dispositivos tentar ingressar na sessão.

Depois que o controlador for solicitado, é possível iniciar uma sessão, chamando o método CreateSession. Esse método retorna um RemoteSystemSessionCreationResult que contém o status e a nova instância da sessão se ela foi bem-sucedida. O RemoteSessionManager armazenará o novo controlador e a sessão em variáveis particulares.

Uma nova propriedade pública, IsHost, deve ser adicionada ao gerente, bem como para determinar o fluxo de trabalho. Durante o método CreateSession, esse valor é definido como true, identificar este aplicativo como o host. Outra propriedade pública CurrentUser, fornece uma instância do usuário no computador e será usada para mensagens. O Gerenciador de sessão também mantém uma ObservableCollection de usuários na sessão atual. Esta coleção é inicializada com o usuário recém-criado. Para o host da sessão, esta instância é criada no método CreateSession. As adições resultantes para o RemoteSessionManager são mostradas na Figura 6.

Figura 6 o método 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;
}

Há três mais itens que precisam ser adicionados à RemoteSessionManager para concluir o método CreateSession. A primeira é um manipulador de eventos quando um usuário tenta ingressar em uma sessão e o evento JoinRequested é gerado na sessão. O método OnJoinRequested aceitará automaticamente qualquer usuário que tentar unir. Isso pode ser estendido para solicitar que o host para aprovação antes que o usuário é associado à sessão. As informações de solicitação são fornecidas como um RemoteSystemSessionJoinRequest incluído no parâmetro RemoteSystemSessionJoinRequestedEventArgs do manipulador de eventos. Invocar o método de aceitação para adicionar o usuário para a sessão. O código a seguir inclui o novo evento para adicionar a RemoteSessionManager, bem como o método OnJoinRequested concluída:

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

O Gerenciador de sessão pode monitorar quando os participantes são adicionados ou removidos da sessão atual por meio de RemoteSystemSessionParticipantWatcher. Essa classe monitora os participantes e gera um adicionado ou removido eventos quando necessário. Quando um usuário ingressa em uma sessão já em andamento, cada participante já na sessão atual recebe um evento adicionado. O aplicativo vai levar essa série de eventos e determinar qual participante é o host por meio da correspondência DisplayName contra ControllerDisplayName da sessão. Isso permitirá que os participantes para se comunicar diretamente com o host. O Gerenciador de sessão mantém um Inspetor de participante como uma variável privada que é inicializada no InitParticipantWatcher. Este método é chamado se a criação de uma sessão ou ingressar em uma sessão existente. Figura 7 contém novas adições. Você observará que, para este fluxo de trabalho você precisa saber quando um participante é removido somente se você for o host, e se um participante for adicionado, se você estiver ingressando em uma sessão. Como um host, o RemoteSessionManager diz respeito apenas quando um participante deixa a sessão. O host será notificado diretamente, o participante quando entrarem, como você verá posteriormente neste artigo. Os participantes só precisam determinar a conta de host atual.

Figura 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);
  }
}

A última coisa que o método CreateSession precisa adicionada à classe é um evento para permitir que os consumidores saber se e quando a sessão será desconectada. O novo evento SessionDisconnected pode ser definido da seguinte maneira:

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

Ingressar em uma sessão

Agora que o aplicativo é capaz de transmitir uma nova sessão remota, o próximo passo para implementar é a capacidade de unir a sessão de outro computador. Há duas etapas para ingressar em uma sessão remota: descoberta e, em seguida, conectando-se à sessão.

Descoberta de uma sessão um aplicativo é capaz de descobrir próximas sessões remotas por meio de RemoteSystemSessionWatcher, que é criado pelo método CreateWatcher estático. Esta classe dispara eventos sempre que uma sessão é adicionada ou removida. Adicionar um novo método — DiscoverSessions — para o RemoteSessionManager. Esse método criará um RemoteSystemSessionWatcher como uma variável privada para a classe e manipular os eventos adicionados e removidos. Esses eventos serão ajustados por dois novos eventos adicionados ao RemoteSessionManager: SessionAdded e SessionRemoved. Como esse será o outro ponto de entrada para os usuários Inicializando sessões remotas, você precisa certificar-se de adicionar uma chamada para RemoteSystem.RequestAccessAsync. Figura 8 contém a variável particular, os dois eventos e o método DiscoverSessions complete.

Figura 8 descobrindo sessões

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;
}

Agora é possível conectar-se no MainViewModel para atualizar a propriedade de sessões com sessões disponíveis localmente. Como o método DiscoverSessions é assíncrono, o construtor de MainViewModel precisa inicializar uma tarefa para invocá-lo. O método de inicialização também deve registrar e manipular os eventos SessionAdded e SessionRemoved. Como esses eventos não ser acionado no thread da interface do usuário quando atualizar a propriedade de sessões, é importante usar um CoreDispatcher. As atualizações para MainViewModel estejam Figura 9.

Figura 9 Adicionar sessão descoberta

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); });
  }
}

Conectar-se à sessão depois de identificar a sessão que deseja ingressar e fornece um nome de um usuário, o RemoteSessionManager deve ser capaz de conectar-se à sessão selecionada. Isso pode ser manipulado por um novo método JoinSession em RemoteSessionManager, que usa a sessão selecionada e o nome para exibição inserido como parâmetros.

O método JoinSession começa chamando o método JoinAsync na sessão fornecida. Em troca, isso acionará o evento JoinRequested na sessão do host. Se o host aprova a solicitação, é retornado um status de êxito e o CurrentUser é definida usando o nome de exibição. Como com o método CreateSession, o método InitParticipantWatcher é chamado para registrar um manipulador de eventos quando os participantes são adicionados à sessão. O método JoinSession é mostrado na Figura 10.

Figura 10 o método 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;
}

A etapa final envolvida em ingressar em uma sessão é usar a funcionalidade criada no RemoteSessionManager para criar ou ingressar em uma sessão. Figura 11 mostra o método Start no MainViewModel que está associado ao botão Iniciar em MainPage. O fluxo de trabalho do método é simples. Dependendo da IsNewSession, ele chama o método CreateSession ou o método JoinSession. Os resultados são retornados gerando eventos de SessionConnected ou ErrorConnecting. Se a sessão for bem-sucedida, o aplicativo navega para MessagePage, o que vou criar na próxima seção.

Figura 11 iniciar uma sessão

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);
      }
    }
  }
}

Manter os aplicativos se comunicando

Neste ponto, o aplicativo com êxito pode criar ou ingressar em uma sessão e tem uma interface de usuário de mensagens está pronto para ser usado. A única etapa restante é permitindo que os dispositivos se comuniquem entre si. Isso é feito usando a API do sistema remoto para enviar instâncias ValueSet entre os computadores. Cada ValueSet é um conjunto de pares chave/valor de cargas serializadas.

Recebendo mensagens as mensagens são transmitidas em uma sessão por meio de um RemoteSystemSessionMessageChannel. Uma sessão pode ter diversos canais. No entanto, este aplicativo será necessário apenas um único canal. RemoteSessionManager, vou adicionar um método StartReceivingMessages. Esse método criará um novo canal de mensagem que é armazenado em uma variável privada e, em seguida, adicione um manipulador para o evento ValueSetReceived.

As mensagens são enviadas como texto e como o aplicativo está usando classes como mensagens, eu preciso serializar os dados. Quando o ValueSet é recebida do canal, um DataContractJsonSerializer é usado para realimentar as classes de mensagens na classe DeserializeMessage. Porque não é possível saber que tipo de mensagem é serializado, o aplicativo enviará cada tipo de mensagem como um valor diferente no ValueSet. A classe DeserializeMessage será determinar qual chave é usada e retornará a classe correta.

Quando a classe de mensagem estiver pronta, o Gerenciador de classe atuará na mensagem dependendo de seu tipo. Como você verá participantes anunciará-se ao host, enviando sua instância CurrentUser. Em resposta, o host serão transmitidas a lista de usuários atualizadas para todos os participantes. Se o Gerenciador de sessão recebe a lista de participantes, ele atualizará as coleções de usuários com os dados atualizados. A opção final, um UserMessage, irá gerar um novo evento MessageReceived que passa a mensagem e o participante que enviou a mensagem. Essas adições para o RemoteSessionManager podem ser encontradas em Figura 12.

Figura 12 recebendo mensagens

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
    });
  }
}

Figura 12 inclui uma nova classe de manipulador de eventos, MessageReceivedEventArgs, que também deve ser criado. Essa classe contém duas propriedades: o remetente e a mensagem:

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

Enviar mensagens a API de sistemas remotos fornece dois métodos para entregar mensagens a outros usuários. A primeira é para transmitir uma mensagem a todos os usuários na sessão. Essa abordagem será usada para dois dos nossos tipos de mensagem, o UserMessage e a lista de usuários. Vamos criar um novo método, Mensagem_de_difusão, em que o RemoteSystemManager. Esse método usa uma chave e a mensagem como parâmetros. Usando um DataContractJsonSerializer, eu serializar os dados e usar o método BroadcastValueSetAsync para enviar a mensagem a todos os usuários, conforme mostrado no Figura 13.

Figura 13 transmitindo uma mensagem

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;
}

A segunda abordagem é enviar uma mensagem para um único participante. Essa abordagem é semelhante ao transmitir uma mensagem, exceto que ele usa o método SendValueSetAsync a mensagem diretamente de um participante. Este método final para RemoteSystemManager, SendMessage, pode ser encontrado no Figura 14.

Figura 14 enviando uma mensagem direta

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;
}

Criando a página de mensagens

Com o sistema de mensagens agora em vigor, é hora de colocá-lo para usar e concluir o aplicativo. Adicione uma nova página em branco para o aplicativo, MessagePage.xaml. Esta página será composto de uma lista de usuários, uma janela de mensagem e campos de entrada para adicionar uma mensagem. O XAML completo pode ser encontrado em Figura 15.

Figura 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>

Como MainPage, MessagePage precisará de um modelo de exibição. Adicione uma nova classe, MessageViewModel, para a pasta ViewModels. Esse modelo de exibição será necessário dar suporte a INotifyPropertyChanged para permitir a associação bidirecional funcione corretamente. Esse modelo de exibição conterá três propriedades: Os usuários, mensagens e NewMessage. Os usuários simplesmente irá expor a coleção de usuários do RemoteSessionManager para o modo de exibição. As mensagens serão uma ObservableCollection de objetos UserMessage recebidos e uma cadeia de caracteres NewMessage que contém o texto para enviar como uma nova mensagem. Também há um único evento, MessageAdded, que será usada pelo code-behind em MessagePage. No construtor do modelo de exibição, eu preciso mapear a propriedade de usuários, invocar o método StartReceivingMessages no RemoteSessionManager e registrar para o evento MessageReceived, conforme mostrado no Figura 16. O construtor também inclui a implementação de INotifiyPropertyChanged.

Figura 16 MessageViewModel construtor

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();
}

No construtor, há uma chamada para RegisterUser. Esse método enviará o CurrentUser foi criado durante o ingresso em uma sessão para o host. Isso apresenta para o host que ingressou em um novo usuário e que o nome de exibição. Em resposta, o host enviará a lista atual de usuários a ser exibido no aplicativo:

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

A parte final do modelo de exibição é uma nova mensagem de difusão do usuário. O método SubmitMessage constrói uma nova UserMessage e chama o método Mensagem_de_difusão a RemoteSessionManager. Em seguida, limpa o valor NewMessage e gera o evento MessageAdded, conforme mostrado no Figura 17.

Figura 17 enviando uma mensagem

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);
}

O code-behind para MessagePage, mostrado em Figura 18, eu preciso fazer duas coisas: cria uma instância de MessageViewModel para o XAML referenciar e manipular o evento MessageAdded. No caso de manipulador posso instruir ListView para rolar para a parte inferior da lista de onde a mensagem mais recente é visível.

Figura 18 MessagePage Codebehind

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();
}

O aplicativo de mensagens de equipe agora deve estar pronto para ser executado. Em um computador, execute o aplicativo e criar uma nova sessão. Em seguida, inicie o aplicativo em uma segunda máquina, que deve mostrar a mensagem recém-criado. Depois que você ingressar na sessão, você será levado para a nova página de mensagem onde você pode começar a conversa com outras pessoas na sessão, conforme mostrado no Figura 19. Você criou um aplicativo de vários usuários usando a API do sistema remoto.

Mensagens de multiusuário
Figura 19 multiusuário mensagens

Conclusão

Criar experiências de usuário com êxito dentro de aplicativos normalmente requer procurando além de um único dispositivo ou até mesmo usuário ou plataforma. A Microsoft desenvolveu o projeto Roma para habilitar os desenvolvedores podem fornecer esse nível de experiência em seus aplicativos. Neste artigo, criei um aplicativo UWP usando a API de sistemas remotos; No entanto, usando os SDKs do projeto Roma disponível para outras plataformas, você pode estender esse aplicativo para trabalhar em várias plataformas. Ao criar a próxima ótima experiência para os usuários, lembre-se de considerar como Roma projeto pode ajudar a tornar seu aplicativo mais pessoal. O código-fonte para este artigo pode ser encontrado em bit.ly/2FWtCc5.


Tony Championé arquiteto de software com mais de 20 anos de experiência de desenvolvimento com tecnologias da Microsoft. Como presidente de especialista DS e seu arquiteto de software de cliente potencial, ele permanece ativo na última tendências e tecnologias, criando soluções personalizadas nas plataformas da Microsoft. A lista de clientes abrangem vários setores e inclui as empresas, como: Schlumberger, Microsoft, Boeing, MLB e divisa/Philips. Champion é um participante ativo na comunidade como MVP da Microsoft seis anos, palestrante internacional, autor publicado e blogger.

Agradecemos ao seguinte especialista técnico da Microsoft pela revisão deste artigo: Shawn Henry


Discuta esse artigo no fórum do MSDN Magazine