I'm apparently not understanding something fundamental about model binding complex properties in Razor Pages. When my model isn't valid, I return Page();
but I get an exception when referencing a complex property of the model. Here's a slim version of what I have:
Models:
public class Movie
{
public int Id { get; set; }
public string Title { get; set; }
public Director Director { get; set; }
public string Description { get; set; }
}
public class Director
{
public int Id { get; set; }
public string Name { get; set; }
}
Index.cshtml.cs:
[BindProperty]
public Movie Movie { get; set; }
[BindProperty, Required]
public string Description { get; set; }
public IActionResult OnGet()
{
// Pretend we're loading from the DB...
Movie = new Movie
{
Id = 1,
Title = "Citizen Kane",
Director = new Director
{
Id = 101,
Name = "Orson Wells"
}
};
return Page();
}
public IActionResult OnPost()
{
if (!ModelState.IsValid)
return Page();
Movie.Description = Description;
//Save to DB
// ...
return RedirectToPage("/Index");
}
Index.cshtml:
@page
@model IndexModel
<h5>@Model.Movie.Title</h5>
<h6>Directed by @Model.Movie.Director.Name</h6>
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Movie.Id" />
<textarea asp-for="Description" placeholder="Enter Description"></textarea>
<button type="submit">Submit</button>
</form>
When I leave Description blank, the model is invalid in OnPost
and returns Page()
as expected. But then I get a System.NullReferenceException; Object reference not set to an instance of an object
here:
<h6>Directed by @Model.Movie.Director.Name</h6>
Why is Movie.Director
null here? Do I have to get the data again when returning Page()
? I was thinking it would fire OnGet()
again but it doesn't. What am I doing wrong?
Why is
Movie.Director
null here? Do I have to get the data again when returningPage()
? I was thinking it would fireOnGet()
again but it doesn't.
No, the OnGet
method is not called automatically when your OnPost
method is actually executing. Both handlers are independent from each other and the only thing they share is the underlying Razor view and the page model. If your view requires data to be loaded into your model, then you will need to do that for the OnPost
as well.
You could call the GET method from within your POST method, but depending on what you actually do in your OnGet
, this might overwrite data that has been set explicitly by the OnPost
. What I would recommend instead is to add an additional method that will be called by both OnGet
and OnPost
to set up shared model properties that your view needs to render.
public IActionResult OnGet()
{
Movie = LoadMovie();
return Page();
}
public IActionResult OnPost()
{
if (!ModelState.IsValid)
{
Movie = LoadMovie();
return Page();
}
// …
return RedirectToPage("/Index");
}
private Movie LoadMovie()
{
// …
}
Note that you will also only need to execute this method if you actually need to render your page view.
The confusion maybe stems from what the OnGet
and OnPost
methods are for: With Razor pages, these methods are the entry point. They are called by the ASP.NET Core framework when a route is being requested that matches the Razor page. If it’s a GET request, the OnGet
method is being called; if it’s a POST request, it’s the OnPost
method. There is no direct relationship between these methods other that they share the same view (the cshtml) and the model.
Since these methods are the entry points, a POST request will not execute the OnGet
method and a GET request will not execute the OnPost
method—unless of course you make those calls yourself within these methods.
Now, when you return Page()
then the only thing that happens is that you tell the ASP.NET Core framework to then render your Razor view with the data that you prepared in the page model. So it just renders the cshtml
passing the page model as the view’s model. This will however not cause any calls to OnGet
or OnPost
at this point since those already ran to get into the Razor page execution.
That means that if you want to execute certain logic always before rendering the view, then you will need to execute that logic explicitly from within your page handlers, before returning Page()
.