In previous parts of this series, we outlined what we want our Solitaire in Blazor game to do, and we built a set of C# classes to represent the different parts of the game.

Solitaire in Blazor Part 1 - Overview
The biggest time waster in history, now in Blazor WebAssembly!
Solitaire in Blazor Part 2 - The C# Classes
We need classes for a Card, the DrawPile, the DiscardPile, SuitPiles, and the Stacks.

In this part, we're going to start building our Blazor components, and implement drawing and discarding cards. After we implement the code in this section, we'll have some key functionality done, and a lot of background work ready to be used. Let's get started!

The Main Blazor Component

We first need to create a main Razor component that will represent a game of Solitaire. The component acts as the container for all other components. Let's recall our game layout to determine what kinds of components we will need:

The layout of a Solitaire game, showing the position of the draw pile, discards, suit piles, and stacks.

From this layout, we can see that we need:

  • A draw pile
  • Three shown discarded cards
  • A pile of discarded cards that are not shown to the user
  • Four suit piles
  • Seven stacks

There are also two other things we need. One of these is straightforward: an instance of GameStatus to keep track of whether or not the game is currently being played.

The other thing is less obvious, and it comes from a particular restriction: the user may only interact with (e.g. drag) one card at any time.

When a user drags a card, they are dragging only that card. From the discards to the stacks, from the discards to the suit piles, and from the stacks to the suit piles, only one card at a time can be moved. Even when the user is dragging a mini-stack to a different stack, the user is only dragging a single card; the game will determine if that card has child cards and whether or not they must be moved to.

Consequently, we need a property of Card that keeps track of the card the user is currently dragging. Here's our initial Blazor component "Solitaire.razor":

@page "/solitaire"

@using BlazorGames.Models.Solitaire;
@using BlazorGames.Models.Common.Enums;
@using BlazorGames.Pages.Partials;
@using BlazorGames.Pages.Partials.Solitaire;
@using System.Threading; 

@code {
    //The card the user is currently dragging
    public Card DraggedCard { get; set; }

    public GameStatus Status { get; set; } = GameStatus.NotStarted;

    //The three discarded cards being shown to the user.
    //FirstDiscard is the topmost of these cards.
    public Card FirstDiscard { get; set; }
    public Card SecondDiscard { get; set; }
    public Card ThirdDiscard { get; set; }

    CardDeck DrawDeck = new CardDeck();
    
    //All discarded cards not shown to the user.
    DiscardPile DiscardPile = new DiscardPile();

    SuitPile ClubsPile = new SuitPile(CardSuit.Clubs);
    SuitPile DiamondsPile = new SuitPile(CardSuit.Diamonds);
    SuitPile SpadesPile = new SuitPile(CardSuit.Spades);
    SuitPile HeartsPile = new SuitPile(CardSuit.Hearts);

    //Stack piles are numbered from left to right (1 is leftmost).
    StackPile StackPile1 = new StackPile();
    StackPile StackPile2 = new StackPile();
    StackPile StackPile3 = new StackPile();
    StackPile StackPile4 = new StackPile();
    StackPile StackPile5 = new StackPile();
    StackPile StackPile6 = new StackPile();
    StackPile StackPile7 = new StackPile();
}
DraggedCard will be useful in the next part of this series.

This main component will be referred to as the Solitaire component, and will be used throughout the rest of this series.

For now, let's move on and build a couple of smaller Razor Components that will help us.

HiddenCard Component

We are going to say that, in Solitaire, there are "hidden" cards (cards that have a suit and value but are not show to the user, e.g. "face-down" cards). These might include the cards in the draw pile, as well as cards in the stacks that haven't been revealed yet.

We therefore are going to create a Razor Component HiddenCard to represent these cards:

@code {
    [Parameter]
    public string CssClass { get; set; }

    [Parameter]
    public EventCallback ClickEvent { get; set; }
}

<div class="@CssClass" @onclick="ClickEvent">
    <img src="images/solitaire/cardBack.png" />
</div>

Notice the property ClickEvent. Different things happen when the player clicks on the draw pile and when the player clicks on a hidden card in the stack. The EventCallback type allows us to pass events to Razor components.

NonDraggableCard Component

We also need another type, called NonDraggableCard. In one case, cards are shown to the user that are not draggable: the bottom two cards in the discard pile.

@using BlazorGames.Models.Solitaire;

@code {
    [Parameter]
    public Card Card { get; set; }
}

@if (Card != null)
{
    @if (Card.IsVisible)
    {
        <img class="solitaire-discards"
             src="images/common/@(Card.ImageName).png"
             ondragstart="return false"
             ondrop="return false" />
    }
    else
    {
        <img class="solitaire-discards"
             src="images/common/cardBack.png"
             ondragstart="return false"
             ondrop="return false" />
    }
}
IsVisible property defined in Part 1

We need one more Razor Component before we can build the interface for drawing and discarding. Since the player can drag the topmost card in the discards, we need to implement a component for a DraggableCard.

DraggableCard Component

"Draggable" cards include nearly every face-up card in a Solitaire game. All face-up cards in the stacks, each top card in the suit piles, and the topmost card in the discards are draggable cards.

Draggable cards need to know more about the current state of the game than either hidden or non-draggable cards do. For example, a draggable card needs to know what other card is being dragged, since that card might be able to be stacked or placed in the suit piles.

In fact, by my reckoning, a DraggableCard component needs the following properties:

@code {
    //The card represented by this component
    [Parameter]
    public Card Card { get; set; }

    //The card currently being dragged
    [Parameter]
    public Card DraggedCard { get; set; }

    [Parameter]
    public EventCallback HandleDragStartEvent { get; set; }

    [Parameter]
    public EventCallback HandleDropEvent { get; set; }

    [Parameter]
    public string CssClass { get; set; }

    public string AdditionalCss { get; set; }
}

The two properties of type EventCallback do different things:

  • HandleDragStartEvent is needed to set the current card as the dragged card, should the user start to drag it.
  • HandleDropEvent is needed to handle other cards being dropped on this card.

Both of those events will be set by the calling component, which (in this case) will also be our main Solitaire component. We'll start building that later.

When the player drags a card over another card, if the card can be placed there, we want to show an additional CSS border, like so:

A GIF showing the green dashed border that appears when a dragged card hovers over a card it can be placed upon.
Since the nine of clubs can be stacked on the ten of diamonds, we want to show that to the user.

This is what the property AdditionalCSS is for, but we will also need events for the drag entering and leaving the current card. Here's those events in the DraggableCard component:

@code {
    //...Rest of implementation
    
    public void CardDragEnter()
    {
        @if (DraggedCard != null)
        {
            //Card being dropped must be opposite color from this card...
            bool isOppositeColor = (Card.IsBlack && DraggedCard.IsRed)
                                    || (Card.IsRed && DraggedCard.IsBlack);

            //...and also one less than this card.
            bool isOneLessThan 
                = (int)DraggedCard.Value == (((int)Card.Value) - 1);

            if (isOppositeColor && isOneLessThan)
            {
                AdditionalCss = " solitaire-can-drop";
            }
        }
    }

    public void CardDragLeave()
    {
        AdditionalCss = "";
    }
}

The last bit we need is the markup for the card element. We'll use an <img> element for this. Here's the markup for the entire DraggableCard component:

@code {
    
    //The card represented by this component
    [Parameter]
    public Card Card { get; set; }

    //The card currently being dragged
    [Parameter]
    public Card DraggedCard { get; set; }

    [Parameter]
    public EventCallback HandleDragStartEvent { get; set; }

    [Parameter]
    public EventCallback HandleDoubleClickEvent { get; set; }

    [Parameter]
    public EventCallback HandleDropEvent { get; set; }

    [Parameter]
    public string CssClass { get; set; }

    public string AdditionalCss { get; set; }

    public void CardDragEnter()
    {
        @if (DraggedCard != null)
        {
            //Card being dropped must be opposite color from this card...
            bool isOppositeColor = (Card.IsBlack && DraggedCard.IsRed)
                                    || (Card.IsRed && DraggedCard.IsBlack);

            //...and also one less than this card.
            bool isOneLessThan 
                = (int)DraggedCard.Value == (((int)Card.Value) - 1);

            if (isOppositeColor && isOneLessThan)
            {
                AdditionalCss = " solitaire-can-drop";
            }
        }
    }

    public void CardDragLeave()
    {
        AdditionalCss = "";
    }
}

<img class="@CssClass @AdditionalCss"
     src="images/common/@(Card.ImageName).png"
     @ondragstart="HandleDragStartEvent"
     @ondragend="StateHasChanged"
     @ondragenter="CardDragEnter"
     @ondragleave="CardDragLeave"
     @ondrop="(async () => { await HandleDropEvent.InvokeAsync(this); AdditionalCss = null; })"
     ondragover="event.preventDefault();"/>
The @ondrop code is necessary to make this sample work in Firefox.

OK then! We now have three smaller components HiddenCard, NonDraggableCard, and DraggableCard written and ready to go. Now let's start coding up drawing and discarding.

Drawing and Discarding Cards

In this version of Solitaire, players will draw one card at a time. That card is shown to the user.

As more cards are drawn, they are placed over the top of previously-drawn cards.

Only the topmost card can be moved to another place.

As more cards are drawn, the earlier-drawn cards begin to disappear from view.

The six of clubs disappears...
...then the five of clubs...
...then the three of clubs.

In our implementation, we have the properties FirstDiscard, SecondDiscard, and ThirdDiscard to represent the three shown cards, and DiscardPile to represent all discarded cards not currently shown. Only cards in the FirstDiscard position can be moved.

Methods and Markup

In our implementation, the user can perform one action on the draw pile Draw(). If there are no more cards to draw, the user can perform a different action ResetDrawPile().

@code {
	//...Rest of implementation
    
    public void Draw()
    {

    }

    public void ResetDrawPile()
    {

    }
}

Let's now build the markup for the draw pile. Here's part of this markup, which uses the HiddenCard component from earlier:

<div class="row">
    <div class="col-2">
        <div>
            @{
                int cardCount = DrawDeck.Count;
            }
            @while (cardCount > 0)
            {
                <HiddenCard CssClass="solitaire-drawdeck"
                            ClickEvent="Draw" />
                cardCount -= 13;
            }
            @if (DrawDeck.Count == 0)
            {
                <div class="solitaire-drawdeck" @onclick="ResetDrawPile">
                    <img src="images/solitaire/cardBackGrey.png" />
                </div>
            }
        </div>
    </div>
    <div class="col-2">
        <!-- Markup for discards -->
    </div>
    <div class="col-8">
        <!-- Markup for suit piles -->
    </div>
</div>
The code highlighting for this entire series is going to be wonky.

If there are still cards in the draw pile, we display a green card back (via the HiddenCard component); the more cards in the draw pile, the more card backs displayed. If the user clicks the green cards, a new card is drawn via the Draw() method.

If instead, there aren't any cards in the draw pile, we show a grey card back, which can be clicked to invoke the ResetDrawPile() method.

We now need to fill in the Draw() and ResetDrawPile() methods:

@code {
    //...Rest of implementation
    
    public void Draw()
    {
        //If the bottommost discard is there...
        if(ThirdDiscard != null)
            DiscardPile.Add(ThirdDiscard); //Add it to the discard pile

        //If the middle discard is there...
        if(SecondDiscard != null)
            ThirdDiscard = SecondDiscard; //Make it the bottommost

        //If the topmost discard is there...
        if(FirstDiscard != null)
            SecondDiscard = FirstDiscard; //Make it the middle discard

        //Set the topmost discard to the top card of the draw pile
        FirstDiscard = DrawDeck.Draw();

        //Let Blazor know to update the display
        StateHasChanged();
    }

    public void ResetDrawPile()
    {
        //First, add all three discards to the discard pile.
        DiscardPile.Add(ThirdDiscard);
        DiscardPile.Add(SecondDiscard);
        DiscardPile.Add(FirstDiscard);

        //Get all cards in the discard pile
        var allCards = DiscardPile.GetAll();

        //Put your thing down, flip it and reverse it
        allCards.Reverse();

        //Add them back to the draw pile
        foreach (var card in allCards)
        {
            DrawDeck.Add(card);
        }

        //Reset the discards and discard pile
        FirstDiscard = null;
        SecondDiscard = null;
        ThirdDiscard = null;
        DiscardPile = new DiscardPile();
    }
}

Markup for Discards

The last bit we need for discards is their markup, which uses the NonDraggableCard and DraggableCard components from earlier and looks like this:

<div class="row">
    <div class="col-2">
        <!-- Markup for draw pile -->
    </div>
    <div class="col-2">
        @if (ThirdDiscard != null)
        {
            <NonDraggableCard Card="ThirdDiscard"/>
        }
        @if (SecondDiscard != null)
        {
            <NonDraggableCard Card="SecondDiscard" />
        }
        @if (FirstDiscard != null)
        {
            <DraggableCard Card="FirstDiscard" 
                           CssClass="solitaire-discards"
                           HandleDragStartEvent="(() => HandleDragStart(FirstDiscard))"/>
        }
    </div>
    <div class="col-8">
        <!-- Markup for suit piles -->
    </div>
</div>

Note that we are passing in a callback to an event called HandleDragStart, and we will implement that in the next part of this series.

GIF Time!

At this point, we have a working draw and discard functionality. Here's a GIF showing it:

Suit Piles

The next part of our implementation is the suit piles, where cards must be stacked by suits in ascending order (Ace, two, three, four, so on).

SuitPile Blazor Component

Each suit pile has the following characteristics:

  • It must know what suit (clubs, spades, hearts, or diamonds) can be dropped on the pile.
  • It must be aware of the dragged card and display the green border if the dragged card can be dropped on the suit pile.
  • It must display the suit it wants before any card is dropped on it.

Due to these rules, the markup for the SuitDiscardPile component has these properties:

@code {
    [Parameter]
    public SuitPile SuitPile { get; set; }

    [Parameter]
    public Card DraggedCard { get; set; }

    [Parameter]
    public EventCallback DragStartEvent { get; set; }

    [Parameter]
    public EventCallback MoveActiveCardEvent { get; set; }

    private string CssClass { get; set; }

    private string ImagePath { get; set; }
}

The event MoveActiveCardCallback will be used in the next part of this series.

The suit piles need to handle situations where a dragged card enters, leaves, and is dropped. We can do that with the following methods:

@code {
    //Properties
    
    //If the dragged card can be dropped onto this card,
    //add a special CSS class.
    public void HandleDragEnter()
    {
        if (DraggedCard.Value == SuitPile.AllowedValue 
            && DraggedCard.Suit == SuitPile.Suit)
        {
            CssClass = "solitaire-can-drop";
        }
    }
    
    //Once the dragged card leaves, always reset the CSS class.
    public void HandleDragLeave()
    {
        CssClass = "";
    }

    public async Task HandleDrop()
    {
        CssClass = ""; //Reset the CSS

        //If the dragged card can be dropped here
        if (DraggedCard.Value == SuitPile.AllowedValue 
            && DraggedCard.Suit == SuitPile.Suit)
        {
            //Invoke the callback to move the dragged card.
            //This callback will be different for stacks and suit piles.
            await MoveActiveCardEvent.InvokeAsync(SuitPile);
        }
    }
}

Finally, we need the <img> element markup, with just a little more C# to determine the correct image path. Here's the complete markup for the SuitDiscardPile Blazor component:

@code {
    [Parameter]
    public SuitPile SuitPile { get; set; }

    [Parameter]
    public Card DraggedCard { get; set; }

    [Parameter]
    public EventCallback DragStartEvent { get; set; }

    [Parameter]
    public EventCallback MoveActiveCardCallback { get; set; }

    private string CssClass { get; set; }

    private string ImagePath { get; set; }

    //If the dragged card can be dropped onto this card,
    //add a special CSS class.
    public void HandleDragEnter()
    {
        if (DraggedCard.Value == SuitPile.AllowedValue
            && DraggedCard.Suit == SuitPile.Suit)
        {
            CssClass = "solitaire-can-drop";
        }
    }

    //Once the dragged card leaves, always reset the CSS class.
    public void HandleDragLeave()
    {
        CssClass = "";
    }

    public async Task HandleDrop()
    {
        CssClass = ""; //Reset the CSS

        //If the dragged card can be dropped here
        if (DraggedCard.Value == SuitPile.AllowedValue
            && DraggedCard.Suit == SuitPile.Suit)
        {
            //Invoke the callback to move the dragged card.
            //This callback will be different for stacks and suit piles.
            await MoveActiveCardCallback.InvokeAsync(SuitPile);
        }
    }
}
@if (SuitPile.Any())
    ImagePath = $"images/common/{SuitPile.Last().ImageName}.png";
else
    ImagePath = $"images/solitaire/cardDashed{SuitPile.Suit.GetDisplayName()}.png";
<div @ondragstart="DragStartEvent"
     @ondragend="StateHasChanged"
     @ondragenter="HandleDragEnter"
     @ondragleave="HandleDragLeave"
     @ondrop="HandleDrop"
     ondragover="event.preventDefault();">
    <img class="solitaire-card @CssClass"
         src="@ImagePath" />
</div>

On the main Solitaire component, the markup for these suit piles looks like this:

<div class="row">
    <div class="col-2">
        <!-- Markup for Draw Pile -->
    </div>
    <div class="col-2">
        <!-- Markup for Discards -->
    </div>
    <div class="col-8">
        <div class="solitaire-suitpile-container">
            <div class="row">
                <div class="col-2">
                    <SuitDiscardPile SuitPile="ClubsPile"
                                     DraggedCard="DraggedCard"
                                     MoveActiveCardEvent="(() => 
                                     MoveActiveCard(ClubsPile))"
                                     DragStartEvent="(() => 
                                     HandleDragStart(ClubsPile.Last()))"/>
                </div>
                <div class="col-2">
                    <SuitDiscardPile SuitPile="DiamondsPile"
                                     DraggedCard="DraggedCard"
                                     MoveActiveCardEvent="(() => 
                                     MoveActiveCard(DiamondsPile))"
                                     DragStartEvent="(() => 
                                     HandleDragStart(DiamondsPile.Last()))"/>
                </div>
                <div class="col-2">
                    <SuitDiscardPile SuitPile="SpadesPile"
                                     DraggedCard="DraggedCard"
                                     MoveActiveCardEvent="(() => 
                                     MoveActiveCard(SpadesPile))"
                                     DragStartEvent="(() =>
                                     HandleDragStart(SpadesPile.Last()))"/>
                </div>
                <div class="col-2">
                    <SuitDiscardPile SuitPile="HeartsPile"
                                     DraggedCard="DraggedCard"
                                     MoveActiveCardEvent="(() => 
                                     MoveActiveCard(HeartsPile))"
                                     DragStartEvent="(() =>
                                     HandleDragStart(HeartsPile.Last()))"/>
                </div>
            </div>
        </div>
    </div>
</div>

We will be implementing the MoveActiveCard() and HandleDragStart() methods in the next part of this series.

The next component we need are the stacks, but before we write that markup, we should write the code to create a new game of Solitaire.

Creating a New Game

You might recall from Part 3 that the main Solitaire component has properties for each stack, each suit pile, the discards, and the draw pile:

@code {
    public Card DraggedCard { get; set; }

    public GameStatus Status { get; set; } = GameStatus.NotStarted;

    public Card FirstDiscard { get; set; }
    public Card SecondDiscard { get; set; }
    public Card ThirdDiscard { get; set; }

    CardDeck DrawDeck = new CardDeck();
    
    DiscardPile DiscardPile = new DiscardPile();

    SuitPile ClubsPile = new SuitPile(CardSuit.Clubs);
    SuitPile DiamondsPile = new SuitPile(CardSuit.Diamonds);
    SuitPile SpadesPile = new SuitPile(CardSuit.Spades);
    SuitPile HeartsPile = new SuitPile(CardSuit.Hearts);

    //Stack piles are numbered from left to right (1 is leftmost).
    StackPile StackPile1 = new StackPile();
    StackPile StackPile2 = new StackPile();
    StackPile StackPile3 = new StackPile();
    StackPile StackPile4 = new StackPile();
    StackPile StackPile5 = new StackPile();
    StackPile StackPile6 = new StackPile();
    StackPile StackPile7 = new StackPile();
}

Let's create a button that starts a new game:

<div class="row">
    <div class="col-2">
        <button class="btn-primary" @onclick="NewGame">New Game</button>
    </div>
</div>

That NewGame() event will need to do the following:

  1. Clear out the remaining cards in all stacks, suit piles, discards, and the draw pile.
  2. Create a new 52-card deck and shuffle it.
  3. Add the correct amount of cards to each stack (one in the first, two in the second, three in the third, up to seven in the seventh).
  4. Put the remaining cards in the draw pile.
  5. Update the GameStatus

All of that results in a rather long method:

@code {
    //...Rest of implementation

    public void NewGame()
    {
        //Set the game status to Playing
        Status = GameStatus.Playing;

        //Create a new draw deck and discard pile
        DrawDeck = new CardDeck();
        DiscardPile = new DiscardPile();
       
        //Reset the discards
        FirstDiscard = null;
        SecondDiscard = null;
        ThirdDiscard = null;
       
        //Create new suit piles
        ClubsPile = new SuitPile(CardSuit.Clubs);
        DiamondsPile = new SuitPile(CardSuit.Diamonds);
        SpadesPile = new SuitPile(CardSuit.Spades);
        HeartsPile = new SuitPile(CardSuit.Hearts);

        //Create new stacks
        StackPile1 = new StackPile();
        StackPile2 = new StackPile();
        StackPile3 = new StackPile();
        StackPile4 = new StackPile();
        StackPile5 = new StackPile();
        StackPile6 = new StackPile();
        StackPile7 = new StackPile();

        //Deal cards to the stacks
        StackPile1.Add(DrawDeck.Draw());
        StackPile2.Add(DrawDeck.DrawHidden());
        StackPile3.Add(DrawDeck.DrawHidden());
        StackPile4.Add(DrawDeck.DrawHidden());
        StackPile5.Add(DrawDeck.DrawHidden());
        StackPile6.Add(DrawDeck.DrawHidden());
        StackPile7.Add(DrawDeck.DrawHidden());

        StackPile2.Add(DrawDeck.Draw());
        StackPile3.Add(DrawDeck.DrawHidden());
        StackPile4.Add(DrawDeck.DrawHidden());
        StackPile5.Add(DrawDeck.DrawHidden());
        StackPile6.Add(DrawDeck.DrawHidden());
        StackPile7.Add(DrawDeck.DrawHidden());

        StackPile3.Add(DrawDeck.Draw());
        StackPile4.Add(DrawDeck.DrawHidden());
        StackPile5.Add(DrawDeck.DrawHidden());
        StackPile6.Add(DrawDeck.DrawHidden());
        StackPile7.Add(DrawDeck.DrawHidden());

        StackPile4.Add(DrawDeck.Draw());
        StackPile5.Add(DrawDeck.DrawHidden());
        StackPile6.Add(DrawDeck.DrawHidden());
        StackPile7.Add(DrawDeck.DrawHidden());

        StackPile5.Add(DrawDeck.Draw());
        StackPile6.Add(DrawDeck.DrawHidden());
        StackPile7.Add(DrawDeck.DrawHidden());

        StackPile6.Add(DrawDeck.Draw());
        StackPile7.Add(DrawDeck.DrawHidden());

        StackPile7.Add(DrawDeck.Draw());

        StateHasChanged();
    }
}

Now we can work on the markup for the stacks. First, we need another small component, one that represents an empty stack.

Empty Stacks

In Solitaire, it is possible for there to be no cards in a stack:

The first stack is empty.

Only a King can be placed on an empty stack. Therefore, an empty stack needs to handle the Drop event (which must be passed to this component by another), needs to know about the currently-dragged card, and needs to know what stack is currently empty. That results in the markup for the EmptyStack component:

@code {
    //The Stack that this empty stack represents
    [Parameter]
    public StackPile Pile { get; set; }

    //The currently dragged card
    [Parameter]
    public Card DraggedCard { get; set; }

    [Parameter]
    public EventCallback DropEvent { get; set; }

    private string CssClass { get; set; }

    public void EmptyStackDragEnter()
    {
        if (DraggedCard.Value == CardValue.King)
        {
            CssClass = "solitaire-can-drop";
        }
    }

    public void EmptyStackDragLeave()
    {
        CssClass = "";
    }
}

<img class="@CssClass"
     src="images/solitaire/cardBackGrey.png"
     @ondragenter="EmptyStackDragEnter"
     @ondragleave="EmptyStackDragLeave"
     @ondrop="DropEvent"
     ondragover="event.preventDefault();"
     ondragstart="event.dataTransfer.setData('', event.target.id);" />

The Stacks

Unlike the other parts of Solitaire (the draw pile, the suit piles, etc.) I was unable to make the stacks a separate component; they require a lot of information to work. Instead, I broke each Stack down into components.

  • If there are no cards in the stack, use the EmptyStack component.
  • If there are cards in the stack, for each card that is "face up", use the DraggableCard component.
  • For each card that is "face down", use the HiddenCard component.

Which results in this markup for a single stack:

@if (StackPile1.Any())
{
    @foreach (var card in StackPile1.GetAllCards())
    {
        if (card.IsVisible)
        {
            <DraggableCard Card="card"
                            DraggedCard="DraggedCard"
                            CssClass="solitaire-stackpile"
                            HandleDragStartEvent="(() => HandleDragStart(card))"
                            HandleDoubleClickEvent="(() => CardDoubleClick(card))"
                            HandleDropEvent="(() => DropCardOntoStack(StackPile1))"/>
        }
        else
        {
            <HiddenCard CssClass="solitaire-stackpile"
                        ClickEvent="(() => RevealCard(card, StackPile1))" />
        }
    }
}
else
{
    <EmptyStack Pile="StackPile1"
                DraggedCard="DraggedCard"
                DropEvent="(() => EmptyStackDrop(StackPile1))"/>
}
This is only one of seven; you can see the rest on GitHub.

There are many events being passed into the Blazor components here, and we'll need to implement each of them in the next part of this series, when we begin work on the actual drag-and-drop implementation.

But for now, we should celebrate that we have each part of a Solitaire game in Blazor ready to be wired up to events!

The Sample Project

Don't forget to take a look at the sample project on GitHub if you haven't already!

Summary

In this part of the series, we implemented:

  • A set of small components, including HiddenCard, NonDraggableCard, and DraggableCard.
  • A draw pile, which appears to shrink as more cards are drawn.
  • A set of discards, which stack appropriately.
  • The suit piles, which stack cards in ascending order by suit.
  • The first part of the stacks implementation.

In the next part of this series, we will begin wiring up drag-and-drop events for each of these components.

See something I could've done better? Got a way to improve my code? I wanna know about it! Share in the comments below.

Happy Coding!