项目代码可以从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: 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" FontSize="16" /> <Label Text="{Binding PlayDate}" LineBreakMode="NoWrap" 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: 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 。
本文来自博客园,作者:寻找无名的特质,转载请注明原文链接:https://www.cnblogs.com/zhenl/p/15851851.html