'How to know which element was clicked in Blazor WASM

I'm trying to make a memory-match game with 16 "cards" (just a list with 8 pairs of ints).

So what I'm trying to do now is to find out which card I have clicked.

My first idea to solve this was by adding an id attribute to each card, but I didn't know how to pass the id value into the method called in the onclick-event.

So instead, I tried to give each card an index, and then passing that index number to the method in the onclick-event.

This is however not working, each time I click a card, it returns a number which is increaed by 16 each time: screenshot 1

And when debuging my code when I click a card, it seems to run the foreach-loop again. Why? The foreachloop should only be run once when the page is created, not everytime I click a card. screenshot 2

And also, why can't the debugger display the value of localIndex in screenshot 2?

Exception message: localIndex Exception of type 'Microsoft.VisualStudio.Shared.VSCodeDebugProtocol.ProtocolException' was thrown. (Unable to evaluate)

@page "/"

<PageTitle>Index</PageTitle>

<h1>Memory Game!</h1>

<div class="card-grid">

    @foreach (var card in cards)
    {
        int localIndex = cardIndex;
        <div class="mem-card" @onclick="() => ClickCard(localIndex)">@card</div>
        cardIndex++;
    }

</div>

@code{

    List<int> cards = new List<int>();

    int cardIndex;

    protected override void OnInitialized()
    {
        for (var i = 1; i <= 8; i++)
        {
            cards.Add(i);
            cards.Add(i);
        }

        var rng = new Random();
        cards = cards.OrderBy(a => rng.Next()).ToList();
    }


    void ClickCard(int cardIndex)
    {
        Console.WriteLine(cardIndex);
    }
}


Solution 1:[1]

Generally speaking, the Component Model in Blazor works as follows: Whenever the state of the component changes or modified, you need to call the StateHasChanged method to notify Blazor that the component state has changed, and that the component should re-render to reflect the changes made.

UI events in Blazor, such as click, do not require adding a call to the StateHasChanged method manually. The StateHasChanged method is automatically called by the framework. This behaviour is based on the assumption that a UI event intrinsically changes the state of the component.

In your code above, when you click on a card, a UI click event is triggered, and your component is re-rendered. Thus, if you click on a button immediately after the initial rendering of the component, a re-rendering occurs, with the value of the variable cardIndex being now 16, and the next click makes it 32, and so on.

To sum up, each time you click on a card, the component is re-rendered, and the value of the variable cardIndex is increased.

Re-rendering is by design...

You can overcome this issue by resetting the cardIndex variable, etc. What is important to take from this, is the re-rendering feature of Blazor, and then find out ways how to properly work with it.

Preliminary suggestions how to do...

I'd suggest to define two components for your game. A parent and a child. The child component should contain the code that render the cards. It should also overrides the base component' ShouldRender that returns false. It tell Blazor not to render the component, except the initial rendering.

@code {
    private bool shouldRender = false;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }
   
} 

Depending on what design you're trying to achieve, you can implement your game in various ways.

UPDATE1:

The above explains what was the issue with your code, how the model component and rendering work. The suggested way to prevent a re-rendering by overriding the ShouldRender method is not much useful here, as your component must re-render again and again. So, a better solution is to use the @key directive as follows:

@foreach (var card in cards)
{
   <div @key="card" class="mem-card" @onclick="() => 
                 ClickCard(card)">@card</div>
 
}

Using the @key directive is a better way to control which element should re-rendered. Note that while overridding the ShouldRender method prevent the re-rendering of the complete component, using the @key directive enable the re-rendering of only elements whose value has changed. It is a good practice to use the @key directive when working with collections.

UPDATE2:

I'm not familiar with games, and I do not really know how you intend to design your game, so what you suggest based only on guess. I'll provide you here the initial code, and suggest you work on it, if you like it... In the course of time I'll add more content. Please, don't hesitate to ask me any question...

CardGame.razor

<div>@selectedCard.Value</div>

<div class="wrapper">
    @foreach (var card in cards2)
    {
       <div @key="card" @onclick="() => ClickCard(card)" 
       class="box">@card.Value</div>
    }
</div>


@code {

        private List<Card> cards;
        private Card selectedCard = new Card();
        private IEnumerable<Card> cards2;
        private List<int> list;
        protected override void OnInitialized()
        {
            Shuffler shuffler = new Shuffler();
            list = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 };
            shuffler.Shuffle(list);


            cards = Enumerable.Range(1, 8).Select(i => new Card { ID = i, Value = i, Order = list[i - 1] }).ToList();
            cards2 = cards.Concat(Enumerable.Range(1, 8).Select(i => new Card { ID = i + 8, Value = i, Order = list[i + 7] }).ToList());

           cards2 = cards2.OrderBy(c => c.Order);
            
        }

         private void ClickCard(Card card)
         {
            selectedCard = card;
         }

        public class Card
        {
            public int ID { get; set; }
           // public string ImageUrl { get; set; }
           // The Value property should contain the number 
           //  displayed to the user: [1...8 and 1...8][1]  
           // As is shown in an image you provided.
            public int Value { get; set; }
            public int Order { get; set; }
        }
        
        // Utility class to produce unique random number. Note:
        // The random number are going to be stored in Card.Order 
        // and they are going to represent the location or box 
        // in a grid of 4X4 cells   
        public class Shuffler
        {
            private Random rnd;

            public Shuffler()
            {
                rnd = new Random();
            }

           
            public void Shuffle<T>(IList<T> array)
            {
                for (int n = array.Count; n > 1;)
                {
                    int k = rnd.Next(n);
                    --n;
                    T temp = array[n];
                    array[n] = array[k];
                    array[k] = temp;
                }
            }


        }
    }

CardGame.razor.css

    body {
    margin: 40px;
}

.box {
    background-color: #444;
    color: #fff;
    border-radius: 4px;
    padding: 20px;
    font-size: 150%;
   
}

    .box:nth-child(even) {
        background-color: #ccc;
        color: #000;
    }

.wrapper {
    width: 600px;
    display: grid;
    grid-gap: 5px;
    grid-template-columns: repeat(6, 100px);
    grid-template-rows: 100px 100px 100px 100px;
    grid-auto-flow: column;
   
}

Index.razor

<CardGame></CardGame>

Copy and test... Remember it's only the beginning.

Solution 2:[2]

You need to compare the card values not their index in the list. I would recommend also creating Two cards for each value. This will help know if clicking same card twice. It would also help the render engine know if there are any updates as you can use @key. You cannot use @key if the same value is used twice.

@page "/"

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.
<div class="card-grid">

    @foreach (var card in cards)
    {
        <div @key=@card class="mem-card" @onclick="() => ClickCard(card)">@card.Value</div>
    }

</div>
@code {
    List<Card> cards = new();

    protected override void OnInitialized()
    {
        var allCardValues = Enum.GetValues<CardValues>();

        foreach (var cardValue in allCardValues)
        {
            cards.Add(new Card(cardValue));
            cards.Add(new Card(cardValue));
        }

        Random rng = new();
        cards = cards.OrderBy(a => rng.Next()).ToList();
    }

    public enum CardValues { Ace, Two, Three, Four, Five, Six, Seven, Eight }

    public class Card
    {
        public Card(CardValues value) => Value = value;
        public CardValues Value { get; }
    }

    void ClickCard(Card card)
    {
        Console.WriteLine(card.Value);
    }
}

Solution 3:[3]

You could get the index in this way:

@foreach (var card in cards.Select((value, index) => (value, index))
{
    <div class="mem-card" @onclick="() => ClickCard(card.index)">@card.value</div>
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2
Solution 3 Santiago