13
votes

I am using ASP.NET Core 2 with Razor Pages and I am trying to have two forms with separate properties (BindProperty) on one page.

@page
@model mfa.Web.Pages.TwoFormsModel
@{
    Layout = null;
}

<form method="post">
    <input asp-for="ProductName" />
    <span asp-validation-for="ProductName" class="text-danger"></span>
    <button type="submit" asp-page-handler="Product">Save product</button>
</form>

<form method="post">
    <input asp-for="MakerName" />
    <span asp-validation-for="MakerName" class="text-danger"></span>
    <button type="submit" asp-page-handler="Maker">Save maker</button>
</form>

And the corresponding PageModel:

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;
using System.Threading.Tasks;

namespace mfa.Web.Pages
{
    public class TwoFormsModel : PageModel
    {
        [BindProperty]
        [Required]
        public string ProductName { get; set; }

        [BindProperty]
        [Required]
        public string MakerName { get; set; }

        public async Task<IActionResult> OnPostProductAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            return Page();
        }

        public async Task<IActionResult> OnPostMakerAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }

            return Page();
        }
    }
}

Hitting any of the two submit buttons brings me in the corresponding post handler. Both "ProdutName" and "MakerName" are populated corectly with whatever I typed in the corresponding input fields. So far, so good.

But: ModelState.IsValid() always returns true - no matter if the value of the corresponding property has a value or not. ModelState.IsValid() is true even when both properties are null.

Also: OnPostProductAsync() should only validate "ProductName" and accordingly OnPostMakerAsync() should only validate "MakerName".

Can this be done at all? Or am I asking too much from Razor Pages? There are plenty of blogs and tutorials that show you how to have two forms on one page ... but they are all using the same model. I need different models!

4
You can do that very simply based on the asp-page-handler attribute in your form. you can see example Here: youtube.com/watch?v=-6PE4p4gUYQ - Farouk Belhocine
2021 and we still dont have nice way to do it!! WTF microsoft? 1 form per page really? What if i want to have form for user settings and application settings in 1 page - i cant. I have to create some monster of Model keep all the values there and so on.. - Tommix

4 Answers

6
votes

In order to make the validation work properly you will have to create a view model which will contain the two properties and define the [Required] for each of the properties that you want to check but because you have two different forms with different validation it is not going to work because if both values are defined as required then when you will try to validate the Product it will validate the Maker as well which will not have a value.

What you can do is to make the check yourself. For example the OnPostProduct can have the following code:

public async Task<IActionResult> OnPostProductAsync()
{
    if (string.IsNullOrEmpty(ProductName))
    {
        ModelState.AddModelError("ProductName", "This field is a required field.");
        return Page();
    }

    // if you reach this point this means that you have data in ProductName in order to continue

    return Page();
}
3
votes

My solution isn't very elegant, but it doesn't require you to manually do the validations. You can keep the [Required] annotations.

Your PageModel will look like this -

    private void ClearFieldErrors(Func<string, bool> predicate)
    {
        foreach (var field in ModelState)
        {
            if (field.Value.ValidationState == Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid)
            {
                if (predicate(field.Key))
                {
                    field.Value.ValidationState = Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Valid;
                }
            }
        }
    }

    public async Task<IActionResult> OnPostProductAsync()
    {
        ClearFieldErrors(key => key.Contains("MakerName"));
        if (!ModelState.IsValid)
        {
            return Page();
        }

        return Page();
    }

    public async Task<IActionResult> OnPostMakerAsync()
    {
        ClearFieldErrors(key => key.Contains("ProductName"));
        if (!ModelState.IsValid)
        {
            return Page();
        }

        return Page();
    }

Not the best idea because you need to compare the binded field names to strings. I used Contains because the field keys are inconsistent and sometimes contain a prefix. Was good enough for me because I had small forms, with distinct field names.

3
votes

One more solution pretty close ...

public static class ModelStateExtensions
{
    public static ModelStateDictionary MarkAllFieldsAsSkipped(this ModelStateDictionary modelState)
    {
        foreach(var state in modelState.Select(x => x.Value))
        {
            state.Errors.Clear();
            state.ValidationState = ModelValidationState.Skipped;
        }
        return modelState;
    }
}

public class IndexModel : PageModel 
{
    public class Form1Model {
        // ...
    }
    public class Form2Model {
        // ...
    }

    [BindProperty]
    public Form1Model Form1 { get; set; }

    [BindProperty]
    public Form2Model Form2 { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        ModelState.MarkAllFieldsAsSkipped();
        if (!TryValidateModel(Form1, nameof(Form1)))
        {
            return Page();
        }
        // ...
    }
}

1
votes

I was facing the same problem and that was my solution.

public class IndexModel : PageModel
{
    private readonly ILogger<IndexModel> _logger;
    [BindProperty]
    public IndexSubscribeInputModel SubscribeInput { get; set; }
    [BindProperty]
    public IndexContactInputModel ContactInput { get; set; }
    public IndexModel(ILogger<IndexModel> logger)
    {
        _logger = logger;
    }

    public void OnGet()
    {
        SubscribeInput = new IndexSubscribeInputModel();
        ContactInput = new IndexContactInputModel();
    }

    public void OnPostSubscribe()
    {
        if (IsValid(SubscribeInput))
        {
            return;
        }
    }

    public void OnPostContact()
    {
        if (IsValid(ContactInput))
        {
            return;
        }
    }

    public class IndexSubscribeInputModel
    {
        [Required(AllowEmptyStrings =false, ErrorMessage ="{0} é obrigatório!")]
        public string Email { get; set; }
    }
    public class IndexContactInputModel
    {
        [Required(AllowEmptyStrings = false, ErrorMessage = "{0} é obrigatório!")]
        public string Email { get; set; }

        [Required(AllowEmptyStrings = false, ErrorMessage = "{0} é obrigatório!")]
        public string Message { get; set; }
    }

    private bool IsValid<T>(T inputModel)
    {
        var property = this.GetType().GetProperties().Where(x => x.PropertyType == inputModel.GetType()).FirstOrDefault();

        var hasErros = ModelState.Values
            .Where(value => value.GetType().GetProperty("Key").GetValue(value).ToString().Contains(property.Name))
            .Any(value =>value.Errors.Any());

        return !hasErros;
    }

I'll probably put the "IsValid" method in a PageModelExtensions class, so it'll be more fancy.

Hope this help someone...