diff --git a/.claude/skills/repo-profile.md b/.claude/skills/repo-profile.md
index 2f67972f..d87e520e 100644
--- a/.claude/skills/repo-profile.md
+++ b/.claude/skills/repo-profile.md
@@ -99,11 +99,22 @@
in nondeterministic order, so a check that reads the first one accepts one project's total as the
whole suite. Confirm all three assemblies reported, or trust the process exit code for the full run
(unpiped). Filter to iterate; run one of these before you claim done.
-- **Format/lint apply:** `dotnet format FormCraft.sln`
-- **Format/lint verify (the gate):** *CI runs no `dotnet format` check.* The enforcing gate is the
- **build itself**: `Directory.Build.props` sets `true`
- (with `NoWarn=CS1591;CS8620`), so any analyzer/compiler warning fails `dotnet build -c Release`.
- `.editorconfig` severities are capped at `warning` and are IDE/`dotnet format` guidance only.
+- **Format/lint apply:** `dotnet format FormCraft.sln` (or `whitespace` / `style` to scope it)
+- **Format/lint verify (the gate):** `./build.sh Format` — a Nuke target wrapping
+ `dotnet format FormCraft.sln --verify-no-changes`, run by `ci.yml` **before** `Test` (#301).
+ There are now **two** independent gates:
+ 1. **The build** — `Directory.Build.props` sets `true`
+ (with `NoWarn=CS1591;CS8620`), so any compiler/package-analyzer warning fails
+ `dotnet build -c Release`. This does **not** cover `.editorconfig`.
+ 2. **The `Format` target** — the only thing that reads `.editorconfig`. `EnforceCodeStyleInBuild`
+ is deliberately unset, so `IDE*` analyzers never run during a build; before #301 that meant
+ nothing anywhere read those severities and 574 violations had accumulated across 201 files.
+- ⚠️ **`dotnet format` corrupts multi-targeted files when *applying*.** `FormCraft` is
+ `net8.0;net10.0`, so a fix can be applied once per TFM and land as a literal
+ `<<<<<<< TODO: Unmerged change from project 'FormCraft(net10.0)'` conflict block **written into the
+ `.cs` file** (measured in #301 on `FieldRendererBase.cs`; the result does not compile). Verify mode
+ never writes, so CI is unaffected. After any *apply* run:
+ `grep -rl '<<<<<<< TODO' --include='*.cs' .` and hand-resolve before committing.
- **Prerequisites / caveats:**
- `global.json` pins SDK `10.0.302` with `rollForward: latestFeature`.
- Multi-target `net8.0;net10.0` — a build error can be TFM-specific; read which TFM the error names.
@@ -113,6 +124,8 @@
`Continuous`). `Test` is `DependsOn(Compile)` with `--no-build --no-restore`.
## CI gates (the exact commands CI fails on — satisfy these locally before ready/merge)
+- `./build.cmd Format` — `.github/workflows/ci.yml`, run **before** `Test` since #301. Verifies
+ `dotnet format --verify-no-changes` is clean; fails on any diff.
- `./build.cmd Test` — `.github/workflows/ci.yml` (Compile then Test, Release config)
- `./build.cmd Pack` — `.github/workflows/continuous.yml` (`Pack` is `DependsOn(Test)`). Since #197
this workflow builds/tests/packs only: no `tags:` trigger, no key, and it invokes `Pack` rather
@@ -121,9 +134,9 @@
*is* the `v3.1.0` commit) and would then hard-fail on `Publish`'s `Requires(NuGetApiKey)`.
- **`pr-title-lint.yml`** — `amannn/action-semantic-pull-request` on `pull_request_target`. The PR
title **must** be a Conventional Commit. Skipped on drafts, and re-evaluated on `ready_for_review`.
-- Locally equivalent: `dotnet build -c Release` && `dotnet test -c Release`.
-- No format/lint check, and **no branch protection** (`required_status_checks` is
- `enforcement_level: off`) — CI is advisory at the API level, so read the run result yourself.
+- Locally equivalent: `./build.sh Format` && `dotnet build -c Release` && `dotnet test -c Release`.
+- **No branch protection** (`required_status_checks` is `enforcement_level: off`) — CI is advisory at
+ the API level, so read the run result yourself.
- `release-please.yml` is not a PR gate: it runs on push to `dev` and on `workflow_dispatch`.
## Integration style
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 77aa0e16..f6918b5b 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -13,6 +13,17 @@ jobs:
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.0.x'
+ # Runs BEFORE Test so a style slip reports in ~30s instead of behind a full build+test.
+ #
+ # This is the only thing that reads .editorconfig (#301). Its severities say `warning` and
+ # Directory.Build.props sets TreatWarningsAsErrors=true, which together look like enforcement
+ # and are not: IDE* analyzers need EnforceCodeStyleInBuild, which is deliberately NOT set —
+ # turning it on would make one missing brace break `dotnet build` mid-edit. Delete this step
+ # and the rules go back to being decorative, which is how 574 violations accumulated.
+ - run: ./build.cmd Format
+ env:
+ DOTNET_CLI_TELEMETRY_OPTOUT: 1
+
- run: ./build.cmd Test
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
diff --git a/CLAUDE.md b/CLAUDE.md
index 06d2ff30..8ef049d0 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -21,6 +21,38 @@ dotnet build /p:TreatWarningsAsErrors=true
./pack-local.ps1 # Windows
```
+### Code style (`.editorconfig`) — enforced in CI, not in the build
+
+```bash
+./build.sh Format # the CI gate: verify only, fails on any diff
+dotnet format FormCraft.sln # apply the fixes
+dotnet format FormCraft.sln whitespace # or scope it: whitespace only
+dotnet format FormCraft.sln style # ...or the IDE* code-style rules only
+```
+
+- **`EnforceCodeStyleInBuild` is deliberately NOT set**, so `IDE*` analyzers do not run during
+ `dotnet build`. That is why `.editorconfig`'s `warning` severities never met
+ `TreatWarningsAsErrors` and 574 violations accumulated across 201 files before #301 — the two
+ settings *look* like enforcement together and are not. ⛔ Don't "fix" that by switching the
+ property on: with warnings-as-errors, one missing brace would break `dotnet build` mid-edit and
+ every incremental build would pay for the analyzer pass. The `Format` CI step is where regression
+ is caught.
+- ⚠️ **`dotnet format` can corrupt multi-targeted files when it *applies* fixes.** `FormCraft` is
+ `net8.0;net10.0`, and a fix applied once per TFM can land as a literal
+ `<<<<<<< TODO: Unmerged change from project 'FormCraft(net10.0)'` conflict block **written into
+ the `.cs` file** — code that does not compile (measured in #301 on `FieldRendererBase.cs`).
+ Verify mode never writes, so CI is safe. After any apply run:
+ `grep -rl '<<<<<<< TODO' --include='*.cs' .` and hand-resolve before committing.
+- ⚠️ **One `dotnet format` run does not always reach a fixpoint.** Measured in #301: after formatting
+ a `field`-keyword property, verify still reported three `WHITESPACE` diagnostics on it, and a
+ second run cleared them. If `./build.sh Format` fails right after you formatted, run the formatter
+ again before assuming the gate is wrong.
+- **`IDE0032` now rewrites a private backing field into C# 14's `field` keyword.** Value-identical,
+ but it silently deletes the XML docs that lived on the field — which on
+ `MudBlazorFileUploadComponentBase.RequiredDescriptionId` were documenting a real correctness
+ invariant (#262: the hint id must be unique per rendered instance). Read those hunks; move the
+ explanation onto the property rather than losing it.
+
### Running Tests
The test projects are **Microsoft.Testing.Platform** hosts, not VSTest. That changes how you filter —
diff --git a/FormCraft.DemoBlazorApp/Components/Dialogs/EditFormDialog.razor.cs b/FormCraft.DemoBlazorApp/Components/Dialogs/EditFormDialog.razor.cs
index 37388073..fa0bfa4c 100644
--- a/FormCraft.DemoBlazorApp/Components/Dialogs/EditFormDialog.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Dialogs/EditFormDialog.razor.cs
@@ -47,4 +47,4 @@ private void Cancel()
{
MudDialog.Cancel();
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Dialogs/SimpleFormDialog.razor.cs b/FormCraft.DemoBlazorApp/Components/Dialogs/SimpleFormDialog.razor.cs
index 34e8a62f..a3dad29b 100644
--- a/FormCraft.DemoBlazorApp/Components/Dialogs/SimpleFormDialog.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Dialogs/SimpleFormDialog.razor.cs
@@ -57,4 +57,4 @@ private void Cancel()
{
MudDialog.Cancel();
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Layout/FormCraftTheme.cs b/FormCraft.DemoBlazorApp/Components/Layout/FormCraftTheme.cs
index c5f158af..2ff4e1db 100644
--- a/FormCraft.DemoBlazorApp/Components/Layout/FormCraftTheme.cs
+++ b/FormCraft.DemoBlazorApp/Components/Layout/FormCraftTheme.cs
@@ -115,67 +115,107 @@ public static class FormCraftTheme
// declared parts — so it earns the largest type on the page.
H1 = new H1Typography
{
- FontFamily = DisplayFont, FontSize = "3.4rem", FontWeight = "800",
- LineHeight = "1.02", LetterSpacing = "-0.03em"
+ FontFamily = DisplayFont,
+ FontSize = "3.4rem",
+ FontWeight = "800",
+ LineHeight = "1.02",
+ LetterSpacing = "-0.03em"
},
H2 = new H2Typography
{
- FontFamily = DisplayFont, FontSize = "2.6rem", FontWeight = "800",
- LineHeight = "1.06", LetterSpacing = "-0.028em"
+ FontFamily = DisplayFont,
+ FontSize = "2.6rem",
+ FontWeight = "800",
+ LineHeight = "1.06",
+ LetterSpacing = "-0.028em"
},
H3 = new H3Typography
{
- FontFamily = DisplayFont, FontSize = "2.1rem", FontWeight = "700",
- LineHeight = "1.1", LetterSpacing = "-0.024em"
+ FontFamily = DisplayFont,
+ FontSize = "2.1rem",
+ FontWeight = "700",
+ LineHeight = "1.1",
+ LetterSpacing = "-0.024em"
},
H4 = new H4Typography
{
- FontFamily = DisplayFont, FontSize = "1.65rem", FontWeight = "700",
- LineHeight = "1.15", LetterSpacing = "-0.02em"
+ FontFamily = DisplayFont,
+ FontSize = "1.65rem",
+ FontWeight = "700",
+ LineHeight = "1.15",
+ LetterSpacing = "-0.02em"
},
H5 = new H5Typography
{
- FontFamily = DisplayFont, FontSize = "1.3rem", FontWeight = "700",
- LineHeight = "1.2", LetterSpacing = "-0.015em"
+ FontFamily = DisplayFont,
+ FontSize = "1.3rem",
+ FontWeight = "700",
+ LineHeight = "1.2",
+ LetterSpacing = "-0.015em"
},
H6 = new H6Typography
{
- FontFamily = DisplayFont, FontSize = "1.075rem", FontWeight = "600",
- LineHeight = "1.3", LetterSpacing = "-0.01em"
+ FontFamily = DisplayFont,
+ FontSize = "1.075rem",
+ FontWeight = "600",
+ LineHeight = "1.3",
+ LetterSpacing = "-0.01em"
},
Subtitle1 = new Subtitle1Typography
{
- FontFamily = BodyFont, FontSize = "1rem", FontWeight = "500", LineHeight = "1.5"
+ FontFamily = BodyFont,
+ FontSize = "1rem",
+ FontWeight = "500",
+ LineHeight = "1.5"
},
Subtitle2 = new Subtitle2Typography
{
- FontFamily = BodyFont, FontSize = "0.875rem", FontWeight = "600", LineHeight = "1.45"
+ FontFamily = BodyFont,
+ FontSize = "0.875rem",
+ FontWeight = "600",
+ LineHeight = "1.45"
},
Body1 = new Body1Typography
{
- FontFamily = BodyFont, FontSize = "0.9375rem", FontWeight = "400", LineHeight = "1.6"
+ FontFamily = BodyFont,
+ FontSize = "0.9375rem",
+ FontWeight = "400",
+ LineHeight = "1.6"
},
Body2 = new Body2Typography
{
- FontFamily = BodyFont, FontSize = "0.875rem", FontWeight = "400", LineHeight = "1.55"
+ FontFamily = BodyFont,
+ FontSize = "0.875rem",
+ FontWeight = "400",
+ LineHeight = "1.55"
},
// Buttons keep sentence case: a control is named for what it does, and
// SHOUTING CAPS makes long labels ("Read the documentation") hard to scan.
Button = new ButtonTypography
{
- FontFamily = BodyFont, FontSize = "0.875rem", FontWeight = "600",
- LineHeight = "1.75", LetterSpacing = "0.01em", TextTransform = "none"
+ FontFamily = BodyFont,
+ FontSize = "0.875rem",
+ FontWeight = "600",
+ LineHeight = "1.75",
+ LetterSpacing = "0.01em",
+ TextTransform = "none"
},
Caption = new CaptionTypography
{
- FontFamily = BodyFont, FontSize = "0.78rem", FontWeight = "400", LineHeight = "1.4"
+ FontFamily = BodyFont,
+ FontSize = "0.78rem",
+ FontWeight = "400",
+ LineHeight = "1.4"
},
// Overline is the eyebrow role, and every eyebrow on this site is mono.
Overline = new OverlineTypography
{
FontFamily = ["IBM Plex Mono", "ui-monospace", "monospace"],
- FontSize = "0.72rem", FontWeight = "500", LineHeight = "1.6",
- LetterSpacing = "0.16em", TextTransform = "uppercase"
+ FontSize = "0.72rem",
+ FontWeight = "500",
+ LineHeight = "1.6",
+ LetterSpacing = "0.16em",
+ TextTransform = "uppercase"
}
}
};
diff --git a/FormCraft.DemoBlazorApp/Components/Layout/FormPageLayout.razor.cs b/FormCraft.DemoBlazorApp/Components/Layout/FormPageLayout.razor.cs
index 7cfaf6f4..5483f275 100644
--- a/FormCraft.DemoBlazorApp/Components/Layout/FormPageLayout.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Layout/FormPageLayout.razor.cs
@@ -13,4 +13,4 @@ public partial class FormPageLayout
[Parameter] public RenderFragment FormContent { get; set; } = null!;
[Parameter] public RenderFragment SidebarContent { get; set; } = null!;
[Parameter] public RenderFragment? SuccessContent { get; set; }
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Pages/AsyncValueProviderDemo.razor.cs b/FormCraft.DemoBlazorApp/Components/Pages/AsyncValueProviderDemo.razor.cs
index f0655c58..fa1ae60c 100644
--- a/FormCraft.DemoBlazorApp/Components/Pages/AsyncValueProviderDemo.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Pages/AsyncValueProviderDemo.razor.cs
@@ -237,7 +237,9 @@ private async Task ValidatePostalCodeAsync(string postalCode)
await Task.Delay(300);
if (string.IsNullOrEmpty(postalCode))
+ {
return true;
+ }
// Simple validation based on country
return _model.Country switch
diff --git a/FormCraft.DemoBlazorApp/Components/Pages/AttributeBasedForms.razor.cs b/FormCraft.DemoBlazorApp/Components/Pages/AttributeBasedForms.razor.cs
index a784893d..bfaf7d40 100644
--- a/FormCraft.DemoBlazorApp/Components/Pages/AttributeBasedForms.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Pages/AttributeBasedForms.razor.cs
@@ -1,8 +1,8 @@
using FormCraft.DemoBlazorApp.Components.Shared;
using FormCraft.DemoBlazorApp.Models;
using FormCraft.DemoBlazorApp.Services;
-using MudBlazor;
using Microsoft.AspNetCore.Components;
+using MudBlazor;
namespace FormCraft.DemoBlazorApp.Components.Pages;
@@ -217,4 +217,4 @@ private static string GetFormGenerationCodeStatic()
Configuration=""@_formConfiguration""
OnValidSubmit=""HandleValidSubmit"" />";
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Pages/AutoFormDemo.razor.cs b/FormCraft.DemoBlazorApp/Components/Pages/AutoFormDemo.razor.cs
index 214de243..d609bd44 100644
--- a/FormCraft.DemoBlazorApp/Components/Pages/AutoFormDemo.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Pages/AutoFormDemo.razor.cs
@@ -12,7 +12,7 @@ public partial class AutoFormDemo
private bool _isPlainSubmitting;
private IFormConfiguration _plainConfiguration = null!;
- private SpeakerProfileModel _annotatedModel = new();
+ private readonly SpeakerProfileModel _annotatedModel = new();
private bool _isAnnotatedSubmitted;
private IFormConfiguration _annotatedConfiguration = null!;
diff --git a/FormCraft.DemoBlazorApp/Components/Pages/CrossFieldValidationDemo.razor.cs b/FormCraft.DemoBlazorApp/Components/Pages/CrossFieldValidationDemo.razor.cs
index 1c52c65c..4d2b0fa1 100644
--- a/FormCraft.DemoBlazorApp/Components/Pages/CrossFieldValidationDemo.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Pages/CrossFieldValidationDemo.razor.cs
@@ -14,7 +14,7 @@ public partial class CrossFieldValidationDemo
private IFormConfiguration? _formConfig;
private bool _submitted;
private bool _isSubmitting;
- private List _validationErrors = [];
+ private readonly List _validationErrors = [];
///
/// Structured documentation for this demo page.
diff --git a/FormCraft.DemoBlazorApp/Components/Pages/CustomRenderers.razor.cs b/FormCraft.DemoBlazorApp/Components/Pages/CustomRenderers.razor.cs
index ab2b8d5e..baf15669 100644
--- a/FormCraft.DemoBlazorApp/Components/Pages/CustomRenderers.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Pages/CustomRenderers.razor.cs
@@ -271,4 +271,4 @@ public static FieldBuilder AsColorPicker(
}
""";
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Pages/DialogDemo.razor.cs b/FormCraft.DemoBlazorApp/Components/Pages/DialogDemo.razor.cs
index ee3b932c..c4679966 100644
--- a/FormCraft.DemoBlazorApp/Components/Pages/DialogDemo.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Pages/DialogDemo.razor.cs
@@ -207,4 +207,4 @@ private async Task OpenDialog()
}
""";
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Pages/FieldGroups.razor.cs b/FormCraft.DemoBlazorApp/Components/Pages/FieldGroups.razor.cs
index 6a45af23..948c9a28 100644
--- a/FormCraft.DemoBlazorApp/Components/Pages/FieldGroups.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Pages/FieldGroups.razor.cs
@@ -232,4 +232,4 @@ private static string GetGeneratedCodeStatic()
return code;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Pages/FileUploadDemo.razor.cs b/FormCraft.DemoBlazorApp/Components/Pages/FileUploadDemo.razor.cs
index f3f8049e..d6fead70 100644
--- a/FormCraft.DemoBlazorApp/Components/Pages/FileUploadDemo.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Pages/FileUploadDemo.razor.cs
@@ -197,4 +197,4 @@ private static string GetGeneratedCodeStatic()
Configuration=""@_formConfiguration""
OnValidSubmit=""@HandleSubmit"" />";
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Pages/FluentForm.razor.cs b/FormCraft.DemoBlazorApp/Components/Pages/FluentForm.razor.cs
index 83e0311d..f9193f0a 100644
--- a/FormCraft.DemoBlazorApp/Components/Pages/FluentForm.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Pages/FluentForm.razor.cs
@@ -152,18 +152,26 @@ private Task HandleFieldChanged(string fieldName, object? value)
};
if (!string.IsNullOrEmpty(_model.City))
+ {
items.Add(new() { Label = "City", Value = _model.City });
+ }
if (_model.ExpectedSalary.HasValue)
+ {
items.Add(new() { Label = "Expected Salary", Value = $"${_model.ExpectedSalary.Value:N2}" });
+ }
if (_model.HourlyRate.HasValue)
+ {
items.Add(new() { Label = "Hourly Rate", Value = $"${_model.HourlyRate.Value:N2}/hr" });
+ }
items.Add(new() { Label = "Newsletter", Value = _model.SubscribeToNewsletter ? "Subscribed" : "Not Subscribed" });
if (_fieldChanges.Any())
+ {
items.Add(new() { Label = "Field Changes", Value = $"{_fieldChanges.Count} changes tracked" });
+ }
return items;
}
@@ -225,4 +233,4 @@ private static string GetGeneratedCodeStatic()
return code;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Pages/FluentValidationDemo.razor.cs b/FormCraft.DemoBlazorApp/Components/Pages/FluentValidationDemo.razor.cs
index ffb7f542..68367beb 100644
--- a/FormCraft.DemoBlazorApp/Components/Pages/FluentValidationDemo.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Pages/FluentValidationDemo.razor.cs
@@ -202,4 +202,4 @@ public CustomerValidator()
.GreaterThanOrEqualTo(18).WithMessage("You must be 18 or older");
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Pages/FormSlots.razor.cs b/FormCraft.DemoBlazorApp/Components/Pages/FormSlots.razor.cs
index 4d8b2c82..68d0d565 100644
--- a/FormCraft.DemoBlazorApp/Components/Pages/FormSlots.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Pages/FormSlots.razor.cs
@@ -283,4 +283,4 @@ public override void Dispose()
_countdownTimer?.Dispose();
base.Dispose();
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Pages/ImprovedForm.razor.cs b/FormCraft.DemoBlazorApp/Components/Pages/ImprovedForm.razor.cs
index 5ac40850..1af98324 100644
--- a/FormCraft.DemoBlazorApp/Components/Pages/ImprovedForm.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Pages/ImprovedForm.razor.cs
@@ -156,7 +156,9 @@ private void ResetForm()
};
if (!string.IsNullOrEmpty(_model.City))
+ {
items.Add(new() { Label = "City", Value = _model.City });
+ }
items.Add(new() { Label = "Newsletter", Value = _model.SubscribeToNewsletter ? "Yes" : "No" });
@@ -227,4 +229,4 @@ private static string GetGeneratedCodeStatic()
return code;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Pages/PasswordFieldDemo.razor.cs b/FormCraft.DemoBlazorApp/Components/Pages/PasswordFieldDemo.razor.cs
index 2dc25b8a..4d9eba34 100644
--- a/FormCraft.DemoBlazorApp/Components/Pages/PasswordFieldDemo.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Pages/PasswordFieldDemo.razor.cs
@@ -285,8 +285,16 @@ private void ResetSecurityForm()
private static string MaskCredential(string value)
{
- if (string.IsNullOrEmpty(value)) return "";
- if (value.Length <= 8) return new string('•', value.Length);
+ if (string.IsNullOrEmpty(value))
+ {
+ return "";
+ }
+
+ if (value.Length <= 8)
+ {
+ return new string('•', value.Length);
+ }
+
return $"{value[..4]}...{value[^4..]}";
}
@@ -300,12 +308,35 @@ private void CalculatePasswordStrength(string password)
}
int score = 0;
- if (password.Length >= 8) score += 20;
- if (password.Length >= 12) score += 10;
- if (password.Any(char.IsUpper)) score += 20;
- if (password.Any(char.IsLower)) score += 20;
- if (password.Any(char.IsDigit)) score += 20;
- if (password.Any(c => !char.IsLetterOrDigit(c))) score += 10;
+ if (password.Length >= 8)
+ {
+ score += 20;
+ }
+
+ if (password.Length >= 12)
+ {
+ score += 10;
+ }
+
+ if (password.Any(char.IsUpper))
+ {
+ score += 20;
+ }
+
+ if (password.Any(char.IsLower))
+ {
+ score += 20;
+ }
+
+ if (password.Any(char.IsDigit))
+ {
+ score += 20;
+ }
+
+ if (password.Any(c => !char.IsLetterOrDigit(c)))
+ {
+ score += 10;
+ }
_passwordStrengthScore = Math.Min(score, 100);
}
@@ -418,4 +449,4 @@ private static string GetSecurityCodeStatic()
.Required(""PIN is required""))
.Build();";
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Pages/SecurityDemo.razor.cs b/FormCraft.DemoBlazorApp/Components/Pages/SecurityDemo.razor.cs
index 56cfa966..ff54d528 100644
--- a/FormCraft.DemoBlazorApp/Components/Pages/SecurityDemo.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Pages/SecurityDemo.razor.cs
@@ -238,7 +238,10 @@ private void ResetForm()
private List GetDataDisplayItems()
{
- if (_lastSubmission == null) return new();
+ if (_lastSubmission == null)
+ {
+ return new();
+ }
return new List
{
@@ -288,4 +291,4 @@ private static string GetSecurityCodeStatic()
}))
.Build();";
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Pages/SimplifiedForm.razor.cs b/FormCraft.DemoBlazorApp/Components/Pages/SimplifiedForm.razor.cs
index f92e5149..fae2c74c 100644
--- a/FormCraft.DemoBlazorApp/Components/Pages/SimplifiedForm.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Pages/SimplifiedForm.razor.cs
@@ -149,12 +149,16 @@ private void ResetForm()
};
if (!string.IsNullOrEmpty(_model.City))
+ {
items.Add(new() { Label = "City", Value = _model.City });
+ }
items.Add(new() { Label = "Newsletter", Value = _model.SubscribeToNewsletter ? "Yes" : "No" });
if (_fieldChanges.Any())
+ {
items.Add(new() { Label = "Field Changes", Value = $"{_fieldChanges.Count} changes tracked" });
+ }
return items;
}
@@ -202,4 +206,4 @@ private static string GetGeneratedCodeStatic()
return actualCode;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Pages/StepperForm.razor.cs b/FormCraft.DemoBlazorApp/Components/Pages/StepperForm.razor.cs
index 4d3d181e..8394cf82 100644
--- a/FormCraft.DemoBlazorApp/Components/Pages/StepperForm.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Pages/StepperForm.razor.cs
@@ -334,4 +334,4 @@ private async Task NextStep()
}
""";
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Pages/TabbedForm.razor.cs b/FormCraft.DemoBlazorApp/Components/Pages/TabbedForm.razor.cs
index ab503756..6469e42f 100644
--- a/FormCraft.DemoBlazorApp/Components/Pages/TabbedForm.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Pages/TabbedForm.razor.cs
@@ -217,9 +217,14 @@ private Color GetBadgeColor()
private object? GetPersonalInfoBadge()
{
if (IsPersonalInfoComplete())
+ {
return Icons.Material.Filled.CheckCircle;
+ }
+
if (HasPersonalInfoData())
+ {
return Icons.Material.Filled.Warning;
+ }
return null;
}
@@ -227,9 +232,14 @@ private Color GetBadgeColor()
private object? GetContactInfoBadge()
{
if (IsContactInfoComplete())
+ {
return Icons.Material.Filled.CheckCircle;
+ }
+
if (HasContactInfoData())
+ {
return Icons.Material.Filled.Warning;
+ }
return null;
}
@@ -237,9 +247,14 @@ private Color GetBadgeColor()
private object? GetProfessionalInfoBadge()
{
if (IsProfessionalInfoComplete())
+ {
return Icons.Material.Filled.CheckCircle;
+ }
+
if (HasProfessionalInfoData())
+ {
return Icons.Material.Filled.Warning;
+ }
return null;
}
@@ -247,9 +262,14 @@ private Color GetBadgeColor()
private Color GetTabBadgeColor(bool isComplete, bool hasData)
{
if (isComplete)
+ {
return Color.Success;
+ }
+
if (hasData)
+ {
return Color.Warning;
+ }
return Color.Default;
}
@@ -496,4 +516,4 @@ private Task SubmitForm()
}
""";
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Shared/ApiGuidelinesTable.razor.cs b/FormCraft.DemoBlazorApp/Components/Shared/ApiGuidelinesTable.razor.cs
index f4827b36..3f4f3eca 100644
--- a/FormCraft.DemoBlazorApp/Components/Shared/ApiGuidelinesTable.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Shared/ApiGuidelinesTable.razor.cs
@@ -15,4 +15,4 @@ public partial class ApiGuidelinesTable
[Parameter]
public RenderFragment? AdditionalContent { get; set; }
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Shared/CodeExample.razor.cs b/FormCraft.DemoBlazorApp/Components/Shared/CodeExample.razor.cs
index 33c386ec..89142f83 100644
--- a/FormCraft.DemoBlazorApp/Components/Shared/CodeExample.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Shared/CodeExample.razor.cs
@@ -99,7 +99,9 @@ private string GetPreClasses()
private string GetLineNumbersHtml()
{
if (string.IsNullOrEmpty(Code))
+ {
return "";
+ }
var lineCount = Code.Split('\n').Length;
return string.Concat(Enumerable.Repeat("", lineCount));
diff --git a/FormCraft.DemoBlazorApp/Components/Shared/DemoComponentBase.cs b/FormCraft.DemoBlazorApp/Components/Shared/DemoComponentBase.cs
index f0bb090b..e6604e77 100644
--- a/FormCraft.DemoBlazorApp/Components/Shared/DemoComponentBase.cs
+++ b/FormCraft.DemoBlazorApp/Components/Shared/DemoComponentBase.cs
@@ -28,14 +28,13 @@ namespace FormCraft.DemoBlazorApp.Components.Shared;
public abstract class DemoComponentBase : ComponentBase, IDisposable
{
private readonly CancellationTokenSource _lifetimeCts = new();
- private bool _disposed;
///
/// Whether the component has been torn down. Check this before calling
/// from anything that can outlive the component — a
/// continuation after any await, or a timer callback.
///
- protected bool IsDisposed => _disposed;
+ protected bool IsDisposed { get; private set; }
///
/// Waits and reports whether the component is still around.
@@ -47,7 +46,7 @@ public abstract class DemoComponentBase : ComponentBase, IDisposable
///
///
/// Two hazards, both live findings on #285: the token covers the wait itself, and the
- /// re-check afterwards covers the gap where the timer has already fired and
+ /// re-check afterwards covers the gap where the timer has already fired and
/// the continuation is queued on the dispatcher — cancellation cannot help once that has happened.
///
///
@@ -64,7 +63,7 @@ public abstract class DemoComponentBase : ComponentBase, IDisposable
///
protected async Task DelayAsync(int milliseconds)
{
- if (_disposed)
+ if (IsDisposed)
{
return false;
}
@@ -78,7 +77,7 @@ protected async Task DelayAsync(int milliseconds)
return false;
}
- return !_disposed;
+ return !IsDisposed;
}
///
@@ -90,12 +89,12 @@ protected async Task DelayAsync(int milliseconds)
///
public virtual void Dispose()
{
- if (_disposed)
+ if (IsDisposed)
{
return;
}
- _disposed = true;
+ IsDisposed = true;
_lifetimeCts.Cancel();
_lifetimeCts.Dispose();
GC.SuppressFinalize(this);
diff --git a/FormCraft.DemoBlazorApp/Components/Shared/DemoPageHeader.razor.cs b/FormCraft.DemoBlazorApp/Components/Shared/DemoPageHeader.razor.cs
index d14c6458..7323aee3 100644
--- a/FormCraft.DemoBlazorApp/Components/Shared/DemoPageHeader.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Shared/DemoPageHeader.razor.cs
@@ -12,4 +12,4 @@ public partial class DemoPageHeader
[Parameter, EditorRequired]
public string Description { get; set; } = "";
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Shared/DocumentationPage.razor.cs b/FormCraft.DemoBlazorApp/Components/Shared/DocumentationPage.razor.cs
index 9fb98b98..3a390fcb 100644
--- a/FormCraft.DemoBlazorApp/Components/Shared/DocumentationPage.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Shared/DocumentationPage.razor.cs
@@ -98,4 +98,4 @@ private class TocSection
public string Title { get; set; } = "";
public string Id { get; set; } = "";
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Shared/FormDemoSection.razor.cs b/FormCraft.DemoBlazorApp/Components/Shared/FormDemoSection.razor.cs
index c233a147..61e212a8 100644
--- a/FormCraft.DemoBlazorApp/Components/Shared/FormDemoSection.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Shared/FormDemoSection.razor.cs
@@ -61,4 +61,4 @@ public partial class FormDemoSection
[Parameter]
public RenderFragment? AfterFormContent { get; set; }
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Shared/FormGuidelines.razor.cs b/FormCraft.DemoBlazorApp/Components/Shared/FormGuidelines.razor.cs
index a6375af7..963b382b 100644
--- a/FormCraft.DemoBlazorApp/Components/Shared/FormGuidelines.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Shared/FormGuidelines.razor.cs
@@ -20,4 +20,4 @@ public class GuidelineItem
public Color Color { get; set; } = Color.Primary;
public string Text { get; set; } = "";
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Shared/FormSuccessDisplay.razor.cs b/FormCraft.DemoBlazorApp/Components/Shared/FormSuccessDisplay.razor.cs
index 4bbdbe9a..881c2896 100644
--- a/FormCraft.DemoBlazorApp/Components/Shared/FormSuccessDisplay.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Shared/FormSuccessDisplay.razor.cs
@@ -25,4 +25,4 @@ public class DataDisplayItem
public string Label { get; set; } = "";
public string Value { get; set; } = "";
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Shared/GuidelinesTable.razor.cs b/FormCraft.DemoBlazorApp/Components/Shared/GuidelinesTable.razor.cs
index b338f56a..773112f2 100644
--- a/FormCraft.DemoBlazorApp/Components/Shared/GuidelinesTable.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Shared/GuidelinesTable.razor.cs
@@ -18,4 +18,4 @@ public partial class GuidelinesTable
[Parameter]
public RenderFragment? AdditionalContent { get; set; }
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Components/Shared/Navigation/DemoBreadcrumb.razor.cs b/FormCraft.DemoBlazorApp/Components/Shared/Navigation/DemoBreadcrumb.razor.cs
index 098425f7..34421154 100644
--- a/FormCraft.DemoBlazorApp/Components/Shared/Navigation/DemoBreadcrumb.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Shared/Navigation/DemoBreadcrumb.razor.cs
@@ -10,7 +10,7 @@ public partial class DemoBreadcrumb
[Parameter]
public string? DemoId { get; set; }
- private List _items = new();
+ private readonly List _items = new();
private DemoMetadata? _currentDemo;
protected override void OnParametersSet()
@@ -27,11 +27,15 @@ private void BuildBreadcrumbs()
_items.Add(new BreadcrumbItem("Home", "home", icon: Icons.Material.Filled.Home));
if (string.IsNullOrEmpty(DemoId))
+ {
return;
+ }
var demo = DemoRegistry.GetDemo(DemoId);
if (demo == null)
+ {
return;
+ }
_currentDemo = demo;
diff --git a/FormCraft.DemoBlazorApp/Components/Shared/Navigation/DemoPrevNextNavigation.razor.cs b/FormCraft.DemoBlazorApp/Components/Shared/Navigation/DemoPrevNextNavigation.razor.cs
index 45281dd5..89dc4756 100644
--- a/FormCraft.DemoBlazorApp/Components/Shared/Navigation/DemoPrevNextNavigation.razor.cs
+++ b/FormCraft.DemoBlazorApp/Components/Shared/Navigation/DemoPrevNextNavigation.razor.cs
@@ -25,11 +25,15 @@ protected override void OnParametersSet()
_nextLevelInfo = null;
if (string.IsNullOrEmpty(DemoId))
+ {
return;
+ }
_current = DemoRegistry.GetDemo(DemoId);
if (_current == null)
+ {
return;
+ }
// Use learning path navigation for form examples
if (_current.Category == "form-examples")
diff --git a/FormCraft.DemoBlazorApp/Helpers/GuidelineHelpers.cs b/FormCraft.DemoBlazorApp/Helpers/GuidelineHelpers.cs
index 7a7cacfe..08025a0a 100644
--- a/FormCraft.DemoBlazorApp/Helpers/GuidelineHelpers.cs
+++ b/FormCraft.DemoBlazorApp/Helpers/GuidelineHelpers.cs
@@ -24,4 +24,4 @@ public static GuidelineItem CreateTextGuideline(string feature, string usage, st
{
return CreateGuideline(feature, usage, example, false);
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Models/ContactModel.cs b/FormCraft.DemoBlazorApp/Models/ContactModel.cs
index e1f746b8..12a8f840 100644
--- a/FormCraft.DemoBlazorApp/Models/ContactModel.cs
+++ b/FormCraft.DemoBlazorApp/Models/ContactModel.cs
@@ -20,4 +20,4 @@ public class ContactModel
public decimal? ExpectedSalary { get; set; }
public decimal? HourlyRate { get; set; }
public double? Rating { get; set; }
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Models/EmployeeModel.cs b/FormCraft.DemoBlazorApp/Models/EmployeeModel.cs
index 280f67b5..4bfaab0d 100644
--- a/FormCraft.DemoBlazorApp/Models/EmployeeModel.cs
+++ b/FormCraft.DemoBlazorApp/Models/EmployeeModel.cs
@@ -21,4 +21,4 @@ public class EmployeeModel
// Additional
public string? Biography { get; set; }
public string? Notes { get; set; }
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Models/GuidelineItem.cs b/FormCraft.DemoBlazorApp/Models/GuidelineItem.cs
index 1484a028..5f58759c 100644
--- a/FormCraft.DemoBlazorApp/Models/GuidelineItem.cs
+++ b/FormCraft.DemoBlazorApp/Models/GuidelineItem.cs
@@ -12,4 +12,4 @@ public class ExtendedGuidelineItem : GuidelineItem
{
public string? AdditionalInfo { get; set; }
public string? Icon { get; set; }
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Models/JobApplicationModel.cs b/FormCraft.DemoBlazorApp/Models/JobApplicationModel.cs
index e15e0099..02a523f7 100644
--- a/FormCraft.DemoBlazorApp/Models/JobApplicationModel.cs
+++ b/FormCraft.DemoBlazorApp/Models/JobApplicationModel.cs
@@ -46,4 +46,4 @@ public class JobApplicationModel
/// Gets or sets whether the applicant agrees to terms and conditions.
///
public bool AgreeToTerms { get; set; }
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Models/LoginFormModel.cs b/FormCraft.DemoBlazorApp/Models/LoginFormModel.cs
index 6e8e75bd..4f7f22a2 100644
--- a/FormCraft.DemoBlazorApp/Models/LoginFormModel.cs
+++ b/FormCraft.DemoBlazorApp/Models/LoginFormModel.cs
@@ -30,4 +30,4 @@ public class SecurityFormModel
public string ApiKey { get; set; } = string.Empty;
public string SecretToken { get; set; } = string.Empty;
public string Pin { get; set; } = string.Empty;
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Models/ProductModel.cs b/FormCraft.DemoBlazorApp/Models/ProductModel.cs
index 41eaf929..8295cb66 100644
--- a/FormCraft.DemoBlazorApp/Models/ProductModel.cs
+++ b/FormCraft.DemoBlazorApp/Models/ProductModel.cs
@@ -15,4 +15,4 @@ public class ProductModel
public bool IsAvailable { get; set; } = true;
public string Category { get; set; } = "";
public DateTime ReleaseDate { get; set; } = DateTime.Today;
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Models/UserRegistrationModel.cs b/FormCraft.DemoBlazorApp/Models/UserRegistrationModel.cs
index 5f6758bd..fa2c3480 100644
--- a/FormCraft.DemoBlazorApp/Models/UserRegistrationModel.cs
+++ b/FormCraft.DemoBlazorApp/Models/UserRegistrationModel.cs
@@ -63,4 +63,4 @@ public class UserRegistrationModel
[TextArea("Additional Comments", "Any additional information...")]
public string Comments { get; set; } = string.Empty;
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Program.cs b/FormCraft.DemoBlazorApp/Program.cs
index 0c7998dc..3eed1fdb 100644
--- a/FormCraft.DemoBlazorApp/Program.cs
+++ b/FormCraft.DemoBlazorApp/Program.cs
@@ -1,12 +1,12 @@
-using Microsoft.AspNetCore.Components.Web;
-using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
+using FluentValidation;
+using FormCraft;
using FormCraft.DemoBlazorApp.Components;
+using FormCraft.DemoBlazorApp.Components.Pages;
using FormCraft.DemoBlazorApp.Services;
-using FormCraft;
using FormCraft.ForMudBlazor.Extensions;
+using Microsoft.AspNetCore.Components.Web;
+using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using MudBlazor.Services;
-using FluentValidation;
-using FormCraft.DemoBlazorApp.Components.Pages;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add("#app");
@@ -31,4 +31,4 @@
// Custom field renderers are now registered by AddFormCraftMudBlazor
-await builder.Build().RunAsync();
\ No newline at end of file
+await builder.Build().RunAsync();
diff --git a/FormCraft.DemoBlazorApp/Services/DemoDocumentationValidator.cs b/FormCraft.DemoBlazorApp/Services/DemoDocumentationValidator.cs
index c255dee3..25158c4b 100644
--- a/FormCraft.DemoBlazorApp/Services/DemoDocumentationValidator.cs
+++ b/FormCraft.DemoBlazorApp/Services/DemoDocumentationValidator.cs
@@ -45,68 +45,111 @@ public DocumentationValidationResult Validate(DemoDocumentation doc)
// Required validations (errors)
if (string.IsNullOrWhiteSpace(doc.DemoId))
+ {
errors.Add("DemoId is required");
+ }
if (string.IsNullOrWhiteSpace(doc.Title))
+ {
errors.Add("Title is required");
+ }
if (string.IsNullOrWhiteSpace(doc.Description))
+ {
errors.Add("Description is required");
+ }
else if (doc.Description.Length < MinDescriptionLength)
+ {
errors.Add($"Description must be at least {MinDescriptionLength} characters (current: {doc.Description.Length})");
+ }
if (string.IsNullOrWhiteSpace(doc.Icon))
+ {
errors.Add("Icon is required");
+ }
if (doc.FeatureHighlights.Count < MinFeatureHighlights)
+ {
errors.Add($"At least {MinFeatureHighlights} feature highlights required (current: {doc.FeatureHighlights.Count})");
+ }
if (doc.ApiGuidelines.Count < MinApiGuidelines)
+ {
errors.Add($"At least {MinApiGuidelines} API guidelines required (current: {doc.ApiGuidelines.Count})");
+ }
if (doc.CodeExamples.Count < MinCodeExamples)
+ {
errors.Add($"At least {MinCodeExamples} code example required (current: {doc.CodeExamples.Count})");
+ }
// Validate feature highlights
foreach (var (highlight, index) in doc.FeatureHighlights.Select((h, i) => (h, i)))
{
if (string.IsNullOrWhiteSpace(highlight.Icon))
+ {
errors.Add($"Feature highlight {index + 1}: Icon is required");
+ }
+
if (string.IsNullOrWhiteSpace(highlight.Text))
+ {
errors.Add($"Feature highlight {index + 1}: Text is required");
+ }
}
// Validate API guidelines
foreach (var (guideline, index) in doc.ApiGuidelines.Select((g, i) => (g, i)))
{
if (string.IsNullOrWhiteSpace(guideline.Feature))
+ {
errors.Add($"API guideline {index + 1}: Feature is required");
+ }
+
if (string.IsNullOrWhiteSpace(guideline.Usage))
+ {
errors.Add($"API guideline {index + 1}: Usage is required");
+ }
+
if (string.IsNullOrWhiteSpace(guideline.Example))
+ {
errors.Add($"API guideline {index + 1}: Example is required");
+ }
}
// Validate code examples
foreach (var (example, index) in doc.CodeExamples.Select((e, i) => (e, i)))
{
if (string.IsNullOrWhiteSpace(example.Title))
+ {
errors.Add($"Code example {index + 1}: Title is required");
+ }
+
if (string.IsNullOrWhiteSpace(example.Language))
+ {
errors.Add($"Code example {index + 1}: Language is required");
+ }
+
if (example.CodeProvider == null)
+ {
errors.Add($"Code example {index + 1}: CodeProvider is required");
+ }
}
// Recommended validations (warnings)
if (string.IsNullOrWhiteSpace(doc.WhenToUse))
+ {
warnings.Add("Consider adding 'WhenToUse' to help users understand when to use this approach");
+ }
if (doc.RelatedDemoIds == null || doc.RelatedDemoIds.Count == 0)
+ {
warnings.Add("Consider adding 'RelatedDemoIds' for cross-referencing");
+ }
if (doc.CommonPitfalls == null || doc.CommonPitfalls.Count == 0)
+ {
warnings.Add("Consider adding 'CommonPitfalls' to help users avoid common mistakes");
+ }
return new DocumentationValidationResult(
doc.DemoId,
diff --git a/FormCraft.DemoBlazorApp/Services/DemoRegistry.cs b/FormCraft.DemoBlazorApp/Services/DemoRegistry.cs
index f751307c..39e08157 100644
--- a/FormCraft.DemoBlazorApp/Services/DemoRegistry.cs
+++ b/FormCraft.DemoBlazorApp/Services/DemoRegistry.cs
@@ -413,7 +413,9 @@ public IReadOnlyList GetDemosByCategory(string category) =>
{
var current = GetDemo(currentId);
if (current == null)
+ {
return (null, null);
+ }
var categoryDemos = GetDemosByCategory(current.Category).ToList();
var index = categoryDemos.FindIndex(d => d.Id.Equals(currentId, StringComparison.OrdinalIgnoreCase));
@@ -437,7 +439,9 @@ public IReadOnlyList GetDemosByLevel(string level) =>
{
var current = GetDemo(currentId);
if (current == null || current.Category != "form-examples")
+ {
return (null, null);
+ }
// Get all form examples ordered by level then by level order
var learningPath = AllDemos
@@ -448,7 +452,9 @@ public IReadOnlyList GetDemosByLevel(string level) =>
var index = learningPath.FindIndex(d => d.Id.Equals(currentId, StringComparison.OrdinalIgnoreCase));
if (index < 0)
+ {
return (null, null);
+ }
var previous = index > 0 ? learningPath[index - 1] : null;
var next = index < learningPath.Count - 1 ? learningPath[index + 1] : null;
diff --git a/FormCraft.DemoBlazorApp/Services/MarkdownService.cs b/FormCraft.DemoBlazorApp/Services/MarkdownService.cs
index 69fdad88..755ff608 100644
--- a/FormCraft.DemoBlazorApp/Services/MarkdownService.cs
+++ b/FormCraft.DemoBlazorApp/Services/MarkdownService.cs
@@ -93,4 +93,4 @@ public async Task LoadDocumentAsync(string fileName)
return $"# Error loading document\n\nThe document `{fileName}.md` could not be loaded.\n\nError: {ex.Message}";
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.DemoBlazorApp/Services/VersionService.cs b/FormCraft.DemoBlazorApp/Services/VersionService.cs
index 48691228..08449fb1 100644
--- a/FormCraft.DemoBlazorApp/Services/VersionService.cs
+++ b/FormCraft.DemoBlazorApp/Services/VersionService.cs
@@ -92,4 +92,4 @@ public async Task GetFormCraftVersionAsync()
// Return a default if all else fails
return "latest";
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.ForFluentUI.UnitTests/GlobalUsings.cs b/FormCraft.ForFluentUI.UnitTests/GlobalUsings.cs
index ecca99e3..24aa93f7 100644
--- a/FormCraft.ForFluentUI.UnitTests/GlobalUsings.cs
+++ b/FormCraft.ForFluentUI.UnitTests/GlobalUsings.cs
@@ -1,11 +1,11 @@
-global using Xunit;
-global using Shouldly;
-global using FakeItEasy;
+global using System.Linq.Expressions;
global using Bunit;
+global using FakeItEasy;
+global using FormCraft;
+global using FormCraft.ForFluentUI;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Forms;
global using Microsoft.Extensions.DependencyInjection;
-global using System.Linq.Expressions;
-global using FormCraft;
-global using FormCraft.ForFluentUI;
global using Microsoft.FluentUI.AspNetCore.Components;
+global using Shouldly;
+global using Xunit;
diff --git a/FormCraft.ForFluentUI/Features/FormContainer/FormCraftComponent.razor.cs b/FormCraft.ForFluentUI/Features/FormContainer/FormCraftComponent.razor.cs
index 93f15ee1..59e80014 100644
--- a/FormCraft.ForFluentUI/Features/FormContainer/FormCraftComponent.razor.cs
+++ b/FormCraft.ForFluentUI/Features/FormContainer/FormCraftComponent.razor.cs
@@ -1,8 +1,8 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
-using Microsoft.FluentUI.AspNetCore.Components;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using Microsoft.FluentUI.AspNetCore.Components;
namespace FormCraft.ForFluentUI;
diff --git a/FormCraft.ForFluentUI/Fields/FileUploadField/FluentUIFileUploadComponentBase.cs b/FormCraft.ForFluentUI/Fields/FileUploadField/FluentUIFileUploadComponentBase.cs
index bfa28363..10f7ecd5 100644
--- a/FormCraft.ForFluentUI/Fields/FileUploadField/FluentUIFileUploadComponentBase.cs
+++ b/FormCraft.ForFluentUI/Fields/FileUploadField/FluentUIFileUploadComponentBase.cs
@@ -23,16 +23,6 @@ namespace FormCraft.ForFluentUI;
///
public abstract class FluentUIFileUploadComponentBase : FluentUIFieldComponentBase
{
- ///
- /// Per-render-instance discriminator for .
- ///
- ///
- /// The field name alone is not unique in a document: item fields render through these very
- /// components, so a collection emits one hint per row; two forms over one model on a page
- /// collide the same way; and two nested fields can share a member name. Duplicate ids are
- /// invalid HTML and, worse, point every later button at the first one's description.
- ///
- private readonly string _instanceDiscriminator = Guid.NewGuid().ToString("N")[..8];
///
/// Whether this field is marked required, by the same rule as every other field type: an
@@ -51,8 +41,11 @@ public abstract class FluentUIFileUploadComponentBase : FluentUI
protected string FileInputAccessibleName => HasLabel ? Label! : "File upload";
/// The id of the requirement hint, unique per rendered field instance.
- protected string RequiredDescriptionId =>
- $"formcraft-{Context.Field.FieldName}-required-{_instanceDiscriminator}";
+ protected string RequiredDescriptionId
+ {
+ get =>
+ $"formcraft-{Context.Field.FieldName}-required-{field}";
+ } = Guid.NewGuid().ToString("N")[..8];
///
/// The value for the browse control's aria-describedby: the hint's id when the field is
diff --git a/FormCraft.ForFluentUI/Fields/FileUploadField/FluentUIFileUploadFieldComponent.razor.cs b/FormCraft.ForFluentUI/Fields/FileUploadField/FluentUIFileUploadFieldComponent.razor.cs
index ef9cb750..f59abe44 100644
--- a/FormCraft.ForFluentUI/Fields/FileUploadField/FluentUIFileUploadFieldComponent.razor.cs
+++ b/FormCraft.ForFluentUI/Fields/FileUploadField/FluentUIFileUploadFieldComponent.razor.cs
@@ -8,13 +8,11 @@ namespace FormCraft.ForFluentUI;
/// The form's model type.
public partial class FluentUIFileUploadFieldComponent
{
- private readonly string _browseButtonId = $"formcraft-upload-{Guid.NewGuid():N}";
-
///
/// The id the hidden file input anchors to, so clicking the visible button opens the picker.
/// Unique per instance for the same reason the hint id is.
///
- private string BrowseButtonId => _browseButtonId;
+ private string BrowseButtonId { get; } = $"formcraft-upload-{Guid.NewGuid():N}";
/// The chosen file's name, shown back to the user once one is picked.
private string? SelectedFileName => CurrentValue?.Name;
diff --git a/FormCraft.ForFluentUI/Fields/FileUploadField/FluentUIMultipleFileUploadComponent.razor.cs b/FormCraft.ForFluentUI/Fields/FileUploadField/FluentUIMultipleFileUploadComponent.razor.cs
index da35fa7e..2a3065e0 100644
--- a/FormCraft.ForFluentUI/Fields/FileUploadField/FluentUIMultipleFileUploadComponent.razor.cs
+++ b/FormCraft.ForFluentUI/Fields/FileUploadField/FluentUIMultipleFileUploadComponent.razor.cs
@@ -14,13 +14,10 @@ namespace FormCraft.ForFluentUI;
///
public partial class FluentUIMultipleFileUploadComponent
{
- private readonly string _browseButtonId = $"formcraft-upload-{Guid.NewGuid():N}";
- private string? _tooManyFilesError;
-
///
/// The id the hidden file input anchors to, so clicking the visible button opens the picker.
///
- private string BrowseButtonId => _browseButtonId;
+ private string BrowseButtonId { get; } = $"formcraft-upload-{Guid.NewGuid():N}";
/// The chosen files, shown back to the user.
private IReadOnlyList SelectedFiles => CurrentValue ?? [];
@@ -28,7 +25,7 @@ public partial class FluentUIMultipleFileUploadComponent
///
/// The message shown when the user picked more files than the field allows, or null.
///
- private string? TooManyFilesError => _tooManyFilesError;
+ private string? TooManyFilesError { get; set; }
private async Task HandleFilesChangedAsync(InputFileChangeEventArgs args)
{
@@ -38,11 +35,11 @@ private async Task HandleFilesChangedAsync(InputFileChangeEventArgs args)
// Checked up front so the limit reports as a message instead.
if (args.FileCount > MaximumFileCount)
{
- _tooManyFilesError = $"Select at most {MaximumFileCount} file{(MaximumFileCount == 1 ? "" : "s")}.";
+ TooManyFilesError = $"Select at most {MaximumFileCount} file{(MaximumFileCount == 1 ? "" : "s")}.";
return;
}
- _tooManyFilesError = null;
+ TooManyFilesError = null;
IReadOnlyList files = args.GetMultipleFiles(MaximumFileCount);
SetValueWithoutNotification(files);
diff --git a/FormCraft.ForFluentUI/Fields/LookupField/FluentUILookupFieldComponent.razor.cs b/FormCraft.ForFluentUI/Fields/LookupField/FluentUILookupFieldComponent.razor.cs
index 0f29884f..584e611c 100644
--- a/FormCraft.ForFluentUI/Fields/LookupField/FluentUILookupFieldComponent.razor.cs
+++ b/FormCraft.ForFluentUI/Fields/LookupField/FluentUILookupFieldComponent.razor.cs
@@ -1,5 +1,5 @@
-using Microsoft.AspNetCore.Components.Web;
using System.Collections;
+using Microsoft.AspNetCore.Components.Web;
namespace FormCraft.ForFluentUI;
@@ -34,10 +34,9 @@ public partial class FluentUILookupFieldComponent
private bool _isOpen;
private bool _isLoading;
private string _searchText = string.Empty;
- private string _displayText = string.Empty;
/// The text shown in the read-only display.
- private string DisplayText => _displayText;
+ private string DisplayText { get; set; } = string.Empty;
///
/// The grid's columns. Falls back to a single display-text column when the field configured
@@ -54,7 +53,7 @@ protected override void OnInitialized()
if (CurrentValue is not null)
{
- _displayText = CurrentValue.ToString() ?? string.Empty;
+ DisplayText = CurrentValue.ToString() ?? string.Empty;
}
}
@@ -142,9 +141,7 @@ private async Task LoadRowsAsync()
private async Task SelectRowAsync(object row)
{
- var valueSelector = GetAttribute
private async Task> ResolveDataAsync(LovQuery query)
{
- if (_lovConfig!.DataProvider is { } provider)
+ if (LovConfig!.DataProvider is { } provider)
{
return await provider(query, CancellationToken.None);
}
- if (_lovConfig.DataProviderServiceType is { } serviceType &&
+ if (LovConfig.DataProviderServiceType is { } serviceType &&
ServiceProvider.GetService(serviceType) is ILovDataProvider service)
{
return await service.GetItemsAsync(query, CancellationToken.None);
@@ -147,7 +145,7 @@ private async Task> ResolveDataAsync(LovQuery query)
private async Task SelectRowAsync(TItem row)
{
- if (_lovConfig is null)
+ if (LovConfig is null)
{
return;
}
@@ -159,13 +157,13 @@ private async Task SelectRowAsync(TItem row)
_selectedItems.Add(row);
}
- _displayText = string.Join(", ", _selectedItems.Select(_lovConfig.DisplaySelector));
+ DisplayText = string.Join(", ", _selectedItems.Select(LovConfig.DisplaySelector));
}
else
{
_selectedItems.Clear();
_selectedItems.Add(row);
- _displayText = _lovConfig.DisplaySelector(row);
+ DisplayText = LovConfig.DisplaySelector(row);
_isOpen = false;
}
@@ -175,13 +173,13 @@ private async Task SelectRowAsync(TItem row)
private async Task RemoveSelectedAsync(TItem row)
{
- if (_lovConfig is null)
+ if (LovConfig is null)
{
return;
}
_selectedItems.Remove(row);
- _displayText = string.Join(", ", _selectedItems.Select(_lovConfig.DisplaySelector));
+ DisplayText = string.Join(", ", _selectedItems.Select(LovConfig.DisplaySelector));
await PublishSelectionAsync();
}
@@ -211,12 +209,12 @@ private async Task PublishSelectionAsync()
private TValue? ResolveSelectionValue()
{
- if (_lovConfig is null || _selectedItems.Count == 0)
+ if (LovConfig is null || _selectedItems.Count == 0)
{
return default;
}
- var values = _selectedItems.Select(_lovConfig.ValueSelector).ToList();
+ var values = _selectedItems.Select(LovConfig.ValueSelector).ToList();
if (!IsMultiSelect)
{
@@ -237,12 +235,12 @@ private async Task PublishSelectionAsync()
///
private async Task ApplyFieldMappingsAsync(TItem row)
{
- if (_lovConfig is null || Context.Model is null)
+ if (LovConfig is null || Context.Model is null)
{
return;
}
- foreach (var mapping in _lovConfig.FieldMappings)
+ foreach (var mapping in LovConfig.FieldMappings)
{
if (mapping is IAsyncLovFieldMapping asyncMapping)
{
diff --git a/FormCraft.ForMudBlazor.UnitTests/GlobalUsings.cs b/FormCraft.ForMudBlazor.UnitTests/GlobalUsings.cs
index 0df42c85..f446fa43 100644
--- a/FormCraft.ForMudBlazor.UnitTests/GlobalUsings.cs
+++ b/FormCraft.ForMudBlazor.UnitTests/GlobalUsings.cs
@@ -1,12 +1,12 @@
-global using Xunit;
-global using Shouldly;
-global using FakeItEasy;
+global using System.Linq.Expressions;
global using Bunit;
+global using FakeItEasy;
+global using FormCraft;
+global using FormCraft.ForMudBlazor;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Forms;
global using Microsoft.Extensions.DependencyInjection;
-global using System.Linq.Expressions;
-global using FormCraft;
-global using FormCraft.ForMudBlazor;
global using MudBlazor;
global using MudBlazor.Services;
+global using Shouldly;
+global using Xunit;
diff --git a/FormCraft.ForMudBlazor/Extensions/FieldBuilderExtensions.cs b/FormCraft.ForMudBlazor/Extensions/FieldBuilderExtensions.cs
index 6e94a7ec..11c9a0d8 100644
--- a/FormCraft.ForMudBlazor/Extensions/FieldBuilderExtensions.cs
+++ b/FormCraft.ForMudBlazor/Extensions/FieldBuilderExtensions.cs
@@ -534,8 +534,10 @@ public static FieldBuilder AsLookup(
}
if (onItemSelected != null)
+ {
builder.WithAttribute("LookupOnItemSelected", onItemSelected);
+ }
return builder;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.ForMudBlazor/Extensions/ServiceCollectionExtensions.cs b/FormCraft.ForMudBlazor/Extensions/ServiceCollectionExtensions.cs
index ab86dc13..6f5587f6 100644
--- a/FormCraft.ForMudBlazor/Extensions/ServiceCollectionExtensions.cs
+++ b/FormCraft.ForMudBlazor/Extensions/ServiceCollectionExtensions.cs
@@ -81,4 +81,4 @@ public IServiceCollection AddFormCraftMudBlazor()
return services;
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.ForMudBlazor/Features/CollectionField/CollectionFieldComponent.razor.cs b/FormCraft.ForMudBlazor/Features/CollectionField/CollectionFieldComponent.razor.cs
index 8b66db44..1a1c0ce3 100644
--- a/FormCraft.ForMudBlazor/Features/CollectionField/CollectionFieldComponent.razor.cs
+++ b/FormCraft.ForMudBlazor/Features/CollectionField/CollectionFieldComponent.razor.cs
@@ -141,7 +141,10 @@ protected override void OnParametersSet()
private async Task AddItem()
{
- if (HasReachedMax) return;
+ if (HasReachedMax)
+ {
+ return;
+ }
Items.Add(new TItem());
await NotifyCollectionChanged();
@@ -163,8 +166,15 @@ private async Task AddItem()
private async Task RemoveItem(int index)
{
- if (HasReachedMin) return;
- if (index < 0 || index >= Items.Count) return;
+ if (HasReachedMin)
+ {
+ return;
+ }
+
+ if (index < 0 || index >= Items.Count)
+ {
+ return;
+ }
Items.RemoveAt(index);
await NotifyCollectionChanged();
@@ -324,7 +334,10 @@ private async Task FocusAfterRemovalAsync(int removedIndex)
private async Task MoveItemUp(int index)
{
- if (index <= 0 || index >= Items.Count) return;
+ if (index <= 0 || index >= Items.Count)
+ {
+ return;
+ }
(Items[index], Items[index - 1]) = (Items[index - 1], Items[index]);
await NotifyCollectionChanged();
@@ -338,7 +351,10 @@ private async Task MoveItemUp(int index)
private async Task MoveItemDown(int index)
{
- if (index < 0 || index >= Items.Count - 1) return;
+ if (index < 0 || index >= Items.Count - 1)
+ {
+ return;
+ }
(Items[index], Items[index + 1]) = (Items[index + 1], Items[index]);
await NotifyCollectionChanged();
@@ -359,7 +375,10 @@ private async Task NotifyCollectionChanged()
private async Task UpdateItemFieldValue(int itemIndex, string fieldName, object? value)
{
- if (itemIndex < 0 || itemIndex >= Items.Count) return;
+ if (itemIndex < 0 || itemIndex >= Items.Count)
+ {
+ return;
+ }
var item = Items[itemIndex];
var property = typeof(TItem).GetProperty(fieldName);
@@ -402,7 +421,10 @@ private RenderFragment RenderItemFields(int itemIndex)
{
return builder =>
{
- if (Configuration.ItemFormConfiguration == null) return;
+ if (Configuration.ItemFormConfiguration == null)
+ {
+ return;
+ }
var item = Items[itemIndex];
diff --git a/FormCraft.ForMudBlazor/Features/CustomFields/MudBlazorColorPickerComponent.razor.cs b/FormCraft.ForMudBlazor/Features/CustomFields/MudBlazorColorPickerComponent.razor.cs
index cc37e361..3f934892 100644
--- a/FormCraft.ForMudBlazor/Features/CustomFields/MudBlazorColorPickerComponent.razor.cs
+++ b/FormCraft.ForMudBlazor/Features/CustomFields/MudBlazorColorPickerComponent.razor.cs
@@ -12,4 +12,4 @@ protected override void OnInitialized()
CurrentValue = "#000000";
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.ForMudBlazor/Features/CustomFields/MudBlazorColorPickerRenderer.cs b/FormCraft.ForMudBlazor/Features/CustomFields/MudBlazorColorPickerRenderer.cs
index 67fddb13..7755c224 100644
--- a/FormCraft.ForMudBlazor/Features/CustomFields/MudBlazorColorPickerRenderer.cs
+++ b/FormCraft.ForMudBlazor/Features/CustomFields/MudBlazorColorPickerRenderer.cs
@@ -34,4 +34,4 @@ public override bool CanRender(Type fieldType, IFieldConfiguration
/// The model type that the form will bind to.
-public partial class MudBlazorSliderComponent where TModel : new();
\ No newline at end of file
+public partial class MudBlazorSliderComponent where TModel : new();
diff --git a/FormCraft.ForMudBlazor/Features/CustomFields/MudBlazorSliderRenderer.cs b/FormCraft.ForMudBlazor/Features/CustomFields/MudBlazorSliderRenderer.cs
index 3c8671dc..6a8dc4c5 100644
--- a/FormCraft.ForMudBlazor/Features/CustomFields/MudBlazorSliderRenderer.cs
+++ b/FormCraft.ForMudBlazor/Features/CustomFields/MudBlazorSliderRenderer.cs
@@ -22,4 +22,4 @@ public override RenderFragment Render(IFieldRenderContext context)
builder.CloseComponent();
};
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.ForMudBlazor/Features/FormContainer/FormCraftComponent.razor.cs b/FormCraft.ForMudBlazor/Features/FormContainer/FormCraftComponent.razor.cs
index f8646438..30fb85cf 100644
--- a/FormCraft.ForMudBlazor/Features/FormContainer/FormCraftComponent.razor.cs
+++ b/FormCraft.ForMudBlazor/Features/FormContainer/FormCraftComponent.razor.cs
@@ -340,7 +340,10 @@ private RenderFragment RenderField(IFieldConfiguration field)
if (field.CustomTemplate != null && _editContext != null)
{
var property = typeof(TModel).GetProperty(field.FieldName);
- if (property == null) return;
+ if (property == null)
+ {
+ return;
+ }
var templateContext = new FieldContext(
Model,
@@ -459,4 +462,4 @@ private async Task HandleSubmit()
await OnValidSubmit.InvokeAsync(Model);
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.ForMudBlazor/Features/Validation/FieldValidationMessage.razor.cs b/FormCraft.ForMudBlazor/Features/Validation/FieldValidationMessage.razor.cs
index d9f1c3fe..b04ff5ac 100644
--- a/FormCraft.ForMudBlazor/Features/Validation/FieldValidationMessage.razor.cs
+++ b/FormCraft.ForMudBlazor/Features/Validation/FieldValidationMessage.razor.cs
@@ -33,4 +33,4 @@ public void Dispose()
EditContext.OnValidationStateChanged -= HandleValidationStateChanged;
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.ForMudBlazor/Fields/AutocompleteField/MudBlazorAutocompleteFieldComponent.razor.cs b/FormCraft.ForMudBlazor/Fields/AutocompleteField/MudBlazorAutocompleteFieldComponent.razor.cs
index e91d4482..976cfbd9 100644
--- a/FormCraft.ForMudBlazor/Fields/AutocompleteField/MudBlazorAutocompleteFieldComponent.razor.cs
+++ b/FormCraft.ForMudBlazor/Fields/AutocompleteField/MudBlazorAutocompleteFieldComponent.razor.cs
@@ -84,7 +84,11 @@ private async Task> SearchAsync(string searchText, Cancellat
var labelList = optionsList.Select(o => (Value: o.Value, Label: o.Label)).ToList();
_toStringFunc = v =>
{
- if (v == null) return string.Empty;
+ if (v == null)
+ {
+ return string.Empty;
+ }
+
var match = labelList.FirstOrDefault(item => EqualityComparer.Default.Equals(item.Value, v));
return match.Label ?? v.ToString() ?? string.Empty;
};
diff --git a/FormCraft.ForMudBlazor/Fields/BooleanField/MudBlazorBooleanFieldComponent.razor.cs b/FormCraft.ForMudBlazor/Fields/BooleanField/MudBlazorBooleanFieldComponent.razor.cs
index 0c5bd894..5c2c2767 100644
--- a/FormCraft.ForMudBlazor/Fields/BooleanField/MudBlazorBooleanFieldComponent.razor.cs
+++ b/FormCraft.ForMudBlazor/Fields/BooleanField/MudBlazorBooleanFieldComponent.razor.cs
@@ -90,4 +90,4 @@ private async Task OnLocalValueChanged()
SetValueWithoutNotification(_localValue);
await Context.OnValueChanged.InvokeAsync(_localValue);
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.ForMudBlazor/Fields/BooleanField/MudBlazorBooleanFieldRenderer.cs b/FormCraft.ForMudBlazor/Fields/BooleanField/MudBlazorBooleanFieldRenderer.cs
index e241a985..c6be4302 100644
--- a/FormCraft.ForMudBlazor/Fields/BooleanField/MudBlazorBooleanFieldRenderer.cs
+++ b/FormCraft.ForMudBlazor/Fields/BooleanField/MudBlazorBooleanFieldRenderer.cs
@@ -13,4 +13,4 @@ public override bool CanRender(Type fieldType, IFieldConfigurationThe field's value type — one file, or a list of them.
public abstract class MudBlazorFileUploadComponentBase : FieldComponentBase
{
- ///
- /// Per-render-instance discriminator for .
- ///
- ///
- /// The field name alone is NOT unique in a document. Item fields render through these very
- /// components since #203, so a required upload inside .WithItemForm(...) emits one hint
- /// per row; two forms over the same model on one page collide the same way; and two nested
- /// fields can share a member name (x => x.Passport.Scan and x => x.Visa.Scan).
- /// Duplicate ids are invalid HTML and, worse, point every later button at the first row's
- /// description. MudBlazor solves this the same way, with a per-component identifier.
- ///
- private readonly string _instanceDiscriminator = Guid.NewGuid().ToString("N")[..8];
///
/// Whether this field is marked as required, resolved by the same rule as every other field
@@ -72,8 +60,31 @@ public abstract class MudBlazorFileUploadComponentBase : FieldCo
///
/// The id of the requirement hint, unique per rendered field instance.
///
- protected string RequiredDescriptionId =>
- $"formcraft-{Context.Field.FieldName}-required-{_instanceDiscriminator}";
+ ///
+ ///
+ /// field is this property's own backing store (C# 14), initialised by the trailing
+ /// initializer to a short GUID once per component instance — that is the whole mechanism,
+ /// so do not "simplify" it to a constant or a shared static.
+ ///
+ ///
+ /// The field name alone is NOT unique in a document. Item fields render through these very
+ /// components since #203, so a required upload inside .WithItemForm(...) emits one hint
+ /// per row; two forms over the same model on one page collide the same way; and two nested
+ /// fields can share a member name (x => x.Passport.Scan and x => x.Visa.Scan).
+ /// Duplicate ids are invalid HTML and, worse, point every later button at the first row's
+ /// description. MudBlazor solves this the same way, with a per-component identifier.
+ ///
+ ///
+ /// This was an explicit _instanceDiscriminator field until #301, when IDE0032 —
+ /// newly reachable now that the format gate is enforced — folded it into the property. The
+ /// rewrite is value-identical; it deleted this explanation, which is why the explanation is
+ /// back.
+ ///
+ ///
+ protected string RequiredDescriptionId
+ {
+ get => $"formcraft-{Context.Field.FieldName}-required-{field}";
+ } = Guid.NewGuid().ToString("N")[..8];
///
/// The value for the focusable button's aria-describedby: the hint's id when the field is
diff --git a/FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorFileUploadFieldComponent.razor.cs b/FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorFileUploadFieldComponent.razor.cs
index 3b4d9d64..0393aa6d 100644
--- a/FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorFileUploadFieldComponent.razor.cs
+++ b/FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorFileUploadFieldComponent.razor.cs
@@ -93,10 +93,12 @@ private static string FormatFileSize(long bytes)
foreach (string order in orders)
{
if (bytes > max)
+ {
return $"{decimal.Divide(bytes, max):##.##} {order}";
+ }
max /= scale;
}
return "0 Bytes";
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorFileUploadFieldRenderer.cs b/FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorFileUploadFieldRenderer.cs
index fd82111c..e4b62854 100644
--- a/FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorFileUploadFieldRenderer.cs
+++ b/FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorFileUploadFieldRenderer.cs
@@ -17,4 +17,4 @@ public override bool CanRender(Type fieldType, IFieldConfiguration) &&
fieldType.GetGenericArguments()[0].Name.Contains("IBrowserFile"));
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorMultipleFileUploadComponent.razor.cs b/FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorMultipleFileUploadComponent.razor.cs
index bc3887fc..fadebda6 100644
--- a/FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorMultipleFileUploadComponent.razor.cs
+++ b/FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorMultipleFileUploadComponent.razor.cs
@@ -109,7 +109,10 @@ private string GetHeight()
private static string FormatFileSize(long bytes)
{
- if (bytes == 0) return "0 Bytes";
+ if (bytes == 0)
+ {
+ return "0 Bytes";
+ }
const int scale = 1024;
string[] orders = { "GB", "MB", "KB", "Bytes" };
@@ -118,10 +121,12 @@ private static string FormatFileSize(long bytes)
foreach (string order in orders)
{
if (bytes > max)
+ {
return $"{decimal.Divide(bytes, max):##.##} {order}";
+ }
max /= scale;
}
return "0 Bytes";
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorMultipleFileUploadRenderer.cs b/FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorMultipleFileUploadRenderer.cs
index dbecae45..f5f4cea6 100644
--- a/FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorMultipleFileUploadRenderer.cs
+++ b/FormCraft.ForMudBlazor/Fields/FileUploadField/MudBlazorMultipleFileUploadRenderer.cs
@@ -15,4 +15,4 @@ public override bool CanRender(Type fieldType, IFieldConfiguration);
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.ForMudBlazor/Fields/LookupField/MudBlazorLookupDialog.razor.cs b/FormCraft.ForMudBlazor/Fields/LookupField/MudBlazorLookupDialog.razor.cs
index 6eba8d56..da176f3e 100644
--- a/FormCraft.ForMudBlazor/Fields/LookupField/MudBlazorLookupDialog.razor.cs
+++ b/FormCraft.ForMudBlazor/Fields/LookupField/MudBlazorLookupDialog.razor.cs
@@ -45,7 +45,7 @@ public partial class MudBlazorLookupDialog : ComponentBase
private string _searchText = string.Empty;
private object? _selectedItem;
private bool _loading;
- private List _columnDefinitions = new();
+ private readonly List _columnDefinitions = new();
protected override void OnInitialized()
{
@@ -55,16 +55,25 @@ protected override void OnInitialized()
private void ExtractColumnDefinitions()
{
- if (Columns == null) return;
+ if (Columns == null)
+ {
+ return;
+ }
// The columns parameter is a List> stored as object.
// We use reflection to extract column info.
var columnsType = Columns.GetType();
- if (!columnsType.IsGenericType) return;
+ if (!columnsType.IsGenericType)
+ {
+ return;
+ }
var enumerableInterface = columnsType.GetInterfaces()
.FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IEnumerable<>));
- if (enumerableInterface == null) return;
+ if (enumerableInterface == null)
+ {
+ return;
+ }
foreach (var col in (System.Collections.IEnumerable)Columns)
{
@@ -102,15 +111,20 @@ private async Task> LoadServerData(TableState state, Cancellat
};
// Invoke the data provider delegate
- var task = ((Delegate)DataProvider).DynamicInvoke(query) as Task;
- if (task == null) return new TableData { Items = Array.Empty(), TotalItems = 0 };
+ if (((Delegate)DataProvider).DynamicInvoke(query) is not Task task)
+ {
+ return new TableData { Items = Array.Empty(), TotalItems = 0 };
+ }
await task;
// Get the result from the completed task
var resultProp = task.GetType().GetProperty("Result");
var result = resultProp?.GetValue(task);
- if (result == null) return new TableData { Items = Array.Empty(), TotalItems = 0 };
+ if (result == null)
+ {
+ return new TableData { Items = Array.Empty(), TotalItems = 0 };
+ }
// Extract Items and TotalCount from LookupResult
var itemsProp = result.GetType().GetProperty("Items");
diff --git a/FormCraft.ForMudBlazor/Fields/LookupField/MudBlazorLookupFieldComponent.razor.cs b/FormCraft.ForMudBlazor/Fields/LookupField/MudBlazorLookupFieldComponent.razor.cs
index b8a109a2..7965e6a2 100644
--- a/FormCraft.ForMudBlazor/Fields/LookupField/MudBlazorLookupFieldComponent.razor.cs
+++ b/FormCraft.ForMudBlazor/Fields/LookupField/MudBlazorLookupFieldComponent.razor.cs
@@ -55,7 +55,9 @@ private void UpdateDisplayText()
private async Task OpenLookupDialog()
{
if (IsReadOnly || IsDisabled)
+ {
return;
+ }
var dataProvider = GetAttribute("LookupDataProvider");
var valueSelector = GetAttribute("LookupValueSelector");
@@ -64,7 +66,9 @@ private async Task OpenLookupDialog()
var onItemSelected = GetAttribute("LookupOnItemSelected");
if (dataProvider == null || valueSelector == null || displaySelector == null)
+ {
return;
+ }
var parameters = new DialogParameters
{
diff --git a/FormCraft.ForMudBlazor/Fields/LovField/LovSelectionDialog.razor.cs b/FormCraft.ForMudBlazor/Fields/LovField/LovSelectionDialog.razor.cs
index bd21d6d0..087dda9a 100644
--- a/FormCraft.ForMudBlazor/Fields/LovField/LovSelectionDialog.razor.cs
+++ b/FormCraft.ForMudBlazor/Fields/LovField/LovSelectionDialog.razor.cs
@@ -60,7 +60,7 @@ protected override void OnInitialized()
if (SelectedItems.Count > 0)
{
- _selectedItemsSet = [..SelectedItems];
+ _selectedItemsSet = [.. SelectedItems];
}
}
@@ -192,8 +192,16 @@ private void ClearAllSelections()
///
private string GetConfirmButtonText()
{
- if (!_selectedItemsSet.Any()) return "Select";
- if (IsMultiSelect) return $"Select ({_selectedItemsSet.Count})";
+ if (!_selectedItemsSet.Any())
+ {
+ return "Select";
+ }
+
+ if (IsMultiSelect)
+ {
+ return $"Select ({_selectedItemsSet.Count})";
+ }
+
return "Select";
}
diff --git a/FormCraft.ForMudBlazor/Fields/LovField/MudBlazorLovFieldComponent.razor.cs b/FormCraft.ForMudBlazor/Fields/LovField/MudBlazorLovFieldComponent.razor.cs
index 1168aa97..d418e466 100644
--- a/FormCraft.ForMudBlazor/Fields/LovField/MudBlazorLovFieldComponent.razor.cs
+++ b/FormCraft.ForMudBlazor/Fields/LovField/MudBlazorLovFieldComponent.razor.cs
@@ -11,16 +11,14 @@ namespace FormCraft.ForMudBlazor;
/// The type of items in the LOV.
public partial class MudBlazorLovFieldComponent
{
- private ILovConfiguration? _lovConfig;
private ILovDataProvider? _dataProvider;
private readonly List _selectedItems = [];
- private string? _displayText;
private bool _isLoading;
///
/// Gets the LOV configuration.
///
- protected ILovConfiguration? LovConfig => _lovConfig;
+ protected ILovConfiguration? LovConfig { get; private set; }
///
/// Suppresses the ShrinkLabel diagnostic (#181) for LOV fields.
@@ -37,7 +35,7 @@ public partial class MudBlazorLovFieldComponent
///
/// Gets whether multiple selection is enabled.
///
- protected bool IsMultiSelect => _lovConfig?.SelectionMode == LovSelectionMode.Multiple;
+ protected bool IsMultiSelect => LovConfig?.SelectionMode == LovSelectionMode.Multiple;
///
/// Gets the list of selected items.
@@ -47,7 +45,7 @@ public partial class MudBlazorLovFieldComponent
///
/// Gets the display text for the field.
///
- protected string? DisplayText => _displayText;
+ protected string? DisplayText { get; private set; }
///
/// Gets the CSS class for the field.
@@ -61,8 +59,16 @@ protected string AdornmentIcon
{
get
{
- if (_isLoading) return Icons.Material.Filled.HourglassEmpty;
- if (CurrentValue != null && !IsMultiSelect) return Icons.Material.Filled.Clear;
+ if (_isLoading)
+ {
+ return Icons.Material.Filled.HourglassEmpty;
+ }
+
+ if (CurrentValue != null && !IsMultiSelect)
+ {
+ return Icons.Material.Filled.Clear;
+ }
+
return Icons.Material.Filled.Search;
}
}
@@ -85,12 +91,12 @@ protected override void OnFieldConfigurationChanged()
// handing the same stale list to the picker dialog as its pre-selection. The reload-not-patch
// rule applies to state derived from the configuration, not only to the properties (#298).
_selectedItems.Clear();
- _displayText = null;
+ DisplayText = null;
_isLoading = false;
- _lovConfig = GetAttribute>("LovConfiguration");
+ LovConfig = GetAttribute>("LovConfiguration");
- if (_lovConfig == null)
+ if (LovConfig == null)
{
throw new InvalidOperationException(
"LovConfiguration is required. Use .AsLov() extension method to configure the field.");
@@ -105,16 +111,18 @@ protected override void OnFieldConfigurationChanged()
private void InitializeDataProvider()
{
- if (_lovConfig == null) return;
+ if (LovConfig == null)
+ {
+ return;
+ }
- var factory = ServiceProvider.GetService(typeof(ILovDataProviderFactory)) as ILovDataProviderFactory;
- if (factory != null)
+ if (ServiceProvider.GetService(typeof(ILovDataProviderFactory)) is ILovDataProviderFactory factory)
{
- _dataProvider = factory.Create(_lovConfig);
+ _dataProvider = factory.Create(LovConfig);
}
- else if (_lovConfig.DataProvider != null)
+ else if (LovConfig.DataProvider != null)
{
- _dataProvider = new LambdaLovDataProvider(_lovConfig.DataProvider, null);
+ _dataProvider = new LambdaLovDataProvider(LovConfig.DataProvider, null);
}
}
@@ -122,24 +130,24 @@ private void UpdateDisplayText()
{
if (CurrentValue == null || EqualityComparer.Default.Equals(CurrentValue, default))
{
- _displayText = null;
+ DisplayText = null;
return;
}
if (IsMultiSelect)
{
- _displayText = _selectedItems.Count > 0
+ DisplayText = _selectedItems.Count > 0
? $"{_selectedItems.Count} item(s) selected"
: null;
}
else if (_selectedItems.Count > 0)
{
- _displayText = GetItemDisplayText(_selectedItems[0]);
+ DisplayText = GetItemDisplayText(_selectedItems[0]);
}
else
{
// Try to get display text from cached value or show the value itself
- _displayText = CurrentValue?.ToString();
+ DisplayText = CurrentValue?.ToString();
}
}
@@ -148,7 +156,7 @@ private void UpdateDisplayText()
///
protected string GetItemDisplayText(TItem item)
{
- return _lovConfig?.DisplaySelector(item) ?? item?.ToString() ?? string.Empty;
+ return LovConfig?.DisplaySelector(item) ?? item?.ToString() ?? string.Empty;
}
///
@@ -156,7 +164,10 @@ protected string GetItemDisplayText(TItem item)
///
private async Task HandleAdornmentClick()
{
- if (IsDisabled || IsReadOnly) return;
+ if (IsDisabled || IsReadOnly)
+ {
+ return;
+ }
// If there's a value and it's single select, clear it
if (CurrentValue != null && !IsMultiSelect)
@@ -173,7 +184,10 @@ private async Task HandleAdornmentClick()
///
private async Task OpenLovDialog()
{
- if (_lovConfig == null || _dataProvider == null) return;
+ if (LovConfig == null || _dataProvider == null)
+ {
+ return;
+ }
_isLoading = true;
StateHasChanged();
@@ -182,7 +196,7 @@ private async Task OpenLovDialog()
{
var parameters = new DialogParameters>
{
- { x => x.LovConfig, _lovConfig },
+ { x => x.LovConfig, LovConfig },
{ x => x.DataProvider, _dataProvider },
{ x => x.SelectedItems, _selectedItems.ToList() },
{ x => x.ServiceProvider, ServiceProvider }
@@ -190,15 +204,15 @@ private async Task OpenLovDialog()
var options = new DialogOptions
{
- MaxWidth = ConvertModalSize(_lovConfig.ModalOptions.Size),
+ MaxWidth = ConvertModalSize(LovConfig.ModalOptions.Size),
FullWidth = true,
CloseButton = true,
- CloseOnEscapeKey = _lovConfig.ModalOptions.CloseOnEscapeKey,
- BackdropClick = _lovConfig.ModalOptions.CloseOnBackdropClick
+ CloseOnEscapeKey = LovConfig.ModalOptions.CloseOnEscapeKey,
+ BackdropClick = LovConfig.ModalOptions.CloseOnBackdropClick
};
var dialog = await DialogService.ShowAsync>(
- _lovConfig.ModalOptions.Title,
+ LovConfig.ModalOptions.Title,
parameters,
options);
@@ -224,13 +238,16 @@ private async Task ApplySelection(List items)
_selectedItems.Clear();
_selectedItems.AddRange(items);
- if (_lovConfig == null) return;
+ if (LovConfig == null)
+ {
+ return;
+ }
if (IsMultiSelect)
{
// For multi-select, we'd need to handle IEnumerable
// This is a simplified implementation
- var values = items.Select(_lovConfig.ValueSelector).ToList();
+ var values = items.Select(LovConfig.ValueSelector).ToList();
// Note: This cast may need adjustment based on actual TValue type
var typedValues = (TValue)(object)values;
SetValueWithoutNotification(typedValues);
@@ -241,7 +258,7 @@ private async Task ApplySelection(List items)
var item = items.FirstOrDefault();
if (item != null)
{
- var value = _lovConfig.ValueSelector(item);
+ var value = LovConfig.ValueSelector(item);
// Update our own state first so the display text and the
// adornment reflect the selection immediately
SetValueWithoutNotification(value);
@@ -265,10 +282,13 @@ private async Task ApplySelection(List items)
///
private async Task ApplyFieldMappings(TItem item)
{
- if (_lovConfig == null || Context.Model == null) return;
+ if (LovConfig == null || Context.Model == null)
+ {
+ return;
+ }
// Apply each mapping directly
- foreach (var mapping in _lovConfig.FieldMappings)
+ foreach (var mapping in LovConfig.FieldMappings)
{
if (mapping is IAsyncLovFieldMapping asyncMapping)
{
@@ -290,15 +310,15 @@ private async Task ApplyFieldMappings(TItem item)
private async Task ClearSelection()
{
_selectedItems.Clear();
- _displayText = null;
+ DisplayText = null;
SetValueWithoutNotification(default);
await NotifyValueChangedAsync(default);
// Clear mapped fields by setting them to default values
- if (_lovConfig != null && Context.Model != null)
+ if (LovConfig != null && Context.Model != null)
{
var modelType = Context.Model.GetType();
- foreach (var mapping in _lovConfig.FieldMappings)
+ foreach (var mapping in LovConfig.FieldMappings)
{
var property = modelType.GetProperty(mapping.TargetProperty);
if (property?.CanWrite == true)
diff --git a/FormCraft.ForMudBlazor/Fields/NumericField/MudBlazorNullableNumericFieldComponent.razor.cs b/FormCraft.ForMudBlazor/Fields/NumericField/MudBlazorNullableNumericFieldComponent.razor.cs
index ac991f43..17d71602 100644
--- a/FormCraft.ForMudBlazor/Fields/NumericField/MudBlazorNullableNumericFieldComponent.razor.cs
+++ b/FormCraft.ForMudBlazor/Fields/NumericField/MudBlazorNullableNumericFieldComponent.razor.cs
@@ -102,11 +102,20 @@ private static TValue GetDefaultStep()
// so each floating type needs its own literal (0.1 boxed as double cannot be
// unboxed as float, and 1 boxed as int cannot be unboxed as long/short/byte).
if (typeof(TValue) == typeof(decimal))
+ {
return (TValue)(object)0.01m;
+ }
+
if (typeof(TValue) == typeof(double))
+ {
return (TValue)(object)0.1d;
+ }
+
if (typeof(TValue) == typeof(float))
+ {
return (TValue)(object)0.1f;
+ }
+
return (TValue)Convert.ChangeType(1, typeof(TValue));
}
diff --git a/FormCraft.ForMudBlazor/Fields/NumericField/MudBlazorNumericFieldComponent.razor.cs b/FormCraft.ForMudBlazor/Fields/NumericField/MudBlazorNumericFieldComponent.razor.cs
index 8b8555da..a4f56f02 100644
--- a/FormCraft.ForMudBlazor/Fields/NumericField/MudBlazorNumericFieldComponent.razor.cs
+++ b/FormCraft.ForMudBlazor/Fields/NumericField/MudBlazorNumericFieldComponent.razor.cs
@@ -94,11 +94,20 @@ private static TValue GetDefaultStep()
// so each floating type needs its own literal (0.1 boxed as double cannot be
// unboxed as float, and 1 boxed as int cannot be unboxed as long/short/byte).
if (typeof(TValue) == typeof(decimal))
+ {
return (TValue)(object)0.01m;
+ }
+
if (typeof(TValue) == typeof(double))
+ {
return (TValue)(object)0.1d;
+ }
+
if (typeof(TValue) == typeof(float))
+ {
return (TValue)(object)0.1f;
+ }
+
return (TValue)Convert.ChangeType(1, typeof(TValue));
}
@@ -119,4 +128,4 @@ private async Task OnLocalValueChanged()
SetValueWithoutNotification(_localValue);
await Context.OnValueChanged.InvokeAsync(_localValue);
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.ForMudBlazor/Fields/NumericField/MudBlazorNumericFieldRenderer.cs b/FormCraft.ForMudBlazor/Fields/NumericField/MudBlazorNumericFieldRenderer.cs
index 3ea95ab2..185df0d1 100644
--- a/FormCraft.ForMudBlazor/Fields/NumericField/MudBlazorNumericFieldRenderer.cs
+++ b/FormCraft.ForMudBlazor/Fields/NumericField/MudBlazorNumericFieldRenderer.cs
@@ -33,4 +33,4 @@ public override bool CanRender(Type fieldType, IFieldConfiguration
public override bool CanRender(Type fieldType, IFieldConfiguration field) => fieldType == typeof(string);
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Builders/FieldBuilderTests.cs b/FormCraft.UnitTests/Builders/FieldBuilderTests.cs
index ea9eb281..25c26371 100644
--- a/FormCraft.UnitTests/Builders/FieldBuilderTests.cs
+++ b/FormCraft.UnitTests/Builders/FieldBuilderTests.cs
@@ -443,4 +443,4 @@ public class TestModel
public string City { get; set; } = string.Empty;
public bool IsActive { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Builders/FieldConfigurationWrapperTests.cs b/FormCraft.UnitTests/Builders/FieldConfigurationWrapperTests.cs
index 22ea5799..b6265965 100644
--- a/FormCraft.UnitTests/Builders/FieldConfigurationWrapperTests.cs
+++ b/FormCraft.UnitTests/Builders/FieldConfigurationWrapperTests.cs
@@ -619,4 +619,4 @@ public class TestModel
public string Email { get; set; } = string.Empty;
public int Age { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Builders/FieldGroupBuilderTests.cs b/FormCraft.UnitTests/Builders/FieldGroupBuilderTests.cs
index 977342b2..30d240f1 100644
--- a/FormCraft.UnitTests/Builders/FieldGroupBuilderTests.cs
+++ b/FormCraft.UnitTests/Builders/FieldGroupBuilderTests.cs
@@ -311,4 +311,4 @@ public void WithHeaderRightContent_Generic_With_Parameters_Should_Work()
fieldGroup.ShouldNotBeNull();
fieldGroup.HeaderRightContent.ShouldNotBeNull();
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Builders/FormBuilderTests.cs b/FormCraft.UnitTests/Builders/FormBuilderTests.cs
index b9f08aa5..5ca6ef2d 100644
--- a/FormCraft.UnitTests/Builders/FormBuilderTests.cs
+++ b/FormCraft.UnitTests/Builders/FormBuilderTests.cs
@@ -251,7 +251,9 @@ public void Complex_Form_Building_Scenario()
.DependsOn(x => x.Country, (m, country) =>
{
if (string.IsNullOrEmpty(country))
+ {
m.City = string.Empty;
+ }
})
.WithOrder(5))
.Build();
@@ -338,4 +340,4 @@ public class TestModel
public string Country { get; set; } = string.Empty;
public string City { get; set; } = string.Empty;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Builders/ValidatorWrapperTests.cs b/FormCraft.UnitTests/Builders/ValidatorWrapperTests.cs
index 981c3462..dbaeeed9 100644
--- a/FormCraft.UnitTests/Builders/ValidatorWrapperTests.cs
+++ b/FormCraft.UnitTests/Builders/ValidatorWrapperTests.cs
@@ -283,4 +283,4 @@ public class TestModel
public string Email { get; set; } = string.Empty;
public int Age { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Components/DynamicFormValidatorTests.cs b/FormCraft.UnitTests/Components/DynamicFormValidatorTests.cs
index c1e1fc60..4d94bc5e 100644
--- a/FormCraft.UnitTests/Components/DynamicFormValidatorTests.cs
+++ b/FormCraft.UnitTests/Components/DynamicFormValidatorTests.cs
@@ -174,4 +174,4 @@ public class AnotherTestModel
public string Title { get; set; } = string.Empty;
public bool IsActive { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Components/FieldValidationMessageTests.cs b/FormCraft.UnitTests/Components/FieldValidationMessageTests.cs
index a3624c2a..b0549de8 100644
--- a/FormCraft.UnitTests/Components/FieldValidationMessageTests.cs
+++ b/FormCraft.UnitTests/Components/FieldValidationMessageTests.cs
@@ -259,4 +259,4 @@ public class TestModel
public string Email { get; set; } = string.Empty;
public int Age { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Components/FormCraftComponentTests.cs b/FormCraft.UnitTests/Components/FormCraftComponentTests.cs
index 6692abc6..da8c06c8 100644
--- a/FormCraft.UnitTests/Components/FormCraftComponentTests.cs
+++ b/FormCraft.UnitTests/Components/FormCraftComponentTests.cs
@@ -244,4 +244,4 @@ public class TestModel
public int Age { get; set; }
public bool ShowOptionalField { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Core/FieldConfigurationTests.cs b/FormCraft.UnitTests/Core/FieldConfigurationTests.cs
index c6fba5a7..18b5221e 100644
--- a/FormCraft.UnitTests/Core/FieldConfigurationTests.cs
+++ b/FormCraft.UnitTests/Core/FieldConfigurationTests.cs
@@ -207,4 +207,4 @@ public class AddressModel
{
public string City { get; set; } = string.Empty;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Core/FieldDependencyTests.cs b/FormCraft.UnitTests/Core/FieldDependencyTests.cs
index 55ec75ae..f4b3fbd1 100644
--- a/FormCraft.UnitTests/Core/FieldDependencyTests.cs
+++ b/FormCraft.UnitTests/Core/FieldDependencyTests.cs
@@ -268,4 +268,4 @@ public class AddressModel
{
public string PostalCode { get; set; } = string.Empty;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Core/FileUploadConfigurationTests.cs b/FormCraft.UnitTests/Core/FileUploadConfigurationTests.cs
index eba68360..4e9ecbfa 100644
--- a/FormCraft.UnitTests/Core/FileUploadConfigurationTests.cs
+++ b/FormCraft.UnitTests/Core/FileUploadConfigurationTests.cs
@@ -261,4 +261,4 @@ public void GetConstraintsDescription_CombinesAllConstraints()
result.ShouldContain("Max files: 5");
result.ShouldContain(" • "); // Separator
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Core/SelectOptionTests.cs b/FormCraft.UnitTests/Core/SelectOptionTests.cs
index 58bc2daa..f34a0f3e 100644
--- a/FormCraft.UnitTests/Core/SelectOptionTests.cs
+++ b/FormCraft.UnitTests/Core/SelectOptionTests.cs
@@ -66,4 +66,4 @@ private class TestClass
{
public int Id { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Core/ValidationResultTests.cs b/FormCraft.UnitTests/Core/ValidationResultTests.cs
index 994cb366..3729f124 100644
--- a/FormCraft.UnitTests/Core/ValidationResultTests.cs
+++ b/FormCraft.UnitTests/Core/ValidationResultTests.cs
@@ -51,4 +51,4 @@ public void Failure_With_Null_Message_Should_Create_Invalid_Result()
result.IsValid.ShouldBeFalse();
result.ErrorMessage.ShouldBeNull();
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Extensions/AttributeFormBuilderExtensionsTests.cs b/FormCraft.UnitTests/Extensions/AttributeFormBuilderExtensionsTests.cs
index 1cefbf15..b76029fe 100644
--- a/FormCraft.UnitTests/Extensions/AttributeFormBuilderExtensionsTests.cs
+++ b/FormCraft.UnitTests/Extensions/AttributeFormBuilderExtensionsTests.cs
@@ -393,4 +393,4 @@ public void Multiple_Attributes_Should_All_Be_Processed()
config.Fields.ShouldContain(f => f.FieldName == "Price");
config.Fields.ShouldContain(f => f.FieldName == "AppointmentDate");
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Extensions/FieldBuilderExtensionsTests.cs b/FormCraft.UnitTests/Extensions/FieldBuilderExtensionsTests.cs
index e317ce0d..42275780 100644
--- a/FormCraft.UnitTests/Extensions/FieldBuilderExtensionsTests.cs
+++ b/FormCraft.UnitTests/Extensions/FieldBuilderExtensionsTests.cs
@@ -408,4 +408,4 @@ public class TestModel
public string Bio { get; set; } = string.Empty;
public int Age { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Extensions/FileUploadExtensionsTests.cs b/FormCraft.UnitTests/Extensions/FileUploadExtensionsTests.cs
index baaf286e..2db7a11c 100644
--- a/FormCraft.UnitTests/Extensions/FileUploadExtensionsTests.cs
+++ b/FormCraft.UnitTests/Extensions/FileUploadExtensionsTests.cs
@@ -214,4 +214,4 @@ public void FileUploadExtensions_DoNotForceARendererOverride()
field.AdditionalAttributes.ShouldNotContainKey("CustomRendererInstance");
field.CustomRendererType.ShouldBeNull();
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Extensions/FluentFormBuilderExtensionsTests.cs b/FormCraft.UnitTests/Extensions/FluentFormBuilderExtensionsTests.cs
index 38eee296..0fce115c 100644
--- a/FormCraft.UnitTests/Extensions/FluentFormBuilderExtensionsTests.cs
+++ b/FormCraft.UnitTests/Extensions/FluentFormBuilderExtensionsTests.cs
@@ -457,4 +457,4 @@ public class TestModel
public string Password { get; set; } = string.Empty;
public bool AcceptTerms { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Extensions/FormConfigurationExtensionsTests.cs b/FormCraft.UnitTests/Extensions/FormConfigurationExtensionsTests.cs
index 812d42b6..32d01aa3 100644
--- a/FormCraft.UnitTests/Extensions/FormConfigurationExtensionsTests.cs
+++ b/FormCraft.UnitTests/Extensions/FormConfigurationExtensionsTests.cs
@@ -231,4 +231,4 @@ public class TestModel
public string Phone { get; set; } = string.Empty;
public bool ShowOptionalFields { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs b/FormCraft.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs
index fb629bfe..d78476c1 100644
--- a/FormCraft.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs
+++ b/FormCraft.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs
@@ -195,4 +195,4 @@ public class TestService : ITestService
{
public string GetMessage() => "Test message";
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/ForMudBlazor/PasswordFieldTests.cs b/FormCraft.UnitTests/ForMudBlazor/PasswordFieldTests.cs
index 5205f67a..8c50782e 100644
--- a/FormCraft.UnitTests/ForMudBlazor/PasswordFieldTests.cs
+++ b/FormCraft.UnitTests/ForMudBlazor/PasswordFieldTests.cs
@@ -250,4 +250,4 @@ public async Task Username_Field_Should_Update_Model_When_User_Types()
// Assert - Model should be updated
model.Username.ShouldBe("john_doe");
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/GlobalUsings.cs b/FormCraft.UnitTests/GlobalUsings.cs
index 01dce6ed..e4edfe8b 100644
--- a/FormCraft.UnitTests/GlobalUsings.cs
+++ b/FormCraft.UnitTests/GlobalUsings.cs
@@ -1,10 +1,10 @@
-global using Xunit;
-global using Shouldly;
-global using FakeItEasy;
+global using System.Linq.Expressions;
global using Bunit;
+global using FakeItEasy;
+global using FormCraft;
+global using FormCraft.ForMudBlazor;
global using Microsoft.AspNetCore.Components;
global using Microsoft.AspNetCore.Components.Forms;
global using Microsoft.Extensions.DependencyInjection;
-global using System.Linq.Expressions;
-global using FormCraft;
-global using FormCraft.ForMudBlazor;
\ No newline at end of file
+global using Shouldly;
+global using Xunit;
diff --git a/FormCraft.UnitTests/Integration/CompleteFormWorkflowTests.cs b/FormCraft.UnitTests/Integration/CompleteFormWorkflowTests.cs
index 96a1e342..674f5445 100644
--- a/FormCraft.UnitTests/Integration/CompleteFormWorkflowTests.cs
+++ b/FormCraft.UnitTests/Integration/CompleteFormWorkflowTests.cs
@@ -301,4 +301,4 @@ public class CompleteTestModel
public bool IsActive { get; set; }
public DateTime BirthDate { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Integration/DependencyInjectionIntegrationTests.cs b/FormCraft.UnitTests/Integration/DependencyInjectionIntegrationTests.cs
index 71e153d4..694e0aeb 100644
--- a/FormCraft.UnitTests/Integration/DependencyInjectionIntegrationTests.cs
+++ b/FormCraft.UnitTests/Integration/DependencyInjectionIntegrationTests.cs
@@ -357,4 +357,4 @@ public class TestModel
{
public string Name { get; set; } = string.Empty;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Integration/FileUploadIntegrationTests.cs b/FormCraft.UnitTests/Integration/FileUploadIntegrationTests.cs
index e14ffb62..01d01651 100644
--- a/FormCraft.UnitTests/Integration/FileUploadIntegrationTests.cs
+++ b/FormCraft.UnitTests/Integration/FileUploadIntegrationTests.cs
@@ -242,4 +242,4 @@ public void ComplexFormWithFileUploads_ConfiguresAllFieldsCorrectly()
fileFields.ShouldAllBe(f => f.CustomRendererType == null);
fileFields.ShouldAllBe(f => f.AdditionalAttributes.ContainsKey("FileUploadConfiguration"));
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Integration/RealModelTemplateTests.cs b/FormCraft.UnitTests/Integration/RealModelTemplateTests.cs
index dcd28bb8..0428db86 100644
--- a/FormCraft.UnitTests/Integration/RealModelTemplateTests.cs
+++ b/FormCraft.UnitTests/Integration/RealModelTemplateTests.cs
@@ -301,4 +301,4 @@ public class ExtendedContactModel
public DateTime DateOfBirth { get; set; }
public bool IsSubscribed { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Integration/ValidationPipelineIntegrationTests.cs b/FormCraft.UnitTests/Integration/ValidationPipelineIntegrationTests.cs
index 885a2bbb..8a789a80 100644
--- a/FormCraft.UnitTests/Integration/ValidationPipelineIntegrationTests.cs
+++ b/FormCraft.UnitTests/Integration/ValidationPipelineIntegrationTests.cs
@@ -296,4 +296,4 @@ public class ComplexValidationModel
public int Age { get; set; }
public bool AcceptTerms { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Rendering/BoolFieldRendererTests.cs b/FormCraft.UnitTests/Rendering/BoolFieldRendererTests.cs
index 8c82aacf..b312c85d 100644
--- a/FormCraft.UnitTests/Rendering/BoolFieldRendererTests.cs
+++ b/FormCraft.UnitTests/Rendering/BoolFieldRendererTests.cs
@@ -633,4 +633,4 @@ public class NullableTestModel
{
public bool? IsEnabled { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Rendering/CustomFieldRendererTests.cs b/FormCraft.UnitTests/Rendering/CustomFieldRendererTests.cs
index 14bbaa84..37b427fe 100644
--- a/FormCraft.UnitTests/Rendering/CustomFieldRendererTests.cs
+++ b/FormCraft.UnitTests/Rendering/CustomFieldRendererTests.cs
@@ -127,4 +127,4 @@ public override RenderFragment Render(IFieldRenderContext context)
public string? TestGetValue(IFieldRenderContext context) => GetValue(context);
public Task TestSetValue(IFieldRenderContext context, string? value) => SetValue(context, value);
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Rendering/CustomRenderers/ColorPickerRendererTests.cs b/FormCraft.UnitTests/Rendering/CustomRenderers/ColorPickerRendererTests.cs
index fd46a743..37c1ad0b 100644
--- a/FormCraft.UnitTests/Rendering/CustomRenderers/ColorPickerRendererTests.cs
+++ b/FormCraft.UnitTests/Rendering/CustomRenderers/ColorPickerRendererTests.cs
@@ -62,4 +62,4 @@ private class TestFieldConfiguration
public bool IsDisabled { get; set; }
public bool IsReadOnly { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Rendering/CustomRenderers/RatingRendererTests.cs b/FormCraft.UnitTests/Rendering/CustomRenderers/RatingRendererTests.cs
index 3a879807..4eac2b9b 100644
--- a/FormCraft.UnitTests/Rendering/CustomRenderers/RatingRendererTests.cs
+++ b/FormCraft.UnitTests/Rendering/CustomRenderers/RatingRendererTests.cs
@@ -88,4 +88,4 @@ private class TestFieldConfiguration
public bool IsReadOnly { get; set; }
public Dictionary AdditionalAttributes { get; set; } = new();
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Rendering/DateTimeFieldRendererTests.cs b/FormCraft.UnitTests/Rendering/DateTimeFieldRendererTests.cs
index 64ffe7b6..e105923c 100644
--- a/FormCraft.UnitTests/Rendering/DateTimeFieldRendererTests.cs
+++ b/FormCraft.UnitTests/Rendering/DateTimeFieldRendererTests.cs
@@ -804,4 +804,4 @@ public class TestModel
{
public DateTime? BirthDate { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Rendering/DecimalFieldRendererTests.cs b/FormCraft.UnitTests/Rendering/DecimalFieldRendererTests.cs
index d70350da..b81f7e2e 100644
--- a/FormCraft.UnitTests/Rendering/DecimalFieldRendererTests.cs
+++ b/FormCraft.UnitTests/Rendering/DecimalFieldRendererTests.cs
@@ -149,4 +149,4 @@ public class TestModel
public decimal Price { get; set; }
public decimal? OptionalAmount { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Rendering/DoubleFieldRendererTests.cs b/FormCraft.UnitTests/Rendering/DoubleFieldRendererTests.cs
index 47f99347..9113cf20 100644
--- a/FormCraft.UnitTests/Rendering/DoubleFieldRendererTests.cs
+++ b/FormCraft.UnitTests/Rendering/DoubleFieldRendererTests.cs
@@ -150,4 +150,4 @@ public class TestModel
public double Temperature { get; set; }
public double? OptionalMeasurement { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Rendering/FieldRenderContextTests.cs b/FormCraft.UnitTests/Rendering/FieldRenderContextTests.cs
index f41bc28d..430fd693 100644
--- a/FormCraft.UnitTests/Rendering/FieldRenderContextTests.cs
+++ b/FormCraft.UnitTests/Rendering/FieldRenderContextTests.cs
@@ -176,4 +176,4 @@ public class TestModel
public DateTime DateCreated { get; set; }
public int? NullableValue { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Rendering/FieldRendererServiceTests.cs b/FormCraft.UnitTests/Rendering/FieldRendererServiceTests.cs
index 63e37279..45b32bb3 100644
--- a/FormCraft.UnitTests/Rendering/FieldRendererServiceTests.cs
+++ b/FormCraft.UnitTests/Rendering/FieldRendererServiceTests.cs
@@ -439,4 +439,4 @@ public RenderFragment Render(IFieldRenderContext context)
return builder => builder.AddContent(0, "Custom");
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Rendering/FileUploadFieldRendererTests.cs b/FormCraft.UnitTests/Rendering/FileUploadFieldRendererTests.cs
index 9e9f2f84..705e2bd5 100644
--- a/FormCraft.UnitTests/Rendering/FileUploadFieldRendererTests.cs
+++ b/FormCraft.UnitTests/Rendering/FileUploadFieldRendererTests.cs
@@ -269,4 +269,4 @@ public class TestModel
public IBrowserFile? Resume { get; set; }
public IReadOnlyList? Documents { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Rendering/IntFieldRendererTests.cs b/FormCraft.UnitTests/Rendering/IntFieldRendererTests.cs
index e301a0c5..654f5905 100644
--- a/FormCraft.UnitTests/Rendering/IntFieldRendererTests.cs
+++ b/FormCraft.UnitTests/Rendering/IntFieldRendererTests.cs
@@ -663,4 +663,4 @@ public class LongTestModel
{
public long Value { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Rendering/StringFieldRendererTests.cs b/FormCraft.UnitTests/Rendering/StringFieldRendererTests.cs
index 4a2bcdf8..dafe915a 100644
--- a/FormCraft.UnitTests/Rendering/StringFieldRendererTests.cs
+++ b/FormCraft.UnitTests/Rendering/StringFieldRendererTests.cs
@@ -581,4 +581,4 @@ public class TestModel
{
public string? Name { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Security/AuditLogServiceTests.cs b/FormCraft.UnitTests/Security/AuditLogServiceTests.cs
index 95050f34..332f6703 100644
--- a/FormCraft.UnitTests/Security/AuditLogServiceTests.cs
+++ b/FormCraft.UnitTests/Security/AuditLogServiceTests.cs
@@ -279,4 +279,4 @@ public async Task Should_Include_Additional_Data_In_Log()
call.GetArgument(0) == LogLevel.Information)
.MustHaveHappenedOnceExactly();
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Security/RateLimitServiceTests.cs b/FormCraft.UnitTests/Security/RateLimitServiceTests.cs
index 4e664fca..9e24a79a 100644
--- a/FormCraft.UnitTests/Security/RateLimitServiceTests.cs
+++ b/FormCraft.UnitTests/Security/RateLimitServiceTests.cs
@@ -181,4 +181,4 @@ public void Should_Allow_Multiple_Dispose_Calls()
service.Dispose();
});
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Security/SecurityBuilderTests.cs b/FormCraft.UnitTests/Security/SecurityBuilderTests.cs
index b4812432..9f193be6 100644
--- a/FormCraft.UnitTests/Security/SecurityBuilderTests.cs
+++ b/FormCraft.UnitTests/Security/SecurityBuilderTests.cs
@@ -195,4 +195,4 @@ public void Should_Work_With_Regular_Form_Configuration()
config.Security.ShouldNotBeNull();
config.Security.EncryptedFields.ShouldContain("SSN");
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Templates/ContactFormTemplateTests.cs b/FormCraft.UnitTests/Templates/ContactFormTemplateTests.cs
index ff963750..72b07de0 100644
--- a/FormCraft.UnitTests/Templates/ContactFormTemplateTests.cs
+++ b/FormCraft.UnitTests/Templates/ContactFormTemplateTests.cs
@@ -283,4 +283,4 @@ public class IncompleteModel
public string Name { get; set; } = string.Empty;
// Missing required properties for contact/registration forms
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Validators/AsyncValidatorTests.cs b/FormCraft.UnitTests/Validators/AsyncValidatorTests.cs
index 683d5f15..f3e4f0f1 100644
--- a/FormCraft.UnitTests/Validators/AsyncValidatorTests.cs
+++ b/FormCraft.UnitTests/Validators/AsyncValidatorTests.cs
@@ -154,13 +154,19 @@ async Task ComplexValidation(string phoneNumber)
// Simulate multiple async operations
await Task.Delay(25); // First service call
- if (string.IsNullOrWhiteSpace(phoneNumber)) return false;
+ if (string.IsNullOrWhiteSpace(phoneNumber))
+ {
+ return false;
+ }
await Task.Delay(25); // Second service call
// Check format
var cleanNumber = phoneNumber.Replace("-", "").Replace(" ", "").Replace("(", "").Replace(")", "");
- if (cleanNumber.Length != 10) return false;
+ if (cleanNumber.Length != 10)
+ {
+ return false;
+ }
await Task.Delay(25); // Third service call
@@ -263,4 +269,4 @@ private class TestModel
public string? Email { get; set; }
public string? PhoneNumber { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Validators/CustomValidatorTests.cs b/FormCraft.UnitTests/Validators/CustomValidatorTests.cs
index e21105ac..2fcbd56f 100644
--- a/FormCraft.UnitTests/Validators/CustomValidatorTests.cs
+++ b/FormCraft.UnitTests/Validators/CustomValidatorTests.cs
@@ -63,7 +63,11 @@ public async Task ValidateAsync_Should_Support_Complex_Validation_Logic()
// Arrange
Func validationFunction = value =>
{
- if (string.IsNullOrEmpty(value)) return false;
+ if (string.IsNullOrEmpty(value))
+ {
+ return false;
+ }
+
return value.Length >= 3 && value.Length <= 20 && value.All(char.IsLetterOrDigit);
};
@@ -174,7 +178,10 @@ public async Task ValidateAsync_Should_Support_Email_Validation()
// Arrange
Func emailValidation = value =>
{
- if (string.IsNullOrWhiteSpace(value)) return false;
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ return false;
+ }
try
{
@@ -209,4 +216,4 @@ public class TestModel
public DateTime BirthDate { get; set; }
public string? Email { get; set; }
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Validators/FluentValidationAdapterTests.cs b/FormCraft.UnitTests/Validators/FluentValidationAdapterTests.cs
index 59fab8a7..6686948b 100644
--- a/FormCraft.UnitTests/Validators/FluentValidationAdapterTests.cs
+++ b/FormCraft.UnitTests/Validators/FluentValidationAdapterTests.cs
@@ -169,4 +169,4 @@ public class Address
public string Street { get; set; } = "";
public string City { get; set; } = "";
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Validators/FluentValidationExtensionsTests.cs b/FormCraft.UnitTests/Validators/FluentValidationExtensionsTests.cs
index 00c527d0..80770188 100644
--- a/FormCraft.UnitTests/Validators/FluentValidationExtensionsTests.cs
+++ b/FormCraft.UnitTests/Validators/FluentValidationExtensionsTests.cs
@@ -130,4 +130,4 @@ public TestModelValidator()
.EmailAddress().WithMessage("Invalid email format");
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft.UnitTests/Validators/RequiredValidatorTests.cs b/FormCraft.UnitTests/Validators/RequiredValidatorTests.cs
index 175c6758..6d9ac90c 100644
--- a/FormCraft.UnitTests/Validators/RequiredValidatorTests.cs
+++ b/FormCraft.UnitTests/Validators/RequiredValidatorTests.cs
@@ -262,4 +262,4 @@ private class CustomType
{
public string Value { get; set; } = string.Empty;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Components/FieldComponentBase.cs b/FormCraft/Components/FieldComponentBase.cs
index ae589f77..2785266e 100644
--- a/FormCraft/Components/FieldComponentBase.cs
+++ b/FormCraft/Components/FieldComponentBase.cs
@@ -168,4 +168,4 @@ private bool ShouldReloadValue()
}
return defaultValue;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Abstractions/ICustomFieldRenderer.cs b/FormCraft/Forms/Abstractions/ICustomFieldRenderer.cs
index cf398e7f..61c541fa 100644
--- a/FormCraft/Forms/Abstractions/ICustomFieldRenderer.cs
+++ b/FormCraft/Forms/Abstractions/ICustomFieldRenderer.cs
@@ -61,4 +61,4 @@ protected async Task SetValue(IFieldRenderContext context, TValue? value)
{
await context.OnValueChanged.InvokeAsync(value);
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Abstractions/IFieldDependency.cs b/FormCraft/Forms/Abstractions/IFieldDependency.cs
index 3ef1d003..ddffe4a0 100644
--- a/FormCraft/Forms/Abstractions/IFieldDependency.cs
+++ b/FormCraft/Forms/Abstractions/IFieldDependency.cs
@@ -45,4 +45,4 @@ Task OnDependencyChangedAsync(TModel model)
OnDependencyChanged(model);
return Task.CompletedTask;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Abstractions/IFieldRenderContext.cs b/FormCraft/Forms/Abstractions/IFieldRenderContext.cs
index b060e79f..e1dbddd2 100644
--- a/FormCraft/Forms/Abstractions/IFieldRenderContext.cs
+++ b/FormCraft/Forms/Abstractions/IFieldRenderContext.cs
@@ -73,4 +73,4 @@ public interface IFieldRenderContext : IFieldRenderContext
/// Gets the callback to invoke when field dependencies change (for conditional visibility).
///
EventCallback OnDependencyChanged { get; }
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Abstractions/IFieldRenderer.cs b/FormCraft/Forms/Abstractions/IFieldRenderer.cs
index 1a0fb936..0691d666 100644
--- a/FormCraft/Forms/Abstractions/IFieldRenderer.cs
+++ b/FormCraft/Forms/Abstractions/IFieldRenderer.cs
@@ -43,4 +43,4 @@ public interface IFieldRenderer
/// The render context containing the field configuration, current value, and callbacks.
/// A RenderFragment that generates the appropriate UI component for the field.
RenderFragment Render(IFieldRenderContext context);
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Abstractions/IFieldRendererService.cs b/FormCraft/Forms/Abstractions/IFieldRendererService.cs
index c3943476..48610f8d 100644
--- a/FormCraft/Forms/Abstractions/IFieldRendererService.cs
+++ b/FormCraft/Forms/Abstractions/IFieldRendererService.cs
@@ -18,4 +18,4 @@ public interface IFieldRendererService
/// A RenderFragment that generates the appropriate UI component for the field.
RenderFragment RenderField(TModel model, IFieldConfiguration field,
EventCallback onValueChanged, EventCallback onDependencyChanged);
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Abstractions/IFieldValidator.cs b/FormCraft/Forms/Abstractions/IFieldValidator.cs
index 0d8526b0..9214859c 100644
--- a/FormCraft/Forms/Abstractions/IFieldValidator.cs
+++ b/FormCraft/Forms/Abstractions/IFieldValidator.cs
@@ -36,4 +36,4 @@ public interface IFieldValidator
/// The service provider for dependency injection if needed.
/// A ValidationResult indicating whether validation passed or failed with an error message.
Task ValidateAsync(TModel model, TValue value, IServiceProvider services);
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Abstractions/Rendering/IBooleanFieldComponent.cs b/FormCraft/Forms/Abstractions/Rendering/IBooleanFieldComponent.cs
index 38db1ac5..e76dec2e 100644
--- a/FormCraft/Forms/Abstractions/Rendering/IBooleanFieldComponent.cs
+++ b/FormCraft/Forms/Abstractions/Rendering/IBooleanFieldComponent.cs
@@ -46,4 +46,4 @@ public enum BooleanDisplayStyle
/// Display as radio buttons.
///
RadioButtons
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Abstractions/Rendering/IDateTimeFieldComponent.cs b/FormCraft/Forms/Abstractions/Rendering/IDateTimeFieldComponent.cs
index de49dc15..ec13a0a9 100644
--- a/FormCraft/Forms/Abstractions/Rendering/IDateTimeFieldComponent.cs
+++ b/FormCraft/Forms/Abstractions/Rendering/IDateTimeFieldComponent.cs
@@ -66,4 +66,4 @@ public enum DateTimeInputMode
/// Year picker only.
///
Year
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Abstractions/Rendering/IFieldComponent.cs b/FormCraft/Forms/Abstractions/Rendering/IFieldComponent.cs
index 899b960d..de94c221 100644
--- a/FormCraft/Forms/Abstractions/Rendering/IFieldComponent.cs
+++ b/FormCraft/Forms/Abstractions/Rendering/IFieldComponent.cs
@@ -10,4 +10,4 @@ public interface IFieldComponent
/// Gets or sets the field render context containing all necessary information for rendering.
///
IFieldRenderContext Context { get; set; }
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Abstractions/Rendering/IFileUploadFieldComponent.cs b/FormCraft/Forms/Abstractions/Rendering/IFileUploadFieldComponent.cs
index e075d2fa..4eed1cbf 100644
--- a/FormCraft/Forms/Abstractions/Rendering/IFileUploadFieldComponent.cs
+++ b/FormCraft/Forms/Abstractions/Rendering/IFileUploadFieldComponent.cs
@@ -61,4 +61,4 @@ public enum FileUploadMode
/// Manual upload triggered by user action.
///
Manual
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Abstractions/Rendering/INumericFieldComponent.cs b/FormCraft/Forms/Abstractions/Rendering/INumericFieldComponent.cs
index 523ba485..a08534c4 100644
--- a/FormCraft/Forms/Abstractions/Rendering/INumericFieldComponent.cs
+++ b/FormCraft/Forms/Abstractions/Rendering/INumericFieldComponent.cs
@@ -32,4 +32,4 @@ public interface INumericFieldComponent : IFieldComponent
bool ShowSpinButtons { get; set; }
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Abstractions/Rendering/ISelectFieldComponent.cs b/FormCraft/Forms/Abstractions/Rendering/ISelectFieldComponent.cs
index e4abbba6..5e14de06 100644
--- a/FormCraft/Forms/Abstractions/Rendering/ISelectFieldComponent.cs
+++ b/FormCraft/Forms/Abstractions/Rendering/ISelectFieldComponent.cs
@@ -41,4 +41,4 @@ public interface ISelectFieldComponent : IFieldComponent
/// Gets or sets whether to group options.
///
bool GroupOptions { get; set; }
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Abstractions/Rendering/ITextFieldComponent.cs b/FormCraft/Forms/Abstractions/Rendering/ITextFieldComponent.cs
index 15b7fb06..d1d43bff 100644
--- a/FormCraft/Forms/Abstractions/Rendering/ITextFieldComponent.cs
+++ b/FormCraft/Forms/Abstractions/Rendering/ITextFieldComponent.cs
@@ -35,4 +35,4 @@ public interface ITextFieldComponent : IFieldComponent
///
///
string? Mask { get; set; }
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Attributes/CheckboxFieldAttribute.cs b/FormCraft/Forms/Attributes/CheckboxFieldAttribute.cs
index f0de89e3..5327bc95 100644
--- a/FormCraft/Forms/Attributes/CheckboxFieldAttribute.cs
+++ b/FormCraft/Forms/Attributes/CheckboxFieldAttribute.cs
@@ -32,4 +32,4 @@ public CheckboxFieldAttribute(string label, string? text = null)
Label = label;
Text = text ?? label;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Attributes/DateFieldAttribute.cs b/FormCraft/Forms/Attributes/DateFieldAttribute.cs
index 6af39114..3e50d25b 100644
--- a/FormCraft/Forms/Attributes/DateFieldAttribute.cs
+++ b/FormCraft/Forms/Attributes/DateFieldAttribute.cs
@@ -42,4 +42,4 @@ public DateFieldAttribute(string label, string? placeholder = null)
Label = label;
Placeholder = placeholder;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Attributes/EmailFieldAttribute.cs b/FormCraft/Forms/Attributes/EmailFieldAttribute.cs
index a49ec82b..2dafc3b1 100644
--- a/FormCraft/Forms/Attributes/EmailFieldAttribute.cs
+++ b/FormCraft/Forms/Attributes/EmailFieldAttribute.cs
@@ -32,4 +32,4 @@ public EmailFieldAttribute(string label, string? placeholder = null)
Label = label;
Placeholder = placeholder ?? "user@example.com";
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Attributes/NumberFieldAttribute.cs b/FormCraft/Forms/Attributes/NumberFieldAttribute.cs
index 310d2b7f..0273c432 100644
--- a/FormCraft/Forms/Attributes/NumberFieldAttribute.cs
+++ b/FormCraft/Forms/Attributes/NumberFieldAttribute.cs
@@ -42,4 +42,4 @@ public NumberFieldAttribute(string label, string? placeholder = null)
Label = label;
Placeholder = placeholder;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Attributes/SelectFieldAttribute.cs b/FormCraft/Forms/Attributes/SelectFieldAttribute.cs
index f1fba2a3..f1e19031 100644
--- a/FormCraft/Forms/Attributes/SelectFieldAttribute.cs
+++ b/FormCraft/Forms/Attributes/SelectFieldAttribute.cs
@@ -56,4 +56,4 @@ public SelectFieldAttribute(string label, params string[] options)
Placeholder = "Select an option";
Options = options;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Attributes/TextAreaAttribute.cs b/FormCraft/Forms/Attributes/TextAreaAttribute.cs
index da550d0e..fc916a1b 100644
--- a/FormCraft/Forms/Attributes/TextAreaAttribute.cs
+++ b/FormCraft/Forms/Attributes/TextAreaAttribute.cs
@@ -42,4 +42,4 @@ public TextAreaAttribute(string label, string? placeholder = null)
Label = label;
Placeholder = placeholder;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Builders/FieldBuilder.cs b/FormCraft/Forms/Builders/FieldBuilder.cs
index 5a23064a..2853daa3 100644
--- a/FormCraft/Forms/Builders/FieldBuilder.cs
+++ b/FormCraft/Forms/Builders/FieldBuilder.cs
@@ -20,18 +20,17 @@ namespace FormCraft;
public class FieldBuilder where TModel : new()
{
private readonly FormBuilder _formBuilder;
- private readonly FieldConfiguration _fieldConfiguration;
internal FieldBuilder(FormBuilder formBuilder, FieldConfiguration fieldConfiguration)
{
_formBuilder = formBuilder;
- _fieldConfiguration = fieldConfiguration;
+ Configuration = fieldConfiguration;
}
///
/// Gets the underlying field configuration for advanced scenarios.
///
- internal FieldConfiguration Configuration => _fieldConfiguration;
+ internal FieldConfiguration Configuration { get; }
///
/// Sets the display label for the field.
@@ -45,7 +44,7 @@ internal FieldBuilder(FormBuilder formBuilder, FieldConfiguration
public FieldBuilder WithLabel(string label)
{
- _fieldConfiguration.Label = label;
+ Configuration.Label = label;
return this;
}
@@ -61,7 +60,7 @@ public FieldBuilder WithLabel(string label)
///
public FieldBuilder WithPlaceholder(string placeholder)
{
- _fieldConfiguration.Placeholder = placeholder;
+ Configuration.Placeholder = placeholder;
return this;
}
@@ -77,7 +76,7 @@ public FieldBuilder WithPlaceholder(string placeholder)
///
public FieldBuilder WithHelpText(string helpText)
{
- _fieldConfiguration.HelpText = helpText;
+ Configuration.HelpText = helpText;
return this;
}
@@ -93,7 +92,7 @@ public FieldBuilder WithHelpText(string helpText)
///
public FieldBuilder WithCssClass(string cssClass)
{
- _fieldConfiguration.CssClass = cssClass;
+ Configuration.CssClass = cssClass;
return this;
}
@@ -110,7 +109,7 @@ public FieldBuilder WithCssClass(string cssClass)
///
public FieldBuilder WithInputType(string inputType)
{
- _fieldConfiguration.InputType = inputType;
+ Configuration.InputType = inputType;
return this;
}
@@ -126,8 +125,8 @@ public FieldBuilder WithInputType(string inputType)
///
public FieldBuilder Required(string? errorMessage = null)
{
- _fieldConfiguration.IsRequired = true;
- _fieldConfiguration.AddValidator(new RequiredValidator(errorMessage));
+ Configuration.IsRequired = true;
+ Configuration.AddValidator(new RequiredValidator(errorMessage));
return this;
}
@@ -143,7 +142,7 @@ public FieldBuilder Required(string? errorMessage = null)
///
public FieldBuilder Disabled(bool disabled = true)
{
- _fieldConfiguration.IsDisabled = disabled;
+ Configuration.IsDisabled = disabled;
return this;
}
@@ -159,7 +158,7 @@ public FieldBuilder Disabled(bool disabled = true)
///
public FieldBuilder ReadOnly(bool readOnly = true)
{
- _fieldConfiguration.IsReadOnly = readOnly;
+ Configuration.IsReadOnly = readOnly;
return this;
}
@@ -175,7 +174,7 @@ public FieldBuilder ReadOnly(bool readOnly = true)
///
public FieldBuilder VisibleWhen(Func condition)
{
- _fieldConfiguration.VisibilityCondition = condition;
+ Configuration.VisibilityCondition = condition;
return this;
}
@@ -191,7 +190,7 @@ public FieldBuilder VisibleWhen(Func condition)
///
public FieldBuilder DisabledWhen(Func condition)
{
- _fieldConfiguration.DisabledCondition = condition;
+ Configuration.DisabledCondition = condition;
return this;
}
@@ -208,7 +207,7 @@ public FieldBuilder DisabledWhen(Func condition)
///
public FieldBuilder WithAttribute(string name, object value)
{
- _fieldConfiguration.AdditionalAttributes[name] = value;
+ Configuration.AdditionalAttributes[name] = value;
return this;
}
@@ -230,7 +229,7 @@ public FieldBuilder WithAttributes(Dictionary at
{
foreach (var attr in attributes)
{
- _fieldConfiguration.AdditionalAttributes[attr.Key] = attr.Value;
+ Configuration.AdditionalAttributes[attr.Key] = attr.Value;
}
return this;
}
@@ -247,7 +246,7 @@ public FieldBuilder WithAttributes(Dictionary at
///
public FieldBuilder WithValidator(IFieldValidator validator)
{
- _fieldConfiguration.AddValidator(validator);
+ Configuration.AddValidator(validator);
return this;
}
@@ -264,7 +263,7 @@ public FieldBuilder WithValidator(IFieldValidator
public FieldBuilder WithValidator(Func validation, string errorMessage)
{
- _fieldConfiguration.AddValidator(new CustomValidator(validation, errorMessage));
+ Configuration.AddValidator(new CustomValidator(validation, errorMessage));
return this;
}
@@ -281,7 +280,7 @@ public FieldBuilder WithValidator(Func validation,
///
public FieldBuilder WithAsyncValidator(Func> validation, string errorMessage)
{
- _fieldConfiguration.AddValidator(new AsyncValidator(validation, errorMessage));
+ Configuration.AddValidator(new AsyncValidator(validation, errorMessage));
return this;
}
@@ -306,7 +305,7 @@ public FieldBuilder DependsOn(
Action onChanged)
{
var dependency = new FieldDependency(dependsOnExpression, onChanged);
- _fieldConfiguration.Dependencies.Add(dependency);
+ Configuration.Dependencies.Add(dependency);
// Key by the WATCHED field's name: the runtime looks the dictionary up with
// the name of the field that just changed to find the callbacks to fire.
_formBuilder.AddFieldDependency(dependency.DependentFieldName, dependency);
@@ -335,7 +334,7 @@ public FieldBuilder DependsOn(
Func onChangedAsync)
{
var dependency = new FieldDependency(dependsOnExpression, onChangedAsync);
- _fieldConfiguration.Dependencies.Add(dependency);
+ Configuration.Dependencies.Add(dependency);
// Key by the WATCHED field's name: the runtime looks the dictionary up with
// the name of the field that just changed to find the callbacks to fire.
_formBuilder.AddFieldDependency(dependency.DependentFieldName, dependency);
@@ -358,7 +357,7 @@ public FieldBuilder DependsOn(
///
public FieldBuilder WithCustomTemplate(RenderFragment> template)
{
- _fieldConfiguration.CustomTemplate = template;
+ Configuration.CustomTemplate = template;
return this;
}
@@ -374,7 +373,7 @@ public FieldBuilder WithCustomTemplate(RenderFragment
public FieldBuilder WithOrder(int order)
{
- _fieldConfiguration.Order = order;
+ Configuration.Order = order;
return this;
}
@@ -390,9 +389,9 @@ public FieldBuilder WithOrder(int order)
///
public FieldBuilder WithCustomRenderer(IFieldRenderer renderer)
{
- _fieldConfiguration.CustomRendererType = renderer.GetType();
- _fieldConfiguration.AdditionalAttributes["CustomRendererInstance"] = renderer;
+ Configuration.CustomRendererType = renderer.GetType();
+ Configuration.AdditionalAttributes["CustomRendererInstance"] = renderer;
return this;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Builders/FieldConfigurationWrapper.cs b/FormCraft/Forms/Builders/FieldConfigurationWrapper.cs
index a8d01fec..491f57ca 100644
--- a/FormCraft/Forms/Builders/FieldConfigurationWrapper.cs
+++ b/FormCraft/Forms/Builders/FieldConfigurationWrapper.cs
@@ -11,7 +11,6 @@ namespace FormCraft;
/// The actual type of the field value.
public class FieldConfigurationWrapper : IFieldConfiguration
{
- private readonly IFieldConfiguration _inner;
///
/// Initializes a new instance of the FieldConfigurationWrapper class.
@@ -19,13 +18,11 @@ public class FieldConfigurationWrapper : IFieldConfigurationThe strongly-typed field configuration to wrap.
public FieldConfigurationWrapper(IFieldConfiguration inner)
{
- _inner = inner;
+ TypedConfiguration = inner;
}
///
- public string FieldName => _inner.FieldName;
-
- private Expression>? _valueExpression;
+ public string FieldName => TypedConfiguration.FieldName;
///
///
@@ -49,42 +46,42 @@ public FieldConfigurationWrapper(IFieldConfiguration inner)
///
///
public Expression> ValueExpression =>
- _valueExpression ??= Expression.Lambda>(
- Expression.Convert(_inner.ValueExpression.Body, typeof(object)),
- _inner.ValueExpression.Parameters);
+ field ??= Expression.Lambda>(
+ Expression.Convert(TypedConfiguration.ValueExpression.Body, typeof(object)),
+ TypedConfiguration.ValueExpression.Parameters);
///
- public string? Label { get => _inner.Label; set => _inner.Label = value; }
+ public string? Label { get => TypedConfiguration.Label; set => TypedConfiguration.Label = value; }
///
- public string? Placeholder { get => _inner.Placeholder; set => _inner.Placeholder = value; }
+ public string? Placeholder { get => TypedConfiguration.Placeholder; set => TypedConfiguration.Placeholder = value; }
///
- public string? HelpText { get => _inner.HelpText; set => _inner.HelpText = value; }
+ public string? HelpText { get => TypedConfiguration.HelpText; set => TypedConfiguration.HelpText = value; }
///
- public string? CssClass { get => _inner.CssClass; set => _inner.CssClass = value; }
+ public string? CssClass { get => TypedConfiguration.CssClass; set => TypedConfiguration.CssClass = value; }
///
- public bool IsRequired { get => _inner.IsRequired; set => _inner.IsRequired = value; }
+ public bool IsRequired { get => TypedConfiguration.IsRequired; set => TypedConfiguration.IsRequired = value; }
///
- public bool IsVisible { get => _inner.IsVisible; set => _inner.IsVisible = value; }
+ public bool IsVisible { get => TypedConfiguration.IsVisible; set => TypedConfiguration.IsVisible = value; }
///
- public bool IsDisabled { get => _inner.IsDisabled; set => _inner.IsDisabled = value; }
+ public bool IsDisabled { get => TypedConfiguration.IsDisabled; set => TypedConfiguration.IsDisabled = value; }
///
- public bool IsReadOnly { get => _inner.IsReadOnly; set => _inner.IsReadOnly = value; }
+ public bool IsReadOnly { get => TypedConfiguration.IsReadOnly; set => TypedConfiguration.IsReadOnly = value; }
///
- public int Order { get => _inner.Order; set => _inner.Order = value; }
+ public int Order { get => TypedConfiguration.Order; set => TypedConfiguration.Order = value; }
///
- public Dictionary AdditionalAttributes => _inner.AdditionalAttributes;
+ public Dictionary AdditionalAttributes => TypedConfiguration.AdditionalAttributes;
///
- public string? InputType { get => _inner.InputType; set => _inner.InputType = value; }
+ public string? InputType { get => TypedConfiguration.InputType; set => TypedConfiguration.InputType = value; }
private List>? _validators;
private int _wrappedInnerValidatorCount;
@@ -103,10 +100,10 @@ public IReadOnlyList> Validators
{
if (_validators == null)
{
- _validators = _inner.Validators
+ _validators = TypedConfiguration.Validators
.Select, IFieldValidator>(v => new ValidatorWrapper(v))
.ToList();
- _wrappedInnerValidatorCount = _inner.Validators.Count;
+ _wrappedInnerValidatorCount = TypedConfiguration.Validators.Count;
}
else
{
@@ -134,24 +131,24 @@ public void AddValidator(IFieldValidator validator)
var typedValidator = validator is ValidatorWrapper wrapper
? wrapper.Inner
: new ObjectValidatorAdapter(validator);
- _inner.AddValidator(typedValidator);
+ TypedConfiguration.AddValidator(typedValidator);
// Appended to the backing list directly: the public view is IReadOnlyList since #155, and
// the whole point of that change is that callers cannot do this. Adding the caller's own
// instance (rather than re-wrapping the one just handed to _inner) keeps reference identity
// through the object-typed view.
_validators!.Add(validator);
- _wrappedInnerValidatorCount = _inner.Validators.Count;
+ _wrappedInnerValidatorCount = TypedConfiguration.Validators.Count;
}
private void SyncNewInnerValidators()
{
- for (var i = _wrappedInnerValidatorCount; i < _inner.Validators.Count; i++)
+ for (var i = _wrappedInnerValidatorCount; i < TypedConfiguration.Validators.Count; i++)
{
- _validators!.Add(new ValidatorWrapper(_inner.Validators[i]));
+ _validators!.Add(new ValidatorWrapper(TypedConfiguration.Validators[i]));
}
- _wrappedInnerValidatorCount = _inner.Validators.Count;
+ _wrappedInnerValidatorCount = TypedConfiguration.Validators.Count;
}
private sealed class ObjectValidatorAdapter : IFieldValidator
@@ -174,45 +171,43 @@ public Task ValidateAsync(TModel model, TValue value, IService
}
///
- public List> Dependencies => _inner.Dependencies;
+ public List> Dependencies => TypedConfiguration.Dependencies;
///
public Func? VisibilityCondition
{
- get => _inner.VisibilityCondition;
- set => _inner.VisibilityCondition = value;
+ get => TypedConfiguration.VisibilityCondition;
+ set => TypedConfiguration.VisibilityCondition = value;
}
///
public Func? DisabledCondition
{
- get => _inner.DisabledCondition;
- set => _inner.DisabledCondition = value;
+ get => TypedConfiguration.DisabledCondition;
+ set => TypedConfiguration.DisabledCondition = value;
}
- private RenderFragment>? _customTemplate;
-
///
public RenderFragment>? CustomTemplate
{
get
{
- if (_customTemplate != null)
+ if (field != null)
{
- return _customTemplate;
+ return field;
}
// Surface templates configured through the typed builder API by adapting
// the object-typed render context to the typed one the template expects.
- var typedTemplate = _inner.CustomTemplate;
+ var typedTemplate = TypedConfiguration.CustomTemplate;
if (typedTemplate == null)
{
return null;
}
- return objectContext => typedTemplate(new TypedFieldContextAdapter(objectContext, _inner));
+ return objectContext => typedTemplate(new TypedFieldContextAdapter(objectContext, TypedConfiguration));
}
- set => _customTemplate = value;
+ set;
}
private sealed class TypedFieldContextAdapter : IFieldContext
@@ -247,18 +242,18 @@ public TValue Value
///
public Type? CustomRendererType
{
- get => _inner.CustomRendererType;
- set => _inner.CustomRendererType = value;
+ get => TypedConfiguration.CustomRendererType;
+ set => TypedConfiguration.CustomRendererType = value;
}
///
/// Gets access to the original typed configuration.
///
- public IFieldConfiguration TypedConfiguration => _inner;
+ public IFieldConfiguration TypedConfiguration { get; }
///
/// Gets the actual runtime type of the field value.
///
/// The Type of TValue, representing the actual field type.
public Type GetActualFieldType() => typeof(TValue);
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Builders/FieldGroupBuilder.cs b/FormCraft/Forms/Builders/FieldGroupBuilder.cs
index b82f9f4c..fefc1494 100644
--- a/FormCraft/Forms/Builders/FieldGroupBuilder.cs
+++ b/FormCraft/Forms/Builders/FieldGroupBuilder.cs
@@ -162,4 +162,4 @@ public FieldGroupBuilder WithHeaderRightContent(Action(Expression> expr
}
throw new ArgumentException("Expression must be a member expression");
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Builders/ValidatorWrapper.cs b/FormCraft/Forms/Builders/ValidatorWrapper.cs
index e387ee13..2717fe73 100644
--- a/FormCraft/Forms/Builders/ValidatorWrapper.cs
+++ b/FormCraft/Forms/Builders/ValidatorWrapper.cs
@@ -8,7 +8,6 @@ namespace FormCraft;
/// The actual type of the field value being validated.
public class ValidatorWrapper : IFieldValidator
{
- private readonly IFieldValidator _inner;
///
/// Initializes a new instance of the ValidatorWrapper class.
@@ -16,19 +15,19 @@ public class ValidatorWrapper : IFieldValidator
/// The strongly-typed validator to wrap.
public ValidatorWrapper(IFieldValidator inner)
{
- _inner = inner;
+ Inner = inner;
}
///
/// Gets the strongly-typed validator wrapped by this instance.
///
- internal IFieldValidator Inner => _inner;
+ internal IFieldValidator Inner { get; }
///
public string? ErrorMessage
{
- get => _inner.ErrorMessage;
- set => _inner.ErrorMessage = value;
+ get => Inner.ErrorMessage;
+ set => Inner.ErrorMessage = value;
}
///
@@ -45,6 +44,6 @@ public async Task ValidateAsync(TModel model, object? value, I
typedValue = default!;
}
- return await _inner.ValidateAsync(model, typedValue, services);
+ return await Inner.ValidateAsync(model, typedValue, services);
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Core/FieldConfiguration.cs b/FormCraft/Forms/Core/FieldConfiguration.cs
index 0f403623..55dcea2a 100644
--- a/FormCraft/Forms/Core/FieldConfiguration.cs
+++ b/FormCraft/Forms/Core/FieldConfiguration.cs
@@ -90,4 +90,4 @@ public FieldConfiguration(Expression> valueExpression)
FieldName = memberExpression?.Member.Name ?? throw new ArgumentException("Invalid expression");
Label = FieldName;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Core/FileUploadConfiguration.cs b/FormCraft/Forms/Core/FileUploadConfiguration.cs
index a3333ece..deb4a880 100644
--- a/FormCraft/Forms/Core/FileUploadConfiguration.cs
+++ b/FormCraft/Forms/Core/FileUploadConfiguration.cs
@@ -55,7 +55,9 @@ public class FileUploadConfiguration
public ValidationResult ValidateFile(IBrowserFile? file)
{
if (file == null)
+ {
return ValidationResult.Success();
+ }
// Check file size
if (MaxFileSize.HasValue && file.Size > MaxFileSize.Value)
@@ -87,7 +89,9 @@ public string GetConstraintsDescription()
var parts = new List();
if (AcceptedFileTypes?.Length > 0)
+ {
parts.Add($"Accepted formats: {string.Join(", ", AcceptedFileTypes)}");
+ }
if (MaxFileSize.HasValue)
{
@@ -96,8 +100,10 @@ public string GetConstraintsDescription()
}
if (MaxFiles > 1)
+ {
parts.Add($"Max files: {MaxFiles}");
+ }
return parts.Count > 0 ? string.Join(" • ", parts) : string.Empty;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Core/IFieldConfiguration.cs b/FormCraft/Forms/Core/IFieldConfiguration.cs
index 370f3870..cba41b9a 100644
--- a/FormCraft/Forms/Core/IFieldConfiguration.cs
+++ b/FormCraft/Forms/Core/IFieldConfiguration.cs
@@ -203,4 +203,4 @@ public interface IFieldConfiguration
///
///
Type? CustomRendererType { get; set; }
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Core/IFieldContext.cs b/FormCraft/Forms/Core/IFieldContext.cs
index 15374e7b..60667e7c 100644
--- a/FormCraft/Forms/Core/IFieldContext.cs
+++ b/FormCraft/Forms/Core/IFieldContext.cs
@@ -90,4 +90,4 @@ public interface IFieldContext
/// Gets the CSS class string to apply to the field, including validation state classes.
///
string FieldCssClass { get; }
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Core/IFormConfiguration.cs b/FormCraft/Forms/Core/IFormConfiguration.cs
index 2a72524d..190bc5d8 100644
--- a/FormCraft/Forms/Core/IFormConfiguration.cs
+++ b/FormCraft/Forms/Core/IFormConfiguration.cs
@@ -140,4 +140,4 @@ public enum FormLayout
///
public List CollectionFields { get; } = new();
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Core/MinimalFieldConfiguration.cs b/FormCraft/Forms/Core/MinimalFieldConfiguration.cs
index b73b36d1..af298fdb 100644
--- a/FormCraft/Forms/Core/MinimalFieldConfiguration.cs
+++ b/FormCraft/Forms/Core/MinimalFieldConfiguration.cs
@@ -37,4 +37,4 @@ internal class MinimalFieldConfiguration : IFieldConfiguration
public Func? VisibilityCondition { get; set; }
public Func? DisabledCondition { get; set; }
public RenderFragment>? CustomTemplate { get; set; }
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Core/SelectOption.cs b/FormCraft/Forms/Core/SelectOption.cs
index 4829fef4..d6c64aff 100644
--- a/FormCraft/Forms/Core/SelectOption.cs
+++ b/FormCraft/Forms/Core/SelectOption.cs
@@ -31,4 +31,4 @@ public SelectOption(T value, string label)
Value = value;
Label = label;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Core/ValidationResult.cs b/FormCraft/Forms/Core/ValidationResult.cs
index cbbd922d..e1b6ef6b 100644
--- a/FormCraft/Forms/Core/ValidationResult.cs
+++ b/FormCraft/Forms/Core/ValidationResult.cs
@@ -33,4 +33,4 @@ private ValidationResult(bool isValid, string? errorMessage = null)
/// The error message describing why validation failed.
/// A ValidationResult indicating failed validation with the specified error message.
public static ValidationResult Failure(string errorMessage) => new(false, errorMessage);
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Extensions/AttributeFormBuilderExtensions.cs b/FormCraft/Forms/Extensions/AttributeFormBuilderExtensions.cs
index c96d0f6d..bbe88c4b 100644
--- a/FormCraft/Forms/Extensions/AttributeFormBuilderExtensions.cs
+++ b/FormCraft/Forms/Extensions/AttributeFormBuilderExtensions.cs
@@ -47,7 +47,9 @@ public FormBuilder AddFieldsFromAttributes()
.WithInputType("email");
if (!string.IsNullOrEmpty(emailAttr.Placeholder))
+ {
field.WithPlaceholder(emailAttr.Placeholder);
+ }
if (emailAttr.ValidateFormat)
{
@@ -75,7 +77,9 @@ public FormBuilder AddFieldsFromAttributes()
field.WithLabel(textAreaAttr.Label);
if (!string.IsNullOrEmpty(textAreaAttr.Placeholder))
+ {
field.WithPlaceholder(textAreaAttr.Placeholder);
+ }
field.WithAttribute("rows", textAreaAttr.Rows);
@@ -86,7 +90,9 @@ public FormBuilder AddFieldsFromAttributes()
}
if (textAreaAttr.AutoResize)
+ {
field.WithAttribute("auto-resize", true);
+ }
ApplyValidationAttributes(field, prop, textAreaAttr.Label);
});
@@ -122,10 +128,14 @@ public FormBuilder AddFieldsFromAttributes()
field.WithLabel(checkboxAttr.Label);
if (!string.IsNullOrEmpty(checkboxAttr.Text))
+ {
field.WithAttribute("text", checkboxAttr.Text);
+ }
if (checkboxAttr.DefaultChecked)
+ {
field.WithAttribute("default-checked", true);
+ }
});
continue;
}
@@ -155,7 +165,9 @@ private static void AddStringField(FormBuilder builder, Property
.WithInputType(inputType);
if (!string.IsNullOrEmpty(placeholder))
+ {
field.WithPlaceholder(placeholder);
+ }
ApplyValidationAttributes(field, prop, label);
});
@@ -202,16 +214,24 @@ private static void ConfigureNumericField(FieldBuilder(FieldBuilder(
field.WithLabel(selectAttr.Label);
if (!string.IsNullOrEmpty(selectAttr.Placeholder))
+ {
field.WithPlaceholder(selectAttr.Placeholder);
+ }
if (selectAttr.Options != null && selectAttr.Options.Length > 0)
+ {
field.WithAttribute("options", selectAttr.Options);
+ }
if (selectAttr.AllowMultiple)
+ {
field.WithAttribute("multiple", true);
+ }
if (!string.IsNullOrEmpty(selectAttr.OptionsProviderName))
+ {
field.WithAttribute("options-provider", selectAttr.OptionsProviderName);
+ }
ApplyValidationAttributes(field, prop, selectAttr.Label);
});
@@ -300,21 +336,26 @@ private static void ApplyValidationAttributes(FieldBuilder();
if (required != null)
+ {
field.Required(required.ErrorMessage ?? $"{label} is required");
+ }
// Only apply string-specific validations for string fields
if (typeof(TValue) == typeof(string))
{
- var stringField = field as FieldBuilder;
- if (stringField != null)
+ if (field is FieldBuilder stringField)
{
var minLength = prop.GetCustomAttribute();
if (minLength != null)
+ {
stringField.WithMinLength(minLength.Length, minLength.ErrorMessage ?? $"Must be at least {minLength.Length} characters");
+ }
var maxLength = prop.GetCustomAttribute();
if (maxLength != null)
+ {
stringField.WithMaxLength(maxLength.Length, maxLength.ErrorMessage ?? $"Must be no more than {maxLength.Length} characters");
+ }
}
}
@@ -333,7 +374,11 @@ private static void ApplyValidationAttributes(FieldBuilder
{
- if (value == null) return true;
+ if (value == null)
+ {
+ return true;
+ }
+
return System.Text.RegularExpressions.Regex.IsMatch(value.ToString()!, pattern.Pattern);
}, pattern.ErrorMessage ?? "Invalid format");
}
@@ -347,4 +392,4 @@ private static bool IsNumericType(Type type)
type == typeof(byte) || type == typeof(uint) || type == typeof(ulong) ||
type == typeof(ushort) || type == typeof(sbyte);
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Extensions/FieldBuilderExtensions.cs b/FormCraft/Forms/Extensions/FieldBuilderExtensions.cs
index 4da8d9b1..d7e2179b 100644
--- a/FormCraft/Forms/Extensions/FieldBuilderExtensions.cs
+++ b/FormCraft/Forms/Extensions/FieldBuilderExtensions.cs
@@ -143,7 +143,7 @@ public FieldBuilder> AsMultiSelect(params (TValue va
return builder.WithAttribute("MultiSelectOptions", selectOptions);
}
}
-
+
/// The FieldBuilder instance for a numeric field.
/// The model type that the form binds to.
/// The numeric type of the field value.
@@ -457,7 +457,10 @@ public static FieldBuilder AsAutocomplete(
builder.WithAttribute("AutocompleteDebounceMs", debounceMs);
builder.WithAttribute("AutocompleteMinCharacters", minCharacters);
if (toStringFunc != null)
+ {
builder.WithAttribute("AutocompleteToStringFunc", toStringFunc);
+ }
+
return builder;
}
@@ -493,14 +496,19 @@ public static FieldBuilder AsAutocomplete(
builder.WithAttribute("AutocompleteDebounceMs", debounceMs);
builder.WithAttribute("AutocompleteMinCharacters", minCharacters);
if (toStringFunc != null)
+ {
builder.WithAttribute("AutocompleteToStringFunc", toStringFunc);
+ }
+
return builder;
}
private static bool IsValidEmail(string email)
{
if (string.IsNullOrWhiteSpace(email))
+ {
return false;
+ }
try
{
diff --git a/FormCraft/Forms/Extensions/FluentFormBuilderExtensions.cs b/FormCraft/Forms/Extensions/FluentFormBuilderExtensions.cs
index 0304fa60..c022917a 100644
--- a/FormCraft/Forms/Extensions/FluentFormBuilderExtensions.cs
+++ b/FormCraft/Forms/Extensions/FluentFormBuilderExtensions.cs
@@ -39,12 +39,16 @@ public FormBuilder AddRequiredTextField(Expression>
.Required($"{label} is required");
if (minLength > 1)
+ {
field.WithMinLength(minLength, $"Must be at least {minLength} characters");
+ }
field.WithMaxLength(maxLength, $"Must be no more than {maxLength} characters");
if (!string.IsNullOrEmpty(placeholder))
+ {
field.WithPlaceholder(placeholder);
+ }
});
}
@@ -102,12 +106,16 @@ public FormBuilder AddNumericField(Expression> express
field.WithLabel(label);
if (required)
+ {
field.Required($"{label} is required");
+ }
var hasMin = min != int.MinValue;
var hasMax = max != int.MaxValue;
if (hasMin || hasMax)
+ {
field.WithRange(min, max, BuildRangeMessage(hasMin, hasMax, min, max));
+ }
});
}
@@ -138,15 +146,21 @@ public FormBuilder AddDecimalField(Expression> exp
field.WithLabel(label);
if (required)
+ {
field.Required($"{label} is required");
+ }
var hasMin = min != decimal.MinValue;
var hasMax = max != decimal.MaxValue;
if (hasMin || hasMax)
+ {
field.WithRange(min, max, BuildRangeMessage(hasMin, hasMax, min, max));
+ }
if (!string.IsNullOrEmpty(placeholder))
+ {
field.WithPlaceholder(placeholder);
+ }
});
}
@@ -175,7 +189,9 @@ public FormBuilder AddCurrencyField(Expression> ex
.WithHelpText($"Enter amount in {currencySymbol}");
if (required)
+ {
field.Required($"{label} is required");
+ }
// Ensure non-negative values for currency
field.WithRange(0, decimal.MaxValue, "Amount must be positive");
@@ -206,7 +222,9 @@ public FormBuilder AddPercentageField(Expression>
.WithRange(0, 100, "Percentage must be between 0 and 100");
if (required)
+ {
field.Required($"{label} is required");
+ }
});
}
@@ -261,7 +279,9 @@ public FormBuilder AddPhoneField(Expression> expres
"Please enter a valid phone number");
if (required)
+ {
field.Required($"{label} is required");
+ }
});
}
@@ -321,11 +341,13 @@ public FormBuilder AddCheckboxField(Expression> expre
field.WithLabel(label);
if (!string.IsNullOrEmpty(helpText))
+ {
field.WithHelpText(helpText);
+ }
});
}
}
-
+
#region Helper Methods
private static string BuildRangeMessage(bool hasMin, bool hasMax, TValue min, TValue max)
@@ -340,7 +362,9 @@ private static string BuildRangeMessage(bool hasMin, bool hasMax, TValue
private static bool IsValidPhone(string phone)
{
if (string.IsNullOrWhiteSpace(phone))
+ {
return false;
+ }
// Remove common phone formatting
var cleanPhone = phone.Replace("(", "").Replace(")", "").Replace("-", "").Replace(" ", "").Replace(".", "");
@@ -386,7 +410,9 @@ public static FormBuilder AddFileUploadField(
.AsFileUpload(acceptedFileTypes, maxFileSize);
if (required)
+ {
field.Required($"{label} is required");
+ }
});
}
@@ -425,7 +451,9 @@ public static FormBuilder AddMultipleFileUploadField(
.AsMultipleFileUpload(maxFiles, acceptedFileTypes, maxFileSize);
if (required)
+ {
field.Required($"At least one {label.ToLower()} is required");
+ }
});
}
@@ -548,4 +576,4 @@ public static FormBuilder AddOptionalField(
}
#endregion
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Extensions/FluentValidationExtensions.cs b/FormCraft/Forms/Extensions/FluentValidationExtensions.cs
index 38dbe4c9..b438cef9 100644
--- a/FormCraft/Forms/Extensions/FluentValidationExtensions.cs
+++ b/FormCraft/Forms/Extensions/FluentValidationExtensions.cs
@@ -57,4 +57,4 @@ public FieldBuilder WithFluentValidator(IValidator va
return builder.WithValidator(new SpecificFluentValidationAdapter(validator, propertyExpression));
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Extensions/FormConfigurationExtensions.cs b/FormCraft/Forms/Extensions/FormConfigurationExtensions.cs
index d076ff11..bdaf81d3 100644
--- a/FormCraft/Forms/Extensions/FormConfigurationExtensions.cs
+++ b/FormCraft/Forms/Extensions/FormConfigurationExtensions.cs
@@ -51,4 +51,4 @@ public IEnumerable GetVisibleFields(TModel model)
.Select(f => f.FieldName);
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Extensions/ServiceCollectionExtensions.cs b/FormCraft/Forms/Extensions/ServiceCollectionExtensions.cs
index 94537a2e..5443fc0d 100644
--- a/FormCraft/Forms/Extensions/ServiceCollectionExtensions.cs
+++ b/FormCraft/Forms/Extensions/ServiceCollectionExtensions.cs
@@ -71,4 +71,4 @@ public IServiceCollection AddFormCraft()
return services;
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Rendering/BoolFieldRenderer.cs b/FormCraft/Forms/Rendering/BoolFieldRenderer.cs
index 720aaad8..02aabca7 100644
--- a/FormCraft/Forms/Rendering/BoolFieldRenderer.cs
+++ b/FormCraft/Forms/Rendering/BoolFieldRenderer.cs
@@ -30,7 +30,10 @@ private class TestStubComponent : ComponentBase
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder)
{
- if (Context == null) return;
+ if (Context == null)
+ {
+ return;
+ }
var sequence = 0;
builder.OpenElement(sequence++, "div");
@@ -49,4 +52,4 @@ protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Renderin
builder.CloseElement();
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Rendering/CustomRenderers/ColorPickerRenderer.cs b/FormCraft/Forms/Rendering/CustomRenderers/ColorPickerRenderer.cs
index 1b264171..3dd8144f 100644
--- a/FormCraft/Forms/Rendering/CustomRenderers/ColorPickerRenderer.cs
+++ b/FormCraft/Forms/Rendering/CustomRenderers/ColorPickerRenderer.cs
@@ -33,4 +33,4 @@ public override RenderFragment Render(IFieldRenderContext context)
builder.CloseElement();
};
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Rendering/CustomRenderers/RatingRenderer.cs b/FormCraft/Forms/Rendering/CustomRenderers/RatingRenderer.cs
index ccab437e..b50a1697 100644
--- a/FormCraft/Forms/Rendering/CustomRenderers/RatingRenderer.cs
+++ b/FormCraft/Forms/Rendering/CustomRenderers/RatingRenderer.cs
@@ -35,4 +35,4 @@ public override RenderFragment Render(IFieldRenderContext context)
builder.CloseElement();
};
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Rendering/DateTimeFieldRenderer.cs b/FormCraft/Forms/Rendering/DateTimeFieldRenderer.cs
index 9792849f..1123f32f 100644
--- a/FormCraft/Forms/Rendering/DateTimeFieldRenderer.cs
+++ b/FormCraft/Forms/Rendering/DateTimeFieldRenderer.cs
@@ -29,7 +29,10 @@ private class TestStubComponent : ComponentBase
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder)
{
- if (Context == null) return;
+ if (Context == null)
+ {
+ return;
+ }
var sequence = 0;
builder.OpenElement(sequence++, "div");
@@ -57,4 +60,4 @@ protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Renderin
builder.CloseElement();
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Rendering/DecimalFieldRenderer.cs b/FormCraft/Forms/Rendering/DecimalFieldRenderer.cs
index 89e2983e..214c1187 100644
--- a/FormCraft/Forms/Rendering/DecimalFieldRenderer.cs
+++ b/FormCraft/Forms/Rendering/DecimalFieldRenderer.cs
@@ -29,7 +29,10 @@ private class TestStubComponent : ComponentBase
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder)
{
- if (Context == null) return;
+ if (Context == null)
+ {
+ return;
+ }
var sequence = 0;
builder.OpenElement(sequence++, "div");
@@ -58,4 +61,4 @@ protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Renderin
builder.CloseElement();
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Rendering/DoubleFieldRenderer.cs b/FormCraft/Forms/Rendering/DoubleFieldRenderer.cs
index 38114d61..f7e0e844 100644
--- a/FormCraft/Forms/Rendering/DoubleFieldRenderer.cs
+++ b/FormCraft/Forms/Rendering/DoubleFieldRenderer.cs
@@ -29,7 +29,10 @@ private class TestStubComponent : ComponentBase
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder)
{
- if (Context == null) return;
+ if (Context == null)
+ {
+ return;
+ }
var sequence = 0;
builder.OpenElement(sequence++, "div");
@@ -58,4 +61,4 @@ protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Renderin
builder.CloseElement();
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Rendering/FieldRenderContext.cs b/FormCraft/Forms/Rendering/FieldRenderContext.cs
index 742b129f..5be8e616 100644
--- a/FormCraft/Forms/Rendering/FieldRenderContext.cs
+++ b/FormCraft/Forms/Rendering/FieldRenderContext.cs
@@ -30,4 +30,4 @@ public class FieldRenderContext : IFieldRenderContext
object IFieldRenderContext.Model => Model!;
object IFieldRenderContext.FieldConfiguration => Field;
EventCallback IFieldRenderContext.OnFieldChanged => OnDependencyChanged;
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Rendering/FieldRendererBase.cs b/FormCraft/Forms/Rendering/FieldRendererBase.cs
index e6cfe537..b149eabc 100644
--- a/FormCraft/Forms/Rendering/FieldRendererBase.cs
+++ b/FormCraft/Forms/Rendering/FieldRendererBase.cs
@@ -1,6 +1,6 @@
+using System.Reflection;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
-using System.Reflection;
namespace FormCraft;
@@ -92,7 +92,9 @@ private static bool HasStructConstraint(Type genericType, int parameterIndex)
{
var genericArgs = genericType.GetGenericArguments();
if (parameterIndex >= genericArgs.Length)
+ {
return false;
+ }
var attributes = genericArgs[parameterIndex].GenericParameterAttributes;
@@ -118,4 +120,4 @@ public override bool CanRender(Type fieldType, IFieldConfiguration
protected virtual bool CanRenderDerivedType(Type fieldType) => false;
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Rendering/FieldRendererService.cs b/FormCraft/Forms/Rendering/FieldRendererService.cs
index fe1a5052..da8f5a3b 100644
--- a/FormCraft/Forms/Rendering/FieldRendererService.cs
+++ b/FormCraft/Forms/Rendering/FieldRendererService.cs
@@ -1,6 +1,6 @@
-using Microsoft.AspNetCore.Components;
using System.Linq.Expressions;
using System.Reflection;
+using Microsoft.AspNetCore.Components;
namespace FormCraft;
diff --git a/FormCraft/Forms/Rendering/FileUploadFieldRenderer.cs b/FormCraft/Forms/Rendering/FileUploadFieldRenderer.cs
index c87ee486..902cd71e 100644
--- a/FormCraft/Forms/Rendering/FileUploadFieldRenderer.cs
+++ b/FormCraft/Forms/Rendering/FileUploadFieldRenderer.cs
@@ -42,7 +42,10 @@ private class TestStubComponent : ComponentBase
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder)
{
- if (Context == null) return;
+ if (Context == null)
+ {
+ return;
+ }
var sequence = 0;
builder.OpenElement(sequence++, "div");
@@ -85,4 +88,4 @@ protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Renderin
builder.CloseElement();
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Rendering/IntFieldRenderer.cs b/FormCraft/Forms/Rendering/IntFieldRenderer.cs
index 0764ec53..1a13c426 100644
--- a/FormCraft/Forms/Rendering/IntFieldRenderer.cs
+++ b/FormCraft/Forms/Rendering/IntFieldRenderer.cs
@@ -30,7 +30,10 @@ private class TestStubComponent : ComponentBase
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder)
{
- if (Context == null) return;
+ if (Context == null)
+ {
+ return;
+ }
var sequence = 0;
builder.OpenElement(sequence++, "div");
@@ -58,4 +61,4 @@ protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Renderin
builder.CloseElement();
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Rendering/StringFieldRenderer.cs b/FormCraft/Forms/Rendering/StringFieldRenderer.cs
index 954d774c..594ca39c 100644
--- a/FormCraft/Forms/Rendering/StringFieldRenderer.cs
+++ b/FormCraft/Forms/Rendering/StringFieldRenderer.cs
@@ -26,7 +26,10 @@ private class TestStubComponent : ComponentBase
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder builder)
{
- if (Context == null) return;
+ if (Context == null)
+ {
+ return;
+ }
var sequence = 0;
builder.OpenElement(sequence++, "div");
@@ -54,4 +57,4 @@ protected override void BuildRenderTree(Microsoft.AspNetCore.Components.Renderin
builder.CloseElement();
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Security/BlazorCsrfTokenService.cs b/FormCraft/Forms/Security/BlazorCsrfTokenService.cs
index 542424d0..3f7be1a2 100644
--- a/FormCraft/Forms/Security/BlazorCsrfTokenService.cs
+++ b/FormCraft/Forms/Security/BlazorCsrfTokenService.cs
@@ -56,7 +56,9 @@ public async Task GenerateTokenAsync()
public async Task ValidateTokenAsync(string token)
{
if (string.IsNullOrEmpty(token))
+ {
return false;
+ }
string? storedToken = null;
try
@@ -69,14 +71,19 @@ public async Task ValidateTokenAsync(string token)
}
if (!string.IsNullOrEmpty(storedToken))
+ {
return FixedTimeEquals(storedToken, token);
+ }
// No stored token: accept the token this service instance issued (it may not have been
// persisted yet because generation happened during prerendering) and try to persist it now.
if (_currentToken != null && FixedTimeEquals(_currentToken, token))
{
if (!_isPersisted)
+ {
_isPersisted = await TryPersistTokenAsync(_currentToken);
+ }
+
return true;
}
diff --git a/FormCraft/Forms/Security/BlazorEncryptionService.cs b/FormCraft/Forms/Security/BlazorEncryptionService.cs
index f147bac3..529d8bab 100644
--- a/FormCraft/Forms/Security/BlazorEncryptionService.cs
+++ b/FormCraft/Forms/Security/BlazorEncryptionService.cs
@@ -55,7 +55,9 @@ public BlazorEncryptionService(IConfiguration? configuration = null)
public string? Encrypt(string? value)
{
if (string.IsNullOrEmpty(value))
+ {
return value;
+ }
var data = Encoding.UTF8.GetBytes(value);
var encrypted = new byte[data.Length];
@@ -76,7 +78,9 @@ public BlazorEncryptionService(IConfiguration? configuration = null)
public string? Decrypt(string? encryptedValue)
{
if (string.IsNullOrEmpty(encryptedValue))
+ {
return encryptedValue;
+ }
byte[] encrypted;
try
@@ -107,12 +111,16 @@ private static byte[] ParseKey(string keyString)
// Prefer a Base64-encoded 32-byte key.
var base64Buffer = new byte[keyString.Length];
if (Convert.TryFromBase64String(keyString, base64Buffer, out var bytesWritten) && bytesWritten == KeySizeInBytes)
+ {
return base64Buffer[..bytesWritten];
+ }
// Fall back to a raw string whose UTF-8 representation is exactly 32 bytes.
var utf8Key = Encoding.UTF8.GetBytes(keyString);
if (utf8Key.Length == KeySizeInBytes)
+ {
return utf8Key;
+ }
throw new InvalidOperationException(
$"The configured '{KeyConfigurationPath}' is invalid. Provide a Base64-encoded {KeySizeInBytes}-byte key " +
diff --git a/FormCraft/Forms/Security/ConsoleAuditLogService.cs b/FormCraft/Forms/Security/ConsoleAuditLogService.cs
index a1d7b63c..76695404 100644
--- a/FormCraft/Forms/Security/ConsoleAuditLogService.cs
+++ b/FormCraft/Forms/Security/ConsoleAuditLogService.cs
@@ -61,12 +61,16 @@ private AuditLogEntry Redact(AuditLogEntry entry)
{
var excludedFields = _configuration?.ExcludedFields;
if (excludedFields == null || excludedFields.Count == 0)
+ {
return entry;
+ }
var isFieldExcluded = entry.FieldName != null && excludedFields.Contains(entry.FieldName);
var hasExcludedAdditionalData = entry.AdditionalData.Keys.Any(excludedFields.Contains);
if (!isFieldExcluded && !hasExcludedAdditionalData)
+ {
return entry;
+ }
// Never mutate the caller's entry — log a redacted copy instead.
return new AuditLogEntry
diff --git a/FormCraft/Forms/Security/DefaultEncryptionService.cs b/FormCraft/Forms/Security/DefaultEncryptionService.cs
index 313c1244..159255da 100644
--- a/FormCraft/Forms/Security/DefaultEncryptionService.cs
+++ b/FormCraft/Forms/Security/DefaultEncryptionService.cs
@@ -60,7 +60,9 @@ public DefaultEncryptionService(IConfiguration? configuration = null)
public string? Encrypt(string? value)
{
if (string.IsNullOrEmpty(value))
+ {
return value;
+ }
using var aes = Aes.Create();
aes.Key = _key;
@@ -87,7 +89,9 @@ public DefaultEncryptionService(IConfiguration? configuration = null)
public string? Decrypt(string? encryptedValue)
{
if (string.IsNullOrEmpty(encryptedValue))
+ {
return encryptedValue;
+ }
try
{
@@ -121,12 +125,16 @@ private static byte[] ParseKey(string keyString)
// Prefer a Base64-encoded 32-byte key.
var base64Buffer = new byte[keyString.Length];
if (Convert.TryFromBase64String(keyString, base64Buffer, out var bytesWritten) && bytesWritten == KeySizeInBytes)
+ {
return base64Buffer[..bytesWritten];
+ }
// Fall back to a raw string whose UTF-8 representation is exactly 32 bytes.
var utf8Key = Encoding.UTF8.GetBytes(keyString);
if (utf8Key.Length == KeySizeInBytes)
+ {
return utf8Key;
+ }
throw new InvalidOperationException(
$"The configured '{KeyConfigurationPath}' is invalid. Provide a Base64-encoded {KeySizeInBytes}-byte key " +
diff --git a/FormCraft/Forms/Security/EncryptedFieldHelper.cs b/FormCraft/Forms/Security/EncryptedFieldHelper.cs
index 6baa3c0e..0c449d1b 100644
--- a/FormCraft/Forms/Security/EncryptedFieldHelper.cs
+++ b/FormCraft/Forms/Security/EncryptedFieldHelper.cs
@@ -14,7 +14,9 @@ public static void EncryptFields(TModel model, IFormSecurity security, I
where TModel : new()
{
if (model == null || security?.EncryptedFields == null || !security.EncryptedFields.Any())
+ {
return;
+ }
foreach (var fieldName in security.EncryptedFields)
{
@@ -38,7 +40,9 @@ public static void DecryptFields(TModel model, IFormSecurity security, I
where TModel : new()
{
if (model == null || security?.EncryptedFields == null || !security.EncryptedFields.Any())
+ {
return;
+ }
foreach (var fieldName in security.EncryptedFields)
{
@@ -62,7 +66,9 @@ public static TModel CreateDecryptedCopy(TModel model, IFormSecurity sec
where TModel : new()
{
if (model == null)
+ {
return new TModel();
+ }
// Create a shallow copy
var copy = (TModel)Activator.CreateInstance(typeof(TModel))!;
@@ -81,4 +87,4 @@ public static TModel CreateDecryptedCopy(TModel model, IFormSecurity sec
return copy;
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Security/FormSecurity.cs b/FormCraft/Forms/Security/FormSecurity.cs
index c03f63a1..c04a254b 100644
--- a/FormCraft/Forms/Security/FormSecurity.cs
+++ b/FormCraft/Forms/Security/FormSecurity.cs
@@ -11,4 +11,4 @@ public class FormSecurity : IFormSecurity
public RateLimitConfiguration? RateLimit { get; set; }
public bool IsAuditLoggingEnabled { get; set; }
public AuditLogConfiguration? AuditLog { get; set; }
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Security/IAuditLogService.cs b/FormCraft/Forms/Security/IAuditLogService.cs
index e2fc67ec..0b5f6131 100644
--- a/FormCraft/Forms/Security/IAuditLogService.cs
+++ b/FormCraft/Forms/Security/IAuditLogService.cs
@@ -86,4 +86,4 @@ public static class AuditEventTypes
public const string FormLoaded = "FormLoaded";
public const string RateLimitExceeded = "RateLimitExceeded";
public const string CsrfValidationFailed = "CsrfValidationFailed";
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Security/ICsrfTokenService.cs b/FormCraft/Forms/Security/ICsrfTokenService.cs
index 716e9352..e18d8971 100644
--- a/FormCraft/Forms/Security/ICsrfTokenService.cs
+++ b/FormCraft/Forms/Security/ICsrfTokenService.cs
@@ -17,4 +17,4 @@ public interface ICsrfTokenService
/// The token to validate.
/// True if the token is valid, false otherwise.
Task ValidateTokenAsync(string token);
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Security/IEncryptionService.cs b/FormCraft/Forms/Security/IEncryptionService.cs
index 26d10d6b..a8d4b25e 100644
--- a/FormCraft/Forms/Security/IEncryptionService.cs
+++ b/FormCraft/Forms/Security/IEncryptionService.cs
@@ -18,4 +18,4 @@ public interface IEncryptionService
/// The encrypted value as a base64 string.
/// The decrypted value.
string? Decrypt(string? encryptedValue);
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Security/IFormSecurity.cs b/FormCraft/Forms/Security/IFormSecurity.cs
index 986e7d15..86002bd3 100644
--- a/FormCraft/Forms/Security/IFormSecurity.cs
+++ b/FormCraft/Forms/Security/IFormSecurity.cs
@@ -81,4 +81,4 @@ public class AuditLogConfiguration
/// Fields to exclude from logging (e.g., passwords).
///
public HashSet ExcludedFields { get; set; } = new();
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Security/IRateLimitService.cs b/FormCraft/Forms/Security/IRateLimitService.cs
index 664da296..56eecf55 100644
--- a/FormCraft/Forms/Security/IRateLimitService.cs
+++ b/FormCraft/Forms/Security/IRateLimitService.cs
@@ -40,4 +40,4 @@ public class RateLimitResult
/// Time until the rate limit resets.
///
public TimeSpan? RetryAfter { get; set; }
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Security/InMemoryRateLimitService.cs b/FormCraft/Forms/Security/InMemoryRateLimitService.cs
index 9a24e988..b0b55af3 100644
--- a/FormCraft/Forms/Security/InMemoryRateLimitService.cs
+++ b/FormCraft/Forms/Security/InMemoryRateLimitService.cs
@@ -48,7 +48,9 @@ public Task CheckRateLimitAsync(string identifier, int maxAttem
var oldestAttempt = attempts.Min();
var computed = oldestAttempt.Add(timeWindow).Subtract(now);
if (computed > TimeSpan.Zero)
+ {
retryAfter = computed;
+ }
}
return Task.FromResult(new RateLimitResult
@@ -123,7 +125,9 @@ private void Cleanup(object? state)
public void Dispose()
{
if (_disposed)
+ {
return;
+ }
_disposed = true;
_cleanupTimer.Dispose();
diff --git a/FormCraft/Forms/Templates/ContactFormTemplate.cs b/FormCraft/Forms/Templates/ContactFormTemplate.cs
index dcc42fd9..29b6790e 100644
--- a/FormCraft/Forms/Templates/ContactFormTemplate.cs
+++ b/FormCraft/Forms/Templates/ContactFormTemplate.cs
@@ -91,4 +91,4 @@ private static Expression> GetBoolPropertyExpression(
var property = Expression.Property(parameter, propertyName);
return Expression.Lambda>(property, parameter);
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Validation/DynamicFormValidator.cs b/FormCraft/Forms/Validation/DynamicFormValidator.cs
index 3e913546..50d32a19 100644
--- a/FormCraft/Forms/Validation/DynamicFormValidator.cs
+++ b/FormCraft/Forms/Validation/DynamicFormValidator.cs
@@ -1,7 +1,7 @@
-using Microsoft.AspNetCore.Components;
-using Microsoft.AspNetCore.Components.Forms;
using System.Reflection;
using System.Runtime.CompilerServices;
+using Microsoft.AspNetCore.Components;
+using Microsoft.AspNetCore.Components.Forms;
namespace FormCraft;
@@ -269,7 +269,10 @@ private async void HandleFieldChanged(object? sender, FieldChangedEventArgs e)
// Find the field configuration for the changed field
var fieldConfig = Configuration.Fields.FirstOrDefault(f => f.FieldName == e.FieldIdentifier.FieldName);
- if (fieldConfig == null) return;
+ if (fieldConfig == null)
+ {
+ return;
+ }
var model = (TModel)_editContext!.Model;
var getter = FieldValueGetterCache.GetOrCompile(fieldConfig);
@@ -298,7 +301,10 @@ private async void HandleFieldChanged(object? sender, FieldChangedEventArgs e)
private async Task ValidateCollectionItemFieldAsync(FieldIdentifier fieldIdentifier, System.Text.RegularExpressions.Match nestedMatch)
{
- if (Configuration is not ICollectionFormConfiguration collectionConfig) return;
+ if (Configuration is not ICollectionFormConfiguration collectionConfig)
+ {
+ return;
+ }
var collectionFieldName = nestedMatch.Groups["collection"].Value;
// TryParse, not Parse: the regex guarantees digits but not that they fit in an int, and an
@@ -312,7 +318,10 @@ private async Task ValidateCollectionItemFieldAsync(FieldIdentifier fieldIdentif
var collectionField = collectionConfig.CollectionFields
.FirstOrDefault(f => f.FieldName == collectionFieldName);
- if (collectionField == null) return;
+ if (collectionField == null)
+ {
+ return;
+ }
var model = (TModel)_editContext!.Model;
@@ -339,4 +348,4 @@ public void Dispose()
_editContext.OnFieldChanged -= HandleFieldChanged;
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Validators/AsyncValidator.cs b/FormCraft/Forms/Validators/AsyncValidator.cs
index dca51714..46670926 100644
--- a/FormCraft/Forms/Validators/AsyncValidator.cs
+++ b/FormCraft/Forms/Validators/AsyncValidator.cs
@@ -63,4 +63,4 @@ public async Task ValidateAsync(TModel model, TValue value, IS
return ValidationResult.Failure($"Validation could not be completed: {ex.Message}");
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Validators/CustomValidator.cs b/FormCraft/Forms/Validators/CustomValidator.cs
index 6392c9fe..01d232b9 100644
--- a/FormCraft/Forms/Validators/CustomValidator.cs
+++ b/FormCraft/Forms/Validators/CustomValidator.cs
@@ -62,4 +62,4 @@ public Task ValidateAsync(TModel model, TValue value, IService
return Task.FromResult(ValidationResult.Failure($"Validation could not be completed: {ex.Message}"));
}
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Validators/FluentValidationAdapter.cs b/FormCraft/Forms/Validators/FluentValidationAdapter.cs
index c014302b..243f69ef 100644
--- a/FormCraft/Forms/Validators/FluentValidationAdapter.cs
+++ b/FormCraft/Forms/Validators/FluentValidationAdapter.cs
@@ -1,6 +1,6 @@
+using System.Linq.Expressions;
using FluentValidation;
using Microsoft.Extensions.DependencyInjection;
-using System.Linq.Expressions;
namespace FormCraft;
@@ -180,4 +180,4 @@ private string GetPropertyName()
return string.Join(".", propertyPath);
}
-}
\ No newline at end of file
+}
diff --git a/FormCraft/Forms/Validators/RequiredValidator.cs b/FormCraft/Forms/Validators/RequiredValidator.cs
index fe3e1b31..7d62cdef 100644
--- a/FormCraft/Forms/Validators/RequiredValidator.cs
+++ b/FormCraft/Forms/Validators/RequiredValidator.cs
@@ -53,4 +53,4 @@ public Task ValidateAsync(TModel model, TValue value, IService
? ValidationResult.Success()
: ValidationResult.Failure(ErrorMessage!));
}
-}
\ No newline at end of file
+}
diff --git a/README.md b/README.md
index b1d7bd09..2acbfdd8 100644
--- a/README.md
+++ b/README.md
@@ -58,6 +58,10 @@ Experience FormCraft in action! Visit our [interactive demo](https://phmatray.gi
## 🎉 Unreleased
+- **`.editorconfig` is now actually enforced, and the tree matches it (#301).** The file declared its code-style rules at `warning` severity and `Directory.Build.props` sets `TreatWarningsAsErrors=true` — which together *look* like enforcement and were not: `IDE*` analyzers only run at build time when `EnforceCodeStyleInBuild` is set, and it wasn't, while no CI job ran `dotnet format` either. Nothing anywhere read those severities, so 574 violations had accumulated across 201 files. There is now a Nuke `Format` target (`./build.sh Format`) wrapping `dotnet format --verify-no-changes`, wired into `ci.yml` ahead of the test run, and a one-off pass has cleared the backlog — split into a whitespace commit and a code-style commit so the shape-changing fixes stayed reviewable. Enforcement lives in CI rather than the build on purpose: with warnings-as-errors, build-time style analysis would break `dotnet build` mid-edit over a missing brace.
+
+ ⚠️ **`dotnet format` can corrupt multi-targeted files when it applies fixes** — `FormCraft` is `net8.0;net10.0`, and a fix applied once per target framework can land as a literal `<<<<<<< TODO: Unmerged change` conflict block written into the `.cs` file, which then doesn't compile. Verify mode never writes, so the CI gate is unaffected. After running the formatter to apply fixes, `grep -rl '<<<<<<< TODO' --include='*.cs' .` before committing.
+
- **⚠️ Collection item validators now run once per validation pass instead of twice (#329).** A form with a collection field validated every item field **twice** on every pass: `ValidateModelAsync` asked for the flat collection-level messages and then for the nested `Items[i].Field` ones, and the first call already performed the per-item walk internally. Nothing looked wrong — the two results are attached to different identifiers, so every message still appeared exactly once — which is why it went unnoticed.
**This is observable if your validators do anything besides return a verdict.** An async validator calling an API fired **twice per row per submit**: 100 requests for a 50-row collection, not 50. After this change it fires once. If you had compensated for the doubling anywhere — a counter, a de-duplication guard, a rate-limit allowance — that compensation is now wrong. Messages, their text, their identifiers and their order are unchanged.
diff --git a/build/Build.cs b/build/Build.cs
index baba4e5b..7766c52c 100644
--- a/build/Build.cs
+++ b/build/Build.cs
@@ -135,6 +135,28 @@ class Build : NukeBuild
.EnableNoRestore());
});
+ // Verifies the tree matches .editorconfig. Fails on any diff; changes nothing (#301).
+ //
+ // ⚠️ Deliberately NOT a DependsOn of Test or Pack, and deliberately not EnforceCodeStyleInBuild.
+ // Those IDE* analyzers are opt-in at build time, and switching them on next to this repo's
+ // TreatWarningsAsErrors=true would make one missing brace break `dotnet build` mid-edit and add
+ // an analyzer pass to every incremental build. The rules are enforced where regression actually
+ // has to be caught — CI — and `./build.sh Test` stays a correctness-only, fast path.
+ //
+ // Before #301 nothing read .editorconfig at all: its severities say `warning`, the build turns
+ // warnings into errors, and the two never met because EnforceCodeStyleInBuild was unset and no
+ // workflow ran `dotnet format`. 574 violations across 201 files had accumulated behind that gap.
+ //
+ // Raw DotNet(...) rather than a typed task: Nuke exposes no DotNetFormat wrapper covering
+ // --verify-no-changes, and the raw call is exactly the command a developer runs to fix a
+ // failure here (drop --verify-no-changes to apply).
+ Target Format => _ => _
+ .DependsOn(Restore)
+ .Executes(() =>
+ {
+ DotNet($"format {Solution.Path} --verify-no-changes --no-restore");
+ });
+
Target Test => _ => _
.DependsOn(Compile)
// Derived from the same list the post-run guard reads, so the promise and the check cannot
@@ -472,10 +494,10 @@ class Build : NukeBuild
Serilog.Log.Information(" - Current version: {Version}", CurrentVersion);
})
.DependsOn(Publish);
- // Note: the GitHub Release is created exclusively by release-please, in
- // .github/workflows/release-please.yml, which then runs this publish in the same job.
- // Do NOT add a release-creating target back into this chain — two producers would race
- // to create a release for the same tag (already_exists errors).
+ // Note: the GitHub Release is created exclusively by release-please, in
+ // .github/workflows/release-please.yml, which then runs this publish in the same job.
+ // Do NOT add a release-creating target back into this chain — two producers would race
+ // to create a release for the same tag (already_exists errors).
Target Release => _ => _
.Description("Creates a new release (NuGet + GitHub)")
@@ -545,4 +567,4 @@ string GetCurrentTag()
/// legal-but-nonsense prerelease label is not excluded (#227).
///
bool IsOnVersionTag() => BuildVersioning.IsVersionTag(GetCurrentTag());
-}
\ No newline at end of file
+}