Search code examples
c#razorrazor-pagesmodel-bindingtwo-way-binding

How can I bind selected properties of a Model to a Razor Page?


I’m just moving to Razor Pages and have hit a wall. Here’s a page model:

public class ThisPersonPageModel : PageModel
{

    [BindProperty]
    public MyApp.Models.Person ThisPerson { get; set; }

    … OnGet() …

    … OnPost() …
}

The entity is an annotated EF Core model class (the ‘domain model’) with all constraints and validations built in. But this particular view on this entity only displays some of the model properties, and the binder wants to bind all of them. Therefore field values get erased when the data is saved. I was expecting to be able to annotate with [BindProperty(Include=”Field1,Field2”)] (similar to MVC’s ‘Bind(Include=”Field1,Field2”)]’). But that ‘field selection’ capability apparently doesn’t exist for Razor Pages. Does it? (Or does anybody know if/when it’s coming?)

To me this is a severe limitation, even a show-stopper for using Razor Pages. I want to get the beautiful DRY benefits of two-way model binding, primarily the client-side validation (from model annotations) and with no need to write (repetitive) binding code when retrieving values. Yet, I have always had domain models (database tables) with broad capability (many properties), but, for each view, only certain properties are displayed to certain users (roles) based on their situation, needs, and permissions. (I think that’s the essence of what a view is: a specific window on the data for a specific user and situation.) For example, a person can update some of their account properties (e.g. name), but only an admin may update other properties (e.g. access level). Or, the same person works on different parts of a complex entity throughout its lifecycle: filling in early properties early, and later properties later. (Think of a wizard UI.) You don’t want the early properties deleted when saving the later properties. For me, this scenario is ubiquitous and has been for many databases over many years, both designed by me and by others.

I’ve looked into several options and nothing pans out. I’ve seen this answer in many places: ‘Just create a PersonViewModel with only the properties you need for this view'. But then of course that’s not DRY (the perfect domain model already exists), and it requires mapping to/from the domain model, and what about all the beautiful, complex constraints and validations on the domain model? Must those be re-created on the local page model too? That would pretty much end it for me. I’ve seen AutoMapper used, but I’m not sure if that will map to/from a model with fewer properties, and if it retains the constraints and validations when mapping from the domain model to the page model. And that just adds a level of WET complexity that ‘[BindProperty(Include=”Field1,Field2”)]’ would solve so easily. To me this, seems like a severe limitation of the two-way model binder in Razor Pages. What am I missing? What's the best way to accomplish this? I welcome your suggested alternatives and/or corrections to my misunderstandings.


Solution

  • You can bind posted form values to the public property, but you don't have to. You can bind to a parameter on the OnPost method instead. You can use [Bind(include:"Field1, Field2")] on the parameter to mitigate overposting attacks:

    public IActionResult OnPost([Bind(include:"Field1, Field2")] MyApp.Models.Person thisPerson)
    {
        if (ModelState.IsValid)
        {
            //process form submission
            return RedirectToPage("/success");
        }
        return Page();
    }
    

    You still have your public property that activates strongly typed input tag helpers:

    public MyApp.Models.Person ThisPerson { get; set; }
    

    You just don't need to bind to it. Just make sure that the parameter and the public property share the same name.