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
- Dossier
Models/Questions
. Dans le projetWeb.Mvc
, créez un dossierQuestions
sous le dossierModels
- Ajouter
CreerQUestionViewModel
. SousWeb.Mvc/Models/Questions
, créez une classeCreerQuestionViewModel
.Snowfall.Web.Mvc/Models/Questions/CreerQuestionViewModel.csnamespace 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énementContenu
: 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:
public class CreerQuestionViewModel
{
[Required]
public int EvenementId { get; set; }
[Required]
public string Contenu { get; set; } = String.Empty;
}
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!
- Ajouter
ModifierQuestionViewModel
. SousWeb.Mvc/Models/Questions
, créez une classeModifierQuestionViewModel
. - Faisons-la hériter de
CreerQuestionViewModel
Snowfall.Web.Mvc/Models/Questions/ModifierQuestionViewModel.csnamespace Snowfall.Web.Mvc.Models.Questions;
public class ModifierQuestionViewModel : CreerQuestionViewModel
{
} - Ajoutez ensuite la propriété
Id
Snowfall.Web.Mvc/Models/Questions/ModifierQuestionViewModel.cspublic 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.
//...
//POST /evenements/{evenementId}/questions
[HttpPost]
[Authorize]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(int evenementId, CreerQuestionViewModel questionViewModel)
{
return Ok();
}
//...
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
.
@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.
@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>
asp-controller
: le formulaire pointe vers le contrôleurQuestionsController
asp-action
: l'actionCreate
est responsable de recevoir la requêteasp-route-evenementId
: le id de l'événement
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.
public IActionResult New(int evenementId)
{
var viewModel = new CreerQuestionViewModel()
{
EvenementId = evenementId
};
return View(viewModel);
}
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.

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.
<!-- ... -->
<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
.
<!-- ... -->
<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!
<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!

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!
public async Task<IActionResult> Create(int evenementId, CreerQuestionViewModel questionViewModel)
{
if (!ModelState.IsValid)
{
return View("New", questionViewModel);
}
return Ok();
}
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.
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 });
}
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.
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.
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;
}
}
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!