專案程式碼可以從Github下載:https://github.com/zhenl/ZL.Shudu 。程式碼隨專案進度更新。
前面我們已經完成了遊戲的大部分功能,玩家可以玩預製的數獨遊戲,也可以自己新增新的遊戲。現在我們實現展示已完成遊戲列表頁面,顯示使用者已經完成的遊戲列表,從這個列表可以進入詳細的覆盤頁面。
前面的頁面我們採用的是傳統的事件驅動模型,在XAML檔案中定義頁面,在後臺的cs檔案中編寫事件響應程式碼。採用這種模型是因為很多頁面需要動態生成控制元件,然後動態改變這些控制元件的屬性,事件驅動模型在這種場景下比較好理解。現在我們採用MVVM方式編寫完成遊戲列表頁面。
MVVM是將頁面繫結到檢視模型,所有的操作和事件響應通過檢視模型完成。檢視模型中沒有頁面控制元件的定義,因此和頁面是解耦的,可以獨立進行測試。在檢視模型中我們只關心資料,而不關心展示資料的控制元件。
首先,我們定義一個檢視模型的基類,下一步在改造其它頁面時,會用到這個基類:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text;
namespace ZL.Shudu.ViewModels
{
public class BaseViewModel : INotifyPropertyChanged
{
bool isBusy = false;
public bool IsBusy
{
get { return isBusy; }
set { SetProperty(ref isBusy, value); }
}
string title = string.Empty;
public string Title
{
get { return title; }
set { SetProperty(ref title, value); }
}
protected bool SetProperty<T>(ref T backingStore, T value,
[CallerMemberName] string propertyName = "",
Action onChanged = null)
{
if (EqualityComparer<T>.Default.Equals(backingStore, value))
return false;
backingStore = value;
onChanged?.Invoke();
OnPropertyChanged(propertyName);
return true;
}
#region INotifyPropertyChanged
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = "")
{
var changed = PropertyChanged;
if (changed == null)
return;
changed.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
}
這個基類實現INotifyPropertyChanged介面,幫助實現屬性在頁面的雙向繫結。然後,定義完成列表頁面的檢視模型:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using ZL.Shudu.Models;
using ZL.Shudu.Views;
namespace ZL.Shudu.ViewModels
{
public class FinishGameViewModel:BaseViewModel
{
private FinishGame _selectedItem;
public ObservableCollection<FinishGame> Items { get; }
public Command LoadItemsCommand { get; }
public Command<FinishGame> ItemTapped { get; }
public FinishGameViewModel()
{
Title = "完成遊戲列表";
Items = new ObservableCollection<FinishGame>();
LoadItemsCommand = new Command(async () => await ExecuteLoadItemsCommand());
ItemTapped = new Command<FinishGame>(OnItemSelected);
}
async Task ExecuteLoadItemsCommand()
{
IsBusy = true;
try
{
Items.Clear();
var items = await App.Database.GetFinishGamesAsync();
foreach (var item in items)
{
Items.Add(item);
}
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
finally
{
IsBusy = false;
}
}
public FinishGame SelectedItem
{
get => _selectedItem;
set
{
SetProperty(ref _selectedItem, value);
OnItemSelected(value);
}
}
public void OnAppearing()
{
IsBusy = true;
SelectedItem = null;
}
async void OnItemSelected(FinishGame item)
{
if (item == null)
return;
// This will push the ItemDetailPage onto the navigation stack
await Shell.Current.GoToAsync($"{nameof(FinishGameDetailPage)}?{nameof(FinishGameDetailPage.ItemId)}={item.Id}");
}
}
}
主要功能有兩個,一是從資料庫中讀取資料,二是響應選中資料事件並跳轉到顯示詳細資訊的頁面。
接下來定義頁面:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:ZL.Shudu.ViewModels" xmlns:model="clr-namespace:ZL.Shudu.Models"
x:Class="ZL.Shudu.Views.FinishGameListPage"
Title="{Binding Title}">
<RefreshView x:DataType="local:FinishGameViewModel" Command="{Binding LoadItemsCommand}" IsRefreshing="{Binding IsBusy, Mode=TwoWay}">
<CollectionView x:Name="ItemsListView"
ItemsSource="{Binding Items}"
SelectionMode="None">
<CollectionView.ItemTemplate>
<DataTemplate>
<StackLayout Padding="10" x:DataType="model:FinishGame">
<Label Text="{Binding Id}"
LineBreakMode="NoWrap"
Style="{DynamicResource ListItemTextStyle}"
FontSize="16" />
<Label Text="{Binding PlayDate}"
LineBreakMode="NoWrap"
Style="{DynamicResource ListItemDetailTextStyle}"
FontSize="13" />
<StackLayout.GestureRecognizers>
<TapGestureRecognizer
NumberOfTapsRequired="1"
Command="{Binding Source={RelativeSource AncestorType={x:Type local:FinishGameViewModel}}, Path=ItemTapped}"
CommandParameter="{Binding .}">
</TapGestureRecognizer>
</StackLayout.GestureRecognizers>
</StackLayout>
</DataTemplate>
</CollectionView.ItemTemplate>
</CollectionView>
</RefreshView>
</ContentPage>
頁面後臺程式碼:
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using ZL.Shudu.ViewModels;
namespace ZL.Shudu.Views
{
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class FinishGameListPage : ContentPage
{
FinishGameViewModel _viewModel;
public FinishGameListPage()
{
InitializeComponent();
BindingContext = _viewModel = new FinishGameViewModel();
}
protected override void OnAppearing()
{
base.OnAppearing();
_viewModel.OnAppearing();
}
}
}
頁面後臺程式碼只負責將頁面繫結到檢視模型,不做其它工作。執行效果如下:
接下來完成遊戲的覆盤頁面。這個頁面調入已完成的遊戲,可以使用向前和向後覆盤遊戲的過程,在實現上與遊戲頁面類似。
XAML頁面程式碼如下:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="ZL.Shudu.Views.FinishGameDetailPage"
Title="遊戲記錄">
<ContentPage.Content>
<StackLayout>
<Grid x:Name="myGrid" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="25" />
<RowDefinition Height="40" />
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<Button Text="開始" Grid.Row="9" Grid.Column="0" Grid.ColumnSpan="2" Clicked="btn_Begin_Clicked"></Button>
<Button Text="結束" Grid.Row="9" Grid.Column="2" Grid.ColumnSpan="2" Clicked="btn_End_Clicked"></Button>
<Button Text="向前" Grid.Row="9" Grid.Column="4" Grid.ColumnSpan="2" Clicked="btn_Forward_Clicked"></Button>
<Button Text="向後" Grid.Row="9" Grid.Column="6" Grid.ColumnSpan="2" Clicked="btn_Back_Clicked"></Button>
<Label x:Name="lbTime" Grid.Row="10" Grid.Column="0" Grid.ColumnSpan="2" Text="" ></Label>
<Label x:Name="lbMessage" Grid.Row="10" Grid.Column="3" Grid.ColumnSpan="4" Text=""></Label>
</Grid>
</StackLayout>
</ContentPage.Content>
</ContentPage>
後臺程式碼如下:
using System;
using System.Collections.Generic;
using Xamarin.Forms;
using Xamarin.Forms.Xaml;
using ZL.Shudu.Models;
namespace ZL.Shudu.Views
{
[QueryProperty(nameof(ItemId), nameof(ItemId))]
[XamlCompilation(XamlCompilationOptions.Compile)]
public partial class FinishGameDetailPage : ContentPage
{
private static int[,] chess = new int[9, 9];
private Button[,] buttons = new Button[9, 9];
int[,] fchess = new int[9, 9];
private List<string> steps = new List<string>();
private int currentsetp = 0;
private long currentDiffer = 0;
private int currentId;
public string ItemId
{
get
{
return currentId.ToString();
}
set
{
currentId = int.Parse(value);
if (currentId > 0)
{
var game = App.Database.GetFinishGameAsync(currentId).Result;
if (game != null) OpenHistory(game);
}
}
}
public string Title { get; set; } = "遊戲記錄";
public FinishGameDetailPage()
{
InitializeComponent();
SetLayout();
}
private void SetResult(bool beginonly = false)
{
currentsetp = beginonly ? 0 : steps.Count - 1;
for (var i = 0; i < 9; i++)
{
for (var j = 0; j < 9; j++)
{
var btn = buttons[i, j];
if (chess[i, j] > 0)
{
btn.Text = chess[i, j].ToString();
btn.TextColor = Color.Red;
}
else
{
btn.Text = beginonly ? "" : fchess[i, j].ToString();
btn.TextColor = Color.Blue;
}
}
}
}
private void SetLayout()
{
for (var i = 0; i < 9; i++)
{
for (var j = 0; j < 9; j++)
{
int m = i / 3;
int n = j / 3;
var btn = new Button();
var c = new Color(0.9, 0.9, 0.9);
var c1 = Color.Green;
if ((m + n) % 2 == 0)
{
c = new Color(1, 1, 1);
}
btn.BackgroundColor = c;
btn.Padding = 0;
btn.Margin = 0;
btn.FontSize = 20;
myGrid.Children.Add(btn, i, j);
buttons[i, j] = btn;
}
}
}
private void btn_Begin_Clicked(object sender, EventArgs e)
{
SetResult(true);
}
private void btn_End_Clicked(object sender, EventArgs e)
{
SetResult(false);
}
private void btn_Forward_Clicked(object sender, EventArgs e)
{
SetStep(false);
currentsetp++;
}
private void btn_Back_Clicked(object sender, EventArgs e)
{
SetStep(true);
currentsetp--;
}
private void SetStep(bool isback)
{
if (currentsetp < 0) currentsetp = 0;
if (currentsetp >= steps.Count) currentsetp = steps.Count - 1;
var laststep = steps[currentsetp];
var arr = laststep.Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
int x = int.Parse(arr[0]), y = int.Parse(arr[1]), num = int.Parse(arr[2]);
if (isback) buttons[x, y].Text = "";
else buttons[x, y].Text = fchess[x, y].ToString();
}
private void OpenHistory(FinishGame item)
{
try
{
if (item!=null)
{
for (var i = 0; i < 9; i++)
{
for (var j = 0; j < 9; j++)
{
chess[i, j] = int.Parse(item.Sudoku.Substring(i * 9 + j, 1));
fchess[i, j] = int.Parse(item.Result.Substring(i * 9 + j, 1));
}
}
SetResult();
if (!string.IsNullOrEmpty(item.Steps))
{
var steparr = item.Steps.Split(";".ToCharArray(), StringSplitOptions.RemoveEmptyEntries);
steps.Clear();
steps.AddRange(steparr);
}
currentDiffer =item.TotalTime;
var diff = currentDiffer / 10000 / 1000 / 60;
lbTime.Text = diff + "分鐘";
}
}
catch (Exception ex)
{
lbMessage.Text = ex.Message;
}
}
}
}
到這裡,數獨遊戲已經基本完成了,當然還有許多需要優化改進的地方,會陸續完成,可以關注https://github.com/zhenl/ZL.Shudu 。