Aller au contenu principal

19-5 Formulaire et enregistrement d'une question

Maintenant qu'il est possible d'afficher la vue New, il nous faudra un formulaire permettant de soumettre les données.

Pour un formulaire, il nous faut un ViewModel associé.

On se rappelle qu'on utilise le ViewModel pour lier les données du formulaire à un objet qui peut être passé entre la vue et le contrôleur.

Créer les ViewModel

LES viewmodels?

En effet, nous utiliserons deux ViewModel:

  • CreerQuestionViewModel
  • ModifierQuestionViewModel

Afin d'éviter la duplication de code puisqu'ils seront très près l'un de l'autre, on utilisera l'héritage.

Créer CreerQuestionViewModel

  1. Dossier Models/Questions. Dans le projet Web.Mvc, créez un dossier Questions sous le dossier Models
  2. Ajouter CreerQUestionViewModel. Sous Web.Mvc/Models/Questions, créez une classe CreerQuestionViewModel.
    Snowfall.Web.Mvc/Models/Questions/CreerQuestionViewModel.cs
    namespace Snowfall.Web.Mvc.Models.Questions;

    public class CreerQuestionViewModel
    {

    }

Ajouter les propriétés requises

Pour la création d'une question, au niveau du formulaire, nous avons besoin des attributs suivants:

  • EvenementId: le id de l'événement pour lequel on crée l'événement
  • Contenu: le texte de la question

Pour ce qui est du id de l'utilisateur, on traitera cela en dehors du formulaire, nous n'aurons donc pas besoin de cette propriété.

Vous pouvez ainsi modifier le ViewModel de création comme ceci:

Snowfall.Web.Mvc/Models/Questions/CreerQuestionViewModel.cs
public class CreerQuestionViewModel
{
[Required]
public int EvenementId { get; set; }

[Required]
public string Contenu { get; set; } = String.Empty;
}
info

On initialise Contenu à String.Empty puisqu'on s'attend à une valeur. Ultimement, ce champ ne devrait pas être null. Cependant, lors de la création initiale d'une question, aucun contenu n'est présent, donc on satisfait ce cas de figure en mettant une valeur par défaut de string vide.

La validation Required vérifie les cas de figure où la string est vide.

Créer ModifierQuestionViewModel

Pour ce qui est du ViewModel de modification, la seule différence est qu'il devra contenir un Id (parce que dans l'édition l'objet existe et a un id). On peut donc le faire hériter de CreerQuestionViewModel et n'ajouter que la propriété supplémentaire requise!

  1. Ajouter ModifierQuestionViewModel. Sous Web.Mvc/Models/Questions, créez une classe ModifierQuestionViewModel.
  2. Faisons-la hériter de CreerQuestionViewModel
    Snowfall.Web.Mvc/Models/Questions/ModifierQuestionViewModel.cs
    namespace Snowfall.Web.Mvc.Models.Questions;

    public class ModifierQuestionViewModel : CreerQuestionViewModel
    {

    }
  3. Ajoutez ensuite la propriété Id
    Snowfall.Web.Mvc/Models/Questions/ModifierQuestionViewModel.cs
    public class ModifierQuestionViewModel : CreerQuestionViewModel
    {
    [Required]
    public int Id { get; set; }
    }

Créer l'action Create (POST) du contrôleur

On peut maintenant créer au niveau du contrôleur l'action Create qui sera responsable de traiter les requêtes POST en provenance du formulaire.

Snowfall.Web.Mvc/Controllers/QuestionsController.cs
//...

//POST /evenements/{evenementId}/questions
[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(int evenementId, CreerQuestionViewModel questionViewModel)
{
return Ok();
}

//...
info

L'action Create reçoit un evenementId en provenance de l'URL et un CreerQuestionViewModel qui sera lié aux données du formulaire.

De plus, on utilise [HttpPost], qui sans utiliser de paramètre est l'équivalent d'utiliser la route de base, soit /evenements/{evenementId}/questions.

On valide via [Authorize] que l'utilisateur est bien connecté et on validera le jeton CSRF via [ValidateAntiForgeryToken].

On ne retourne que Ok() pour l'instant, mais on modifiera évidemment.

Ajout du formulaire dans la vue New

Référence au ViewModel pour le binding

Dans le haut de la vue, ajoutez la référence à CreerQuestionViewModel.

Snowfall.Web.Mvc/Views/Questions/New.cshtml
@model Snowfall.Web.Mvc.Models.Questions.CreerQuestionViewModel

<!-- ... -->

Ajout de la balise form

Commençons par déclarer la balise form qui précise vers où le formulaire pointera. À noter que quelques balises supplémentaires ont été ajoutées pour centrer le formulaire dans la page.

Snowfall.Web.Mvc/Views/Questions/New.cshtml
@model Snowfall.Web.Mvc.Models.Questions.CreerQuestionViewModel

@{
ViewBag.Title = "Poser une question";
}

<div class="row">
<div class="col-md-6 offset-md-3">
<h1>@ViewBag.Title</h1>

<form asp-controller="Questions"
asp-action="Create"
asp-route-evenementId="@Model.EvenementId">
</form>

</div>
</div>
info
  • asp-controller: le formulaire pointe vers le contrôleur QuestionsController
  • asp-action: l'action Create est responsable de recevoir la requête
  • asp-route-evenementId: le id de l'événement
attention

Vous pouvez exécuter le projet et vous rendre vers la page poser une question pour un événement. Vous obtiendrez l'erreur suivante:

```NullReferenceException: Object reference not set to an instance of an object.````

Cette erreur est liée à la propriété EvenementId du ViewModel qui est null.

En effet, on doit initialiser le viewmodel avec la propriété EvenementId et le passer via New pour que le formulaire puisse l'utiliser.

Initialisation de CreerQuestionViewModel dans New

Pour permettre d'initialiser le formulaire avec le bon EvenementId, on peut lui passer via le ViewModel.

Snowfall.Web.Mvc/Controllers/QuestionsController.cs
public IActionResult New(int evenementId)
{
var viewModel = new CreerQuestionViewModel()
{
EvenementId = evenementId
};

return View(viewModel);
}
info

On passe à la vue, via le ViewModel, l'identifiant de l'événement qui nous provient de la route (URL).

Vous pouvez essayer dans le navigateur d'accéder à la page de question et vous obtiendrez ceci.

http://localhost:4200

Peu excitant pour le moment, j'en conviens!

Ajout du textarea, champ hidden et du bouton soumettre

Champ hidden pour EvenementId

Afin que le ViewModel puisse transmettre le id de l'événement pour la question à partir du formulaire, on peut ajouter un champ caché dans le formulaire.

Snowfall.Web.Mvc/Views/Questions/New.cshtml
<!-- ... -->

<form asp-controller="Questions"
asp-action="Create"
asp-route-evenementId="@Model.EvenementId">

<input type="hidden" asp-for="EvenementId"/>
</form>

<!-- ... -->

textarea pour Contenu

Assez simple dans ce cas-ci, on ajoute le champ et on le lie à la propriété correspondante du ViewModel via asp-for.

Snowfall.Web.Mvc/Views/Questions/New.cshtml
<!-- ... -->

<form asp-controller="Questions"
asp-action="Create"
asp-route-evenementId="@Model.EvenementId">

<input type="hidden" asp-for="EvenementId"/>

<div class="mb-3">
<textarea asp-for="Contenu" class="form-control" rows="5"></textarea>
<span asp-validation-for="Contenu" class="invalid-feedback"></span>
</div>
</form>

<!-- ... -->

Bouton soumettre

Finalement, il ne reste qu'à ajouter le bouton soumettre et on a une vue complète!

Snowfall.Web.Mvc/Views/Questions/New.cshtml
<div class="row">
<div class="col-md-6 offset-md-3">
<h1>@ViewBag.Title</h1>

<form asp-controller="Questions"
asp-action="Create"
asp-route-evenementId="@Model.EvenementId">

<input type="hidden" asp-for="EvenementId"/>

<div class="mb-3">
<textarea asp-for="Contenu" class="form-control" rows="5"></textarea>
<span asp-validation-for="Contenu" class="invalid-feedback"></span>
</div>

<button class="btn btn-primary">Soumettre</button>
</form>

</div>
</div>

1,2,1,2 test

Assurez-vous que le tout fonctionne et qu'un formulaire est affiché.

Beaucoup mieux!

http://localhost:4200

Valider le formulaire

Si vous avez essayer d'appuyer sur soumettre, vous avez probablement remarqué que vous avez été redirigé vers une page vide. La raison est que l'action Create retourne simplement Ok(), sans effectuer aucun traitement ni vérification des validations.

Remédions à la situation!

Snowfall.Web.Mvc/Controllers/QuestionsController.cs
public async Task<IActionResult> Create(int evenementId, CreerQuestionViewModel questionViewModel)
{
if (!ModelState.IsValid)
{
return View("New", questionViewModel);
}

return Ok();
}
info

Si le ViewModel, donc le formulaire, n'est pas valide, on retourne la vue New avec le même ViewModel et son contenu.

Vous ne devriez plus être en mesure de soumettre le formulaire s'il n'y a rien dans le champ contenu.

Créer la question et appel au service

Si tout se passe bien et que les validations n'échouent pas, on devrait créer la question et l'envoyer au service pour que ce dernier communique avec la couche de données, permettant ainsi d'effectuer l'enregistrement.

À partir du contrôleur, on peut donc créer une question en utilisant le ViewModel et ensuite appeler la fonction Create du service.

Snowfall.Web.Mvc/Controllers/QuestionsController.cs
public async Task<IActionResult> Create(int evenementId, CreerQuestionViewModel questionViewModel)
{
if (!ModelState.IsValid)
{
return View("New", questionViewModel);
}

Question question = new Question()
{
UtilisateurId = User.Identity!.Id(),
EvenementId = questionViewModel.EvenementId,
Contenu = questionViewModel.Contenu
};

await _questionService.Create(question);

return RedirectToAction("Show", "Evenements", new { Id = evenementId });
}
info

La question est créée avec les deux propriétés provenant du ViewModel, mais aussi à partir du Id de l'utilisateur connecté.

On peut obtenir, comme dans la vue, les données reliées à l'utilisateur via User.Identity. À noter que ces données ne sont pas mises à jour à partir de la BD à chaque requête, mais plutôt récupérées du cookie de session.

Le service est appelé avec la question pour qu'elle soit créée et finalement on redirige vers la page de l'événement. Éventuellement, nous verrons une façon d'afficher les questions de l'utilisateur.

Si vous faites l'envoi du formulaire, vous aurez malheureusement une erreur puisque la fonction Create dans le repository n'est pas implémentée.

Requête Dapper INSERT

Injection du DbContext

Ajoutons l'objet DbContext permettant d'ouvrir des connexions au repository via son constructeur.

Snowfall.Data/Repositories/QuestionRepository.cs
public class QuestionRepository : IQuestionRepository
{
private readonly DapperContext _dbContext;

public QuestionRepository(DapperContext dbContext)
{
_dbContext = dbContext;
}

public async Task<Question> Create(Question question)
{
throw new NotImplementedException();
}
}

Requête Dapper d'insertion

Ajoutez la requête SQL et le code permettant l'insertion.

Snowfall.Data/Repositories/QuestionRepository.cs
public async Task<Question> Create(Question question)
{
string sql = @"
INSERT INTO questions (evenement_id, utilisateur_id, contenu, created_at, updated_at)
VALUES(@EvenementId, @UtilisateurId, @Contenu, @CreatedAt, @UpdatedAt)
RETURNING id";

question.CreatedAt = DateTime.Now;
question.UpdatedAt = DateTime.Now;

using (IDbConnection connection = _dbContext.CreateConnection())
{
question.Id = await connection.QuerySingleAsync<int>(sql, question);
return question;
}
}
info

Dans la requête, les paramètres @ doivent correspondre exactement aux noms de propriétés de l'objet question. En effet, c'est cet objet que nous passons à Dapper pour la requete via QuerySingleAsync. Il utilisera les propriétés de l'objet et remplacera les occurences de @ dans la requête par la proptiété correspondante de l'objet question.

On en profite aussi pour associer les deux colonnes CreatedAt et UpdatedAt à l'heure et la date actuelles.

À noter que comme les utilisateurs sont parfois dans plusieurs fuseaux horaires, on stock habituellement les timestamp au format UTC, soit dans un seul fuseau horaire standardisé. On s'occupe ensuite d'afficher la date selon le fuseau horaire de l'utilisateur, mais à tout de moins nous avons un référent commun pour tous les timestamps de la base de données. On se contentera pour le cours de sauvegarder dans la BD avec l'heure locale, mais sachez que cette nuance existe.

Test de création

Vous pouvez faire un test de création de questions et vous devriez être redirigé vers la page de l'événement après avoir ajouté la question. De plus, vérifiez dans votre BD qu'une nouvelle entrée de question est présente!