Desarrollo de Windows Phone 7

Un Sudoku para Windows Phone 7

Adam Miller

Descargar el código de muestra

En el transcurso de los diez últimos años, el Sudoku se ha vuelto un juego muy popular, y en la mayoría de los periódicos ha conseguido su lugar fijo al lado del crucigrama. Incluso en la televisión se han creado programas de juegos basados en el Sudoku. Por si no lo conoce, el Sudoku es un rompecabezas que consiste en rellenar las casillas de una plantilla con números. El tablero del juego está formado por una cuadrícula de 9×9, y la meta consiste en ubicar los números del uno al nueve de tal manera que cada fila, cada columna, y cada sub-cuadrícula contenga cada número una vez. La propia naturaleza de este rompecabezas se presta muy bien para jugarlo en un dispositivo portátil, y Windows Phone 7 no tiene por qué ser la excepción. Después del lanzamiento reciente de Windows Phone 7 puede esperar que muy pronto aparezca un puñado de aplicaciones de Sudoku en el mercado, e incluso puede agregar la suya propia si sigue este artículo.

Introducción al MVVM

Mi aplicación seguirá básicamente el patrón de diseño Model-View-ViewModel (MVVM). Aunque en verdad no va a haber ningún Model (porque esta aplicación no tiene la necesidad de almacenar nada en una base de datos), de todos modos es una buena herramienta para el aprendizaje, ya que en verdad el ViewModel es el núcleo del patrón.

Puede ser un poco complejo entender el patrón MVVM, pero una vez que lo entienda, le permitirá lograr una excelente separación entre la UI y la lógica empresarial. Además, expresa el poder del enlace de datos en Silverlight, a la vez que lo libera mayormente de la tarea tediosa de tener que codificar la actualización de una UI (los FirstNameTextBox.Text = MyPerson.FirstName serán cosa del pasado). Para obtener más información acerca del enlace de datos en Silverlight, consulte el artículo “Enlace de datos” en tinyurl.com/SLdatabind.

Debido al tamaño y sencillez de esta aplicación, y al enfoque de este artículo, aquí no se empleará un marco MVVM de terceros. Sin embargo, es probable que su aplicación termine siendo más compleja que esta, por lo que sería conveniente que comenzara con un marco externo como el MVVM Light Toolkit, por ejemplo (mvvmlight.codeplex.com). Este le entrega un código libre y probado que terminaría escribiendo de todos modos (comentando desde la experiencia).

Creación de la aplicación

Después de instalar las herramientas de desarrollo de http://xbox.create.msdn.com, comience por crear su nuevo proyecto Windows Phone 7 abriendo Visual Studio y seleccionando Archivo | Nuevo | Proyecto, y luego Visual C# | Silverlight para Windows Phone | Aplicación Windows Phone en el cuadro de diálogo del nuevo proyecto. Comience por crear dos carpetas nuevas, Views y ViewModels, siguiendo el patrón común en MVVM. En este momento ya puede comenzar a depurar si tiene ganas de echar un vistazo al emulador proporcionado como parte del SDK.

El juego de Sudoku puede dividirse en tres tipos conceptuales: cada una de las celdas individuales (un total de 81 en un tablero típico de 9×9), el tablero mismo que contiene las casillas y una cuadrícula para la entrada de los números del uno al nueve. Para crear las vistas de estos elementos, haga clic con el botón secundario en la carpeta Views y seleccione Agregar | Nuevo elemento. Seleccione Control de usuario Windows Phone del cuadro de diálogo y póngale el nombre GameBoardView.xaml al primer archivo. Repita lo mismo para SquareView.xaml e InputView.xaml. Agregue ahora las siguientes clases a la carpeta ViewModel: GameBoardViewModel y SquareViewModel. La vista de entrada en este caso no requiere de un ViewModel. También le convendrá crear una clase base para sus ViewModel, para evitar así la duplicación de código. Agregue para esto una clase ViewModelBase a su carpeta ViewModels. Ahora su solución debería verse parecida a la Figura 1.

image: Sudoku Windows Phone 7 Solution with Views and ViewModels

Figura 1 <strong>Solución del Sudoku en Windows Phone 7 con Views y ViewModels</strong>

La clase base ViewModel

La clase ViewModelBase tendrá que implementar la interfaz INotifyPropertyChanged que se encuentra en System.ComponentModel. Esta es la interfaz que permite enlazar las propiedades públicas de ViewModels con los controles en las vistas. La implementación de la interfaz INotifyPropertyChanged es bastante sencilla: basta con implementar el evento PropertyChanged. Su clase ViewModelBase.cs debería verse similar a lo que sigue (no olvide la instrucción using para System.ComponentModel):

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

La mayoría de los marcos MVVM externos ya contienen una clase base ViewModel con este código reutilizable. Todos los ViewModel se heredarán de ViewModelBase. Las propiedades en un ViewModel que sean enlazadas por la UI tendrán que llamar a NotifyPropertyChanged en el establecedor. Esto es lo que permite la actualización automática de la UI cuando el valor de una propiedad cambie. Resulta un poco tedioso tener que implementar cada una de las propiedades de esta forma, pero es algo que de alguna manera se tranza a cambio de no tener que escribir el código que actualiza la UI.

Implementación de las celdas individuales

Comience por implementar la clase SquareViewModel. Agregue propiedades públicas para Value, Row y Column del tipo entero y para IsSelected, IsValid e IsEditable del tipo Booleano. Aunque la UI se podría enlazar directamente a la propiedad Value, esto causaría problemas, ya que se mostraría un “0” para las celdas que no han sido asignadas. Para solucionar esto, puede implementar un conversor de enlaces o crear una propiedad “StringValue” de sólo lectura que devolverá una cadena vacía cuando la propiedad Value sea cero.

La clase SquareViewModel también será responsable de notificar la UI de su estado actual. En esta aplicación, los cuatro estados para una celda en particular son Default, Invalid, Selected y UnEditable. Normalmente, esto se implementaría como una enumeración. Pero las enumeraciones en el marco Silverlight carecen de algunos de los métodos que tienen las enumeraciones en el marco Microsoft .NET Framework completo. Esto causa que se genere una excepción durante la serialización, por lo que los estados se implementaron 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;
}

Abra ahora SquareView.xaml. Se percatará que algunos estilos fueron aplicados a nivel del control para el tamaño y color de la fuente. Los recursos preestablecidos de estilo generalmente se ubican en un archivo de recursos independiente, pero en este caso, Windows Phone 7 los proporciona a su aplicación en forma predeterminada. Estos recursos están descritos en la página de MSDN Library “Recursos para los temas de Windows Phone” en tinyurl.com/WP7Resources. En esta aplicación se emplearán algunos de estos estilos, para que los colores de la aplicación concuerden con el tema elegido por el usuario. Se puede elegir un tema en la pantalla principal del emulador, haciendo clic en la flecha más | Configuración | Tema. Esto le permite cambiar los colores del fondo y los acentos (Figura 2).

image: Windows Phone 7 Theme Settings Screen

Figura 2 La pantalla Configuración de temas en Windows Phone 7

Coloque un Border y un TextBlock dentro de la cuadrícula en SquareView.xaml:

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

Puede ver el código subyacente para SquareView.xaml.cs en la descarga que acompaña este artículo. El constructor requiere de una instancia de SquareViewModel. Ésta se proporcionará una vez que el tablero esté enlazado. También hay un evento personalizado que se genera cuando el usuario hace clic dentro de la cuadrícula. Los eventos personalizados son una de las formas posibles de lograr que los ViewModel se comuniquen entre ellos. En las aplicaciones más grandes, sin embargo, este método puede volverse muy complicado. Otra opción posible es implementar una clase Messenger para facilitar la comunicación. La mayoría de los marcos MVVM proveen una clase Messenger (llamada también Mediator).

Desde el punto de vista de un purista del MVVM puede parecer desorganizado actualizar la UI en el código subyacente, pero estos elementos no se prestan bien para un BindingConverter. La propiedad BorderThickness de BoxGridBorder está basada en dos propiedades, y los pinceles Foreground y Background provienen de los recursos de la aplicación, que no son fácilmente accesibles en un BindingConverter.

Implementación del tablero del juego

Ahora podemos implementar la vista GameBoard y ViewModel. La vista es sencilla, tan sólo una cuadrícula de 9×9. El código subyacente, disponible en la descarga que acompaña este artículo, es casi igual de sencillo. Tan sólo una propiedad pública para exponer ViewModel y un par de métodos privados para procesar el clic en el cuadro secundario y enlazar la matriz del juego.

 El ViewModel contiene el grueso del código. Contiene los métodos para validar el tablero después de la intervención del usuario y para resolver el rompecabezas y para guardar y cargar el estado del tablero. Al guardar, el tablero se serializa a XML, y se usa IsolatedStorage para guardar el archivo. Para ver la implementación completa, revise el código de muestra que acompaña este artículo. El código de almacenamiento es el más interesante y se muestra en la Figura 3 (observe que tendrá que hacer referencia a System.Xml.Serialization).

Figura 3 <strong>El código de almacenamiento del tablero</strong>

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

Implementación de la cuadrícula de entrada

La vista de entrada también es sencilla, simplemente algunos botones anidados dentro de paneles StackPanel. El código subyacente que se muestra en la Figura 4 expone un evento personalizado para enviar el valor del botón seleccionado a la aplicación, además de dos métodos que permitirán que el juego se pueda jugar en modo horizontal o vertical.

Figura 4 El código subyacente para la vista 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;
}

Reunir las vistas en MainPage.xaml

Finalmente, la aplicación se une con la implementación de MainPage.xaml. Las vistas de Input y GameBoard se colocan dentro de una cuadrícula. Esta aplicación ocupará todo el espacio disponible en la pantalla, así que hay que eliminar el TextBlock de PageTitle que se insertó automáticamente durante la creación del proyecto. El TextBlock de ApplicationTitle sólo será visible en modo vertical. También se aprovechará la Barra de aplicaciones de Windows Phone 7. El uso de la Barra de aplicaciones hace que la aplicación se vea más integrada con el teléfono, y le entrega a la aplicación de Sudoku una buena interfaz para que el usuario resuelva, reinicie e inicie un nuevo rompecabezas:

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

Las imágenes fueron sacadas de un conjunto de iconos proporcionados por Microsoft específicamente para Windows Phone 7 que están instalados con las herramientas en C:\Archivos de programa (x86)\Microsoft SDKs\Windows Phone\v7.0\Icons. Una vez importadas al proyecto, seleccione las propiedades de las imágenes y cambie la Acción de generación de “Recurso” a “Contenido”, y Copiar en el directorio de resultados de “No copiar” a “Copiar si es posterior”.

La última pieza del rompecabezas de esta aplicación es la implementación del código subyacente de MainPage. En el constructor, se establece la propiedad SupportedOrientations para permitir que la aplicación rote cuando el usuario gire el teléfono. También se procesa el evento SendInput de InputView, y el valor de entrada se remite a 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);
}

También hay que implementar los métodos de navegación para administrar el cargado y guardado del tablero del juego:

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

Cuando se rote el teléfono, la aplicación recibirá una notificación. Esto es donde InputView se mueve desde su ubicación debajo del tablero a la derecha del tablero y se rota (como puede ver en la Figura 5).

Figura 5 Código que controla la rotación del teléfono

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

Es aquí donde también se procesan los clics en los elementos del menú:

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

Llegados a este punto, el juego está completo y se puede jugar (vea las Figuras 6 y 7).

image: Sudoku Game in Portrait Mode

Figura 6 El juego de Sudoku en modo vertical

image: Solved Game in Landscape Mode

Figura 7 El juego resuelto en modo horizontal

Aquí lo tiene, un lindo juego al alcance de la mano para la próxima vez que tenga que esperar en una fila. Este artículo ilustró cómo comenzar a crear aplicaciones basadas en Silverlight para Windows Phone 7. También enseñó cómo usar la serialización y el almacenamiento por usuario para admitir varias orientaciones del teléfono. Además debería haberse familiarizado con el patrón MVVM, y cómo usar el enlace de datos con él.

Adam Miller es un ingeniero de software para Nebraska Global en Lincoln, Neb. Puede seguirlo en blog.milrr.com.

Gracias a los siguientes expertos técnicos por su ayuda en la revisión de este artículo: Larry Lieberman y Nick Sherrill