Por que eu devo ler este artigo:O avanço tecnológico trouxe-nos diversas novidades, e uma delas são os assistentes digitais, que não são novidades, mas estão a cada dia mais sofisticados.

Imagine poder acessar diversas funcionalidades de seu sistema operacional e até mesmo controlar sua aplicação Windows / Windows Phone através de comandos de voz? “No princípio havia a Cortana, e agora se tem a Cortana API”.

O que estamos dizendo é que em suas versões iniciais era possível somente realizar algumas funções em dispositivos computacionais através do comando de voz, mas que agora é possível utilizar todo esse poder em suas próprias aplicações — pode-se inclusive estender o dicionário de palavras reconhecidas em tempo de execução.

No artigo em questão será feita uma introdução ao desenvolvimento de um aplicativo para restaurantes com Windows Universal utilizando a Cortana API, no qual será possível pedir para que o aplicativo mostre o cardápio. A partir desse modelo, qualquer outra aplicação que utilize esses recursos poderá ser desenvolvida com muita facilidade.

A cada dia novas ideias para o desenvolvimento de softwares surgem baseadas em novas tecnologias, conceitos de desenvolvimento ou novos produtos ou serviços que apresentam grande potencialidade de aceitação no mercado, ou até mesmo que permitam a criação de aplicações que venham a oferecer novas facilidades aos usuários ou resolver problemas antes computacionalmente inviáveis. Em alguns casos (como o que será tratado neste artigo), o surgimento de uma nova tecnologia ou conceito torna possível a criação de aplicações antes inimagináveis, ou simplesmente nos permite acrescentar em uma aplicação algum recurso capaz de torná-la destaque em sua área, atraindo assim a atenção de um público específico.

Neste artigo será desenvolvida parte de uma aplicação que simula um “atendente” virtual para restaurantes. O propósito é permitir que o cliente realize seus pedidos sem a necessidade de requisitar um garçom. Para que isso seja possível, deve-se considerar que o restaurante disponibiliza em cada uma de suas mesas algum dispositivo com sistema operacional Windows Phone.

O que é Cortana?

A Cortana é um assistente pessoal digital que promete auxiliar os usuários de um sistema computacional a realizar diversas atividades. Não se pode visualizar a Cortana como um simples assistente que permite a realização de atividades através do comando de voz, apesar de essa parecer ser sua principal finalidade. Usado corretamente, esse assistente pode ajudar seu utilizador a se manter sempre bem informado, permitindo-o realizar diversas atividades através de dispositivos e plataformas distintas.

Muito além do que serviços de lembrete, ou até mesmo uma interface interativa de pesquisa, a Cortana fornece uma arquitetura que permite facilmente a incorporação de outras atividades ou serviços, melhorando assim sua experiência. Sem demagogias, trata-se de um recurso capaz de aprender com o usuário para melhor servi-lo.

A Cortana permite que o usuário interaja com o computador por qualquer uma de suas interfaces. Caberá ao desenvolvedor, dependendo do contexto, determinar qual ação será desencadeada, ou seja, o usuário pode interagir via texto ou voz e o desenvolvedor decidirá como irá tratar cada uma das interfaces de entrada. Além de prático e fácil de utilizar, a Cortana é compatível com qualquer versão do Windows 10 ou superior, além do Android.

Para o desenvolvedor, é possível a integração das funcionalidades da Cortana às suas aplicações, podendo essa interação ocorrer através de solicitações explícitas ou até mesmo com base no contexto do usuário (análise de seu comportamento).

Ao desenvolvedor, a Cortana oferece também suporte a uma série de ações pré-determinadas, sendo necessário somente fornecer à API uma ligação capaz de indicar como sua aplicação deve responder/completar a ação. O desenvolvedor pode, entretanto, a qualquer momento personalizar uma ação pré-definida (se julgar necessário), buscando assim atender às necessidades de sua aplicação.

Iniciando o projeto

Inicialmente, é preciso criar um novo projeto Universal Windows. Para isso, siga as seguintes instruções:

  1. Crie um novo projeto através do menu File > New > Project (esse projeto foi realizado no Visual Studio 2015);
  2. Na tela que será aberta (veja Figura 1), selecione a opção Installed > Templates > C# > Windows > Universal > Blank App (Universal Windows).
  3. Escolha um nome para o projeto (no caso deste artigo ComandoVozApp), um nome para a Solution (que repetirá o nome do projeto) e clique em OK.

Iniciando um novo projeto Universal Windows
Figura 1. Iniciando um novo projeto Universal Windows

Criando o arquivo VCD

Criado o projeto, deve-se então criar o arquivo de definição de vocabulário (Voice Command Definitions - VCD). Esse arquivo concentrará as definições de comandos de voz que serão recebidos e tratados pela aplicação, bem como alguns que poderão ser enviados como feedback para o usuário.

O processo para criação do arquivo é simples, basta navegar pelo Solution Explorer até a pasta raiz do projeto e, nela, clicar com o botão direito do mouse. No menu que será apresentado escolha a opção Add > New Item... como pode ser observado na Figura 2.

Criando arquivo VCD – Parte I
Figura 2. Criando o arquivo VCD – Parte I

A tela da Figura 3 será apresentada. Nela, escolha a opção Installed > Visual C# > XML File e nomeie o arquivo como ComandosSuportados.xml.

Criando arquivo VCD – Parte II
Figura 3. Criando arquivo VCD – Parte II

Criado o arquivo, volte ao Solution Explorer, selecione o arquivo criado (ComandosSuportados.xml), clique com o botão direito do mouse sobre o mesmo e selecione a opção Properties. Nas propriedades do arquivo, marque a opção Build Action como Content, para que o arquivo não seja compilado junto da aplicação, mas sim inserido aos arquivos que serão publicados com ela, e a opção Copy to output directory como Copy if newer (veja a Figura 4). Com essa última opção, sempre que o documento for alterado, ele substituirá o documento já existente.

Configurando as propriedades do arquivo VCD
Figura 4. Configurando as propriedades do arquivo VCD

Uma vez que as devidas configurações referentes ao arquivo foram realizadas, deve-se então editá-lo permitindo que o mesmo tenha utilidade para o projeto em construção.

Editando o arquivo VCD

Antes de editar o arquivo, é importante entender um pouco sobre a sua estrutura. A Tabela 1 apresenta um resumo dos principais elementos.

Tabela 1. Principais elementos do arquivo VCD

Elemento

Descrição

VoiceCommands

Obrigatório. Este é o elemento raiz de um arquivo VCD. O valor desse atributo deve estar todo em minúsculo.

CommandSet

Obrigatório. Tem como elemento pai VoiceCommands.

Define o idioma pelo atributo xml:lang, que será único para todo o documento. Obs.: Idiomas especificados, mas não suportados serão ignorados.

Tem também o atributo Name, que é opcional, mas necessário para que se possa associar este com outros elementos.

Mutuamente exclusivos

CommandPrefix

Opcional. Se presente, deve ser o primeiro elemento filho do CommandSet.

Define um nome amigável para o aplicativo que o usuário deseja ativar pelo comando de voz.

AppName

Opcional. Se presente deve ser o primeiro elemento do CommandSet. Substitui o elemento CommandPrefix. Define um nome amigável para o aplicativo que o usuário deseja ativar pelo comando de voz e fornece outras propriedades.

Command

Obrigatório. Possui um atributo Name que o identifica unicamente. Agrupa os elementos Example, que indica para o usuário o que ele deve dizer, ListenFor, que especifica o que se espera “ouvir”, podendo conter até 10 itens, Feedback, elemento que envia mensagens para o usuário, e Navigate ou VoiceCommandService (um ou outro).

Navigate: Especifica a página para a qual o aplicativo deve navegar quando o comando for ativado.

VoiceCommandService: Especifica que o comando de voz é feito através de um serviço de aplicativo.

PharseList

Opcional. Armazena um conjunto de comandos que não pode ultrapassar 2.000 itens. Cada item representa uma palavra ou frase que pode ser reconhecida para iniciar o comando referenciado pelo PharseList através do atributo Label.

Os itens de um PharseList são agrupados/inseridos através do atributo Item.

Ao final deste artigo existe um link para os interessados em aprofundarem seu conhecimento na estrutura do arquivo VCD.

Após editado, o arquivo ComandosSuportados.xml deve apresentar uma estrutura semelhante à da Listagem 1.

Listagem 1. Arquivo VCD com comandos suportados.

  1.  <?xml version="1.0" encoding="utf-8"?>
  2.  <VoiceCommands xmlns="http://schemas.microsoft.com/voicecommands/1.2">
  3.         <CommandSet xml:lang="pt-br" Name="commandSet_pt_br">
  4.                     <CommandPrefix> Garçom, </CommandPrefix>
  5.                     <Example> Apresente o cardápio </Example>
  6.                     <Command Name="apresentaMenu">
  7.                                 <Example> Apresente o cardápio </Example>
  8.                                 <ListenFor> [Por favor] apresente {opcao} </ListenFor>
  9.                                 <Feedback> Apresentando { opcao } </Feedback>
  10.                               <Navigate Target="PaginaCardapio.xaml"/>
  11.                   </Command>
  12.                   <PhraseList Label="opcao">
  13.                               <Item> cardápio</Item>
  14.                               <Item> o que tem</Item>
  15.                               <Item> menu </Item>
  16.                   </PhraseList>
  17.       </CommandSet>
  18. </VoiceCommands>

Na linha 1 é apresentado o cabeçalho padrão de qualquer arquivo XML, na linha 2, um cabeçalho padrão para o arquivo VCD, e por fim, na linha 3, indica-se que o arquivo está configurado para linguagem português do Brasil e que o elemento será nomeado como commandSet_pt_br. O código da linha 4 define o prefixo dos comandos, indicando ao aplicativo o que deve ser dito antes de qualquer comando (no caso dessa aplicação sempre deverá ser dito “Garçom”). O elemento Example, na linha 5, indica qual texto deve ser apresentado ao usuário como exemplo para o que deve ser dito.

A linha 6 inicia o grupo Command, sendo que esse primeiro grupo tem sua propriedade Name configurada como apresentaMenu. Esse elemento será utilizado frequentemente nas classes que compõem a aplicação em desenvolvimento. Dentro do elemento Command¸ pode-se encontrar um elemento Example (linha 7), que tem a mesma funcionalidade do elemento Example utilizado na linha 5. Na linha 8, o elemento ListenFor indica o que se espera de entrada. Note que esse elemento tem um texto entre colchetes — todo texto entre colchetes é opcional, ou seja, indica algo que pode ou não ser dito pelo usuário. Ainda nessa linha encontra-se um texto entre chaves. Por ter sido utilizado o PharseList nesse exemplo, o texto contido dentro das chaves indica quais palavras substituem o termo “{opcao}” constante no arquivo. Assim, se a palavra dita pelo usuário estiver entre algumas das palavras indicadas pelas TAGs <item> filhas do item PharseList, o sistema será capaz de realizar a operação adequada.

Na linha 9, cria-se o elemento que recebe o texto que será apresentado ao usuário (feedback) caso o comando seja reconhecido, indicando o que está sendo realizado no momento. Na linha 10, o elemento Navigate (que é opcional) atua como um roteador, redirecionando o usuário para uma página específica caso o comando seja entendido corretamente. A página para onde o usuário será redirecionado deve ser inserida na propriedade Target do elemento. Como dito o campo é opcional, podendo, então, esse processo ser realizado dentro das classes que controlam o aplicativo. Na linha 11, fecha-se o elemento Command.

Na linha 12, inicia-se o elemento PharseList que referencia a opção. Entre as linhas 13 e 15 estão as palavras que serão referenciadas pela expressão {opção}. O elemento PharseList é encerrado na linha 16. Assim como o elemento Command, caso o desenvolvedor tenha mais de um Command e queira associar novos PharseList a esses Commands, basta inseri-los no arquivo, repetindo o que já se observa no arquivo apresentado.

Na linha 17, encerra-se o elemento CommandSet e na linha 18, o elemento VoiceCommands.

Controlando estados com a classe SuspensionManager

Antes de apresentar as alterações que iremos realizar na classe App.xaml.cs, precisamos criar as classes que darão suporte ao funcionamento do sistema. Uma dessas classes é a SuspensionManager, que foi criada com base em alguns projetos semelhantes observados na base de projetos da Microsoft. Assim sendo, o código pode parecer à primeira vista um pouco complexo, mas pode também ser considerado como uma classe padrão aplicável a qualquer projeto de propósito semelhante sem necessidade de alterações. O propósito dessa classe é captar o estado global da sessão do aplicativo, permitindo assim um melhor gerenciamento do tempo de vida de cada processo (não se trata de algo obrigatório, mas sim recomendável). Em uma aplicação o estado de cada sessão é automaticamente apagado sob uma variedade de condições, devendo ser armazenadas somente informações realmente convenientes, e para evitar a perda da sessão foi criada a classe SuspensionManager.

Antes de criar essa classe, crie uma nova pasta no projeto denominada Common. Criada a pasta, crie a nova classe que receberá os códigos apresentado nas Listagens 2 a 5.

Listagem 2. Classe SuspensionManager.cs – Parte I

  1.    using System;
  2.    using System.Collections.Generic;
  3.    using System.IO;
  4.    using System.Linq;
  5.    using System.Runtime.Serialization;
  6.    using System.Text;
  7.    using System.Threading.Tasks;
  8.    using Windows.ApplicationModel;
  9.    using Windows.Storage;
  10.  using Windows.Storage.Streams;
  11.  using Windows.UI.Xaml;
  12.  using Windows.UI.Xaml.Controls;
  13.  namespace ComandoVozApp.Common {
  14.      internal sealed class SuspensionManager {
  15.          private static Dictionary<string, object> _sessionState = new Dictionary<string, object>();
  16.   
  17.          private static List<Type> _knownTypes = new List<Type>();
  18.          private const string sessionStateFilename = "_sessionState.xml";
  19.   
  20.          public static Dictionary<string, object> SessionState{
  21.              get { return _sessionState; }
  22.          }
  23.   
  24.          public static List<Type> KnownTypes{
  25.              get { return _knownTypes; }
  26.          }
  27.   
  28.          public static async Task SaveAsync(){
  29.              try {
  30.                  foreach (var weakFrameReference in _registeredFrames) {
  31.                      Frame frame;
  32.                      if (weakFrameReference.TryGetTarget(out frame)) {
  33.                          SaveFrameNavigationState(frame);
  34.                      }
  35.                   }
  36.   
  37.                   MemoryStream sessionData = new MemoryStream();
  38.                   DataContractSerializer serializer = new DataContractSerializer(typeof(Dictionary<string, object>), _knownTypes);
  39.   
  40.                   serializer.WriteObject(sessionData, _sessionState);
  41.                   StorageFile file = await ApplicationData.Current.LocalFolder.Create
  42.                   FileAsync(sessionStateFilename,             CreationCollisionOption.ReplaceExisting);
  43.   
  44.                   using (Stream fileStream = await file.OpenStreamForWriteAsync()) {
  45.                       sessionData.Seek(0, SeekOrigin.Begin);
  46.                       await sessionData.CopyToAsync(fileStream);
  47.                   }
  48.             }
  49.            catch (Exception e) {
  50.                throw new SuspensionManagerException(e);
  51.            }
  52.         }

Entre as linhas 1 e 12, são realizadas importações normais do sistema, a maioria inserida automaticamente pela IDE, exceto a biblioteca constante na linha 7. A inserção da biblioteca System.Threading.Tasks na linha 7 é necessária para que se possa trabalhar com os comandos async e await, comandos esses que serão explicados posteriormente. Pode-se observar na linha 14 o comando para criação de uma classe interna selada (internal sealed), isso significa que a classe somente será acessível por qualquer código do mesmo assembly e que a mesma não pode ser herdada por outras classes.

Cria-se na linha 15 um objeto _sessionState para armazenar o estado das seções. Tal objeto implementa a interface Dictionary, que permite criar coleções, no caso do objeto em questão, coleções de strings e objetos. Na linha 17 criamos um objeto _knownTypes que implementa a interface List, criando assim uma lista genérica, e na linha 18 definimos uma constante que indica o nome do arquivo, _sessionState.xml, que armazena os estados das seções do aplicativo.

Na linha 20 observa-se o método para recuperação do _sessionState, e na linha 24, da lista de tipos, procedimentos simples que dispensam maiores comentários.

Na linha 28 é definido o método que armazena o estado atual da aplicação; na linha 30 é realizado um laço por todos os frames (páginas) registrados no sistema; na linha 32, se for possível recuperar a origem/caminho do frame, invoca-se o método SaveFrameNavigationState, linha 33.

Continuando com a classe, tem-se na Listagem 3 mais de sua implementação.

Listagem 3. Classe SuspensionManager.cs – Parte II

  53.      public static async Task RestoreAsync() {
  54.          _sessionState = new Dictionary<String, Object>();
  55.          try{
  56.              StorageFile file = await ApplicationData.Current.LocalFolder.GetFileAsync    
  57.              (sessionStateFilename);
  58.              using (IInputStream inStream = await file.OpenSequentialReadAsync()) {
  59.                  DataContractSerializer serializer = new DataContractSerializer(typeof(Dictionary<string, object>), _knownTypes);
  60.   
  61.                  _sessionState = (Dictionary<string, object>)serializer.ReadObject(  inStream.AsStreamForRead());
  62.   
  63.              }
  64.              foreach (var weakFrameReference in _registeredFrames) {
  65.                      Frame frame;
  66.                      if (weakFrameReference.TryGetTarget(out frame)) {
  67.                          frame.ClearValue(FrameSessionStateProperty);
  68.                          RestoreFrameNavigationState(frame);
  69.                      }
  70.                  }
  71.              }
  72.          catch (Exception e){
  73.              throw new SuspensionManagerException(e);
  74.          }
  75.      }

Na linha 53 inicia-se o método que recupera o estado da sessão do aplicativo. Assim como na listagem anterior, esse método não será comentado linha a linha, mas sim seu propósito geral. Entre as linhas 55 e 63 são criados os objetos e variáveis necessários para o funcionamento do método. Na linha 64, percorre-se os frames da aplicação — a cada frame que for acessado, seus dados de sessão serão restaurados. Isso se faz necessário pois uma vez que a página (frame) é enviada para segundo plano ela perde informações sobre a sessão. Com o método em questão, essas informações, que foram guardadas através do método SaveFrameNavigationState apresentado na Listagem 2, podem ser recuperadas e podem mudar de acordo com a navegação feita pelo usuário. Continuando na linha 67 executa-se o método frame.ClearValue(FrameSessionStateProperty), através do qual o estado do frame é apagado para que na linha 68, através do método RestoreFrameNavigationState, que será explicado adiante, a página possa possuir o estado que estava antes de ser enviada para segundo plano.

Listagem 4. Classe SuspensionManager.cs – Parte III

  76.      private static DependencyProperty FrameSessionStateKeyProperty = Dependency    
  77.      Property.RegisterAttached("_FrameSessionStateKey",typeof(String),
  78.      typeof(SuspensionManager), null);
  79.      private static DependencyProperty FrameSessionStateProperty =Dependency 
  80.      Property.RegisterAttached("_FrameSessionState",typeof(Dictionary<String,
  81.      Object>), typeof(SuspensionManager), null);
  82.      private static List<WeakReference<Frame>> _registeredFrames = new     List<     
  83.      WeakReference<Frame>>();
  84.   
  85.      public static void RegisterFrame(Frame frame, String sessionStateKey) {
  86.          if (frame.GetValue(FrameSessionStateKeyProperty) != null) {
  87.              throw new InvalidOperationException("Frames can only be registered to one
  88.              session state key");
  89.          }
  90.          if (frame.GetValue(FrameSessionStateProperty) != null) {
  91.              throw new InvalidOperationException("Frames must be either be registered
  92.              before accessing frame session state, or not registered at all");
  93.          }
  94.          frame.SetValue(FrameSessionStateKeyProperty, sessionStateKey);
  95.          _registeredFrames.Add(new WeakReference<Frame>(frame));
  96.          RestoreFrameNavigationState(frame);
  97.      }
  98.      public static void UnregisterFrame(Frame frame) {
  99.          SessionState.Remove((String)frame.GetValue(FrameSessionStateKey 
  100.       Property));
  101.        _registeredFrames.RemoveAll((weakFrameReference) => {
  102.            Frame testFrame;
  103.            return !weakFrameReference.TryGetTarget(out testFrame) || testFrame ==   
  104.            frame; });
  105.    }

Entre as linhas 76 e 83, observa-se a criação de alguns objetos específicos de classes que controlam os estados das sessões da aplicação, dispensando assim maiores comentários. Na linha 85, é criado o método RegisterFrame, que registra/armazena o estado de um frame, e na linha 86, verifica-se se existe alguma chave de estado para recuperar — caso não seja encontrada, um erro é retornado (linha 87); na linha 90, verifica-se se existe algum estado para recuperar, caso não exista, novamente envia-se uma mensagem de erro ao usuário (linhas 91 e 92). Na linha 94, o frame recebe os valores da chave e do estado atuais, e, posteriormente, na linha 95, adiciona-se o frame à lista de frames registrados, restaurando o estado do referido frame (linha 96).

Entre as linhas 98 e 105 é realizada a remoção do frame da lista de frames registrados. Na linha 99 remove-se a sessão, e na linha 101 remove-se todos os frames que correspondem aos requisitos da condição. Finalizando o código da referida classe, tem-se a Listagem 5.

Listagem 5. Classe SuspensionManager.cs – Parte IV

  106.       public static Dictionary<String, Object> SessionStateForFrame(Frame frame) {
  107.           var frameState = (Dictionary<String, Object>)frame.GetValue 
  108.           (FrameSessionStateProperty);
  109.           if (frameState == null) {
  110.               var frameSessionKey = (String)frame.GetValue(FrameSessionState 
  111.               KeyProperty);
  112.               if (frameSessionKey != null) {
  113.                   if (!_sessionState.ContainsKey(frameSessionKey)) {
  114.                       _sessionState[frameSessionKey] = new Dictionary<String, Object>();
  115.                   }
  116.                   frameState = (Dictionary<String, Object>)_sessionState[ 
  117.                   frameSessionKey];
  118.               }
  119.               else {
  120.                   frameState = new Dictionary<String, Object>();
  121.               }
  122.               frame.SetValue(FrameSessionStateProperty, frameState);
  123.          }
  124.          return frameState;
  125.       }
  126.       private static void RestoreFrameNavigationState(Frame frame) {
  127.           var frameState = SessionStateForFrame(frame);
  128.           if (frameState.ContainsKey("Navigation")) {
  129.               frame.SetNavigationState((String)frameState["Navigation"]);
  130.           }
  131.       }
  132.       private static void SaveFrameNavigationState(Frame frame) {
  133.           var frameState = SessionStateForFrame(frame);
  134.           frameState["Navigation"] = frame.GetNavigationState();
  135.       }
  136.    }
  137.    public class SuspensionManagerException : Exception {
  138.        public SuspensionManagerException() { }
  139.        public SuspensionManagerException(Exception e) : base("         
  140.        SuspensionManagerfailed", e) {}
  141.    }
  142.}

Observa-se no código a implementação de diversos métodos utilizados nas listagens anteriormente apresentadas. Na linha 106, o método SessionStateForFrame recupera o estado atual da sessão para armazenamento no dicionário de objetos. Na linha 126, o método RestoreFrameNavigationState retorna o estado de navegação do frame, permitindo que esse permaneça com seus dados originais. Na linha 132, o método SaveFrameNavigationState armazena o estado de uma sessão — esse método será invocado sempre que uma sessão for iniciada. Por fim, na linha 137, é criado o método SuspensioManagerException, que herda as propriedades da classe Exception. O propósito desse método é realizar um tratamento diferenciado nos erros que podem vir a ocorrer na execução da classe. O leitor deve se lembrar de que essa é uma classe genérica, assim sendo, os comentários sobre o código utilizado na mesma foram reduzidos, devendo o usuário, caso queira, pesquisar sobre o assunto nas referências da linguagem C# uma vez que esse não é o foco desse trabalho e pelo fato de a referida classe ser recomendada, mas não obrigatória.

Implementação da página principal

Terminada a classe de gerenciamento de sessões, pode-se então criar a interface gráfica MainPage.xaml da aplicação. Para o projeto em questão, foi utilizado o modelo Windows Universal Application, logo as interfaces gráficas seguem um modelo semelhante ao HTML. A primeira tela criada é a da página principal, e seu código pode ser observado na Listagem 6.

Listagem 6. MainPage.xaml

  1.        <Page x:Class=" ComandoVozApp.MainPage"
  2.               xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  3.               xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  4.               xmlns:local="using: ComandoVozApp "
  5.               xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  6.               xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  7.               mc:Ignorable="d"
  8.               Background="{ThemeResourceApplicationPageBackgroundThemeBrush}">
  9.               <Grid x:Name="LayoutRaiz">
  10.                <Grid.ChildrenTransitions>
  11.                     <TransitionCollection>
  12.                         <EntranceThemeTransition/>
  13.                     </TransitionCollection>
  14.                </Grid.ChildrenTransitions>
  15.                <Grid.RowDefinitions>
  16.                      <RowDefinition Height="Auto"/>
  17.                      <RowDefinition Height="*"/>
  18.                </Grid.RowDefinitions>
  19.                <!--Área do Título -->
  20.                <StackPanel Margin="19,0,0,0">
  21.                     <StackPanel Orientation="Horizontal" Margin="0,12,0,0">
  22.                          <Image Source="Assets/toolbox-icon.png" Stretch="None"
  23.                           VerticalAlignment="Bottom"/>
  24.                          <TextBlock Text="Introdução ao Cortana API" Margin="6,0"
  25.                          Style="{StaticResource TitleTextBlockStyle}" />
  26.                      </StackPanel>
  27.                      <TextBlock Text="MEU GARÇOM – COMANDO VOZ"
  28.                      Style="{ThemeResource TitleTextBlockStyle}"/>
  29.                      <TextBlock Text="Como Utilizar" Margin="0,-6.5,0,26.5"
  30.                      Style="{ThemeResource HeaderTextBlockStyle}"  
  31.                      CharacterSpacing="{ThemeResource
  32.                      PivotHeaderItemCharacterSpacing}"/>
  33.                </StackPanel>
  34.                <!--Área do Corpo-->
  35.                <Grid Grid.Row="1" Margin="19,0">
  36.                  <Grid.RowDefinitions>
  37.                       <RowDefinition Height="*"/>
  38.                  <RowDefinition Height="Auto"/>
  39.              </Grid.RowDefinitions>
  40.              <StackPanel Margin="0,0,19,0">
  41.                    <TextBlock Text=" Pressione o botão de busca para iniciar a cortana."            
  42.                    TextWrapping="Wrap" Style="{StaticResourceBaseTextBlockStyle}"/>              
  43.                    <Button x:Name="addMaisItens" Margin="0,7,0,0" Content="Adicione 
  44.                    mais itens" Click="AddMaisItens_Click"/>
  45.              </StackPanel>
  46.              <!--Campo Footer -->
  47.              <Grid Grid.Row="1">
  48.                   <Grid.RowDefinitions>
  49.                        <RowDefinition Height="Auto"/>
  50.                        <RowDefinition Height="Auto"/>
  51.                   </Grid.RowDefinitions>
  52.                   <Grid.ColumnDefinitions>
  53.                        <ColumnDefinition Width="Auto"/>
  54.                        <ColumnDefinition Width="Auto"/>
  55.                   </Grid.ColumnDefinitions>
  56.                   <TextBlock Text="DevMedia – Ubiratan Roberte." Margin="0,9,0,0" 
  57.                   FontSize="12" HorizontalAlignment="Right"/>
  58.                   <Image Source="Assets/105x25microsoft-logo.png" Width="105" 
  59.                   Height="25" HorizontalAlignment="Left" Grid.Column="1" 
  60.                   Margin="6,6,0,0" Grid.RowSpan="2" VerticalAlignment="Top"/>
  61.              </Grid>
  62.          </Grid>
  63.       </Grid>
  64.    </Page>

Observa-se no código que, entre as linhas 1 e 8, tem-se somente os cabeçalhos padrões da página de apresentação dos dados, onde realizamos importações e definições de configuração do modelo da página que será apresentada.

Entre as linhas 9 e 18, são definidos os critérios do layout, e entre as linhas 19 e 33, encontra-se a área de controle do título da aplicação, que será: “MEU GARÇON – COMANDO VOZ”. Das linhas 34 a 45, pode-se observar os códigos referentes ao corpo da página de apresentação. Assim como na parte referente ao título, não existem muitas particularidades nesse bloco de código, sendo que na linha 41, é apresentada uma instrução inicial de como iniciar o comando por voz, e na linha 43, encontra-se o código responsável por apresentar o botão “Adicione mais itens”, que irá disparar o evento AddMaisItens_Click.

Por fim entre as linhas 46 e 61, vê-se a área de rodapé da página. Em uma estrutura Windows Universal, as páginas têm estrutura semelhante a códigos XML ou HTML, sendo possível inserir nessas visões códigos JavaScript, CSS e outros. No bloco do rodapé é inserido um texto, linha 56, e uma imagem, linha 58. Concluído o código, uma tela similar à que se pode ver na Figura 5 deve ser apresentada.

Tela inicial da aplicação
Figura 5. Tela inicial da aplicação

Implementação da página do cardápio

Seguindo com a aplicação, será desenvolvida a página de apresentação do cardápio. Todo código dessa página pode ser observado na Listagem 7.

Listagem 7. PaginaCardapio.xaml

  1.      <Page
  2.          x:Class=" ComandoVozApp.PaginaCardapio"
  3.          xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  4.          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  5.          xmlns:local="using: ComandoVozApp "
  6.          xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
  7.          xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
  8.          mc:Ignorable="d"
  9.          Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
  10.      <Grid x:Name="LayoutRaiz">
  11.          <Grid.ChildrenTransitions>
  12.              <TransitionCollection>
  13.                  <EntranceThemeTransition/>
  14.              </TransitionCollection>
  15.          </Grid.ChildrenTransitions>
  16.          <Grid.RowDefinitions>
  17.              <RowDefinition Height="Auto"/>
  18.              <RowDefinition Height="*"/>
  19.          </Grid.RowDefinitions>
  20.          <!—Área do Título-->
  21.          <StackPanel Margin="19,0,0,0">
  22.              <StackPanel Orientation="Horizontal" Margin="0,12,0,0">
  23.                  <Image Source="Assets/toolbox-icon.png" Stretch="None"    
  24.                   VerticalAlignment="Bottom"/>
  25.                   <TextBlock Text="Windows platform sample" Margin="6,0" 
  26.                   Style="{StaticResource TitleTextBlockStyle}" />
  27.              </StackPanel>
  28.              <TextBlock Text="MEU GARÇON - COMANDO VOZ" 
  29.              Style="{ThemeResource TitleTextBlockStyle}"/>
  30.              <TextBlock Text="Cardápio" Margin="0,-6.5,0,26.5" 
  31.              Style="{ThemeResource HeaderTextBlockStyle}" 
  32.              CharacterSpacing="{ThemeResource         
  33.              PivotHeaderItemCharacterSpacing}"/>
  34.          </StackPanel>
  35.          <!-- Corpo da Página -->
  36.          <Grid Grid.Row="1" Margin="19,0">
  37.              <Grid.RowDefinitions>
  38.                  <RowDefinition Height="*"/>
  39.                  <RowDefinition Height="Auto"/>
  40.              </Grid.RowDefinitions>
  41.              <StackPanel Margin="0,0,19,0">
  42.                  <TextBlock x:Name="CardapioPorcoes" Text="Porções" 
  43.                  TextWrapping="Wrap" Style="{StaticResource 
  44.                  SubheaderTextBlockStyle}"/>
  45.                  <Image Source="Assets/escondidim.png" Width="119" Height="89
  46.                  " HorizontalAlignment="Left" Grid.Column="1" Margin="6,6,0,0"          
  47.                  Grid.RowSpan="2" VerticalAlignment="Top"/>
  48.                  <TextBlock x:Name="escondidinho" Margin="0,14.5,0,0" 
  49.                  Text="[Escondidinho]" TextWrapping="Wrap" 
  50.                  Style="{StaticResource  BodyTextBlockStyle}"/>
  51.                  <Button x:Name="addPedido_1" Margin="0,14.5,0,0" 
  52.                  Content="Adicionar Pedido" Click=" adicionar _Click"/>
  53.                  <Image Source="Assets/moqueca.png" Width="119" Height="89
  54.                  " HorizontalAlignment="Left" Grid.Column="1" Margin="6,6,0,0"             
  55.                  Grid.RowSpan="2" VerticalAlignment="Top"/>
  56.                   <TextBlock x:Name="moqueca" Margin="0,14.5,0,0" 
  57.                  Text="Moqueca Capixaba" TextWrapping="Wrap"   
  58.                  Style="{StaticResource  BodyTextBlockStyle}"/>
  59.                   <Button x:Name="addPedido_2" Margin="0,14.5,0,0" 
  60.                  Content="Adicionar Pedido" Click="adicionar_Click"/>
  61.              </StackPanel>
  62.              <!-- Footer panel -->
  63.              <Grid Grid.Row="1">
  64.                  <Grid.RowDefinitions>
  65.                      <RowDefinition Height="Auto"/>
  66.                      <RowDefinition Height="Auto"/>
  67.                  </Grid.RowDefinitions>
  68.                  <Grid.ColumnDefinitions>
  69.                      <ColumnDefinition Width="Auto"/>
  70.                      <ColumnDefinition Width="Auto"/>
  71.                  </Grid.ColumnDefinitions>
  72.                  <TextBlock Text="DevMedia - Ubiratan." Margin="0,9,0,0" 
  73.                  FontSize="12" HorizontalAlignment="Right"/>
  74.                  <Image Source="Assets/devmedia.png" Width="105" Height="25"
  75.                  HorizontalAlignment="Left" Grid.Column="1" Margin="6,6,0,0" 
  76.                  Grid.RowSpan="2" VerticalAlignment="Top"/>
  77.              </Grid>
  78.          </Grid>
  79.      </Grid>
  80.  </Page>

Das linhas 1 a 9, o que se observa são importações básicas de qualquer arquivo xaml produzido pelo Visual Studio, exceto pelas linhas 2, que determina o nome da página na aplicação, e 5, que realizada a devida importações de classe. Na linha 42, é adicionado um TextBlock, que apresenta ao usuário o texto Porções, que servirá de título para a página. Na linha 45 é inserida uma imagem que representa um item disponível no cardápio, e, na sequência (linha 48) é inserido um texto com o nome do item do menu, no caso do pedido em questão, “escondidinho”. Por fim, na linha 51 temos um botão que fará a chamada de um evento para inserir o item ao pedido da mesa.

Nos trecho que segue até a linha 60 é realiza uma repetição do código compreendido entre as linhas 45 e 52, trocando somente o nome e imagem do prato.

As linhas que não foram comentadas apresentam funcionamento semelhante ao visto na Listagem 6. O resultado após a implementação do código deve ser similar ao apresentado na Figura 6.

Tela de cardápio
Figura 6. Tela de cardápio

Classe da Página PaginaCardapio

Para que a página seja corretamente apresentada, faz-se necessário também criar sua classe de retaguarda. O código dessa classe é apresentado na Listagem 8.

Listagem 8. PaginaCardapio.xaml.cs

  1.        using System;
  2.        using ComandoVozApp.Common;
  3.        using Windows.UI.Xaml;
  4.        using Windows.UI.Xaml.Controls;
  5.        using Windows.UI.Xaml.Navigation;
  6.         
  7.            namespace ComandoVozApp{
  8.                public sealed partial class PaginaCardapio : Page {
  9.                    public PaginaCardapio() {
  10.                    this.InitializeComponent();
  11.                }
  12.                protected override void OnNavigatedTo(NavigationEventArgs e) {
  13.                    if (e.NavigationMode == NavigationMode.New) {
  14.                        SuspensionManager.SessionState["PaginaCardapio"] =
  15.                        e.Parameter.ToString();
  16.                  }
  17.            }
  18.            private void adicionar_Click(object sender, RoutedEventArgs e) {
  19.                (Window.Current.Content as Frame).Navigate(typeof(MainPage));
  20.            }
  21.        }
  22.    }

Entre as linhas 1 e 5 do código apresentado na Listagem 8, são realizadas importações de bibliotecas que serão utilizadas na classe. Entre as linhas 7 e 11, observamos comandos básicos de definição da classe, especificação do método construtor e inicialização dos componentes. Na linha 12, sobrescreve-se o método OnNavigatedTo, e na linha 13 verifica-se se o parâmetro de entrada do método refere-se a uma nova página (apresentação): se sim, então uma referência dessa página é armazenada através do método SessionState da classe SuspensionManager.

Efetua-se na linha 18 a implementação do método adicionar_Click, que nesse projeto simplesmente redirecionará o usuário para a página inicial do projeto (como se trata de um artigo de introdução, alguns eventos não serão totalmente implementados, devendo o leitor ajustá-los para suas necessidades).

Classe do arquivo principal

Criadas as páginas primária e secundária, deve-se realizar algumas alterações na classe principal da aplicação, a App.xaml.cs. Navegue pelo Solution Explorer, encontre o referido arquivo e abra-o. Devido a sua extensão, o código da classe será apresentado em partes que conterão o conteúdo que deve ser inserido ou alterado no arquivo.

Na Listagem 9, é possível observar as importações necessárias para a classe.

Listagem 9. App.xaml.cs – Parte I

  1.             using System;
  2.             using ComandoVozApp.Common;
  3.             using Windows.ApplicationModel;
  4.             using Windows.ApplicationModel.Activation;
  5.             using Windows.Phone.UI.Input;
  6.             using Windows.UI.Xaml;
  7.             using Windows.UI.Xaml.Controls;
  8.             using Windows.UI.Xaml.Media.Animation;
  9.             using Windows.UI.Xaml.Navigation;
  10.         namespace ComandoVozApp {
  11.          public sealed partial class App : Application {
  12.              private Frame rootFrame;
  13.              private TransitionCollection transitions;
  14.              public App() {
  15.                  this.InitializeComponent();
  16.                  this.Suspending += this.OnSuspending;
  17.                  HardwareButtons.BackPressed += HardwareButtons_BackPressed;
  18.              }
  19.              private void HardwareButtons_BackPressed(object sender, 
  20.              BackPressedEventArgs e) {
  21.                  if (this.rootFrame == null) {
  22.                      return;
  23.                  }
  24.                  if (this.rootFrame.CanGoBack) {
  25.                      this.rootFrame.GoBack();
  26.                      e.Handled = true;
  27.                  }
  28.              }

Na linha 2 é realizada importação do pacote Common da aplicação. As demais linhas devem ser inseridas automaticamente, caso não sejam, basta copiá-las para o projeto. Na linha 14, é implementado o método construtor da classe e na linha 19, o método que efetua o retorno para página anterior, independente de em que página o usuário se encontre.

Na linha 21 verifica-se se existe algum frame raiz para o frame em que o usuário se encontra no momento; caso não exista o método não terá retorno. Na linha 24, caso exista alguma raiz (pai) para o frame, então verifica-se se é possível realizar o retorno para o mesmo: se sim, na linha 25 é invocado o método de retorno, e na linha 26 informa-se ao aplicativo que um handle foi utilizado (Handled = true).

Dando sequência ao código do arquivo App.xaml.cs, tem-se a Listagem 10.

Listagem 10. App.xaml.cs – Parte II

  29.          private async void EnsureRootFrame(ApplicationExecutionState
  30.          previousExecutionState) {
  31.          this.rootFrame = Window.Current.Content as Frame;
  32.              if (this.rootFrame == null) {
  33.                  this.rootFrame = new Frame();
  34.                  SuspensionManager.RegisterFrame(this.rootFrame, "AppFrame");
  35.                  this.rootFrame.CacheSize = 1;
  36.                  if (previousExecutionState == ApplicationExecutionState.Terminated) {
  37.                      try {
  38.                          await SuspensionManager.RestoreAsync();
  39.                      }
  40.                      catch (SuspensionManagerException) { }
  41.                  }
  42.                  Window.Current.Content = this.rootFrame;
  43.              }
  44.          Window.Current.Activate();
  45.      }

O código apresentado trata de um método que deve ser criado automaticamente pela IDE, no qual serão inseridos os códigos presentes entre as linhas 34 e 41. Na linha 34 o frame principal, AppFrame, é enviado para o método RegisterFrame, da classe SuspensionManager, cujas funções já foram discutidas neste artigo. Na linha 36, verifica-se se o atual estado da sessão permanece ativo, caso não esteja, o método RestoreAsync, também pertencente à classe SuspensionManager, é invocado, restaurando o estado da sessão. Seguindo para o terceiro bloco de código, é apresentada a Listagem 11.

Listagem 11. App.xaml.cs – Parte III

  46.      protected override void OnActivated(IActivatedEventArgs e) {
  47.          if (e.Kind != Windows.ApplicationModel.Activation.
  48.          ActivationKind.VoiceCommand) {
  49.              return;
  50.          }
  51.          var commandArgs = e as Windows.ApplicationModel.Activation. 
  52.          VoiceCommandActivatedEventArgs;
  53.          Windows.Media.SpeechRecognition.SpeechRecognitionResult 
  54.          speechRecognitionResult = commandArgs.Result;
  55.          string commandMode = this.SemanticInterpretation("commandMode", 
  56.          speechRecognitionResult);
  57.          string voiceCommandName = speechRecognitionResult.RulePath[0];
  58.          string textSpoken = speechRecognitionResult.Text;
  59.          string navigationTarget = this.SemanticInterpretation("NavigationTarget",    
  60.          speechRecognitionResult);
  61.   
  62.          Type navigateToPageType = typeof(MainPage);
  63.          string navigationParameterString = string.Empty;
  64.   
  65.          switch (voiceCommandName) {
  66.              case "apresentaMenu":
  67.                  navigateToPageType = typeof(PaginaCardapio);
  68.              break;
  69.              default:
  70.              break;
  71.          }
  72.   
  73.          this.EnsureRootFrame(e.PreviousExecutionState);
  74.          if (!this.rootFrame.Navigate(navigateToPageType, 
  75.          navigationParameterString)) {
  76.              throw new Exception("Falha ao Redirecionar a página");
  77.          }
  78.      }
  79.   
  80.      private string SemanticInterpretation(string key,
  81.      Windows.Media.SpeechRecognition.SpeechRecognitionResult 
  82.      speechRecognitionResult) {
  83.          if (speechRecognitionResult.SemanticInterpretation.Properties. 
  84.          ContainsKey(key)) {
  85.              return speechRecognitionResult.SemanticInterpretation. 
  86.              Properties[key][0];
  87.          }
  88.          else {
  89.              return "unknown";
  90.          }
  91.      }

Inicia-se na linha 46 o método OnActivated. O referido método é padrão e mesmo que não seja implementado estará presente no arquivo, logo, encontre-o para inserir os demais códigos que se fazem necessários. Na linha 48 verifica-se se a aplicação recebeu comando de voz, se não recebeu, então é realizado o retorno e o restante do código não será executado. Da linha 51 até a linha 63 são realizadas inicializações de diversas variáveis, das quais merecem destaque: linha 55: o método SemanticInterpretation recupera uma string com o entendimento semântico que a aplicação teve sobre a fala do usuário; na linha 57: é recuperada também em forma de string o nome do comando que foi interpretado pelo sistema; por fim, na linha 58 todo texto dito pelo usuário é armazenado na variável textSpoken.

Na linha 65 insere-se um switch para determinar, de acordo com o retorno realizado pela interpretação do texto falado pelo usuário, qual deve ser a ação da aplicação. Na linha 67, é realizado o redirecionamento da página. O código contido entre as linhas 73 e 78 se tratam de código inserido automaticamente pela IDE. Na linha 80, inicia-se a implementação do método SemanticInterpretation, que, de acordo com o que foi dito pelo usuário, retornará a propriedade Name do Command associado ao comando dito. Por exemplo: caso o usuário tenha dito “apresente cardápio”, a aplicação retornará o valor da propriedade Name do elemento Command referenciado pela palavra cardápio, nesse caso, apresentaMenu. A parte final do código em estudo é apresentada na Listagem 12.

Listagem 12. App.xaml.cs – Parte VI

  92.      protected override async void OnLaunched(LaunchActivatedEventArgs e) {
  93.          #if DEBUG
  94.          if (System.Diagnostics.Debugger.IsAttached) {
  95.              this.DebugSettings.EnableFrameRateCounter = true;
  96.          }
  97.          #endif
  98.          this.EnsureRootFrame(e.PreviousExecutionState);
  99.          if (this.rootFrame.Content == null) {
  100.                 if (this.rootFrame.ContentTransitions != null) {
  101.                     this.transitions = new TransitionCollection();
  102.                     foreach (var c in this.rootFrame.ContentTransitions) {
  103.                         this.transitions.Add(c);
  104.                     }
  105.                 }
  106.                 this.rootFrame.ContentTransitions = null;
  107.                 this.rootFrame.Navigated += this.RootFrame_FirstNavigated;
  108.                 if (!this.rootFrame.Navigate(typeof(MainPage), e.Arguments)) {
  109.                     throw new Exception("Failed to create initial page");
  110.                 }
  111.             }
  112.             var storageFile = await Windows.Storage.StorageFile.GetFileFrom          
  113.            ApplicationUriAsync(new Uri("ms-appx:///ComandosSuportados.xml"));
  114.            await Windows.Media.SpeechRecognition.VoiceCommandManager.InstallCommandSetsFromStorageFileAsync(storageFile);
  115.         }
  116.         private void RootFrame_FirstNavigated(object sender,
  117.         NavigationEventArgs e){
  118.             var rootFrame = sender as Frame;
  119.             rootFrame.ContentTransitions = this.transitions ?? newTransitionCollection() 
  120.             { new NavigationThemeTransition() };
  121.             rootFrame.Navigated -= this.RootFrame_FirstNavigated;
  122.         }
  123.         private async void OnSuspending(object sender, SuspendingEventArgs e) {
  124.             var deferral = e.SuspendingOperation.GetDeferral();
  125.             await SuspensionManager.SaveAsync();
  126.             deferral.Complete();
  127.         }
  128.     }
  129.}

O método OnLaunched, iniciado na linha 92, é padrão da aplicação, contudo algumas alterações foram realizadas no mesmo visando a instalação do arquivo de comandos suportados (VCD). Ainda na linha 92, foi adicionado ao método o termo async, que em conjunto com o await, permite determinar um sincronismo no fluxo da troca de mensagens do aplicativo. Todo código constante entre as linhas 93 e 111 é de geração automática, e por isso não será comentado.

Executa-se nas linhas 112 e 113 o comando que recupera o arquivo contendo os comandos suportados, ComandosSuportados.xml, arquivo esse que é inserido na Cortana (observer a linha 113). O restante do código também é gerado automaticamente, assim sendo, não será comentado, exceto o código da linha 125, que invoca o método SaveAsync da classe SuspensionManager.

Concluídas as implementações a aplicação pode então ser testada. Sua interface gráfica final é bem simples, como se pode notar nas imagens anteriormente apresentadas.

A sequência de funcionamento da aplicação é apresentada na Figura 7. Como pode ser visto, a tela inicial da aplicação somente indica ao usuário o que deve ser feito para iniciar a utilização da aplicação. Após dito o comando (corretamente), a nova página com o cardápio é aberta. O fluxo da aplicação encerra-se neste momento, dado que essa é apenas uma aplicação introdutória (exemplo).

Fluxo de
funcionamento da aplicação
Figura 7. Fluxo de funcionamento da aplicação

Utilizar os recursos da Cortana API em suas aplicações Windows Universal mostrou-se ser uma tarefa relativamente simples. Ainda que o código para uma simples aplicação seja extenso, grande parte desse é gerada automaticamente. Essa API permite estender significantemente os recursos de sua aplicação, sendo possível inclusive inserir novos termos a qualquer momento, sem a necessidade de grandes alterações.