Search code examples
c#asp.net-corerazor-pages

ASP.NET Core Razor Pages - Complex model property is null after returning Page()


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?


Solution

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

    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().