Skip to content

Use the unified validation API for Blazor forms #62045

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 145 additions & 3 deletions src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Reflection;
using System.Reflection.Metadata;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Microsoft.AspNetCore.Http.Validation;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;

[assembly: MetadataUpdateHandler(typeof(Microsoft.AspNetCore.Components.Forms.EditContextDataAnnotationsExtensions))]

Expand All @@ -15,7 +21,7 @@
/// <summary>
/// Extension methods to add DataAnnotations validation to an <see cref="EditContext"/>.
/// </summary>
public static class EditContextDataAnnotationsExtensions
public static partial class EditContextDataAnnotationsExtensions
{
/// <summary>
/// Adds DataAnnotations validation support to the <see cref="EditContext"/>.
Expand Down Expand Up @@ -59,7 +65,7 @@
}
#pragma warning restore IDE0051 // Remove unused private members

private sealed class DataAnnotationsEventSubscriptions : IDisposable
private sealed partial class DataAnnotationsEventSubscriptions : IDisposable
{
private static readonly ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo?> _propertyInfoCache = new();

Expand All @@ -82,6 +88,7 @@
}
}

// TODO(OR): Should this also use ValidatablePropertyInfo.ValidateAsync?
[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Model types are expected to be defined in assemblies that do not get trimmed.")]
private void OnFieldChanged(object? sender, FieldChangedEventArgs eventArgs)
{
Expand Down Expand Up @@ -112,6 +119,18 @@
private void OnValidationRequested(object? sender, ValidationRequestedEventArgs e)
{
var validationContext = new ValidationContext(_editContext.Model, _serviceProvider, items: null);

if (!TryValidateTypeInfo(validationContext))
{
ValidateWithDefaultValidator(validationContext);
}

_editContext.NotifyValidationStateChanged();
}

[UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Model types are expected to be defined in assemblies that do not get trimmed.")]
private void ValidateWithDefaultValidator(ValidationContext validationContext)
{
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(_editContext.Model, validationContext, validationResults, true);

Expand All @@ -136,8 +155,125 @@
_messages.Add(new FieldIdentifier(_editContext.Model, fieldName: string.Empty), validationResult.ErrorMessage!);
}
}
}

_editContext.NotifyValidationStateChanged();
#pragma warning disable ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
private bool TryValidateTypeInfo(ValidationContext validationContext)
{
var options = _serviceProvider?.GetService<IOptions<ValidationOptions>>()?.Value;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be wrong, but I don't think we need the null check here.

I believe IOptions will always resolve a non-null value (we can assume that services.AddOptions has been called as its a dependency we have). I also believe that .Value will always be populated with a default instance


if (options == null || !options.TryGetValidatableTypeInfo(_editContext.Model.GetType(), out var typeInfo))
{
return false;
}

var validateContext = new ValidateContext
{
ValidationOptions = options,
ValidationContext = validationContext,
};

var containerMapping = new Dictionary<string, object?>();

validateContext.OnValidationError += (key, _, container) => containerMapping[key] = container;

var validationTask = typeInfo.ValidateAsync(_editContext.Model, validateContext, CancellationToken.None);

if (!validationTask.IsCompleted)
{
throw new InvalidOperationException("Async validation is not supported");
}

var validationErrors = validateContext.ValidationErrors;

// Transfer results to the ValidationMessageStore
_messages.Clear();

if (validationErrors is not null && validationErrors.Count > 0)
{
foreach (var (fieldKey, messages) in validationErrors)
{
// Reverse mapping based on storing references during validation.
// With this approach, we could skip iterating over ValidateContext.ValidationErrors and pass the errors
// directly to ValidationMessageStore in the OnValidationError handler.
var fieldContainer = containerMapping[fieldKey] ?? _editContext.Model;

// Alternative: Reverse mapping based on object graph walk.
//var fieldContainer = GetFieldContainer(_editContext.Model, fieldKey);

var lastDotIndex = fieldKey.LastIndexOf('.');
var fieldName = lastDotIndex >= 0 ? fieldKey[(lastDotIndex + 1)..] : fieldKey;

_messages.Add(new FieldIdentifier(fieldContainer, fieldName), messages);
}
}

return true;
}
#pragma warning restore ASP0029 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.

// TODO(OR): Replace this with a more robust implementation or a different approach. E.g. collect references during the validation process itself.
[UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Model types are expected to be defined in assemblies that do not get trimmed.")]
private static object GetFieldContainer(object obj, string fieldKey)

Check failure on line 217 in src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Source-Build (Managed))

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L217

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs(217,31): error IDE0051: (NETCORE_ENGINEERING_TELEMETRY=Build) Private member 'EditContextDataAnnotationsExtensions.DataAnnotationsEventSubscriptions.GetFieldContainer' is unused (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051)

Check failure on line 217 in src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux ARM)

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L217

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs(217,31): error IDE0051: (NETCORE_ENGINEERING_TELEMETRY=Build) Private member 'EditContextDataAnnotationsExtensions.DataAnnotationsEventSubscriptions.GetFieldContainer' is unused (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051)

Check failure on line 217 in src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux Musl x64)

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L217

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs(217,31): error IDE0051: (NETCORE_ENGINEERING_TELEMETRY=Build) Private member 'EditContextDataAnnotationsExtensions.DataAnnotationsEventSubscriptions.GetFieldContainer' is unused (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051)

Check failure on line 217 in src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux ARM64)

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L217

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs(217,31): error IDE0051: (NETCORE_ENGINEERING_TELEMETRY=Build) Private member 'EditContextDataAnnotationsExtensions.DataAnnotationsEventSubscriptions.GetFieldContainer' is unused (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051)

Check failure on line 217 in src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux x64)

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L217

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs(217,31): error IDE0051: (NETCORE_ENGINEERING_TELEMETRY=Build) Private member 'EditContextDataAnnotationsExtensions.DataAnnotationsEventSubscriptions.GetFieldContainer' is unused (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051)

Check failure on line 217 in src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux Musl ARM)

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L217

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs(217,31): error IDE0051: (NETCORE_ENGINEERING_TELEMETRY=Build) Private member 'EditContextDataAnnotationsExtensions.DataAnnotationsEventSubscriptions.GetFieldContainer' is unused (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051)

Check failure on line 217 in src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: macOS arm64)

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L217

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs(217,31): error IDE0051: (NETCORE_ENGINEERING_TELEMETRY=Build) Private member 'EditContextDataAnnotationsExtensions.DataAnnotationsEventSubscriptions.GetFieldContainer' is unused (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051)

Check failure on line 217 in src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: Linux Musl ARM64)

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L217

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs(217,31): error IDE0051: (NETCORE_ENGINEERING_TELEMETRY=Build) Private member 'EditContextDataAnnotationsExtensions.DataAnnotationsEventSubscriptions.GetFieldContainer' is unused (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051)

Check failure on line 217 in src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Build: macOS x64)

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L217

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs(217,31): error IDE0051: (NETCORE_ENGINEERING_TELEMETRY=Build) Private member 'EditContextDataAnnotationsExtensions.DataAnnotationsEventSubscriptions.GetFieldContainer' is unused (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051)

Check failure on line 217 in src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr (Tests: Ubuntu x64)

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L217

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs(217,31): error IDE0051: (NETCORE_ENGINEERING_TELEMETRY=Build) Private member 'EditContextDataAnnotationsExtensions.DataAnnotationsEventSubscriptions.GetFieldContainer' is unused (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051)

Check failure on line 217 in src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr (Tests: macOS)

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L217

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs(217,31): error IDE0051: (NETCORE_ENGINEERING_TELEMETRY=Build) Private member 'EditContextDataAnnotationsExtensions.DataAnnotationsEventSubscriptions.GetFieldContainer' is unused (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051)

Check failure on line 217 in src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Test: Ubuntu x64)

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L217

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs(217,31): error IDE0051: (NETCORE_ENGINEERING_TELEMETRY=Build) Private member 'EditContextDataAnnotationsExtensions.DataAnnotationsEventSubscriptions.GetFieldContainer' is unused (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051)

Check failure on line 217 in src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci (Build Test: macOS)

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L217

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs(217,31): error IDE0051: (NETCORE_ENGINEERING_TELEMETRY=Build) Private member 'EditContextDataAnnotationsExtensions.DataAnnotationsEventSubscriptions.GetFieldContainer' is unused (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051)

Check failure on line 217 in src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L217

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs(217,31): error IDE0051: (NETCORE_ENGINEERING_TELEMETRY=Build) Private member 'EditContextDataAnnotationsExtensions.DataAnnotationsEventSubscriptions.GetFieldContainer' is unused (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051)

Check failure on line 217 in src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-quarantined-pr

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L217

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs(217,31): error IDE0051: (NETCORE_ENGINEERING_TELEMETRY=Build) Private member 'EditContextDataAnnotationsExtensions.DataAnnotationsEventSubscriptions.GetFieldContainer' is unused (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051)

Check failure on line 217 in src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L217

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs(217,31): error IDE0051: (NETCORE_ENGINEERING_TELEMETRY=Build) Private member 'EditContextDataAnnotationsExtensions.DataAnnotationsEventSubscriptions.GetFieldContainer' is unused (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051)

Check failure on line 217 in src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L217

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs(217,31): error IDE0051: (NETCORE_ENGINEERING_TELEMETRY=Build) Private member 'EditContextDataAnnotationsExtensions.DataAnnotationsEventSubscriptions.GetFieldContainer' is unused (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051)

Check failure on line 217 in src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs

View check run for this annotation

Azure Pipelines / aspnetcore-ci

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L217

src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs(217,31): error IDE0051: (NETCORE_ENGINEERING_TELEMETRY=Build) Private member 'EditContextDataAnnotationsExtensions.DataAnnotationsEventSubscriptions.GetFieldContainer' is unused (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide0051)
{
// The method does not check all possible null access and index bound errors as the path is constructed internally and assumed to be correct.
var dotSegments = fieldKey.Split('.')[..^1];
var currentObject = obj;

for (int i = 0; i < dotSegments.Length; i++)
{
string segment = dotSegments[i];

if (currentObject == null)
{
string traversedPath = string.Join(".", dotSegments.Take(i));
throw new ArgumentException($"Cannot access segment '{segment}' because the path '{traversedPath}' resolved to null.");
}

Match match = _pathSegmentRegex.Match(segment);
if (!match.Success)
{
throw new ArgumentException($"Invalid path segment: '{segment}'.");
}

string propertyName = match.Groups[1].Value;
string? indexStr = match.Groups[2].Success ? match.Groups[2].Value : null;

Type currentType = currentObject.GetType();
PropertyInfo propertyInfo = currentType!.GetProperty(propertyName, BindingFlags.Public | BindingFlags.Instance)!;
object propertyValue = propertyInfo!.GetValue(currentObject)!;

if (indexStr == null) // Simple property access
{
currentObject = propertyValue;
}
else // Indexed access
{
if (!int.TryParse(indexStr, out int index))
{
throw new ArgumentException($"Invalid index '{indexStr}' in segment '{segment}'.");
}

if (propertyValue is Array array)
{
currentObject = array.GetValue(index)!;
}
else if (propertyValue is IList list)
{
currentObject = list[index]!;
}
else if (propertyValue is IEnumerable enumerable)
{
currentObject = enumerable.Cast<object>().ElementAt(index);
}
else
{
throw new ArgumentException($"Property '{propertyName}' is not an array, list, or enumerable. Cannot access by index in segment '{segment}'.");
}
}

}
return currentObject!;
Comment on lines +218 to +276
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For a prototype this is "ok", but we essentially have two options here. Either:

  • We are able to get the container from the validation process as we walk the tree.
  • We do something like what MVC does.
    • Process the string into a System.Linq.Expression, compile it and cache it (and I really don't want us to do this if we can avoid it).

}

public void Dispose()
Expand Down Expand Up @@ -174,5 +310,11 @@
{
_propertyInfoCache.Clear();
}

private static readonly Regex _pathSegmentRegex = PathSegmentRegexGen();

// Regex to parse "PropertyName" or "PropertyName[index]"
[GeneratedRegex(@"^([a-zA-Z_]\w*)(?:\[(\d+)\])?$", RegexOptions.Compiled)]
private static partial Regex PathSegmentRegexGen();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Components" />
<Reference Include="Microsoft.AspNetCore.Http.Abstractions" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is problematic.

We can't depend on stuff that brings in HttpContext and a bunch of other framework related types. Is there anything in the validation stuff that requires things from Microsoft.AspNetCore.Http?

This is more of a question for @captainsafia

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Per our offline discussion, let's create a new Microsoft.Extensions.Validation package under src/Validation and move things into it there.

Happy to help if you need any guidance moving the testing infrastructure...

We'll also want to consider whether the source generator goes into the extensions package or not. My inclination is to say it does. In that case, we need to add more sanity checks to it to handle cases where the package might be referenced outside the context of ASP.NET Core. Specifically, this would just be better fallbacks for places where it looks for symbols that are defined in ASP.NET Core namespaces...

</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<IsShippingPackage>false</IsShippingPackage>
<Nullable>enable</Nullable>

<InterceptorsNamespaces>$(InterceptorsNamespaces);Microsoft.AspNetCore.Http.Validation.Generated</InterceptorsNamespaces>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully, you won't need this soon once we take a new SDK update and absorb dotnet/sdk#48891.

<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\BlazorUnitedApp.Client\BlazorUnitedApp.Client.csproj" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="$(RepoRoot)/src/Http/Http.Extensions/gen/Microsoft.AspNetCore.Http.ValidationsGenerator/Microsoft.AspNetCore.Http.ValidationsGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>

<ItemGroup>
<Reference Include="Microsoft.AspNetCore" />
<Reference Include="Microsoft.AspNetCore.Components.Endpoints" />
Expand Down
163 changes: 163 additions & 0 deletions src/Components/Samples/BlazorUnitedApp/Pages/ComplexFormPage.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
@page "/complex-form"
@rendermode InteractiveServer

@using System.ComponentModel.DataAnnotations
@using BlazorUnitedApp.Validation

<PageTitle>Validated Order Form</PageTitle>

<EditForm Model="@order" OnValidSubmit="@HandleValidSubmit" OnInvalidSubmit="@HandleInvalidSubmit">
<DataAnnotationsValidator />

<div class="container mt-4">
<h4>Order Details</h4>
<div class="mb-3">
<label for="orderName" class="form-label">Order Name</label>
<InputText id="orderName" @bind-Value="order.OrderName" class="form-control" />
<ValidationMessage For="@(() => order.OrderName)" />
</div>

<hr />

<h4>Customer Details</h4>
<div class="card mb-3">
<div class="card-body">
<div class="mb-3">
<label for="customerFullName" class="form-label">Full Name</label>
<InputText id="customerFullName" @bind-Value="order.CustomerDetails.FullName" class="form-control" />
<ValidationMessage For="@(() => order.CustomerDetails.FullName)" />
</div>
<div class="mb-3">
<label for="customerEmail" class="form-label">Email</label>
<InputText id="customerEmail" @bind-Value="order.CustomerDetails.Email" class="form-control" />
<ValidationMessage For="@(() => order.CustomerDetails.Email)" />
</div>

<h5>Shipping Address</h5>
<div class="card mb-3">
<div class="card-body">
<div class="row">
<div class="mb-3 col-sm-8">
<label for="shippingStreet" class="form-label">Street</label>
<InputText id="shippingStreet" @bind-Value="order.CustomerDetails.ShippingAddress.Street" class="form-control" />
<ValidationMessage For="@(() => order.CustomerDetails.ShippingAddress.Street)" />
</div>
<div class="mb-3 col-sm">
<label for="shippingZipCode" class="form-label">Zip Code</label>
<InputText id="shippingZipCode" @bind-Value="order.CustomerDetails.ShippingAddress.ZipCode" class="form-control" />
<ValidationMessage For="@(() => order.CustomerDetails.ShippingAddress.ZipCode)" />
</div>
</div>
</div>
</div>
</div>
</div>

<hr />

<h4>Order Items</h4>
@if (order.OrderItems.Any())
{
for (int i = 0; i < order.OrderItems.Count; i++)
{
var itemIndex = i;
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Item @(itemIndex + 1)</span>
<button type="button" class="btn btn-sm btn-danger" @onclick="() => RemoveOrderItem(itemIndex)">Remove</button>
</div>
<div class="card-body">
<div class="row">
<div class="mb-3 col-sm-8">
<label for="@($"productName_{itemIndex}")" class="form-label">Product Name</label>
<InputText id="@($"productName_{itemIndex}")" @bind-Value="order.OrderItems[itemIndex].ProductName" class="form-control" />
<ValidationMessage For="@(() => order.OrderItems[itemIndex].ProductName)" />
</div>
<div class="mb-3 col-sm">
<label for="@($"quantity_{itemIndex}")" class="form-label">Quantity</label>
<InputNumber id="@($"quantity_{itemIndex}")" @bind-Value="order.OrderItems[itemIndex].Quantity" class="form-control" />
<ValidationMessage For="@(() => order.OrderItems[itemIndex].Quantity)" />
</div>
</div>
</div>
</div>
}
}
else
{
<p>No order items. Add one below.</p>
}

<button type="button" class="btn btn-success mb-3" @onclick="AddOrderItem">Add Order Item</button>

<hr />

<div class="mb-3">
<button type="submit" class="btn btn-primary">Submit Order</button>
</div>

<ValidationSummary />
</div>
</EditForm>

@if (submitted)
{
<div class="mt-4 alert alert-success" role="alert">
<h4>Form Submitted Successfully!</h4>
<p>Order Name: @order.OrderName</p>
<p>Customer: @order.CustomerDetails.FullName (@order.CustomerDetails.Email)</p>
<h5>Order Items:</h5>
<ul>
@foreach (var item in order.OrderItems)
{
<li>@item.Quantity x @item.ProductName</li>
}
</ul>
</div>
}

@if (submitFailed)
{
<div class="mt-4 alert alert-danger" role="alert">
<h4>Form Submission Failed!</h4>
<p>Please correct the validation errors and try again.</p>
</div>
}


@code {
private OrderModel order = new OrderModel();
private bool submitted = false;
private bool submitFailed = false;

private void HandleValidSubmit()
{
Console.WriteLine("Form submitted successfully!");
submitted = true;
submitFailed = false;
}

private void HandleInvalidSubmit()
{
Console.WriteLine("Form submission failed due to validation errors.");
submitted = false;
submitFailed = true;
}

private void AddOrderItem()
{
order.OrderItems.Add(new OrderItemModel());
submitted = false;
submitFailed = false;
}

private void RemoveOrderItem(int index)
{
if (index >= 0 && index < order.OrderItems.Count)
{
order.OrderItems.RemoveAt(index);
}
submitted = false;
submitFailed = false;
}
}
Loading
Loading