Reputation: 1447
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!
Upvotes: 21
Views: 16313
Reputation: 20957
No hacks:
MyPage.cshtml.cs
public FooDto Foo { get; set; } // do not use `[BindProperty]`
public BarDto Bar { get; set; } // ''
public async Task<IActionResult> OnGet()
{
Foo = new FooDto(await LoadFoo());
Bar = new BarDto(await LoadBar());
}
public async Task<IActionResult> OnPostFoo(FooDto foo)
{
Debug.Assert(nameof(foo).ToLower() == nameof(Foo).ToLower());
if (!ModelState.IsValid) return Page();
// use `foo` rather than `Foo` ...
}
public async Task<IActionResult> OnPostBar(BarDto bar)
{
Debug.Assert(nameof(bar).ToLower() == nameof(Bar).ToLower());
if (!ModelState.IsValid) return Page();
// use `bar` rather than `Bar` ...
}
MyPage.cshtml
<form method="post" asp-page-handler=(nameof(MyPageModel.OnPostFoo).Replace("OnPost", ""))>
<!-- use `Model.Foo` ... -->
<button type="submit">Save</button>
</form>
<form method="post" asp-page-handler=(nameof(MyPageModel.OnPostBar).Replace("OnPost", ""))>
<!-- use `Model.Bar` ... -->
<button type="submit">Save</button>
</form>
Notes:
[BindProperty]
as that is the source of the problem; rather bind them within the handler methodsasp-page-handler
; use strong typing to ensure they always matchUpvotes: 0
Reputation: 19
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...
Upvotes: 1
Reputation: 131
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();
}
// ...
}
}
Upvotes: 3
Reputation: 1727
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.
Upvotes: 4
Reputation: 2983
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();
}
Upvotes: 5