Windows Phone 7 开发

Sudoku for Windows Phone 7

Adam Miller

下载示例代码

Sudoku 近 10 年来已变成一款很流行的游戏,在大多数报纸上,填字游戏的旁边都会有 Sudoku。甚至还根据 Sudoku 创办了游戏节目。如果您不了解 Sudoku,您可以将它理解为一种数字填写游戏。游戏板是一个 9x9 网格,目标是将数字 1 到数字 9 填入网格中,使每个数字在每个行和列以及 3x3 子网格中仅出现一次。这个游戏的特性决定了它非常适合在便携式设备上玩,Windows Phone 7 也不例外。在近期发布 Windows Phone 7 之后,市场上很快就会出现一些 Sudoku 应用程序,并且您甚至可以按照本文中所述方法将自己的 Sudoku 应用程序添加到相应的列表中。

MVVM 简介

我的应用程序大致上将遵循 Model-View-ViewModel (MVVM) 设计模式。虽然没有任何实际模型(因为此应用程序不需要数据库存储),但它仍是一种很好的学习工具,因为 ViewModel 确实是这一模式的核心。

了解 MVVM 模式可能需要一个学习过程,但在您完成相关知识的学习后,便可以巧妙地实现 UI 与业务逻辑的分离。此外,它揭示了数据绑定在 Silverlight 中起到的作用,同时使您在更新 UI 时无需编写大多数繁琐的代码(FirstNameTextBox.Text = MyPerson.FirstName 将一去不返!)。有关 Silverlight 中的数据绑定的详细信息,请参阅 tinyurl.com/SLdatabind 上的 MSDN 库文章“数据绑定”。

考虑到此应用程序的大小、简单性和本文的重点,将不使用第三方 MVVM 框架。但由于您的应用程序很可能变得比这个应用程序更为复杂,因此从第三方框架(如 MVVM Light Toolkit)开始将是您明智的选择 (mvvmlight.codeplex.com)。它将为您提供经测试的免费代码,您最后可以任意编写这些代码(经验所得)。

创建应用程序

从 xbox.http://xbox.create.msdn.com 安装了开发人员工具之后,首先通过打开 Visual Studio 并选择“文件”|“新建”|“项目”来创建一个新的 Windows Phone 7 项目,然后在“新建项目”对话框中,选择“Visual C#”|“Silverlight for Windows Phone”|“Windows Phone Application”。首先按照常用 MVVM 模式创建两个新文件夹,即 Views 和 ViewModels。此时,如果您想浏览已作为 SDK 的一部分提供的仿真器,则还可以开始调试。

Sudoku 游戏在概念上可分为三个类型:各个方格(9x9 游戏板中通常共有 81 个方格);容纳这些方格的整体游戏板;用于输入数字 1 到数字 9 的网格。若要创建这些项的视图,请右键单击 Views 文件夹,再选择“添加”|“新建项目”。从对话框中选择“Windows Phone 用户控件”,并将第一个文件命名为 GameBoardView.xaml。对 SquareView.xaml 和 InputView.xaml 重复上述操作。此时,在 ViewModel 文件夹中添加以下类:GameBoardViewModel 和 SquareViewModel。“输入视图”不需要 ViewModel。您还需要为 ViewModels 创建一个基类以避免代码重复。向 ViewModels 文件夹添加 ViewModelBase 类。此时,您的解决方案应与图 1 中所示内容类似。

图 1 包含 Views 和 ViewModels 的 Sudoku Windows Phone 7 解决方案

ViewModel 基类

ViewModelBase 类将需要实现在 System.ComponentModel 中找到的 INotifyPropertyChanged 接口。此接口允许将 ViewModels 中的公共属性绑定到视图中的控件。INotifyPropertyChanged 接口的实现相当简单 - 只需实现 PropertyChanged 事件即可。您的 ViewModelBase.cs 类看起来应与下面的内容类似(不要忘记 System.ComponentModel 的 using 语句):

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

大多数第三方 MVVM 框架将包括一个 ViewModel 基类,其中包含此样本代码。 您所有的 ViewModel 都将从 ViewModelBase 继承。 UI 将绑定到的 ViewModel 中的属性必须调用 setter 中的 NotifyPropertyChanged。 这是允许 UI 在属性值发生更改时自动更新的设置。 按此方式实现所有属性确实有点繁琐,但这样做换来的好处是,您不必编写代码来更新 UI。

实现各个方格

首先实现 SquareViewModel 类。 将 Value、Row、Column 的公共属性添加为整数;将 IsSelected、IsValid 和 IsEditable 添加为布尔值。 虽然可将 UI 直接绑定到 Value 属性,但这将导致出现问题,因为将为未分配的方格显示“0”。 若要解决此问题,可以实现绑定转换器或创建只读“StringValue”属性,该属性将在 Value 属性为零时返回空字符串。

SquareViewModel 还负责向 UI 通知其当前状态。 此应用程序中的单个方格具有四种状态,即“Default”(默认)、“Invalid”(无效)、“Selected”(已选定)和“UnEditable”(不可编辑)。 通常,这将作为枚举实现;但 Silverlight 框架中的枚举不包含完整 Microsoft .NET Framework 的枚举所具有的几种方法。 这会导致在序列化期间引发异常,因此已将状态实现为常数:

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

现在打开 SquareView.xaml。您会发现,已在控件级别为字号和颜色应用了某些样式。通常,可以在单独的资源文件中找到预设置样式资源,但在此示例中,Windows Phone 7 默认情况下将向您的应用程序提供这些资源。tinyurl.com/WP7Resources 上的 MSDN 库页“Windows Phone 的主题资源”中对这些资源进行了描述。此应用程序将使用其中的一些样式,以使其颜色与用户选定的主题匹配。可通过转到主页屏幕,并单击“更多”箭头|“设置”|“主题”在仿真器中选择主题。您可以从此处更改背景色和强调文字颜色(图 2)。

图 2 Windows Phone 7 主题设置屏幕

在 SquareView.xaml 中的网格内,放置一个 Border 和一个 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>

可在附带的代码下载中看到 SquareView.xaml.cs 的隐藏代码。 此构造函数需要 SquareViewModel 的实例。 这将在绑定游戏板时提供。 此外,还有一个当用户在网格内单击时引发的自定义事件。 使用自定义事件是允许 ViewModel 互相通信的一种方法;但对于大型应用程序,这种方法会比较麻烦。 您还有一个选择,即实现将推动通信的 Messenger 类。 大多数 MVVM 框架都提供了 Messenger(有时称作 Mediator)类。

单就 MVVM 而言,使用隐藏代码更新 UI 可能看起来比较麻烦,但这些项本身不适合于 BindingConverter。 BoxGridBorder 的 BorderThickness 基于两个属性,并且前景画笔和背景画笔来自应用程序资源,无法从 BindingConverter 中轻易访问它们。

实现游戏板

现在可以实现 GameBoard 视图和 ViewModel。 此视图就是一个 9x9 网格,非常简单。 代码下载中提供的代码隐藏很简单 - 只是一个用于公开 ViewModel 的公共属性和几个用于处理子框单击和绑定游戏数组的私有方法。

ViewModel 包含大部分代码。 它包含用于在用户输入后验证游戏板的方法、用于解决难题的方法以及用于保存和加载存储中的游戏板的方法。 在保存时会将游戏板序列化为 XML,并使用 IsolatedStorage 保存此文件。 有关完全实现,请参阅源代码下载;存储代码是最有趣的,如图 3 中所示(请注意,您需要对 System.Xml.Serialization 的引用)。

图 3 游戏板存储代码

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

实现输入网格

输入视图也非常简单,它只是堆栈面板中嵌入的几个按钮。 图 4 中所示的隐藏代码公开了自定义事件以向应用程序发送已单击按钮的值,以及两个用于帮助使该游戏能在纵向模式或横向模式中可玩的方法。

图 4 输入视图的隐藏代码

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

将视图都集中到 MainPage.xaml 上

最后,将此应用程序与 MainPage.xaml 的实现结合在一起。 将输入视图和游戏板视图置于一个网格中。 由于此应用程序需要所有可用的屏幕空间,因此必须删除在创建项目时自动插入的 PageTitle TextBlock。 ApplicationTitle TextBlock 仅在纵向模式中可见。 还将利用 Windows Phone 7 应用程序栏。 通过使用此应用程序栏,可使应用程序感觉上与手机的集成性更高,并将为 Sudoku 应用程序提供一个很好的接口,以允许用户解答、重置和开始新谜题:

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

从 Microsoft 专门为 Windows Phone 7 提供的图标集中获取图像,这些图像将与工具一起安装到 C:\Program Files (x86)\Microsoft SDKs\Windows Phone\v7.0\Icons。 在将图像导入项目后,选择图像属性,并将“生成操作”从“资源”更改为“内容”,然后将“复制到输出目录”从“不复制”更改为“如果较新则复制”。

此应用程序谜题的最后一个部分是实现 MainPage 隐藏代码。 在构造函数中,设置 SupportedOrientations 属性以允许应用程序在用户旋转手机时随之旋转。 另外,处理 InputView 的 SendInput 事件,并将输入值传送到 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);
}

还必须实现导航方法以便加载和保存游戏板:

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

在旋转手机时,应用程序将收到一个通知。 InputView 会从该位置开始从游戏板下方移动到其右侧并进行旋转(参见图 5)。

图 5 用于处理手机旋转的代码

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

这也是处理菜单项单击的位置:

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

此时,该游戏已完成,可以开始玩了(参见图 67)。

图 6 纵向模式下的 Sudoku

图 7 横向模式下的已解答的游戏

现在您就可以玩这个很好玩的游戏了,下次您就要排队等候了。本文演示了如何开始创建基于 Silverlight 的 Windows Phone 7 应用程序。本文还演示了如何使用序列化和用户存储来保持应用程序,以及如何允许应用程序支持多个方向。另外,现在您应熟悉了 MVVM 模式以及如何将其与数据绑定一起使用。

Adam Miller 是位于 Lincoln, Neb 的 Nebraska Global 的一名软件工程师。您可以访问他的博客:blog.milrr.com

衷心感谢以下技术专家对本文的审阅:Larry Lieberman 和 Nick Sherrill