Skip to content

Commit 417ce3b

Browse files
[release/8.0-preview6] [Blazor] Form mapping error handling and validation integration for SSR Blazor (#49031)
Backport of #48990 to release/8.0-preview6 /cc @javiercn # [Blazor] Form mapping error handling and validation integration for SSR Blazor Adds support for error handling and validation to Server Side Rendered Blazor. ## Description Integrates the form data binding experience with Blazor and provides support for error handling. Fixes #46983 (in this specific format) ## Customer Impact This is one of the remaining major functionalities for Blazor Server Side rendering, so we want to make sure we can get as much feedback as possible on the experience. Specially since we are planning an app building exercise. ## Regression? - [ ] Yes - [X] No [If yes, specify the version the behavior has regressed from] ## Risk - [ ] High - [ ] Medium - [X] Low The changes should be additive, and we've included a significant amount of test coverage for the new functionality. ## Verification - [X] Manual (required) - [X] Automated ## Packaging changes reviewed? - [ ] Yes - [ ] No - [X] N/A ---- ## When servicing release/2.1 - [ ] Make necessary changes in eng/PatchConfig.props
1 parent 7d75f00 commit 417ce3b

File tree

59 files changed

+2684
-463
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+2684
-463
lines changed

src/Components/Authorization/test/AuthorizeRouteViewTest.cs

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -472,20 +472,7 @@ public TestNavigationManager()
472472

473473
private class TestFormValueSupplier : IFormValueSupplier
474474
{
475-
public bool CanBind(string formName, Type valueType)
476-
{
477-
return false;
478-
}
479-
480-
public bool CanConvertSingleValue(Type type)
481-
{
482-
return false;
483-
}
484-
485-
public bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object boundValue)
486-
{
487-
boundValue = null;
488-
return false;
489-
}
475+
public bool CanBind(Type valueType, string formName = null) => false;
476+
public void Bind(FormValueSupplierContext context) { }
490477
}
491478
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Components;
5+
6+
/// <summary>
7+
/// An error that occurred during the form mapping process.
8+
/// </summary>
9+
public class BindingError
10+
{
11+
private static readonly char[] Separators = new char[] { '.', '[' };
12+
private readonly List<FormattableString> _errorMessages;
13+
14+
/// <summary>
15+
/// Initializes a new instance of <see cref="BindingError"/>.
16+
/// </summary>
17+
/// <param name="path">The path from the root of the binding operation to the property or element that failed to bind.</param>
18+
/// <param name="errorMessages">The error messages associated with the binding error.</param>
19+
/// <param name="attemptedValue">The attempted value that failed to bind.</param>
20+
internal BindingError(string path, List<FormattableString> errorMessages, string? attemptedValue)
21+
{
22+
_errorMessages = errorMessages;
23+
AttemptedValue = attemptedValue;
24+
Path = path;
25+
Name = GetName(Path);
26+
}
27+
28+
/// <summary>
29+
/// Gets or sets the instance that contains the property or element that failed to bind.
30+
/// </summary>
31+
/// <remarks>
32+
/// For object models, this is the instance of the object that contains the property that failed to bind.
33+
/// For collection models, this is the collection instance that contains the element that failed to bind.
34+
/// For dictionaries, this is the dictionary instance that contains the element that failed to bind.
35+
/// </remarks>
36+
public object Container { get; internal set; } = null!;
37+
38+
/// <summary>
39+
/// Gets or sets the name of the property or element that failed to bind.
40+
/// </summary>
41+
public string Name { get; }
42+
43+
/// <summary>
44+
/// Gets or sets the full path from the model root to the property or element that failed to bind.
45+
/// </summary>
46+
public string Path { get; }
47+
48+
/// <summary>
49+
/// Gets the list of error messages associated with the binding errors for this field.
50+
/// </summary>
51+
public IReadOnlyList<FormattableString> ErrorMessages => _errorMessages;
52+
53+
/// <summary>
54+
/// Gets the attempted value that failed to bind (if any).
55+
/// </summary>
56+
public string? AttemptedValue { get; }
57+
58+
private static string GetName(string path)
59+
{
60+
var errorKey = path;
61+
var lastSeparatorIndex = path.LastIndexOfAny(Separators);
62+
if (lastSeparatorIndex >= 0)
63+
{
64+
if (path[lastSeparatorIndex] == '[')
65+
{
66+
var closingBracket = path.IndexOf(']', lastSeparatorIndex);
67+
// content within brackets
68+
errorKey = path[(lastSeparatorIndex + 1)..closingBracket];
69+
}
70+
else
71+
{
72+
errorKey = path[(lastSeparatorIndex + 1)..];
73+
}
74+
}
75+
76+
return errorKey;
77+
}
78+
79+
internal void AddError(FormattableString error)
80+
{
81+
_errorMessages.Add(error);
82+
}
83+
}

src/Components/Components/src/Binding/CascadingModelBinder.cs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,8 @@ _bindingContext is null ||
132132
throw new InvalidOperationException($"'{nameof(CascadingModelBinder)}' 'Name' can't change after initialized.");
133133
}
134134

135-
_bindingContext = new ModelBindingContext(name, bindingId, CanBind);
135+
_bindingContext = new ModelBindingContext(name, bindingId);
136+
ParentContext?.SetErrors(name, _bindingContext);
136137
}
137138

138139
string GenerateBindingContextId(string name)
@@ -141,19 +142,6 @@ string GenerateBindingContextId(string name)
141142
var hashIndex = bindingId.IndexOf('#');
142143
return hashIndex == -1 ? bindingId : new string(bindingId.AsSpan(0, hashIndex));
143144
}
144-
145-
bool CanBind(Type type)
146-
{
147-
foreach (var provider in ModelBindingProviders)
148-
{
149-
if (provider.SupportsParameterType(type))
150-
{
151-
return true;
152-
}
153-
}
154-
155-
return false;
156-
}
157145
}
158146

159147
bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterInfo)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Components.Binding;
5+
6+
/// <summary>
7+
/// Context for binding a form value.
8+
/// </summary>
9+
public class FormValueSupplierContext
10+
{
11+
private bool _resultSet;
12+
13+
/// <summary>
14+
/// Initializes a new instance of <see cref="FormValueSupplierContext"/>.
15+
/// </summary>
16+
/// <param name="formName">The name of the form to bind data from.</param>
17+
/// <param name="valueType">The <see cref="Type"/> of the value to bind.</param>
18+
/// <param name="parameterName">The name of the parameter to bind data to.</param>
19+
public FormValueSupplierContext(
20+
string formName,
21+
Type valueType,
22+
string parameterName)
23+
{
24+
ArgumentNullException.ThrowIfNull(formName, nameof(formName));
25+
ArgumentNullException.ThrowIfNull(valueType, nameof(valueType));
26+
ArgumentNullException.ThrowIfNull(parameterName, nameof(parameterName));
27+
FormName = formName;
28+
ParameterName = parameterName;
29+
ValueType = valueType;
30+
}
31+
32+
/// <summary>
33+
/// Gets the name of the form to bind data from.
34+
/// </summary>
35+
public string FormName { get; }
36+
37+
/// <summary>
38+
/// Gets the name of the parameter to bind data to.
39+
/// </summary>
40+
public string ParameterName { get; }
41+
42+
/// <summary>
43+
/// Gets the <see cref="Type"/> of the value to bind.
44+
/// </summary>
45+
public Type ValueType { get; }
46+
47+
/// <summary>
48+
/// Gets the callback to invoke when an error occurs.
49+
/// </summary>
50+
public Action<string, FormattableString, string?>? OnError { get; set; }
51+
52+
/// <summary>
53+
/// Maps a set of errors to a concrete containing instance.
54+
/// </summary>
55+
/// <remarks>
56+
/// For example, maps errors for a given property in a class to the class instance.
57+
/// This is required so that validation can work without the need of the full identifier.
58+
/// </remarks>
59+
public Action<string, object>? MapErrorToContainer { get; set; }
60+
61+
/// <summary>
62+
/// Gets the result of the binding operation.
63+
/// </summary>
64+
public object? Result { get; private set; }
65+
66+
/// <summary>
67+
/// Sets the result of the binding operation.
68+
/// </summary>
69+
/// <param name="result">The result of the binding operation.</param>
70+
/// <exception cref="InvalidOperationException">Thrown if the result has already been set.</exception>
71+
public void SetResult(object? result)
72+
{
73+
if (_resultSet)
74+
{
75+
throw new InvalidOperationException($"The result has already been set to '{Result}'.");
76+
}
77+
_resultSet = true;
78+
Result = result;
79+
}
80+
}
Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Diagnostics.CodeAnalysis;
5-
64
namespace Microsoft.AspNetCore.Components.Binding;
75

86
/// <summary>
@@ -13,25 +11,14 @@ public interface IFormValueSupplier
1311
/// <summary>
1412
/// Determines whether the specified value type can be bound.
1513
/// </summary>
16-
/// <param name="formName">The form name to bind data from.</param>
1714
/// <param name="valueType">The <see cref="Type"/> for the value to bind.</param>
15+
/// <param name="formName">The form name to bind data from or null to only validate the type can be bound.</param>
1816
/// <returns><c>true</c> if the value type can be bound; otherwise, <c>false</c>.</returns>
19-
bool CanBind(string formName, Type valueType);
17+
bool CanBind(Type valueType, string? formName = null);
2018

2119
/// <summary>
22-
/// Determines whether a given <see cref="Type"/> can be converted from a single string value.
23-
/// For example, strings, numbers, boolean values, enums, guids, etc. fall in this category.
20+
/// Binds the form with the specified name to a value of the specified type.
21+
/// <param name="context">The <see cref="FormValueSupplierContext"/>.</param>
2422
/// </summary>
25-
/// <param name="type">The <see cref="Type"/> to check.</param>
26-
/// <returns><c>true</c> if the type can be converted from a single string value; otherwise, <c>false</c>.</returns>
27-
bool CanConvertSingleValue(Type type);
28-
29-
/// <summary>
30-
/// Tries to bind the form with the specified name to a value of the specified type.
31-
/// </summary>
32-
/// <param name="formName">The form name to bind data from.</param>
33-
/// <param name="valueType">The <see cref="Type"/> for the value to bind.</param>
34-
/// <param name="boundValue">The bound value if succeeded.</param>
35-
/// <returns><c>true</c> if the form was bound successfully; otherwise, <c>false</c>.</returns>
36-
bool TryBind(string formName, Type valueType, [NotNullWhen(true)] out object? boundValue);
23+
void Bind(FormValueSupplierContext context);
3724
}

0 commit comments

Comments
 (0)