Search code examples
javascriptc#asp.net-mvcasp.net-corefetch-api

Get null in controller when implementing IEnumerable in ASP NET model


I want use foreach for list in C# in controller Create([FromBody] CardList cards). I get the error Type 'WordStudy.Models.CardList' can only be used in 'foreach' statements if it implements 'IEnumerable' or 'IEnumerable', or if it has a suitable 'GetEnumerator' method whose return type has a 'Current' property and a 'MoveNext' method. When I implemented IEnumerable in my class it gets null.

script.js

ar currentCardNumber = 1;

function addCard() {

    currentCardNumber++;
    let newCardHTML = `
                    <div class="header d-flex justify-content-between p-3">
                        <h4 class="card-counter">${currentCardNumber}</h4>
                    </div>
                    <div class="card-body row">
                        <div class="term col">
                            <h3>Term</h3>
                            <input class="form-control w-100" name = "term" >
                        </div>
                        <div class="definition col">
                            <h3>Definition </h3>
                            <input class="form-control w-100" name="definition" >
                        </div>
                    </div>
            </div> `;
    let newCard = document.createElement('div');
    newCard.classList.add('card');
    newCard.classList.add('mt-3');
    newCard.innerHTML = newCardHTML;

    let addNewCard = document.querySelector('.cards');
    let referenceNode = document.getElementById('create-cards');
    addNewCard.insertBefore(newCard, referenceNode);

}
function saveCardDataToServer() {
    let cards = [];
    document.querySelectorAll('.cards > .card   ').forEach((card) => {

        let termInput = card.querySelector('input[name="term"]');
        let definitionInput = card.querySelector('input[name="definition"]');

        if (termInput && definitionInput) {
            let term = termInput.value.trim();
            let definition = definitionInput.value.trim();
            cards.push({
                Term: term,
                Definition: definition
            });
        }
        else {
            console.error("Input elements not found in card:", card);
        }
    })

    fetch('/Cards/Create/', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({Cards: cards})
    })
        .then(response => response.json())
        .then((data => {
            if (data.success) {
                console.log("Data successfully saved: ", data);
                window.location.href = "/Cards/Learning";
            } else {
                console.error("Error saving card: ", data.message);
            }
        }))
}
document.addEventListener('DOMContentLoaded', function() {
  let newCard = document.querySelector('.new-card');
  newCard.addEventListener('click', function() {
    addCard();
  });
  document.querySelector('form.cards').addEventListener('submit', function() {
    saveCardDataToServer();
  });
});

View

@model CardList

<!DOCTYPE html>
<html lang="en">

<head>
    <script src="~/js/script.js">
    </script>
    <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
          integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous" asp-append-version="true">    
</head>

<body>

<main class="main">
  <div class="container">
        <div class="new-module d-flex justify-content-between mt-5">
            <h1>Create new module</h1>
            <a class="btn btn-primary" asp-controller="Cards" asp-action="learning" type="submit">Создать</a>
        </div>

        <div class="card mt-5 p-2 pb-3">
            <h3>Title</h3>
            <input class="form-control w-100" placeholder="Enter a name">
        </div>
        <form asp-controller="Cards" asp-action="Create" method="post" class="cards">
            <div class="card mt-6" id="card">
                <div class="header d-flex justify-content-between p-3">
                    <h4 class="card-counter">1</h4>
                </div>
                <div class="card-body row">
                    <div class="term col">
                        <h3>Term</h3>
                        <input class="form-control w-100" name = "term" >
                    </div>
                    <div class="definition col">
                        <h3>Definition </h3>
                        <input class="form-control w-100" name = "definition"  >
                    </div>
                </div>
            </div>

            <input type="submit" value="Create" id="create-cards" class="btn btn-primary mt-2"/>
        </form>
        <button class="new-card w-100 d-flex justify-content-center align-items-center mt-4 mt-s rounded-2" style="height: 5rem"  type="button">
            <h3>+ Add card</h3>
        </button>
  </div>

</main>
</body>

</html>

Models

public class Card
{
    public int CardId { get; set; }
    [Required]
    public string Term { get; set; } = string.Empty;
    public string Definition { get; set; } = string.Empty;
}

public class CardList : IEnumerable<Card>
{
    public List<Card> Cards { get; set; }

    public CardList()
    {
        Cards = new List<Card>();
    }
    
    public IEnumerator<Card> GetEnumerator()
    {
        return Cards.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}

Controller

public class CardsController : Controller
{
    public IActionResult Learning()
    {
        var cards = CardsRepository.GetCards();
        return View(cards);
    }

    public IActionResult Create()
    {
        return View();
    }
    [HttpPost]
    [Route("Cards/Create/")]
    public IActionResult Create([FromBody] CardList cards)
    
    {
        if (cards != null)
        {
            foreach (var card in cards)
            {
                if (!string.IsNullOrWhiteSpace(card.Term) && !string.IsNullOrWhiteSpace(card.Definition))
                {
                    CardsRepository.AddCard(card);
                }

            }

            return Json(new { success = true, message = "Card saved successfully " });
        }

        var errors = ModelState.Values.SelectMany(v => v.Errors).Select(e => e.ErrorMessage);
        return Json(new { success = false, message = "Validation error", errors = errors });
    }
}

If IEnumerable is not implemented in the CardList controller, Create([FromBody] CardList cards) will get the correct data.


Solution

  • I suggest not extending IEnumerable at all. Go simpler.

    public class Card
    {
        public int CardId { get; set; }
        [Required]
        public string Term { get; set; } = string.Empty;
        public string Definition { get; set; } = string.Empty;
    }
    
    public class Deck
    {
        public List<Card> Cards { get; set; }
    
        public Deck()
        {
            Cards = new List<Card>();
        }
    }
    

    Which gives you simple json to work from:

    {
      "Cards": [
        {
          "CardId": 1,
          "Term": "Term 1",
          "Definition": "Definition 1"
        },
        {
          "CardId": 2,
          "Term": "Term 2",
          "Definition": "Definition 2"
        },
        {
          "CardId": 3,
          "Term": "Term 3",
          "Definition": "Definition 3"
        }
      ]
    }
    

    It's best practice to make your DTO's (data transfer objects) as simple as possible.