Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/rjygraham/maui.dataforms

This is a proof of concept library for easily creating validable data entry forms in .NET MAUI. This is not published as a Nuget package yet, so please clone locally and add reference to the Maui.FluentForms project to use.
https://github.com/rjygraham/maui.dataforms

Last synced: 3 months ago
JSON representation

This is a proof of concept library for easily creating validable data entry forms in .NET MAUI. This is not published as a Nuget package yet, so please clone locally and add reference to the Maui.FluentForms project to use.

Awesome Lists containing this project

README

        

# Maui.DataForms

This is a proof of concept for easily creating validable data entry forms in .NET MAUI. This is not published as a Nuget package yet, so please clone locally and add reference to the `Maui.DataForms.Core`, `Maui.DataForms.Controls`, and either `Maui.DataForms.Dynamic` or `Maui.DataForms.Fluent` projects to use.

`Maui.DataForms` provides 2 modes of defining data forms within your applications:

- Fluent: this method is strongly typed and uses a classes representing your model and `Maui.DataForms` form.
- Dynamic: this method allows for easy creation of dynamic forms at runtime. Forms can be created in code based on certain criteria or even generated by a server-side API and then deserialized from JSON into a `DynamicDataForm`.

# Fluent Demo

The included `Maui.DataForms.Sample` project illustrates how to use Mail.DataForms. Below are the highlights of using the Fluent API. There is also a Dynamic API which will be discussed at the bottom of this page.

## Person

The `Person` class represents the underlying data model from which the form will be created.

```csharp
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime DateOfBirth { get; set; }
public TimeSpan TimeOfBirth { get; set; }
public string Biography { get; set; }
public double Height { get; set; }
public double Weight { get; set; }
public bool LikesPizza { get; set; }
public bool IsActive { get; set; }
}
```

## PersonValidator

The `PersonValidator` class is the FluentValidation validator used to validate user inputs in our data form. You can use any validation framewwork you wish, just be sure to implement `Maui.DataForms.Validation.IDataFormValidator` where `TModel` is the data model class (in this case `Person`) to be validated.

```csharp
public class PersonValidator : AbstractValidator, IDataFormValidator
{
public PersonValidator()
{
RuleFor(r => r.FirstName)
.NotEmpty()
.MaximumLength(20);

RuleFor(r => r.LastName)
.NotEmpty()
.MaximumLength(50);

RuleFor(r => r.DateOfBirth)
.NotEmpty()
.GreaterThanOrEqualTo(new DateTime(2000, 1, 1, 0, 0, 0))
.LessThanOrEqualTo(new DateTime(2021, 12, 31, 23, 59, 59));

RuleFor(r => r.Biography)
.NotEmpty()
.MaximumLength(500);

RuleFor(r => r.Height)
.GreaterThan(0.2)
.LessThanOrEqualTo(0.8);

RuleFor(r => r.Weight)
.GreaterThan(20.0)
.LessThanOrEqualTo(80.0);
}

public FormFieldValidationResult ValidateField(Person model, string formFieldName)
{
var members = new string[] { formFieldName };
var validationContext = new ValidationContext(model, new PropertyChain(), new MemberNameValidatorSelector(members));

var validationResults = Validate(validationContext);

var errors = validationResults.IsValid
? Array.Empty()
: validationResults.Errors.Select(s => s.ErrorMessage).ToArray();

return new FormFieldValidationResult(validationResults.IsValid, errors);
}

public DataFormValidationResult ValidateForm(Person model)
{
var validationResults = Validate(model);

var errors = validationResults.IsValid
? new Dictionary()
: validationResults.ToDictionary();

return new DataFormValidationResult(validationResults.IsValid, errors);
}
}
```

## PersonForm

The `PersonDataForm` class is where the UI elements for the data entry form are defined. To build the form, just inherit from `FluentFormBase` and then create a constructor which passes the model and validator (optional) instances to the base class and the define your fields using the fluent syntax. Finally call `Build()`

```csharp
public class PersonDataForm : FluentFormBase
{
public PersonDataForm(Person model, IDataFormValidator validator = null)
: base(model, validator)
{
FieldFor(f => f.FirstName)
.AsEntry()
.WithConfiguration(config => config.Placeholder = "First Name")
.WithLayout(layout => layout.GridRow = 0)
.WithValidationMode(ValidationMode.Auto);

FieldFor(f => f.LastName)
.AsEntry()
.WithConfiguration(config => config.Placeholder = "Last Name")
.WithLayout(layout => layout.GridRow = 1)
.WithValidationMode(ValidationMode.Auto);

FieldFor(f => f.DateOfBirth)
.AsDatePicker()
.WithConfiguration(config =>
{
config.Format = "D";
config.MinimumDate = DateTime.MinValue;
config.MaximumDate = DateTime.MaxValue;
})
.WithLayout(layout => layout.GridRow = 2)
.WithValidationMode(ValidationMode.Auto);

FieldFor(f => f.TimeOfBirth)
.AsTimePicker()
.WithConfiguration(config => config.Format = "t")
.WithLayout(layout => layout.GridRow = 3)
.WithValidationMode(ValidationMode.Auto);

FieldFor(f => f.Biography)
.AsEditor()
.WithConfiguration(config => config.Placeholder = "Biography")
.WithLayout(layout => layout.GridRow = 4)
.WithValidationMode(ValidationMode.Auto);

FieldFor(f => f.Height)
.AsSlider()
.WithConfiguration(config =>
{
config.Minimum = 0.1;
config.Maximum = 0.9;
})
.WithLayout(layout => layout.GridRow = 5)
.WithValidationMode(ValidationMode.Auto);

FieldFor(f => f.Weight)
.AsStepper()
.WithConfiguration(config =>
{
config.Minimum = 10.0;
config.Maximum = 90.0;
})
.WithLayout(layout => layout.GridRow = 6)
.WithValidationMode(ValidationMode.Auto);

FieldFor(f => f.LikesPizza)
.AsSwitch()
.WithLayout(layout => layout.GridRow = 7);

FieldFor(f => f.IsActive)
.AsCheckBox()
.WithLayout(layout => layout.GridRow = 8);

Build();
}
}
```

## FluentDemoPageViewModel.cs

The `FluentDemoPageViewModel` class then sets the `PersonDataForm` (autogenerated by CTK MVVM source generators) to a new instance of `PersonDataForm` with an instances of `Person` model and `PersonValidator` as constructor parameters.

```csharp
public partial class FluentDemoPageViewModel : ObservableObject
{
[ObservableProperty]
private PersonDataForm personDataForm;

public FluentDemoPageViewModel()
{
PersonDataForm = new PersonDataForm(new Models.Person(), new PersonValidator());
}

[RelayCommand]
private async Task Submit()
{
// no-op for now.
}
}
```

## FluentDemoPage.xaml.cs

Set the `BindingContext` to a new instance of `FluentDemoPageViewModel`. In the sample, the `FluentDemoPageViewModel` is configured in `IServiceCollection` and automatically injected by MAUI at runtime.

```csharp
public partial class FluentDemoPage : ContentPage
{
public FluentDemoPage(FluentDemoPageViewModel viewModel)
{
InitializeComponent();
BindingContext = viewModel;
}
}
```

## FluentDemoPage.xaml

Add the `mdfc` namespace and then a `Grid` with `BindableLayout.ItemsSource="{Binding PersonDataForm.Fields}"` which binds to the `Fields` property of the `PersonDataForm` which is a property of the view model previously set to the `BindingContext`. Finally, set the Forms `BindableLayout.ItemTemplateSelector` to an instance of `DataFormsDataTemplateSelector`.

```xml











```

## FormField Controls

The default FormField controls are defined in the `Maui.DataForms.Controls` project. For now, the controls are very basic to prove the proof of concept. The controls must be registered at application startup in `MauiProgram` using the `UseDefaultFormFieldContentControls` extension method:

```csharp
using CommunityToolkit.Maui;
using Maui.DataForms.Sample.ViewModels;
using Maui.DataForms.Sample.Views;
using Maui.DataForms.Controls;

namespace Maui.DataForms.Sample;

public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp()
.UseMauiCommunityToolkit()
.UseDefaultFormFieldContentControls()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
})
.Services
.AddTransient()
.AddTransient();

return builder.Build();
}
}
```

Additionaly custom control can be registered using the `MapFormFieldContentControl(this MauiAppBuilder builder, string formFieldTemplateName)` extension method.

## WithConfiguration()

As shown in the `EntryFormFieldControl.xaml` section above, you can set many values of the `Entry` control using `Maui.DataForms`. These values are set in the `WithConfiguration()` method call when defining your field. The `EntryFormFieldConfiguration.cs` snippet below shows all configuration that can be set for `Entry` controls.

> By default, `Maui.DataForms` uses all the control default values for the various properties, so unless there is a specific need to change the default value, you may consider removing the binding from the `FormField` control definition to minimize cycles binding to values which already contain the default value.

```csharp
using System.Windows.Input;

namespace Maui.DataForms.Configuration;

public sealed class EntryFormFieldConfiguration : FormFieldConfigurationBase
{
public ClearButtonVisibility ClearButtonVisibility { get; set; } = ClearButtonVisibility.Never;
public bool FontAutoScalingEnabled { get; set; } = true;
public Keyboard Keyboard { get; set; } = Keyboard.Default;
public bool IsPassword { get; set; } = false;
public bool IsTextPredictionEnabled { get; set; } = true;
public string Placeholder { get; set; } = string.Empty;
public ICommand ReturnCommand { get; set; }
public object ReturnCommandParameter { get; set; }
public ReturnType ReturnType { get; set; } = ReturnType.Default;
}
```

## WithLayout()

This method provides you fine grained control over the placement of the control by allowing you to specify the `Grid.Row`, `Grid.Column`, `Grid.RowSpan`, and `Grid.ColumnSpan` properties.

## WithValidationMode()

This method allows you ability to control whether the field is validated when the underlying property value is changed or if validation must be manually invoked. Valid values are:

```csharp
namespace Maui.DataForms.FormFields;

public enum ValidationMode
{
Auto = 0,
Manual = 1
}
```
## Styling

Since styling is something unique to every application and can vary greatly across controls, `Maui.DataForms` doesn't provide any options to provide styling and instead encourages developers to use styles set in the application resource dictionary.

# Dynamic Forms

In addition to the `Maui.DataForms.Fluent` API which is great for creating strongly typed forms from model classes, the `Maui.DataForms.Dynamic` project allows for creating form definitions directly or via JSON and then dynamically rendering the form at runtime. This can be useful in situations where forms need to vary based on certain criteria or you want to dynamically define a form server-side.

## DynamicDemoPageViewModel.cs

The `DynamicDemoPageViewModel` illustrates how to dynamically define a `DynamicDataForm` which is deserialized from JSON embedded in the view model. This JSON could have just as easily been a response from an API call:

```csharp
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Maui.DataForms.Models;
using System.Text.Json;

namespace Maui.DataForms.Sample.ViewModels;

public partial class DynamicDemoPageViewModel : ObservableObject
{
[ObservableProperty]
private DynamicDataForm personDataForm;

public DynamicDemoPageViewModel()
{
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
options.Converters.Add(new SystemObjectNewtonsoftCompatibleConverter());

var dataFormDefinition = JsonSerializer.Deserialize(json, options);

PersonDataForm = DynamicDataForm.Create(dataFormDefinition);
}

[RelayCommand]
private async Task Submit()
{
// no-op for now.
}

private const string json =
"""
{
"id": "personForm",
"name": "Person Form",
"etag": 1664738021,
"fields": [
{
"id": "firstName",
"name": "First Name",
"dataType": "string",
"controlTemplateName": "Entry",
"validationMode": 0,
"validationRules": [
{
"ruleName": "notEmpty",
"errorMessageFormat": "First Name must not be empty."
},
{
"ruleName": "maximumLength",
"ruleValue": 20,
"errorMessageFormat": "First Name must not be longer than {0} characters."
}
],
"configuration": {
"placeholder": "First Name"
},
"layout": {
"gridColumn": 0,
"gridRow": 0
}
},
{
"id": "lastName",
"name": "Last Name",
"dataType": "string",
"controlTemplateName": "Entry",
"validationMode": 0,
"validationRules": [
{
"ruleName": "notEmpty",
"errorMessageFormat": "Last Name must not be empty."
},
{
"ruleName": "maximumLength",
"ruleValue": 50,
"errorMessageFormat": "Last Name must not be longer than {0} characters."
}
],
"configuration": {
"placeholder": "Last Name"
},
"layout": {
"gridColumn": 0,
"gridRow": 1
}
},
{
"id": "dateOfBirth",
"name": "Date Of Birth",
"dataType": "DateTime",
"controlTemplateName": "DatePicker",
"validationMode": 0,
"validationRules": [
{
"ruleName": "greaterThanOrEqual",
"ruleValue": "2000-01-01T00:00:00",
"errorMessageFormat": "Date Of Birth must not be greater than or equal to {0}."
},
{
"ruleName": "lessThanOrEqual",
"ruleValue": "2021-12-31T23:59:59",
"errorMessageFormat": "Date Of Birth must not be less than or equal to {0}."
}
],
"configuration": {
"format": "D",
"minimumDate": "0001-01-01T00:00:00",
"maximumDate": "9999-12-31T23:59:59"
},
"layout": {
"gridColumn": 0,
"gridRow": 2
}
},
{
"id": "timeOfBirth",
"name": "Time Of Birth",
"dataType": "TimeSpan",
"controlTemplateName": "TimePicker",
"validationMode": 0,
"configuration": {
"format": "t"
},
"layout": {
"gridColumn": 0,
"gridRow": 3
}
},
{
"id": "biography",
"name": "Biography",
"dataType": "string",
"controlTemplateName": "Editor",
"validationMode": 0,
"validationRules": [
{
"ruleName": "notEmpty",
"errorMessageFormat": "Biography must not be empty."
},
{
"ruleName": "maximumLength",
"ruleValue": 500,
"errorMessageFormat": "Biography must not be longer than {0} characters."
}
],
"configuration": {
"placeholder": "Biography"
},
"layout": {
"gridColumn": 0,
"gridRow": 4
}
},
{
"id": "height",
"name": "Height",
"dataType": "double",
"controlTemplateName": "Slider",
"validationMode": 0,
"validationRules": [
{
"ruleName": "greaterThan",
"ruleValue": 0.2,
"errorMessageFormat": "Height must not be greater than {0}."
},
{
"ruleName": "lessThanOrEqual",
"ruleValue": 0.8,
"errorMessageFormat": "Height must be less than or equal to {0}."
}
],
"configuration": {
"minimum": 0.1,
"maximum": 0.9
},
"layout": {
"gridColumn": 0,
"gridRow": 5
}
},
{
"id": "weight",
"name": "Weight",
"dataType": "double",
"controlTemplateName": "Stepper",
"validationMode": 0,
"validationRules": [
{
"ruleName": "greaterThan",
"ruleValue": 20.0,
"errorMessageFormat": "Weight must not be greater than {0}."
},
{
"ruleName": "lessThanOrEqual",
"ruleValue": 80.0,
"errorMessageFormat": "Weight must be less than or equal to {0}."
}
],
"configuration": {
"minimum": 10.0,
"maximum": 90.0
},
"layout": {
"gridColumn": 0,
"gridRow": 6
}
},
{
"id": "likesPizza",
"name": "Likes Pizza",
"dataType": "bool",
"controlTemplateName": "Switch",
"validationMode": 0,
"layout": {
"gridColumn": 0,
"gridRow": 7
}
},
{
"id": "isActive",
"name": "Is Active",
"dataType": "bool",
"controlTemplateName": "CheckBox",
"validationMode": 0,
"layout": {
"gridColumn": 0,
"gridRow": 8
}
}
]
}
""";
}
```

## Dynamic Form Validation

Since the form is dynamically generated, it is now easily possible to use a validation libray like `FluentValidation` to do the validation. Therefore, the `Maui.DataForms.Dynamic` project defines a handful of built-in validation rules. This library will expand and beome more robust over time. It will also be possible to define your own custom validation rules.

# Customization

As previously mentioned, every application is different and therefore data entry forms may require input controls beyond those built into .NET MAUI. For this reason, `Maui.DataForms` allows custom FormFields to be defined.