DEV Community

loading...

Creating New Entities

Douglas Starnes
・7 min read

The previous post was actually the first step of what is called a CRUD app. CRUD is an acronym that describes the four basic operations of a database:

  • Create
  • Read
  • Update
  • Delete

Getting a list of Question objects from the database and displaying them in a Razor Page fulfills reading data. It is logical to next work on creating new objects that do not exist and storing them in the database. This will introduce you to forms in Razor Pages and model binding and validation.

The Create Form

Start off by creating a new Razor Page that will display a form to the user to accept the values for a new Question.

$ dotnet aspnet-codegenerator razorpage Create Empty -udl -outDir Pages\Questions

Two new files will be created inside the Pages\Questions directory: Create.cshtml and Create.cshtml.cs. First take a look at the CreateModel class in Create.cshtml.cs. Create a new property of type Question.

public Question Question { get; set; }

When values from the form are provided by the user, they will populate this instance of Question which will be inserted into the database. The issue is how to get those values out of the form fields. It turns out, if you follow some simple rules, ASP.NET Core Razor Pages will do this all for you through model binding. First, you need to apply the BindProperty attribute to the Question property.

[BindProperty]
public Question Question { get; set; }

Next, in the Create.cshtml file, add a form tag with a method of post.

<form method="post">

</form>

Notice that no action is specified and thus the form will post back to the URL for the GET request. To make sure the form fields have the correct names for model binding to work, once again you will rely on tag helpers. Create a label tag that will provide a text description of the first field, for the QuestionTitleproperty.

<label></label>

The asp-for tag helper, will accept a property from the data model class. You added a Question to the CreateModel. To generate the attributes for the label simply provide the asp-for tag helper a property from the Question data model class.

<label asp-for="@Model.Question.QuestionTitle"></label>

Notice the Model has to be prefixed with an @ since it is a C# value. Next add an input tag and again use the asp-for tag helper to do the rest.

<input asp-for="@Model.Question.QuestionTitle" />

To make it look nicer surround both the label and input in a div and apply some Bootstrap styles.

<div class="form-group">
    <label asp-for="@Model.Question.QuestionTitle"></label>
    <input class="form-control" asp-for="@Model.Question.QuestionTitle" />
</div>  

For the QuestionText, simply copy and paste the above and replace QuestionTitle with QuestionText. Also, make the input a textarea to give users room for a longer description of the question.

<div class="form-group">
    <label asp-for="@Model.Question.QuestionText"></label>
    <textarea class="form-control" asp-for="@Model.Question.QuestionText"></textarea>
</div>  

And finally add an input of type submit and style it to look like a button with Bootstrap.

<input class="btn btn-primary" type="submit" value="Create" />

And that's the entire form. You don't need to add a field for the QuestionId property because it's generated by the database. And the Created and Edited values will use the current time when the object is saved. Fire up the development server with dotnet watch run and go to /Questions/Create in the browser.

Alt Text

The values for the labels were taken from names of the properties. You'll see how to change them later on. But the combination of ASP.NET Core tag helpers and Bootstrap have created a nice form with very little effort on your part. If you look at the generated HTML you'll see that the names and ids of the form fields follow a pattern. And this pattern is what model binding uses to automatically populate a model object.

Saving the Entity

Switch over to the CreateModel class. Thanks to model binding, the Question property has been populated with the values from the form fields. To persist the Question to the database, you'll need a database context. So create a constructor that receives a QuandaDbContext and create and initialize a field for it. You saw how to do this with shortcuts for the IndexModel class in the last post.

private readonly QuandaDbContext context;

public CreateModel(QuandaDbContext context)
{
    this.context = context;
}

In the IndexModel you used the OnGet method to handle an GET request. You actually did the same with the CreateModel but there was nothing special so the default result was to render the Create.cshtml page. But when the button in the form is clicked it will send a POST request. The aspnet-codegenerator does not stub out an OnPost method but you can add one.

public void OnPost()
{

}

Set the Created and Edited properties on the Question to be equal to the current DateTime.

Question.Created = Question.Edited = DateTime.Now;

Add the Question to the Questions DbSet in the context.

context.Questions.Add(Question);

One more step is needed to commit the change to the database.

context.SaveChanges();

It is possible to make multiple changes to the database and then commit all at the same time. And you'll see an example of this when you implement the ratings for the questions.

That's everything in the database, but what next? There is a best practice with web apps that after a POST you always redirect somewhere else with a GET. This helps prevent accidental form resubmission. And it's easy to redirect the user with a call to RedirectToPage. This method take a page to redirect to. For this form, it would be nice to redirect back to the list of questions so you can see the new question is in the database. Add this final line to the OnPost method.

return RedirectToPage("./Index");

This will redirect the user to the Index page in the Questions directory with a GET. But notice that there is now an error. The OnPost method has a return type of void yet something is returned. Hover over the RedirectToPage method and see that is returns a RedirectToPageResult object. This is one of many results that implement the IActionResult interface. Change the return type of OnPost to IActionResult and the application will now build.

Run the app with dotnet watch run and go to the Questions/Create page in the browser. Fill in the form.

Alt Text

Click the Create button to submit the form, save the question to the database and be redirected to the list of questions.

Alt Text

Awesome! It works. But there are a few rough edges that need to be cleaned up.

Data Annotations and Model Validation

If you go back to the form, notice the labels for QuestionTitle and QuestionText. These reflect the names of the properties in the CreateModel class. These are not the best user facing names to use. By applying data annotation attributes to properties of the Question model class, you can provide alternative display names for the properties. Open the Question class and make these changes.

[Display(Name="Title")]
public string QuestionTitle { get; set; }

[Display(Name="Description")]
public string QuestionText { get; set; }

The Display attribute is in the System.ComponentModel.DataAnnotations namespace which you can import with Ctrl-.. The Name property is the text to be shown instead of the property name. That change alone will display the new names on the form.

Alt Text

There is one other problem. Right now a blank form can be submitted. You can prevent this by adding the Required attribute to the QuestionTitle and QuestionText properties. There are many other attributes to validate string length, ranges, data types you have the options to create custom validation attributes as well.

For the Required attribute to have an effect there are two steps to complete. First, in the CreateModel class, at the top of the OnPost method, check that the ModelState property is valid. The ModelState property will ensure that all of the validation attributes have been satisfied or else the IsValid property will be false.

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {

    }
    // ...
}

If IsValid is false, what happens? The method must return a result that implements IActionResult. The user should also get another chance to fill the form out with valid values and even better, get feedback about the errors the form contained. The first one is easy. Simply call and return the Page method. This will render and return the Create.cshtml page. In fact, it's the default action. The OnGet method could have been written

public IActionResult OnGet()
{
    return Page();
}

So the first lines of OnPost should be

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }
    // ...
}

Now what can you do about the feedback? In the Create.cshtml page, add a span under each of the fields for QuestionTitle and QuestionText. For each one, add a tag helper asp-validation-for and provide it with a property name. These will provide feedback about the errors for each form field, if the value is not valid. The Bootstrap classes will make the text bold and red.

<div class="form-group">
    <label asp-for="@Model.Question.QuestionTitle"></label>
    <input class="form-control" asp-for="@Model.Question.QuestionTitle" />
    <span class="font-weight-bold text-danger" asp-validation-for="@Model.Question.QuestionTitle"></span>
</div>
<div class="form-group">
    <label asp-for="@Model.Question.QuestionText"></label>
    <textarea class="form-control" asp-for="@Model.Question.QuestionText"></textarea>
    <span class="font-weight-bold text-danger" asp-validation-for="@Model.Question.QuestionText"></span>
</div>

Now if you try to submit a form and leave either of the fields blank, you'll see the errors in the form.

Alt Text

Also, there is one last change you should make. Create a button above the table in the Index page that will take the user to the Create page.

<div>
    <a class="btn btn-primary" asp-page="./Create">Create a question</a>
</div>

And that's it, you've made the round trip! There are still two more CRUD functions, update and delete. You'll get to those eventually. The next post will show how to view a single question with more detail than what is seen in the Index page. And it will include a special twist, switching to Linux.

Discussion (0)