Bagikan melalui


Learn how the Reversi sample uses Windows Store app features

[This article is for Windows 8.x and Windows Phone 8.x developers writing Windows Runtime apps. If you’re developing for Windows 10, see the latest documentation]

The Reversi sample uses several common features of Windows Store apps using XAML and C#. This topic describes how the sample uses some of these features, and provides links to key feature topics.

This topic does not require you to understand the whole sample, but it does assume that you already know XAML and C#, and already understand the basics of each feature, or are willing to learn by reading the linked topics. For info on app development fundamentals, see Create your first Windows Store app using C# or Visual Basic.

For a general introduction to the sample, see Reversi, a Windows Store game in XAML, C#, and C++. To understand how the various features work together as a whole, see Understand the Reversi app structure. To learn how the original C# game engine was ported to C++, see Learn about the Reversi C++ game engine.

Download the Reversi sample app or browse the source code.

Tile and splash screen

The app tile and splash screen are the first things that a user of your app sees. You can use them to provide a compelling entry point and to display your branding. The basics are trivial, but you can also do some more complex things, as described in the documentation.

Key resources:

Reversi provides only basic tile and splash-screen support. This includes square and wide tiles and a splash screen, shown here at reduced size.

The image file names are set in the Package.appxmanifest file. You can provide images at multiple scales to support multiple screen sizes. No other implementation is required for this simple usage.

App bar

App bars provide a standard place to put app commands. By default, users can show or hide an app bar as needed, making it a good place for commands used less frequently. This helps keep your main UI focused on direct interactions with your content.

Key resources:

Reversi includes a few secondary commands that are well suited to the app bar: the ability to pause the clock and to undo or redo moves. During normal game play the app bar is hidden, but the user can swipe from the top or bottom of the screen to display or hide it.

This code, from GamePage.xaml, shows the app bar definition. Although the background and border are transparent, the Background property is set to {x:Null} to prevent the invisible app bar from blocking taps and clicks. This setting is necessary because the app bar extends across the whole screen and overlaps the bottom row of the game board.

<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 uses the CommandBar and AppBarButton controls to get the default behavior and style. The button behavior and its enabled state are provided by view-model commands bound to the button Command properties, as described in the Commands section.

The Play and Pause buttons work like a single toggle button. To achieve this effect, the buttons' Visibility properties are bound to the same view-model property. Both bindings use a BooleanToVisibilityConverter, but one of them also has a ConverterParameter property setting that reverses the effect of the binding. That way, each button is visible only when the other one isn't. For more info, see the Data binding section.

Toast notifications

Toast notifications alert your users when an important event occurs in your app, even if another app is currently active.

Key resources:

In Reversi, the computer may take a while to make its move. If you switch to another app while you wait, a toast notification will alert you when it's your turn.

Reversi uses the minimum code required for toast notifications, and sets the Toast capable field to Yes in the Package.appxmanifest designer. The toast code is easily reusable, so it's in a helper class in the Common folder.

In GameViewModel.cs:

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

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

Settings flyouts

The Settings charm provides standardized access to app settings.

Key resources:

Reversi has two Settings flyouts, one for display options, and one for new-game options.

This code from App.xaml.cs shows how Reversi handles the SettingsPane.CommandsRequested event to create SettingsCommand objects. When activated, each command creates and shows a SettingsFlyout control.

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

Sharing content

The share contract lets your app share data that users can send to other apps. For example, users can share data from your app into an email app to create a new message.

Key resources:

Windows provides built-in support for sharing an image of the app, and Reversi needs no additional functionality.

Data binding

Data binding lets you connect UI controls to the data they display so that changes in one will update the other. Data binding is common for data-entry forms, but you can also use it to drive your entire UI and to keep your UI separate from your app logic.

Key resources:

Reversi uses data bindings to connect its UI (or "view" layer) to its app logic (or "view model" layer). This layering helps separate the UI from other code, and is known as the Model-View-ViewModel (MVVM) pattern. For info about how Reversi uses this pattern, see Reversi app structure. For a short intro to MVVM, see Using the Model-View-ViewModel pattern.

Most of the bindings in Reversi are defined in XAML by means of the Binding markup extension, although code-behind is used in a few cases (for example, in the Board.xaml.cs file). Each page sets its DataContext property, which all the elements on the page use as the data source for their bindings.

UI updates

The data bindings drive the UI in Reversi. UI interactions cause changes to the data source properties, and the data bindings respond to these changes by updating the UI.

These updates work because the Reversi view-model classes inherit the BindableBase class. This class is in the Common/BindableBase.cs file, and provides a standard INotifyPropertyChanged implementation and a few support methods. The SetProperty method updates a property's backing value and also any bound UI by using a single method call. The OnPropertyChanged method updates the UI that is bound to specified properties. This is useful to control the timing of the updates, and for properties that get their values from other properties.

This code from GameViewModel.cs shows the basic usage of both SetProperty and OnPropertyChanged.

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

Value conversion

You can convert any property values to a form more suitable for binding by creating calculated properties, which are properties that get their values from other properties.

This code from GameViewModel.cs shows a simple calculated property. UI that is bound to this property is updated by the matching OnPropertyChanged call from the previous example.

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

Calculated properties are easy to create for any kind of conversion you might need, but they tend to clutter your code. For common conversions, it is better to put the conversion code into a reusable IValueConverter implementation. Reversi uses the NullStateToVisibilityConverter and BooleanToVisibilityConverter classes in the Common/Converters folder for bindings that show and hide various UI elements.

This binding from StartPage.xaml shows or hides a panel depending on whether a property has a value.

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

This binding, from NewGameSettings.xaml, shows or hides a panel depending on the state of a ToggleSwitch control.

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

For more examples, see App bar.

Commands

Button behaviors are often implemented with Click event handlers in code-behind files. Reversi does this for navigation buttons, but for other buttons, it separates the button UI from the non-UI code that the button invokes. To do this, the Button.Command properties are bound to view-model properties that return ICommand implementations.

Reversi command properties are of type DelegateCommand or DelegateCommand<T>. These classes are in the Common/DelegateCommand.cs file, and they provide standard, reusable ICommand implementations. You can use these classes to simplify the creation of single-use commands and to keep the necessary code confined to single property implementations.

This code from GameViewModel.cs shows the move command used by the board spaces, which are custom buttons. The ?? or "null-coalescing" operator means that the field value is returned only if it isn't null; otherwise, the field is set and the new value is returned. This means that a single command object is created the first time a property is accessed, and the same object is reused for all future accesses. The command object is initialized by calling the DelegateCommand<ISpace>.FromAsyncHandler method with references to the MoveAsync and CanMove methods. These methods provide the implementation for the ICommand.Execute and CanExecute methods.

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

The CanExecute method is called by the data binding to update the enabled state of the button. However, command bindings rely on change notification similar to that of other bindings (discussed in UI updates). This code from GameViewModel.cs shows how the UpdateView method synchronizes the view-model state with the model state and then calls OnCanExecuteChanged for each command before continuing with the next move.

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

Custom dependency properties

Reversi uses custom dependency properties in its custom controls so that it can use data binding updates to drive visual state changes. Visual states and animated transitions are defined in XAML by using the VisualStateManager class. However, there is no way to bind a visual state directly to a view-model property. Custom dependency properties provide targets for binding to view-model properties. The dependency properties include property-changed callbacks that make the necessary VisualStateManager.GoToState method calls.

This code shows how the PlayerStatus control uses code-behind to bind its custom dependency properties to view-model properties. Only one of the dependency properties is shown here, including its property-changed callback method. The callback and the OnApplyTemplate method override both call the update method. However, the OnApplyTemplate call initializes the control for its first appearance on screen, so it does not use animated transitions.

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

Asynchronous code

Asynchronous code helps your UI stay responsive while your app is busy with time-consuming operations.

Key resources:

Reversi uses asynchronous code to perform moves in a game. Each move takes at least a second to complete, including the move animation, and AI moves can take much longer. However, the UI stays responsive at all times, and user commands (such as undo) will cancel a move in progress.

This code from GameViewModel.cs shows how Reversi uses the async and await keywords, the Task class, and cancellation tokens. Note the use of AsTask to integrate with the Windows Runtime asynchronous code in the Game class. (For more info, see the next section.)

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

Using a Windows Runtime Component

Implementing some of your code as a Windows Runtime Component enables you to reuse that code in different apps, on different platforms, or with different languages. You can also more easily replace the component with an alternate implementation in another language.

Key resource:

Reversi implements its core game logic as a Windows Runtime Component in order to fully decouple it from the app. This enables it to support future extensibility and code reuse. Reversi also includes a C++ version of the game engine as a higher-performance alternative to the original C# version. For more info, see Learn about the Reversi C++ game engine.

This code from Game.cs shows how Reversi uses Task-based asynchronous code (including the async and await keywords) but exposes the results through Windows Runtime asynchronous interfaces. It also shows how the cancellation token from the GameViewModel code is consumed by the Game class.

The first and third methods in the example code call the AsyncInfo.Run method to return an IAsyncOperation<T>. This wraps the return value of the task and enables cancellation. The second example calls the WindowsRuntimeSystemExtensions.AsAsyncAction method to return an IAsyncAction. This is useful for tasks that don't have return values and don't need cancellation.

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

Reversi, a Windows Store game in XAML, C#, and C++

Use the Model-View-ViewModel (MVVM) pattern

Learn how the Reversi sample uses Windows Store app features

Understand the Reversi app structure

Learn about the Reversi C++ game engine

Create your first Windows Store app using C# or Visual Basic

Roadmap for Windows Runtime apps using C# or Visual Basic

Data binding