了解 Reversi 示例如何使用 Windows 应用商店应用功能

Applies to Windows only

Reversi 示例使用采用 XAML 和 C# 的 Windows 应用商店应用的一些常用功能。本主题介绍示例如何使用其中的一些功能,并提供指向重要功能主题的链接。

本主题并不需要你了解整个示例,但它假定你已经了解 XAML 和 C#,并且已经了解每个功能的基础知识,或者愿意通过阅读链接的主题来学习。有关应用开发基础知识的信息,请参阅使用 C# 或 Visual Basic 创建你的第一个 Windows 应用商店应用

有关此示例的大致简介,请参阅 Reversi,一种使用 XAML、C# 和 C++ 的 Windows 应用商店游戏。若要了解各种功能如何作为一个整体协同工作,请参阅了解 Reversi 应用结构。若要了解原始 C# 游戏引擎如何移植到 C++,请参阅了解 Reversi C++ 游戏引擎

下载 Reversi 示例应用浏览源代码

磁贴和初始屏幕

应用磁贴和初始屏幕是应用的用户首先看到的内容。可以使用它们提供吸引人的入口点以及显示你的品牌。基础知识非常繁琐,但你还可以完成一些更复杂的操作,如本文档中所述。

重要资源:

Reversi 只提供基本的磁贴和初始屏幕支持。这包括方形和加宽磁贴以及一个初始屏幕,此处以缩小的大小显示。

Reversi 磁贴和初始屏幕

在 Package.appxmanifest 文件中设置图像文件名。你可以提供多种大小的图像以支持多种屏幕大小。这个简单的用法不需要其他任何实现。

应用栏

应用栏提供一个放置应用命令的标准位置。默认情况下,用户可以根据需要显示或隐藏应用栏,从而使其成为不常用的命令的最佳位置。这有助于使主要 UI 侧重于与内容的直接交互。

重要资源:

Reversi 包含几个辅助命令,这些命令也适合应用栏:能够暂停时钟以及撤消或恢复移动。在正常的游戏播放期间,应用栏是隐藏的,但用户可以从屏幕页首或底部轻扫以显示或隐藏它。

Reversi 应用栏

该代码来自 GamePage.xaml,它显示了应用栏定义。尽管背景和边界都是透明的,但 Background 属性设置为 {x:Null} 以防止不可见的应用栏阻止点击和单击。该设置是必需的,因为应用栏延伸到整个屏幕并且与游戏桌面的底部行重叠。


<Page.BottomAppBar>
  <CommandBar x:Name="GamePageAppBar" Background="{x:Null}" 
    BorderBrush="Transparent" IsSticky="True" Margin="9,0">
    <CommandBar.SecondaryCommands>
      <AppBarButton Icon="Pause" Label="Pause"
        Command="{Binding Clock.PauseCommand}" Click="DismissAppBar"
        Visibility="{Binding Clock.IsPauseButtonVisible, 
          Converter={StaticResource BooleanToVisibilityConverter}}"/>
      <AppBarButton Icon="Play" Label="Play"
        Command="{Binding Clock.PlayCommand}" Click="DismissAppBar"          
        Visibility="{Binding Clock.IsPauseButtonVisible,
          Converter={StaticResource BooleanToVisibilityConverter}, 
          ConverterParameter=Reverse}"/>
      <AppBarButton Icon="Undo" Label="Undo" Command="{Binding UndoCommand}"/>
      <AppBarButton Icon="Redo" Label="Redo" Command="{Binding RedoCommand}"/>
    </CommandBar.SecondaryCommands>
  </CommandBar>
</Page.BottomAppBar>


Reversi 使用 CommandBarAppBarButton 控件来获取默认行为和样式。该按钮行为及其启用状态是由绑定到按钮 Command 属性的视图模型命令提供的,如命令部分所述。

“播放”和“暂停”按钮以单个切换按钮的形式工作。为达到该效果,两种按钮的 Visibility 属性将绑定到相同的视图模型属性。两个绑定都使用 BooleanToVisibilityConverter,但其中一个绑定还具有反转绑定效果的 ConverterParameter 属性设置。这样,每个按钮仅在其他按钮不可见时可见。有关详细信息,请参阅数据绑定

消息通知

当应用中发生重要事件时 Toast 通知会警告用户,即使另一个应用当前处于活动状态也是如此。

重要资源:

在 Reversi 中,计算机可能需要一段时间才能进行移动。如果等待时切换到另一个应用,则当轮到你时 Toast 通知会通知你。

Reversi Toast 通知

Reversi 为 Toast 通知使用所需的最少代码,并且在 Package.appxmanifest 设计器中将“支持 Toast 通知”字段设置为“是”。Toast 代码可轻松重复使用,因此它位于 Common 文件夹的某个帮助程序类中。

在 GameViewModel.cs 中:


var window = Windows.UI.Xaml.Window.Current;
if (window != null && !window.Visible && !IsCurrentPlayerAi)
{
    Toast.Show("It's your turn!");
}


在 Toast.cs 中:


public static void Show(string text)
{
    const string template = 
        "<toast duration='short'><visual><binding template='ToastText01'>" +
        "<text id='1'>{0}</text></binding></visual></toast>";
    var toastXml = new XmlDocument();
    toastXml.LoadXml(String.Format(template, text));
    var toast = new ToastNotification(toastXml);
    ToastNotificationManager.CreateToastNotifier().Show(toast);
}


设置弹出窗口

“设置”超级按钮提供对应用设置的标准化访问。

重要资源:

Reversi 具有两个“设置”浮出控件,一个用于显示选项,一个用于新游戏选项。

Reversi 设置弹出窗口

该代码来自 App.xaml.cs,显示 Reversi 如何处理 SettingsPane.CommandsRequested 事件来创建 SettingsCommand 对象。在激活时,每个命令会创建和显示 SettingsFlyout 控件。


SettingsPane.GetForCurrentView().CommandsRequested += OnCommandsRequested;



private void OnCommandsRequested(SettingsPane sender,
    SettingsPaneCommandsRequestedEventArgs args)
{
    args.Request.ApplicationCommands.Add(new SettingsCommand("Display", "Display options", 
        _ => (new DisplaySettings() { DataContext = SettingsViewModel }).Show()));
    args.Request.ApplicationCommands.Add(new SettingsCommand("NewGame", "New game options", 
        _ => (new NewGameSettings() { DataContext = SettingsViewModel }).Show()));
}


共享内容

“共享”合约允许你的应用共享用户可发送到其他应用的数据。例如,用户可从你的应用中将数据共享到电子邮件应用以创建新邮件。

重要资源:

Windows 提供对于共享应用图像的内置支持,Reversi 不需要其他功能。

数据绑定

数据绑定允许你将 UI 控件连接到它们显示的数据,以便一个控件中的更改将更新其他控件。数据绑定对于数据输入表单来说很常见,但你可以使用它来驱动你的整个 UI 并且使你的 UI 与应用逻辑分离。

重要资源:

Reversi 使用数据绑定将其 UI(或“视图”层)连接到其应用逻辑(或“视图模型”层)。该分层方法可帮助 UI 与其他代码分离,该方法称为 Model-View-ViewModel (MVVM) 模式。 有关 Reversi 如何使用此模式的信息,请参阅 Reversi 应用结构。有关 MVVM 的简短介绍,请参阅使用 Model-View-ViewModel 模式

Reversi 中的大多数绑定都是利用绑定标记扩展在 XAML 中定义的,在少数情况下使用代码隐藏文件(例如,在 Board.xaml.cs 文件中。)每个页面都设置它的 DataContext 属性,其中该页面上的所有元素都用作其绑定的数据源。

UI 更新

在 Reversi 中数据绑定驱动 UI。UI 交互导致对数据源属性进行更改,并且数据绑定通过更新 UI 来响应这些更改。

因为 Reversi 视图模型类继承了 BindableBase 类,所以这些更新有效。此类位于 Common/BindableBase.cs 文件中,并提供标准 INotifyPropertyChanged 实现和几个支持方法。SetProperty 方法使用一个方法调用来更新属性的支持值以及任何绑定的 UI。OnPropertyChanged 方法更新绑定到指定属性的 UI。这对于控制更新的计时,以及对于从其他属性获取值的属性都非常有用。

该代码来自 GameViewModel.cs,显示 SetPropertyOnPropertyChanged 的基本用法。


public State CurrentPlayer
{
    get { return _currentPlayer; }
    set
    {
        SetProperty(ref _currentPlayer, value);
        OnPropertyChanged("IsCurrentPlayerAi");
        OnPropertyChanged("IsPlayerOneAi");
        OnPropertyChanged("IsPlayerTwoAi");
        OnPropertyChanged("CurrentPlayerAiSearchDepth");
    }
}


值转换

你可以通过创建计算的属性(从其他属性获取值的属性)将任何属性值转换为更适合绑定的格式。

该代码来自GameViewModel.cs,显示一个简单的计算属性。通过匹配上一个示例中的 OnPropertyChanged 调用来更新绑定到该属性的 UI。


public bool IsPlayerOneAi { get { return (int)PlayerOne > 0; } }


为你可能需要的任何种类的转换创建计算的属性非常容易,但它们通常会弄乱你的代码。对于常用的转换,最好将转换代码放到一个可重复使用的 IValueConverter 实现中。Reversi 对显示和隐藏各种 UI 元素的绑定使用 Common/Converters 文件夹中的 NullStateToVisibilityConverterBooleanToVisibilityConverter 类。

该绑定来自StartPage.xaml 根据属性是否具有值显示或隐藏面板。


<StackPanel Visibility="{Binding GameViewModel, 
  Converter={StaticResource NullStateToVisibilityConverter}}">


该绑定来自NewGameSettings.xaml,根据 ToggleSwitch 控件的状态显示或隐藏面板。


<StackPanel Orientation="Horizontal" 
  Visibility="{Binding IsOn, ElementName=PlayerOneSwitch, 
    Converter={StaticResource BooleanToVisibilityConverter}}">


有关更多示例,请参阅应用栏

命令

Button 行为通常是使用代码隐藏文件中的 Click 事件处理程序实现的。Reversi 对导航按钮执行该操作,但对于其他按钮,它将按钮 UI 与该按钮调用的非 UI 代码分离。 为此,将 Button.Command 属性绑定到返回 ICommand 实现的视图模型属性。

Reversi 命令属性的类型为 DelegateCommandDelegateCommand<T>。这些类位于 Common/DelegateCommand.cs 文件中,它们提供标准的可重复使用的 ICommand 实现。你可以使用这些类简化一次性命令的创建并且使所需的代码仅限于单个属性实现。

该代码来自GameViewModel.cs,显示板空间(即自定义按钮)使用的移动命令。 ?? 或“null-coalescing”操作符意味着仅当它不为 null 时才返回该字段值;否则,设置该字段并返回新值。这意味着第一次访问属性时创建一个命令对象,并且该对象可重复用于后来的所有访问。通过调用 DelegateCommand<ISpace>.FromAsyncHandler 方法并引用 MoveAsyncCanMove 方法初始化命令对象。这些方法提供 ICommand.ExecuteCanExecute 方法的实现。


public DelegateCommand<ISpace> MoveCommand 
{ 
    get 
    { 
        return _moveCommand ?? (_moveCommand = 
            DelegateCommand<ISpace>.FromAsyncHandler(MoveAsync, CanMove));
    } 
}


数据绑定调用 CanExecute 方法来更新该按钮的启用状态。但是,命令绑定依赖更改通知(与其他绑定的通知类似),这将在 UI 更新中讨论。该代码来自GameViewModel.cs,显示 UpdateView 方法如何将视图模型状态与模型状态同步,然后在继续下一个移动之前为每个命令调用 OnCanExecuteChanged


private void UpdateView()
{
    SyncModelProperties();
    UpdateBoard();
    UndoCommand.RaiseCanExecuteChanged();
    RedoCommand.RaiseCanExecuteChanged();
    MoveCommand.RaiseCanExecuteChanged();
}


自定义依赖属性

Reversi 在其自定义控件中使用自定义依赖属性,以便它可以使用数据绑定更新来驱动视觉状态更改。视觉状态和动画过渡是使用 VisualStateManager 类在 XAML 中定义的。但是,决不可以将视觉状态直接绑定到视图模型属性。自定义依赖属性为绑定到视图模型属性提供目标。依赖属性包括进行所需的 VisualStateManager.GoToState 方法调用的属性更改回调。

该代码显示 PlayerStatus 控件如何使用代码隐藏文件将其自定义依赖属性绑定到视图模型属性。此处只显示一个依赖属性,包括它的属性更改回调方法。回调和 OnApplyTemplate 方法重写替代均调用更新方法。但是,OnApplyTemplate 调用会为第一次出现在屏幕上初始化该控件,因此它不使用动画过渡。


public PlayerStatus()
{
    DefaultStyleKey = typeof(PlayerStatus);
    SetBinding(CurrentPlayerProperty, new Binding { 
        Path = new PropertyPath("CurrentPlayer") });
    SetBinding(IsClockShowingProperty, new Binding { 
        Path = new PropertyPath("Settings.IsClockShowing") });
    SetBinding(IsGameOverProperty, new Binding { 
        Path = new PropertyPath("IsGameOver") });
    SetBinding(WinnerProperty, new Binding { 
        Path = new PropertyPath("Winner") });
}

protected override void OnApplyTemplate()
{
    base.OnApplyTemplate();
    UpdatePlayerState(false);
    UpdateClockState(false);
    UpdateGameOverState(false);
}

public bool IsClockShowing
{
    get { return (bool)GetValue(IsClockShowingProperty); }
    set { SetValue(IsClockShowingProperty, value); }
}

public static readonly DependencyProperty IsClockShowingProperty =
    DependencyProperty.Register("IsClockShowing", typeof(bool),
    typeof(PlayerStatus), new PropertyMetadata(true, IsClockShowingChanged));

private static void IsClockShowingChanged(DependencyObject d, 
    DependencyPropertyChangedEventArgs e)
{
    (d as PlayerStatus).UpdateClockState(true);
}

private void UpdateClockState(bool useTransitions)
{
    GoToState(IsClockShowing ? "ClockShowing" : "ClockHidden", useTransitions);
}

private void GoToState(string state, bool useTransitions)
{
    VisualStateManager.GoToState(this, state, useTransitions);
}


异步代码

异步代码帮助使 UI 在应用忙于执行耗时操作时保持响应。

重要资源:

Reversi 在游戏中使用异步代码执行移动。每次移动(包括移动动画)都至少需要一秒钟来完成,AI 移动需要的时间会更长。但是,UI 始终保持响应,用户命令(如撤消)将取消正在进行的移动。

以下代码来自 GameViewModel.cs,它显示 Reversi 如何使用 async 关键字、await 关键字、Task 类和取消令牌。请注意,使用 AsTaskGame 类中的 Windows 运行时异步代码集成(有关详细信息,请参阅下一部分)。


private async Task MoveAsync(ISpace move)
{
    var cancellationToken = GetNewCancellationToken();
    LastMoveAffectedSpaces = await Game.MoveAsync(move).AsTask(cancellationToken);
    if (cancellationToken.IsCancellationRequested) return;
    await OnMoveCompletedAsync(cancellationToken);
}



private async Task AiMoveAsync()
{
    var cancellationToken = GetNewCancellationToken();

    // Unlike the MoveAsync method, the AiMoveAsync method requires a try/catch 
    // block for cancellation. This is because the AI search checks for 
    // cancellation deep within a recursive, iterative search process
    // that is easiest to halt by throwing an exception. 
    try
    {
        // The WhenAll method call enables the delay and the AI search to 
        // occur concurrently. However, in order to retrieve the return 
        // value of the first task, both tasks must have the same signature,
        // thus requiring the delay task to have a (meaningless) return value.  
        var results = await Task.WhenAll(
            Game.GetBestMoveAsync(CurrentPlayerAiSearchDepth)
                .AsTask(cancellationToken),
            Task.Run(async () =>
            {
                await DelayAsync(MinimumTurnLength, cancellationToken);
                return (ISpace)null;
            })
        );

        // Perform the AI move only after both the 
        // search and the minimum delay have passed.
        LastMoveAffectedSpaces = await Game.MoveAsync(
            results[0]).AsTask(cancellationToken);
        if (cancellationToken.IsCancellationRequested) return;

        await OnMoveCompletedAsync(cancellationToken);
    }
    catch (OperationCanceledException)
    {
        System.Diagnostics.Debug.WriteLine("cancelled with exception");
    }
}


使用 Windows 运行时组件

通过以 Windows 运行时组件的形式实现你的一些代码,可在其他应用中、在其他平台上或使用其他语言重用该代码。你还可以更加轻松地将该组件替换为其他语言的其他实现。

重要资源:

Reversi 以 Windows 运行时组件的形式实现它的核心游戏逻辑,以便将其从应用中完全分离。这使它能够支持重用代码以及在将来进行扩展。Reversi 还包含一个 C++ 版本的游戏引擎,作为原始 C# 版本的替代选项,此替代选项的性能更高。有关详细信息,请参阅了解 Reversi C++ 游戏引擎

以下代码来自 Game.cs,它显示 Reversi 如何使用基于 Task 的异步代码(包括 asyncawait 关键字),但是通过 Windows 运行时异步接口公开结果。还显示 GameViewModel 代码中的取消令牌由 Game 类使用。

示例代码中的第一个方法和第三个方法调用 AsyncInfo.Run 方法来返回 IAsyncOperation<T>。这会包装此任务的返回值并启用取消。第二个示例调用 WindowsRuntimeSystemExtensions.AsAsyncAction 方法以返回 IAsyncAction。这对于不返回值且不需要取消的任务非常有用。


public IAsyncOperation<IList<ISpace>> MoveAsync(ISpace move)
{
    // Use a lock to prevent the ResetAsync method from modifying the game 
    // state at the same time that a different thread is in this method.
    lock (_lockObject)
    {
        return AsyncInfo.Run(cancellationToken => Task.Run(() =>
        {
            if (cancellationToken.IsCancellationRequested) return null;
            var changedSpaces = Move(move);
            SyncMoveStack(move);
            return changedSpaces;
        }, cancellationToken));
    }
}



public IAsyncAction AiMoveAsync(int searchDepth)
{
    return Task.Run(async () => 
    {
        // If it is the AI's turn and we're not at the end of the move stack,
        // just use the next move in the stack. This is necessary to preserve
        // the forward stack, but it also prevents the AI from having to search again. 
        var bestMove = Moves.Count < MoveStack.Count ? 
            MoveStack[Moves.Count] : await GetBestMoveAsync(searchDepth);
        await MoveAsync(bestMove);
    }).AsAsyncAction();
}

public IAsyncOperation<ISpace> GetBestMoveAsync(int searchDepth)
{
    if (searchDepth < 1) throw new ArgumentException(
        "must be 1 or greater.", "searchDepth");

    return AsyncInfo.Run(cancellationToken => Task.Run(() => 
    {
        return (ISpace)reversiAI.GetBestMove(Board, 
            CurrentPlayer == State.One, searchDepth, cancellationToken);
    }, cancellationToken));
}


相关主题

Reversi 示例应用
Reversi,一种使用 XAML、C# 和 C++ 的 Windows 应用商店游戏
使用 Model-View-ViewModel (MVVM) 模式
了解 Reversi 示例如何使用 Windows 应用商店应用功能
了解 Reversi 应用结构
了解有关 Reversi C++ 游戏引擎的信息
使用 C# 或 Visual Basic 创建你的第一个 Windows 应用商店应用
使用 C# 或 Visual Basic 的 Windows 运行时应用的路线图
数据绑定

 

 

显示:
© 2014 Microsoft