Este artigo foi traduzido por máquina.

Desenvolvimento para Windows Phone 7

Sudoku para Windows Phone 7

Adam Miller

Baixe o código de exemplo

O Sudoku tornou-se um jogo popular nos últimos 10 anos, conquistando espaço na maioria dos jornais bem ao lado das palavras cruzadas. Já foram até mesmo criados game shows baseados no Sudoku. Se você não está familiarizado com ele, o Sudoku é um jogo de substituição de números. O tabuleiro consiste em uma grade de 9x9, e o objetivo é colocar os números de 1 a 9 na grade de forma que cada linha, coluna e subgrade 3x3 contenha cada número uma vez. A natureza do jogo o torna agradável para jogar em dispositivo portátil, e o Windows Phone 7 não deve ser uma exceção. Em breve, você verá uma infinidade de aplicativos Sudoku no mercado após o lançamento recente do Windows Phone 7, e poderá até mesmo adicionar os seus próprios a essa lista, seguindo as orientações deste artigo.

Introdução ao MVVM

Meu aplicativo seguirá um pouco o padrão de design Model-View-ViewModel (MVVM). Embora não haja nenhum modelo propriamente dito (porque esse aplicativo não requer armazenamento de banco de dados), mesmo assim ele será uma boa ferramenta de aprendizado porque ViewModel é a base do padrão.

Pode haver uma pequena curva de aprendizado para entender o padrão MVVM, mas uma vez que o conceito for assimilado, você perceberá que pode obter uma separação realmente boa entre lógica de interface do usuário e lógica comercial. Além disso, ele revela a força da associação de dados do Silverlight e, ao mesmo tempo, libera você da maior parte do código enfadonho relacionado com a atualização de uma interface do usuário (FirstNameTextBox.Text = MyPerson.FirstName será coisa do passado!). Para obter mais informações sobre associação de dados no Silverlight, consulte o artigo da Biblioteca MSDN intitulado “Associação de dados”, em tinyurl.com/SLdatabind.

Devido ao tamanho e à simplicidade desse aplicativo, e ao enfoque deste artigo, não será usado uma estrutura MVVM de terceiros. No entanto, é provável que seu aplicativo se torne mais complexo do que este, e seria sensato iniciar com uma estrutura de terceiros, como o MVVM Light Toolkit (mvvmlight.codeplex.com). Ele fornecerá código gratuito e testado, que no final das contas você acabará escrevendo (por experiência própria).

Criando o aplicativo

Depois de instalar as ferramentas para desenvolvedor disponíveis em http://xbox.create.msdn.com, inicie criando seu novo projeto do Windows Phone 7 abrindo o Visual Studio e selecionando Arquivo | Novo | Projeto; em seguida, na caixa de diálogo do novo projeto, escolha Visual C# | Silverlight para Windows Phone | Aplicativo para Windows Phone. Comece criando duas novas pastas, Views e ViewModels, seguindo um padrão MVVM comum. Nesta etapa, você também pode começar a depurar se quiser dar uma espiada no emulador fornecido como parte do SDK.

O jogo Sudoku pode ser dividido em três tipos conceituais: cada um dos quadrados individuais (81, no total, no tabuleiro típico de 9x9), o tabuleiro como um todo que contém os quadrados e uma grade para inserir os números de 1-9. Para criar as exibições para esses itens, clique com o botão direito do mouse na pasta Views e selecione Adicionar | Novo Item. Selecione Controle de Usuário do Windows Phone na caixa de diálogo e nomeie o primeiro arquivo como GameBoardView.xaml. Repita o procedimento para SquareView.xaml e InputView.xaml. Agora, na pasta ViewModel, adicione as seguintes classes: GameBoardViewModel e SquareViewModel. InputView não exigirá um ViewModel. Você também pode criar uma classe base para ViewModels para evitar a duplicação de código. Adicione uma classe ViewModelBase à pasta ViewModels. Nesta etapa, a solução deve estar parecida com a mostrada na Figura 1.

image: Sudoku Windows Phone 7 Solution with Views and ViewModels

Figura 1 Solução Sudoku para Windows Phone 7 com Views e ViewModels

Classe base ViewModel

A classe ViewModelBase precisará implementar a interface INotifyPropertyChanged encontrada em System.ComponentModel. Essa interface é o que permite às propriedades públicas em ViewModels se associar a controles nas exibições. A implementação da interface INotifyPropertyChanged é bastante simples – só é preciso implementar o evento PropertyChanged. A classe ViewModelBase.cs não deve ser parecida com o seguinte (não se esqueça de usar a declaração para System.ComponentModel):

public class ViewModelBase : INotifyPropertyChanged
{
  public event PropertyChangedEventHandler  
    PropertyChanged;
  private void NotifyPropertyChanged(String info)
  {
    if (PropertyChanged != null)
    {
      PropertyChanged(this, 
        new PropertyChangedEventArgs(info));
    }
  }
}

A maioria das estruturas MVVM de terceiros incluirá uma classe base ViewModel que contém esse código de texto clichê. Todas as suas ViewModels herdarão de ViewModelBase. As propriedades de uma ViewModel às quais a interface do usuário irá se associar precisam chamar NotifyPropertyChanged no setter. É isso que permite que a interface do usuário seja automaticamente atualizada quando o valor de uma propriedade é alterado. De fato é um pouco enfadonho implementar todas as propriedades desse modo, por isso não deixa de ser uma compensação quanto ao código que você não precisa escrever para atualizar a interface do usuário.

Implementando os quadrados individuais

Comece implementando a classe SquareViewModel. Adicione propriedades públicas para Value, Row e Column como inteiros; e IsSelected, IsValid e IsEditable como booleanos. Embora a interface do usuário possa se associar à propriedade Value diretamente, isso causará problemas porque “0” será exibido para quadrados não atribuídos. Para resolver isso, você pode implementar um conversor de associação ou criar uma propriedade “StringValue” somente leitura que retornará uma cadeia de caracteres vazia quando a propriedade Value for zero.

SquareViewModel também será responsável por notificar a interface do usuário sobre seu estado atual. Os quatro estados de um quadrado neste aplicativo são Default, Invalid, Selected e UnEditable. Normalmente isso seria implementado como enumeração; porém, na estrutura do Silverlight, as enumerações não têm alguns métodos que as enumerações do Microsoft .NET Framework completo têm. Isso gera uma exceção durante a serialização, por isso os estados foram implementados como constantes:

public class BoxStates
{
  public const int Default = 1;
  public const int Invalid = 2;
  public const int Selected = 3;
  public const int UnEditable = 4;
}

Agora abra SquareView.xaml. Você perceberá que alguns estilos foram aplicados no nível de controle de tamanho e cor de fonte. Os recursos de estilo predefinido geralmente são encontrados em um arquivo de recursos à parte, mas nesse caso, por padrão, o Windows Phone 7 os fornece para seu aplicativo. Os recursos estão descritos na página da Biblioteca MSDN intitulada “Recursos de tema para Windows Phone”, em tinyurl.com/WP7Resources. Alguns desses estilos serão usados neste aplicativo para que suas cores correspondam ao tema selecionado pelo usuário. O tema pode ser selecionado no emulador; para isso, vá para a tela inicial e clique na seta mais | Configurações | tema. Ali é possível alterar o plano de fundo e as cores de ênfase (Figura 2).

image: Windows Phone 7 Theme Settings Screen

Figura 2 Tela Configurações de Tema do Windows Phone 7

Dentro da grade em SquareView.xaml, coloque um Border e um TextBlock:

<Grid x:Name="LayoutRoot" MouseLeftButtonDown=
    "LayoutRoot_MouseLeftButtonDown">
    <Border x:Name="BoxGridBorder" 
      BorderBrush="{StaticResource PhoneForegroundBrush}" 
      BorderThickness="{Binding Path=BorderThickness}">
      <TextBlock x:Name="MainText" 
        VerticalAlignment="Center" Margin="0" Padding="0" 
        TextAlignment="Center" Text=
        "{Binding Path=StringValue}">
      </TextBlock>
    </Border>
  </Grid>

O code-behind para SquareView.xaml.cs pode ser visto no download de código fornecido. O construtor exige uma instância de SquareViewModel. Ela será fornecida quando o tabuleiro for associado. Além disso, existe um evento personalizado que é gerado quando o usuário clica dentro da grade. O uso de eventos personalizados é uma das formas de permitir que ViewModels se comuniquem entre si; todavia, no caso de aplicativos maiores, esta abordagem pode ficar um pouco confusa. Outra opção é implementar uma classe Messenger que facilitará a comunicação. A maioria das estruturas MVVM fornece uma classe Messenger (também chamada de Mediator).

Pode parecer confuso do ponto de vista de um purista do MVVM atualizar a interface do usuário utilizando o code-behind, mas esses itens não servem para um BindingConverter. O BorderThickness de BoxGridBorder é baseado em duas propriedades, e os pincéis Foreground e Background são provenientes dos recursos do aplicativo, que não estão prontamente acessíveis em um BindingConverter.

Implementando o tabuleiro

Agora é possível implementar a exibição GameBoard e ViewModel. A exibição é simples, apenas uma grade de 9x9. O code-behind, disponível no download de código, é quase tão simples quanto a exibição — uma propriedade pública para expor ViewModel e alguns métodos particulares para processar a caixa de clicar e associar a matriz de jogo.

ViewModel contém grande parte do código. Ele contém métodos para validar o tabuleiro após entrada do usuário, resolver o quebra-cabeça e salvar e carregar o tabuleiro do armazenamento. O tabuleiro é serializado para XML ao salvar, e IsolatedStorage é usado para salvar o arquivo. Para a implementação completa, veja o download do código-fonte; o código de armazenamento é muito interessante e está ilustrado na Figura 3 (será necessária uma referência a System.Xml.Serialization).

Figura 3 O código de armazenamento do tabuleiro

public void SaveToDisk()
{
  using (IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForApplication())
  {
    if (store.FileExists(FileName))
    {
      store.DeleteFile(FileName);
    }

    using (IsolatedStorageFileStream stream = store.CreateFile(FileName))
    {
      using (StreamWriter writer = new StreamWriter(stream))
      {
        List<SquareViewModel> s = new List<SquareViewModel>();
        foreach (SquareViewModel item in GameArray)
          s.Add(item);

        XmlSerializer serializer = new XmlSerializer(s.GetType());
        serializer.Serialize(writer, s);
      }
    }
  }
}

public static GameBoardViewModel LoadFromDisk()
{
  GameBoardViewModel result = null;

  using (IsolatedStorageFile store = IsolatedStorageFile.
GetUserStoreForApplication())
  {
    if (store.FileExists(FileName))
    {
      using (IsolatedStorageFileStream stream = 
        store.OpenFile(FileName, FileMode.Open))
      {
        using (StreamReader reader = new StreamReader(stream))
        {
          List<SquareViewModel> s = new List<SquareViewModel>();
          XmlSerializer serializer = new XmlSerializer(s.GetType());
          s = (List<SquareViewModel>)serializer.Deserialize(
            new StringReader(reader.ReadToEnd()));

          result = new GameBoardViewModel();
          result.GameArray = LoadFromSquareList(s);
        }
      }
    }
  }

  return result;
}

Implementando a grade de entrada

A exibição de entrada é igualmente simples, apenas alguns botões aninhados em StackPanels. O code-behind, mostrado na Figura 4, expõe um evento personalizado para enviar o valor do botão clicado ao aplicativo, bem como dois métodos que ajudarão a tornar o jogo reproduzível no modo retrato ou paisagem.

Figura 4 O code-behind da exibição de entrada

public event EventHandler SendInput;

private void UserInput_Click(object sender, RoutedEventArgs e)
{
  int inputValue = int.Parse(((Button)sender).Tag.ToString());
  if (SendInput != null)
      SendInput(inputValue, null);
}

public void RotateVertical()
{
  TopRow.Orientation = Orientation.Vertical;
  BottomRow.Orientation = Orientation.Vertical;
  OuterPanel.Orientation = Orientation.Horizontal;
}

public void RotateHorizontal()
{
  TopRow.Orientation = Orientation.Horizontal;
  BottomRow.Orientation = Orientation.Horizontal;
  OuterPanel.Orientation = Orientation.Vertical;
}

Reunindo exibições em MainPage.xaml

Para finalizar, o aplicativo é combinado com a implementação de MainPage.xaml. As exibições Input e GameBoard são colocadas em uma grade. Este aplicativo exigirá que toda a tela esteja disponível, por isso é preciso remover o PageTitle TextBlock que foi inserido automaticamente na criação do projeto. O TextBlock de ApplicationTitle só ficará visível no modo de retrato. Também tiraremos proveito da Barra de Aplicativos do Windows Phone 7. O uso da Barra de Aplicativos fará com que o aplicativo pareça mais integrado ao telefone e dará ao Sudoku uma bela interface para que os usuários possam solucionar, zerar e iniciar um novo quebra-cabeça:

<phone:PhoneApplicationPage.ApplicationBar>
   <shell:ApplicationBar IsVisible="True" IsMenuEnabled="True">
     <shell:ApplicationBarIconButton x:Name="NewGame"  
      IconUri="/Images/appbar.favs.rest.png" Text="New Game" 
      Click="NewGame_Click"></shell:ApplicationBarIconButton>
     <shell:ApplicationBarIconButton x:Name="Solve" 
      IconUri="/Images/appbar.share.rest.png" Text="Solve" 
      Click="Solve_Click"></shell:ApplicationBarIconButton>
     <shell:ApplicationBarIconButton x:Name="Clear" 
      IconUri="/Images/appbar.refresh.rest.png" Text="Clear" 
      Click="Clear_Click"></shell:ApplicationBarIconButton>
  </shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>

As imagens são extraídas de um conjunto de ícones fornecidos pela Microsoft especificamente para o Windows Phone 7, instalados com as ferramentas em C:\Arquivos de Programas (x86)\Microsoft SDKs\Windows Phone\v7.0\Icons. Depois que as imagens forem importadas para o projeto, selecione as propriedades de imagem e altere Ação de Compilação de “Recurso” para “Conteúdo” e Copiar para Diretório de Saída de “Não Copiar” para “Copiar se Mais Novo”.

A parte final desse aplicativo de quebra-cabeça é implementar o code-behind de MainPage. No construtor, a propriedade SupportedOrientations é definida para permitir que o aplicativo gire quando o usuário gira o telefone. Além disso, o evento SendInput de InputView é processado e o valor de entrada encaminhado para GameBoard:

public MainPage()
{
  InitializeComponent();
  SupportedOrientations = SupportedPageOrientation.Portrait |
    SupportedPageOrientation.Landscape;
  InputControl.SendInput += new 
    EventHandler(InputControl_SendInput);
}

void InputControl_SendInput(object sender, EventArgs e)
{
  MainBoard.GameBoard.SendInput((int)sender);
}

Também é necessário implementar os métodos Navigation para processar o carregamento e a gravação do tabuleiro:

protected override void OnNavigatedTo(NavigationEventArgs e)
{
  GameBoardViewModel board = 
    GameBoardViewModel.LoadFromDisk();
  if (board == null)
    board = GameBoardViewModel.LoadNewPuzzle();

  MainBoard.GameBoard = board;
  base.OnNavigatedTo(e);
}

protected override void OnNavigatedFrom(NavigationEventArgs e)
{
  MainBoard.GameBoard.SaveToDisk();
  base.OnNavigatedFrom(e);
}

Quando o telefone for girado, o aplicativo receberá uma notificação. É nesse momento que InputView deixa de ficar embaixo do tabuleiro e passa para a sua direita e é girado (veja a Figura 5).

Figura 5 Código para processar o giro do telefone

protected override void OnOrientationChanged(OrientationChangedEventArgs e)
{
  switch (e.Orientation)
  {
    case PageOrientation.Landscape:
    case PageOrientation.LandscapeLeft:
    case PageOrientation.LandscapeRight:
      TitlePanel.Visibility = Visibility.Collapsed;
      Grid.SetColumn(InputControl, 1);
      Grid.SetRow(InputControl, 0);
      InputControl.RotateVertical();
      break;
    case PageOrientation.Portrait:
    case PageOrientation.PortraitUp:
    case PageOrientation.PortraitDown:
      TitlePanel.Visibility = Visibility.Visible;
      Grid.SetColumn(InputControl, 0);
      Grid.SetRow(InputControl, 1);
      InputControl.RotateHorizontal();
      break;
    default:
      break;
  }
  base.OnOrientationChanged(e);
}

É ali também que ocorre o processamento dos cliques nos itens de menu:

private void NewGame_Click(object sender, EventArgs e)
{
  MainBoard.GameBoard = GameBoardViewModel.LoadNewPuzzle();
}

private void Solve_Click(object sender, EventArgs e)
{
  MainBoard.GameBoard.Solve();
}

private void Clear_Click(object sender, EventArgs e)
{
  MainBoard.GameBoard.Clear();
}

Neste ponto, o jogo está concluído e pode ser reproduzido (veja as Figuras 6 e 7).

Figura 6 Jogo Sudoku no modo Retrato

Figura 7 Jogo resolvido no modo Paisagem

Aqui está, um belo jogo esperando por você na próxima vez que estiver esperando na linha. Este artigo demonstrou como começar a criar aplicativos para Windows Phone 7 baseados no Silverlight. Ele também mostrou como usar a serialização e o armazenamento de usuário para manter um aplicativo e como permitir que o aplicativo suporte várias orientações. Além disso, agora você deve estar familiarizado com o padrão MVVM e como utilizar a associação de dados com ele.

Adam Miller is a software engineer for Nebraska Global in Lincoln, Neb. You can follow him at blog.milrr.com.

Graças aos seguintes especialistas técnicos para revisão deste artigo: Larry Lieberman e Nick Sherrill