0

I have a Razor page in ASP.NET core meant for editing product data. It receives a productId and has input controls for editing the properties of said product. It has a binding to the product instance saved in the PageModel. I populate the product instance OnGet, which works fine, as it populates the fields properly. When I submit the form, and OnPost is called, I see that the product instance is now null.

After debugging I realised that when I submit the form, the PageModel is re-instantiated (the constructor is called again), so the product instance is reset and is now null.

Is this expected behaviour?

Should this binding between the property in my PageModel and it's Razor Page presentation not persist?

In The course I am following, this behaves differently, the product instance persists to the OnPost method call. My Page Model:

public class EditModel : PageModel
{
    private readonly IProductData productData;
    private readonly IHtmlHelper htmlHelper;

    [BindProperty]
    public Product Product { get; set; }
    public IEnumerable<SelectListItem> Category { get; set; }

    public EditModel(IProductData productData, IHtmlHelper htmlHelper)
    {
        this.productData = productData;
        this.htmlHelper = htmlHelper;
    }

    public IActionResult OnGet(int productId)
    {
        Category = htmlHelper.GetEnumSelectList<ProductType>();
        Product = productData.GetById(productId);
        if(Product == null) 
        {
            return RedirectToPage("./NotFound");
        }
        return Page();
    }

    public IActionResult OnPost() 
    {
        Product = productData.Update(Product);
        productData.Commit();
        return Page();
    }

}

My Razor Page:

@page "{productId:int}"
@model LearningASPdotNETCore.Pages.Products.EditModel
@{
    ViewData["Title"] = "Edit";
}

<h1>Editing @Model.Product.Name</h1>

<form method="post">

<input type="hidden" asp-for="Product.Id" />
<div class="form-group">
    <label asp-for="Product.Name"></label>
    <input asp-for="Product.Name" class="form-control" />
</div>

<div class="form-group">
    <label asp-for="Product.Country"></label>
    <input asp-for="Product.Country" class="form-control" />
</div>

<div class="form-group">
    <label asp-for="Product.Type"></label>
    <select class="form-control" asp-for="Product.Type" asp-items="Model.Category">

    </select>
</div>

<button type="submit" class="btn btn-primary">Save</button>

DoubleL
  • 1
  • 1

2 Answers2

1

Yes, this is expected behavior. Virtually everything in the request pipeline, including PageModel, controllers, etc. are request-scoped. They are instantiated at the beginning of the request and disposed when the request is finished. If you need something set, then that should be set for each request, regardless of method (GET, POST, etc.).

Chris Pratt
  • 232,153
  • 36
  • 385
  • 444
  • Has this changed since ASP.NET Core 2? In this tutorial that I am following, this exact code works: https://app.pluralsight.com/course-player?clipId=732ec6ea-749e-4ce4-9199-6a69f0ca416d Here is the source code for it: https://github.com/OdeToCode/OdeToFood/tree/master/OdeToFood/OdeToFood/Pages/Restaurants – DoubleL Apr 13 '20 at 16:00
  • No. This has been the case always. Even going back into things like classic ASP and Web Forms. Each request is a separate self-contained thing. That's just a fundamental aspect for how web applications work. – Chris Pratt Apr 13 '20 at 16:09
  • I'm not sure what you're talking about specifically with the reference code, but in the Edit page, for example, the `Restaurant` property has `BindProperty`, so in the case of the post handler it's set via the model binder, versus directly as in the get handler. However, the select list is set explictly in both the get and post handlers because it's not coming from the request body. In other words, the reference code is actually compensating for this, so your code must diverge in some way if it's not working. – Chris Pratt Apr 13 '20 at 16:15
  • Yes, mine is the exact same code, except I have replaced the Restaurant with Product. It also has BindProperty. This is the commit where I believe the author is at the same point in the tutorials: https://github.com/OdeToCode/OdeToFood/commit/4c5913ffb6140b0b5b7cd8b3c65303f7fa760c3e I have checked for divergence, will check again now. thanks – DoubleL Apr 13 '20 at 16:59
  • Thanks for the help Chris. It was a divergence, but not where I had expected it. – DoubleL Apr 13 '20 at 17:23
0

I have found the solution. It seems like the product instance could not be instantiated because I have not implemented the property get/set. This confuses me as I thought these two were equal, but I guess not. Before (did't work):

public class Product
{
    public int Id;
    public string Name;
    public string Country;
    public ProductType Type;
}

After (works):

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Country { get; set; }
    public ProductType Type { get; set; }
}
DoubleL
  • 1
  • 1
  • A useful feature which I will employ from now on is the code snipet: _prop_ – DoubleL Apr 13 '20 at 17:28
  • More context on why this worked: https://stackoverflow.com/questions/1180860/public-fields-versus-automatic-properties – DoubleL Apr 13 '20 at 17:42