diff --git a/README.md b/README.md
index 2bdd03a..b902091 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,190 @@
# BlazorJsonForm
-Build Blazor forms from JSON Schema using MudBlazor.
+[](https://apollo3zehn.github.io/BlazorJsonForm)
+[](https://www.nuget.org/packages/BlazorJsonForm)
-The main use case for this library is a Single-Page Blazor application which needs to provide a proper UI for configuration data. The corresponding C# types may be defined in the backend (or in plugins loaded by the backend). With the help of the library `NJsonSchema` it is then easy to generate a JSON schema from these configuration types, send the resulting JSON to the frontend and then use this library to render a nice UI. The backing storage is a `JsonNode` and when the configuration made by the user should be saved, it can be transferred back to the backend as a JSON string. In the backend you can deserialize and validate the data as shown below.
+## Introduction
-It is also possible to validate everything in the frontend and best practice would be to do both. You can do so by using `MudForm` (MudBlazor) or `EditContext` (Microsoft) which is shown below as well.
+Build Blazor forms from JSON Schema using MudBlazor. Inspiration comes from the [JSON Forms](https://jsonforms.io/examples) project.
-[Here](https://apollo3zehn.github.io/BlazorJsonForm/) is a live example with a predefined configuration type. This type has many different property to test all different types of data. The button `Nullable` mode makes the live example to use the same configuration type but now with all types being nullable. The effect is that now all properties are allowed to be null and so the validation is expected to succeed.
+The main use case for this library is a Single-Page Blazor application (Wasm) that needs to provide a proper UI for configuration data. The corresponding C# types can be defined in the backend (or in plugins loaded by the backend). Using the external library [NJsonSchema](https://github.com/RicoSuter/NJsonSchema) it is then easy to generate a JSON schema from these configuration types, send the resulting JSON to the frontend and finally use this library to render a nice UI. The backing store is a `JsonNode` that can be passed back to the backend as a JSON string when the user's configuration is about to be saved. The backend can easily deserialize the data into a strongly typed instance and validate it afterwards.
-The button `Validate form` validates the current state of the form in the frontend. The button `Validate object` causes the JSON form data to be deseralized and validated using data annotations [validator](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.validator) class.
+Additionally to the validation in the backend, the frontend can validate the input data as well. This can be achieved by using `MudForm` (MudBlazor) or `EditContext` (Microsoft).
-The following data annotation attributes are supported and tested:
+[Here is a live example](https://apollo3zehn.github.io/BlazorJsonForm/) with a predefined configuration type. It has many properties to test all kinds of data. The `Nullable mode` button switches between a type without nullable properties and one with only nullable properties (to be able to test both variants).
+
+The `Validate form` button validates the current state of the form in the frontend. And the `Validate object` button causes the JSON form data to be deseralized and validated using data annotations [validator](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.validator) class. This would normally be done in the backend.
+
+[](https://apollo3zehn.github.io/BlazorJsonForm)
+
+## Getting started
+
+### Requirements
+
+- .NET 8+
+- MudBlazor ([installation guide](https://mudblazor.com/getting-started/installation#online-playground))
+
+Ensure these four components are present at the top level (e.g. in `MainLayout.razor`):
+
+```razor
+
+
+
+
+```
+
+- this library: `dotnet add package BlazorJsonForm --prerelease`
+
+### Type definition
+
+The following data types are supported:
+
+- Integer: `byte`, `int`, `ulong`, `long`
+- Floating point: `float`, `double`
+- Enum, underlying type: `byte`, `sbyte`, `ushort`, `short`, `uint`, `int`, `ulong`, `long`
+- `bool`
+- `string`
+- Object: `class` or `struct` (including `record`)
+- Array: `T[]`, `List`, `IList`
+- Dictionary: `Dictionary`, `IDictionary`
+
+All listed types can also be nullable (e.g. `int?` or `string?`).
+
+The simplest way to define you configuration type is to use C# records. Make sure to add proper XML documentation to each property.
+
+```cs
+/// Number of engines
+/// Amount of fuel in L
+/// Message from mankind
+/// Launch coordinates
+record RocketData(
+ int EngineCount,
+ double Fuel,
+ string? Message,
+ int[] LaunchCoordinates
+);
+```
+
+> [!NOTE]
+> See also [Types.cs](https://github.com/Apollo3zehn/BlazorJsonForm/blob/dev/src/BlazorJsonFormTester.Core/Types.cs) for a complete example.
+
+### JSON Schema
+
+The JSON schema can be easily created in the backend via:
+
+```cs
+var schema = JsonSchema.FromType();
+```
+
+### Blazor
+
+```html
+@if (_schema is not null)
+{
+
+}
+
+@code
+{
+ private JsonSchema _schema;
+ private JsonNode? _data;
+
+ protected override async Task OnInitializedAsync()
+ {
+ _schema = await GetJsonSchemaFromBackendAsync(...);
+ }
+}
+```
+
+### Frontend Validation
+
+Wrap `JsonForm` in a `MudForm` as shown below and validate the form via `_form.Validate()`:
+
+```html
+
+ Validate Form
+
+
+
+
+
+
+@code
+{
+ // ...
+ private MudForm _form = default!;
+
+ private async Task ValidateForm()
+ {
+ await _form.Validate();
+
+ if (_form.IsValid)
+ ...
+
+ else
+ ...
+ }
+}
+```
+
+### Desialization & Backend Validation
+
+AS shown above, the actual configuration data is stored in the instance variable `_data` which is of type `JsonNode?`.
+
+When the frontend validation succeeds, you can serialize the data via `var jsonString = JsonSerializer.Serialize(_data)` and send it to the backend.
+
+The backend can then deserialize the JSON string into a strongly-typed object and validate it:
+
+```cs
+var config = JsonSerializer.Deserialize();
+```
+
+> [!NOTE]
+> If you already use .NET 9 you should enable the `RespectNullableAnnotations` property of the `JsonSerializerOptions` which ensures that for instance a non-nullable string (`string`) is not being populated with a `null` value. Otherwise an exception is being thrown.
+
+The deserialized object can be further validated by using the .NET built-in `Validator` class:
+
+```cs
+var validationResults = new List();
+
+var isValid = Validator.TryValidateObject(
+ config,
+ new ValidationContext(config),
+ validationResults,
+ validateAllProperties: true
+);
+```
+
+The validator validates all properties against certain conditions. These are being expressed using [data annotation attributes](https://learn.microsoft.com/de-de/dotnet/api/system.componentmodel.dataannotations?view=net-8.0). Currently, the following three data annotation attributes are supported and tested:
```cs
[Range(...)]
+int Foo { get; set; }
+
[StringLength(...)]
+string Bar { get; set; }
+
[RegularExpression(...)]
+string FooBar { get; set; }
```
-You should consider addings the `Required` attribute next to `RegularExpression` attribute because otherwise [empty strings are always valid](https://stackoverflow.com/a/32945086).
+> [!NOTE]
+> You should consider adding the `Required` attribute next to `RegularExpression` attribute because otherwise [empty strings are always valid](https://stackoverflow.com/a/32945086).
+
+## Extras
-
+You can define custom attrbutes which will change the generated JSON schema as described below.
-You can define custom attrbutes which will change the generated JSON schema. Here are two example about how to add a [helper text](https://mudblazor.com/components/textfield#form-props-helper-text) to the inputs and about how to specify custom enum member names to be displayed in the UI:
+### Helper text
-# How to: Helper text
+Add a [helper text](https://mudblazor.com/components/textfield#form-props-helper-text) to inputs:
```cs
using NJsonSchema.Annotations;
-namespace BlazorJsonFormTester;
-
[AttributeUsage(AttributeTargets.Property)]
class HelperTextAttribute : Attribute, IJsonSchemaExtensionDataAttribute
{
@@ -51,17 +205,17 @@ internal record MyConfigurationType(
);
```
-# How to: Enum display names
+### Enum display names
+
+Specify custom enum member names to be displayed in the UI:
```cs
using NJsonSchema.Annotations;
-namespace BlazorJsonFormTester;
-
[AttributeUsage(AttributeTargets.Enum)]
-internal class EnumDisplayNameAttribute : Attribute, IJsonSchemaExtensionDataAttribute
+internal class EnumDisplayNamesAttribute : Attribute, IJsonSchemaExtensionDataAttribute
{
- public EnumDisplayNameAttribute(params string[] displayNames)
+ public EnumDisplayNamesAttribute(params string[] displayNames)
{
ExtensionData = new Dictionary()
{
@@ -72,7 +226,7 @@ internal class EnumDisplayNameAttribute : Attribute, IJsonSchemaExtensionDataAtt
public IReadOnlyDictionary ExtensionData { get; }
}
-[EnumDisplayName(
+[EnumDisplayNames(
"The Mercury",
"The Venus",
"The Mars",
@@ -91,4 +245,8 @@ internal enum MissionTarget
Uranus,
Neptune
}
-```
\ No newline at end of file
+```
+
+# Known issues
+
+- When using `[RegularExpression]` attribute on a string property, `null` values are not supported anymore. This is because the library `NJsonSchema` which is used to generate the schema is treating a `[RegularExpression]` annotated property as non-nullable and so the schema does not carry nullability information anymore.
\ No newline at end of file
diff --git a/image.avif b/image.avif
index f7ff3da..d8ebe50 100644
Binary files a/image.avif and b/image.avif differ
diff --git a/src/BlazorJsonForm/JsonForm.razor b/src/BlazorJsonForm/JsonForm.razor
index ec6c5e8..797924a 100644
--- a/src/BlazorJsonForm/JsonForm.razor
+++ b/src/BlazorJsonForm/JsonForm.razor
@@ -697,12 +697,12 @@
Required="!isNullable"
MaxLength="schema.MaxLength is null ? int.MaxValue : (int)schema.MaxLength"
Counter="schema.MaxLength is null ? null : schema.MaxLength"
- Validation="schema.Pattern is null ? null : (Func>)(value => RegexValidation(value, schema.Pattern))"
+ Validation="schema.Pattern is null ? null : (Func>)(value => RegexValidation(value, schema.Pattern, isNullable))"
Clearable="true"
OnClearButtonClick="() => setValue?.Invoke(default)"
HelperText="@(GetHelperTextOrDefault(schema))"
@bind-Value:get="data is null ? null : data.GetValue()"
- @bind-Value:set="value => setValue?.Invoke(JsonValue.Create(value))" />
+ @bind-Value:set="value => setValue?.Invoke(JsonValue.Create(value))" />
}
private void RenderEnum(
@@ -763,7 +763,7 @@
T="T?"
Label="@label"
Required="!isNullable"
- ToStringFunc="item => valueNamePairs.First(x => x.First.Equals(item)).Second"
+ ToStringFunc="item => item is null ? string.Empty : valueNamePairs.First(x => x.First.Equals(item)).Second"
Clearable="true"
OnClearButtonClick="() => setValue?.Invoke(default)"
HelperText="@(GetHelperTextOrDefault(schema))"
@@ -870,16 +870,26 @@
return ["Required"];
}
- private IEnumerable RegexValidation(string? input, string pattern)
+ private IEnumerable RegexValidation(string? input, string pattern, bool isNullable)
{
if (input is null)
- return ["Required"];
+ {
+ if (isNullable)
+ return Enumerable.Empty();
+
+ else
+ return ["Required"];
+ }
else if (Regex.IsMatch(input, pattern))
+ {
return Enumerable.Empty();
+ }
else
+ {
return ["The input does not match the expected pattern"];
+ }
}
private string? GetHelperTextOrDefault(JsonSchema schema)
diff --git a/src/BlazorJsonForm/MudSelectEnhanced.razor b/src/BlazorJsonForm/MudSelectEnhanced.razor
index 3170b2d..ae72d61 100644
--- a/src/BlazorJsonForm/MudSelectEnhanced.razor
+++ b/src/BlazorJsonForm/MudSelectEnhanced.razor
@@ -8,28 +8,37 @@
@code
{
- /* Problem:
+ /* Problem:
*
- * When we have a MudSelect in combination with MultiSelection="true", the
- * validation function is called before the property SelectedValues is set,
- * which renders the validation function useles. The task is not to workaround
- * this issue.
- */
+ * When we use a MudSelect in combination with MultiSelection="true" and
+ * Required="true" to represent an enum with a [Flags] attribute, it is
+ * difficult to track if the value is null or empty (0). Normally this
+ * combination would now allow having no selected values (i.e. no flags
+ * set) but in reality this is a valid value. Only null should be invalid
+ * in case Required="true". MudSelect also does not support resetting the
+ * value to null, so this is also being worked around.
+ */
- private bool _isCleared;
+ private bool _hasValue;
- /* https://github.com/MudBlazor/MudBlazor/issues/4328#issuecomment-1086940659 */
- protected override bool HasValue(T value)
+ protected override void OnParametersSet()
{
- if (MultiSelection)
+ var originalSelectedValuesChanged = SelectedValuesChanged;
+
+ SelectedValuesChanged = new EventCallback>(this, async () =>
{
- var hasValue = !_isCleared;
+ if (originalSelectedValuesChanged.HasDelegate)
+ await originalSelectedValuesChanged.InvokeAsync(SelectedValues);
- if (_isCleared)
- _isCleared = false;
+ _hasValue = true;
+ await Validate();
+ });
+ }
- return hasValue;
- }
+ protected override bool HasValue(T value)
+ {
+ if (MultiSelection)
+ return _hasValue;
else
return base.HasValue(value);
@@ -41,11 +50,12 @@
{
set
{
- Action realCallback = () =>
+ Func realCallback = () =>
{
- _isCleared = true;
- Validate();
value();
+ _hasValue = false;
+
+ return Validate();
};
OnClearButtonClick = new EventCallback(this, realCallback);
diff --git a/src/BlazorJsonFormTester.Core/EnumDisplayNameAttribute.cs b/src/BlazorJsonFormTester.Core/EnumDisplayNameAttribute.cs
index def3948..d0412e6 100644
--- a/src/BlazorJsonFormTester.Core/EnumDisplayNameAttribute.cs
+++ b/src/BlazorJsonFormTester.Core/EnumDisplayNameAttribute.cs
@@ -3,9 +3,9 @@
namespace BlazorJsonFormTester;
[AttributeUsage(AttributeTargets.Enum)]
-internal class EnumDisplayNameAttribute : Attribute, IJsonSchemaExtensionDataAttribute
+internal class EnumDisplayNamesAttribute : Attribute, IJsonSchemaExtensionDataAttribute
{
- public EnumDisplayNameAttribute(params string[] displayNames)
+ public EnumDisplayNamesAttribute(params string[] displayNames)
{
ExtensionData = new Dictionary()
{
diff --git a/src/BlazorJsonFormTester.Core/TestingComponent.razor b/src/BlazorJsonFormTester.Core/TestingComponent.razor
index 0c3e267..504e717 100644
--- a/src/BlazorJsonFormTester.Core/TestingComponent.razor
+++ b/src/BlazorJsonFormTester.Core/TestingComponent.razor
@@ -1,5 +1,4 @@
-@using System.Text.Json.Schema
-@using System.Text.Json.Nodes
+@using System.Text.Json.Nodes
@using System.Text.Json
@using NJsonSchema
@using System.ComponentModel.DataAnnotations
@@ -139,7 +138,7 @@
Severity.Error
);
}
-}
+ }
private void ValidateObject()
{
diff --git a/src/BlazorJsonFormTester.Core/Types.cs b/src/BlazorJsonFormTester.Core/Types.cs
index 79a874a..b8077f2 100644
--- a/src/BlazorJsonFormTester.Core/Types.cs
+++ b/src/BlazorJsonFormTester.Core/Types.cs
@@ -12,7 +12,7 @@ public enum RocketStatus: ushort
}
[Flags]
-[EnumDisplayName(
+[EnumDisplayNames(
"The Mercury",
"The Venus",
"The Mars",
@@ -139,7 +139,8 @@ public record Rocket_Nullable(
[
property:
HelperText("Example: /path/to/mission/data"),
- RegularExpression(@"^(?:\/[a-zA-Z_][a-zA-Z_0-9]*)+$")
+ RegularExpression(@"^(?:\/[a-zA-Z_][a-zA-Z_0-9]*)+$"),
+ Required /* https://stackoverflow.com/a/32945086 */
]
string? MissionDataPath,