Skip to content

Commit c2e3f46

Browse files
javiercngithub-actions
authored andcommitted
Update GetError* APIs to use BindingError
1 parent 2908d40 commit c2e3f46

File tree

9 files changed

+142
-76
lines changed

9 files changed

+142
-76
lines changed
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/ModelBindingContext.cs

Lines changed: 14 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@ public sealed class ModelBindingContext
1111
private Dictionary<string, BindingError>? _errors;
1212
private List<KeyValuePair<string, BindingError>>? _pendingErrors;
1313
private Dictionary<string, Dictionary<string, BindingError>>? _errorsByFormName;
14-
private static readonly char[] Separators = new char[] { '.', '[' };
1514

16-
internal ModelBindingContext(string name, string bindingContextId)
15+
internal ModelBindingContext(string name, string bindingContextId)
1716
{
1817
ArgumentNullException.ThrowIfNull(name);
1918
ArgumentNullException.ThrowIfNull(bindingContextId);
@@ -45,66 +44,48 @@ internal ModelBindingContext(string name, string bindingContextId)
4544
/// </summary>
4645
/// <param name="key">The key used to identify the specific part of the model.</param>
4746
/// <returns>The list of errors associated with that part of the model if any.</returns>
48-
public IReadOnlyList<FormattableString> GetErrors(string key) =>
49-
_errors?.TryGetValue(key, out var bindingError) == true ? bindingError.ErrorMessages : Array.Empty<FormattableString>();
47+
public BindingError? GetErrors(string key) =>
48+
_errors?.TryGetValue(key, out var bindingError) == true ? bindingError : null;
5049

5150
/// <summary>
5251
/// Retrieves the list of errors for a given model key.
5352
/// </summary>
5453
/// <param name="key">The key used to identify the specific part of the model.</param>
5554
/// <param name="formName">Form name for a form under this context.</param>
5655
/// <returns>The list of errors associated with that part of the model if any.</returns>
57-
public IReadOnlyList<FormattableString> GetErrors(string formName, string key) =>
56+
public BindingError? GetErrors(string formName, string key) =>
5857
_errorsByFormName?.TryGetValue(formName, out var formErrors) == true &&
59-
formErrors.TryGetValue(key, out var bindingError) == true ? bindingError.ErrorMessages : Array.Empty<FormattableString>();
58+
formErrors.TryGetValue(key, out var bindingError) == true ? bindingError : null;
6059

6160
/// <summary>
6261
/// Retrieves all the errors for the model.
6362
/// </summary>
6463
/// <returns>The list of errors associated with the model if any.</returns>
65-
public IEnumerable<KeyValuePair<(object, string), IReadOnlyList<FormattableString>>> GetAllErrors()
64+
public IEnumerable<BindingError> GetAllErrors()
6665
{
6766
return GetAllErrorsCore(_errors);
6867
}
6968

70-
private static IEnumerable<KeyValuePair<(object, string), IReadOnlyList<FormattableString>>> GetAllErrorsCore(Dictionary<string, BindingError>? errors)
69+
private static IEnumerable<BindingError> GetAllErrorsCore(Dictionary<string, BindingError>? errors)
7170
{
7271
if (errors == null)
7372
{
74-
yield break;
73+
return Array.Empty<BindingError>();
7574
}
7675

77-
foreach (var (key, value) in errors)
78-
{
79-
var errorKey = key;
80-
var lastSeparatorIndex = key.LastIndexOfAny(Separators);
81-
if (lastSeparatorIndex >= 0)
82-
{
83-
if (key[lastSeparatorIndex] == '[')
84-
{
85-
var closingBracket = key.IndexOf(']', lastSeparatorIndex);
86-
// content within brackets
87-
errorKey = key[(lastSeparatorIndex + 1)..closingBracket];
88-
}
89-
else
90-
{
91-
errorKey = key[(lastSeparatorIndex + 1)..];
92-
}
93-
}
94-
yield return new KeyValuePair<(object, string), IReadOnlyList<FormattableString>>((value.Parent, errorKey), value.ErrorMessages);
95-
}
76+
return errors.Values;
9677
}
9778

9879
/// <summary>
9980
/// Retrieves all the errors for the model.
10081
/// </summary>
10182
/// <param name="formName">Form name for a form under this context.</param>
10283
/// <returns>The list of errors associated with the model if any.</returns>
103-
public IEnumerable<KeyValuePair<(object, string), IReadOnlyList<FormattableString>>> GetAllErrors(string formName)
84+
public IEnumerable<BindingError> GetAllErrors(string formName)
10485
{
10586
return _errorsByFormName?.TryGetValue(formName, out var formErrors) == true ?
10687
GetAllErrorsCore(formErrors) :
107-
Array.Empty<KeyValuePair<(object, string), IReadOnlyList<FormattableString>>>();
88+
Array.Empty<BindingError>();
10889
}
10990

11091
/// <summary>
@@ -138,14 +119,14 @@ private static void AddErrorCore(Dictionary<string, BindingError> errors, string
138119
{
139120
if (!errors.TryGetValue(key, out var bindingError))
140121
{
141-
bindingError = new BindingError(new List<FormattableString>() { error }, attemptedValue);
122+
bindingError = new BindingError(key, new List<FormattableString>() { error }, attemptedValue);
142123
errors.Add(key, bindingError);
143124
pendingErrors ??= new();
144125
pendingErrors.Add(new KeyValuePair<string, BindingError>(key, bindingError));
145126
}
146127
else
147128
{
148-
bindingError.ErrorMessages.Add(error);
129+
bindingError.AddError(error);
149130
}
150131
}
151132

@@ -175,7 +156,7 @@ internal void AttachParentValue(string key, object value)
175156
throw new InvalidOperationException($"'{errorKey}' does must start with '{key}'");
176157
}
177158

178-
error.Parent = value;
159+
error.Container = value;
179160
}
180161

181162
_pendingErrors.Clear();
@@ -190,11 +171,4 @@ internal void SetErrors(string formName, ModelBindingContext childContext)
190171

191172
childContext._errors = formErrors;
192173
}
193-
194-
private class BindingError(List<FormattableString> errorMessages, string? attemptedValue)
195-
{
196-
public object Parent { get; set; } = null!;
197-
public List<FormattableString> ErrorMessages { get; } = errorMessages;
198-
public string? AttemptedValue { get; } = attemptedValue;
199-
}
200174
}

src/Components/Components/src/PublicAPI.Unshipped.txt

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ Microsoft.AspNetCore.Components.Binding.FormValueSupplierContext.ValueType.get -
2525
Microsoft.AspNetCore.Components.Binding.IFormValueSupplier
2626
Microsoft.AspNetCore.Components.Binding.IFormValueSupplier.Bind(Microsoft.AspNetCore.Components.Binding.FormValueSupplierContext! context) -> void
2727
Microsoft.AspNetCore.Components.Binding.IFormValueSupplier.CanBind(System.Type! valueType, string? formName = null) -> bool
28+
Microsoft.AspNetCore.Components.BindingError
29+
Microsoft.AspNetCore.Components.BindingError.AttemptedValue.get -> string?
30+
Microsoft.AspNetCore.Components.BindingError.Container.get -> object!
31+
Microsoft.AspNetCore.Components.BindingError.ErrorMessages.get -> System.Collections.Generic.IReadOnlyList<System.FormattableString!>!
32+
Microsoft.AspNetCore.Components.BindingError.Name.get -> string!
33+
Microsoft.AspNetCore.Components.BindingError.Path.get -> string!
2834
Microsoft.AspNetCore.Components.CascadingModelBinder
2935
Microsoft.AspNetCore.Components.CascadingModelBinder.CascadingModelBinder() -> void
3036
Microsoft.AspNetCore.Components.CascadingModelBinder.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.ModelBindingContext!>!
@@ -45,12 +51,12 @@ Microsoft.AspNetCore.Components.IComponentRenderMode
4551
Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.PersistStateAsync(Microsoft.AspNetCore.Components.IPersistentComponentStateStore! store, Microsoft.AspNetCore.Components.Dispatcher! dispatcher) -> System.Threading.Tasks.Task!
4652
Microsoft.AspNetCore.Components.ModelBindingContext
4753
Microsoft.AspNetCore.Components.ModelBindingContext.BindingContextId.get -> string!
48-
Microsoft.AspNetCore.Components.ModelBindingContext.GetAllErrors() -> System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<(object!, string!), System.Collections.Generic.IReadOnlyList<System.FormattableString!>!>>!
49-
Microsoft.AspNetCore.Components.ModelBindingContext.GetAllErrors(string! formName) -> System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<(object!, string!), System.Collections.Generic.IReadOnlyList<System.FormattableString!>!>>!
54+
Microsoft.AspNetCore.Components.ModelBindingContext.GetAllErrors() -> System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Components.BindingError!>!
55+
Microsoft.AspNetCore.Components.ModelBindingContext.GetAllErrors(string! formName) -> System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Components.BindingError!>!
5056
Microsoft.AspNetCore.Components.ModelBindingContext.GetAttemptedValue(string! formName, string! key) -> string?
5157
Microsoft.AspNetCore.Components.ModelBindingContext.GetAttemptedValue(string! key) -> string?
52-
Microsoft.AspNetCore.Components.ModelBindingContext.GetErrors(string! formName, string! key) -> System.Collections.Generic.IReadOnlyList<System.FormattableString!>!
53-
Microsoft.AspNetCore.Components.ModelBindingContext.GetErrors(string! key) -> System.Collections.Generic.IReadOnlyList<System.FormattableString!>!
58+
Microsoft.AspNetCore.Components.ModelBindingContext.GetErrors(string! formName, string! key) -> Microsoft.AspNetCore.Components.BindingError?
59+
Microsoft.AspNetCore.Components.ModelBindingContext.GetErrors(string! key) -> Microsoft.AspNetCore.Components.BindingError?
5460
Microsoft.AspNetCore.Components.ModelBindingContext.Name.get -> string!
5561
Microsoft.AspNetCore.Components.ParameterView.ToDictionary() -> System.Collections.Generic.IReadOnlyDictionary<string!, object?>!
5662
*REMOVED*Microsoft.AspNetCore.Components.ParameterView.ToDictionary() -> System.Collections.Generic.IReadOnlyDictionary<string!, object!>!

src/Components/Forms/test/ExpressionFormatterTest.cs

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public void Works_MemberAccessOnly()
1515
var result = ExpressionFormatter.FormatLambda(() => person.Parent.Name);
1616

1717
// Assert
18-
Assert.Equal("person.Parent.Name", result);
18+
Assert.Equal("person.Container.Name", result);
1919
}
2020

2121
[Fact]
@@ -28,7 +28,7 @@ public void Works_MemberAccessWithConstIndex()
2828
var result = ExpressionFormatter.FormatLambda(() => person.Parent.Children[3].Name);
2929

3030
// Assert
31-
Assert.Equal("person.Parent.Children[3].Name", result);
31+
Assert.Equal("person.Container.Children[3].Name", result);
3232
}
3333

3434
[Fact]
@@ -45,9 +45,9 @@ public void Works_MemberAccessWithConstIndex_SameLambdaMultipleTimes()
4545
}
4646

4747
// Assert
48-
Assert.Equal("person.Parent.Children[3].Name", result[0]);
49-
Assert.Equal("person.Parent.Children[3].Name", result[1]);
50-
Assert.Equal("person.Parent.Children[3].Name", result[2]);
48+
Assert.Equal("person.Container.Children[3].Name", result[0]);
49+
Assert.Equal("person.Container.Children[3].Name", result[1]);
50+
Assert.Equal("person.Container.Children[3].Name", result[2]);
5151
}
5252

5353
[Fact]
@@ -61,7 +61,7 @@ public void Works_MemberAccessWithVariableIndex()
6161
var result = ExpressionFormatter.FormatLambda(() => person.Parent.Children[i].Name);
6262

6363
// Assert
64-
Assert.Equal("person.Parent.Children[42].Name", result);
64+
Assert.Equal("person.Container.Children[42].Name", result);
6565
}
6666

6767
[Fact]
@@ -79,9 +79,9 @@ public void Works_ForLoopIteratorVariableIndex_Short()
7979
}
8080

8181
// Assert
82-
Assert.Equal("person.Parent.Children[0].Name", result[0]);
83-
Assert.Equal("person.Parent.Children[1].Name", result[1]);
84-
Assert.Equal("person.Parent.Children[2].Name", result[2]);
82+
Assert.Equal("person.Container.Children[0].Name", result[0]);
83+
Assert.Equal("person.Container.Children[1].Name", result[1]);
84+
Assert.Equal("person.Container.Children[2].Name", result[2]);
8585
}
8686

8787
[Fact]
@@ -95,13 +95,13 @@ public void Works_ForLoopIteratorVariableIndex_MultipleClosures()
9595
var result2 = ComputeResult();
9696

9797
// Assert
98-
Assert.Equal("person.Parent.Children[0].Name", result1[0]);
99-
Assert.Equal("person.Parent.Children[1].Name", result1[1]);
100-
Assert.Equal("person.Parent.Children[2].Name", result1[2]);
98+
Assert.Equal("person.Container.Children[0].Name", result1[0]);
99+
Assert.Equal("person.Container.Children[1].Name", result1[1]);
100+
Assert.Equal("person.Container.Children[2].Name", result1[2]);
101101

102-
Assert.Equal("person.Parent.Children[0].Name", result2[0]);
103-
Assert.Equal("person.Parent.Children[1].Name", result2[1]);
104-
Assert.Equal("person.Parent.Children[2].Name", result2[2]);
102+
Assert.Equal("person.Container.Children[0].Name", result2[0]);
103+
Assert.Equal("person.Container.Children[1].Name", result2[1]);
104+
Assert.Equal("person.Container.Children[2].Name", result2[2]);
105105

106106
string[] ComputeResult()
107107
{
@@ -131,9 +131,9 @@ public void Works_ForLoopIteratorVariableIndex_Long()
131131
}
132132

133133
// Assert
134-
Assert.Equal("person.Parent.Parent.Children[0].Parent.Children[0].Children[0].Name", result[0]);
135-
Assert.Equal("person.Parent.Parent.Children[1].Parent.Children[1].Children[1].Name", result[1]);
136-
Assert.Equal("person.Parent.Parent.Children[2].Parent.Children[2].Children[2].Name", result[2]);
134+
Assert.Equal("person.Container.Container.Children[0].Container.Children[0].Children[0].Name", result[0]);
135+
Assert.Equal("person.Container.Container.Children[1].Container.Children[1].Children[1].Name", result[1]);
136+
Assert.Equal("person.Container.Container.Children[2].Container.Children[2].Children[2].Name", result[2]);
137137
}
138138

139139
[Fact]
@@ -151,9 +151,9 @@ public void Works_ForLoopIteratorVariableIndex_NonArrayType()
151151
}
152152

153153
// Assert
154-
Assert.Equal("person.Parent.Nicknames[0]", result[0]);
155-
Assert.Equal("person.Parent.Nicknames[1]", result[1]);
156-
Assert.Equal("person.Parent.Nicknames[2]", result[2]);
154+
Assert.Equal("person.Container.Nicknames[0]", result[0]);
155+
Assert.Equal("person.Container.Nicknames[1]", result[1]);
156+
Assert.Equal("person.Container.Nicknames[2]", result[2]);
157157
}
158158

159159
public void Dispose()

src/Components/Web/src/Forms/EditContextModelBindingContextExtensions.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,16 +72,19 @@ private void OnValidationRequested(object? sender, ValidationRequestedEventArgs
7272

7373
_messages = new ValidationMessageStore(_editContext);
7474
var adddedMessages = false;
75-
foreach (var ((owner, key), errors) in _bindingContext.GetAllErrors())
75+
foreach (var error in _bindingContext.GetAllErrors())
7676
{
77+
var owner = error.Container;
78+
var key = error.Name;
79+
var errors = error.ErrorMessages;
7780
FieldIdentifier fieldIdentifier;
7881
fieldIdentifier = new FieldIdentifier(owner ?? _editContext.Model, key);
7982

80-
foreach (var error in errors)
83+
foreach (var errorMessage in errors)
8184
{
8285
adddedMessages = true;
8386
// TODO: We need to support localizing the error message.
84-
_messages.Add(fieldIdentifier, error.ToString(CultureInfo.CurrentCulture));
87+
_messages.Add(fieldIdentifier, errorMessage.ToString(CultureInfo.CurrentCulture));
8588
}
8689
}
8790

src/Components/test/E2ETest/Tests/ComponentRenderingTestBase.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ public void CanAddAndRemoveEventHandlersDynamically()
149149
public void CanRenderChildComponents()
150150
{
151151
var appElement = Browser.MountTestComponent<ParentChildComponent>();
152-
Assert.Equal("Parent component",
152+
Assert.Equal("Container component",
153153
appElement.FindElement(By.CssSelector("fieldset > legend")).Text);
154154

155155
var styledElement = appElement.FindElement(By.CssSelector("fieldset > h1"));

src/Components/test/testassets/Components.TestServer/RazorComponents/Pages/Forms/DefaultFormBoundMultiplePrimitiveParameters.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
string _attemptedParameterValue = null;
3333
string _attemptedOtherValue = null;
3434

35-
IReadOnlyList<FormattableString> _errors;
35+
IList<FormattableString> _errors;
3636

3737
[SupplyParameterFromForm] public int Parameter { get; set; } = 0;
3838
[SupplyParameterFromForm] public bool OtherParameter { get; set; } = false;
@@ -41,7 +41,7 @@
4141

4242
protected override void OnInitialized()
4343
{
44-
_errors = Context.GetErrors(nameof(Parameter)).Concat(Context.GetErrors(nameof(OtherParameter))).ToList();
44+
_errors = Context.GetErrors(nameof(Parameter)).ErrorMessages.Concat(Context.GetErrors(nameof(OtherParameter)).ErrorMessages).ToList();
4545
if (_errors.Count > 0)
4646
{
4747
_attemptedParameterValue = Context.GetAttemptedValue(nameof(Parameter));

0 commit comments

Comments
 (0)