Creating a dynamic forms framework in .NET with Blazor

Luke Canvin

One of the benefits of Blazor is that you can share C# objects between the server-side processing and the client-side processing. In this year’s Dev Camp we wanted to put this to the test with form validation.

Validation: client-side and server-side

When processing a form, you always want to validate server-side – never trust anything you receive from the client. However it’s a rather poor user experience if the only time a user gets feedback about their form validity is at form submission. So traditional application design involves a degree of duplication of validation rules. For example code to check if a telephone number is valid needs to be created in both JavaScript and in the server-side language. But with Blazor comes the ability to run the same C# code to perform both client-side and server-side validation. No chance of inconsistencies. No need to maintain two sets of code.

Dynamic forms

Blazor has built in support for form validation using data annotations. However for our Dev Camp, we wanted to try dynamic form generation. We envisaged a form builder UI that would allow our users to create forms (like you can in Google Forms or Survey Monkey). The user would define the fields, but also the validation rules applying. The definition result would be stored in a database as JSON.

For Dev Camp we had to prioritise what we were to work on, so we didn’t create the form builder UI. We did however create an example of the JSON it might produce, and we fed this non-so-dynamically into our front and back ends.

Defining forms

What should the JSON to define our form look like? We concluded that it may as well be a direct serialisation of our domain objects. Most fundamentally, a form consists of a series of form fields. We started with each HTML field type being represented by a form class. Surname could be represented by a TextField class, which derives from the Field class. The Field class has Name, Description and GUID properties. Then TextField had a maximum length. This made rendering forms straightforward.

But how about a minimum length for the text field? Should that be a property on the TextField class. It’s not something you need to know in order to display a text box. And it’s a less common requirement than maximum length. It didn’t seem appropriate to make every possible validation rule an essential part of the definition of a text field. It’s not just minimum length. There’s also defining a field as mandatory (or not), requiring the input to match a regex, etc. And that’s before we think about making these rules dependent on other fields. For example UK and Irish postcodes have different formats, so the postcode validation would depend on the country selection. Or whether a field is mandatory might depend on what has been entered elsewhere on the form.

All sorts of rules

As we started trying to create actual forms, we realised that a form doesn’t consist of just a list of fields and a list of validation rules. It’s quite common for the form itself to change depending on what you select. For example, if you select “Other” in a dropdown, it’s not just that the text field that allows you to be more specific was optional and is now mandatory (or may not be mandatory, depending on how you want the form to behave). More strongly, in this case, it should have been forbidden to enter anything in that box unless “Other” is selected. Perhaps it shouldn’t even be visible.

For these reasons we decided to implement a form with not only validation rules, but also invisibility rules (the name was chosen because visibility is the default, you only need a rule when that isn’t so).

     public class Form
     {
         public Identifier.FormIdentifier FormId { get; set; } = Guid.NewGuid();
         public IEnumerable<Field> Fields { get; set; }
         public string Title { get; set; }
         public IEnumerable<ValidationRule> ValidationRules { get; set; }
         public IEnumerable<InvisibilityRule> InvisibilityRules { get; set; }
     } 

Validation rules and invisibility rules are defined very similarly.

     public class ValidationRule
     {
         public Predicate.Predicate ValidWhen { get; set; }
         public IEnumerable<Identifier.CaseFieldIdentifier> InvalidFields;
         public string ErrorMessage { get; set; }
     }
     public class InvisibilityRule
     {
         public Predicate.Predicate InvisibleWhen { get; set; }
         public IEnumerable<Identifier.FormElementIdentifier> Elements { get; set; }
     } 

We also included rules for making form elements readonly or disabled – you can imagine for yourself what they might look like.

The rules themselves

The logic is mostly contained within our Predicate class, which is how ValidWhen and InvisibleWhen are defined. Take a look at how it is defined and the example of LengthLessThanOrEqualTo:

     public abstract class Predicate
     {
         public abstract bool IsTrue(ValueRetriever vr);
     }
     public abstract class ValueRetriever
     {
         public abstract object Retrieve(Identifier.CaseFieldIdentifier fieldID);
     }
     public class LengthLessThanOrEqualTo : Predicate
     {
         public Identifier.CaseFieldIdentifier Field { get; protected set; }
         public int Length { get; private set; }
  
         public LengthLessThanOrEqualTo(Identifier.CaseFieldIdentifier field, int length)
         {
             Field = field;
             Length = length;
         }
  
         protected override bool GetIsTrue(ValueRetriever vr)
         {
             return ((string)vr.Retrieve(Field)).Length <= Length;
         }
     } 

The LengthLessThanOrEqualTo predicate can be used to define a maximum length rule. If the predicate is true, the form is invalid.

This isn’t as generic as it could be. The sorts of validation rules you can use are defined by the Predicate classes that are provided. If you don’t have a Predicate class that does what you want, you can’t specify an anonymous function in your form definition. We ended up with this limitation due to serialisation constraints (you don’t want to try and serialise anonymous functions!).

However the validation framework is not as limited as it might appear. As well as predicates like NonEmpty, LengthGreaterThan and MatchesPattern, we also provided predicates that combine other predicates:

     public class All : Predicate
     {
         private readonly IEnumerable<Predicate> Predicates;
  
         public All(IEnumerable<Predicate> predicates)
         {
             Predicates = predicates;
         }
  
         public override bool IsTrue(ValueRetriever vr)
         {
             return Predicates.All(x => x.IsTrue(vr));
         }
     }
  
     public class Any : Predicate
     {
         private readonly IEnumerable<Predicate> Predicates;
  
         public Any(IEnumerable<Predicate> predicates)
         {
             Predicates = predicates;
         }
  
         public override bool IsTrue(ValueRetriever vr)
         {
             return Predicates.Any(x => x.IsTrue(vr));
         }
     }
     public class Not : Predicate
     {
         private readonly Predicate Predicate;
  
         public Not(Predicate predicate)
         {
             Predicate = predicate;
         }
  
         public override bool IsTrue(ValueRetriever vr) => !Predicate.IsTrue(vr);
     } 

This gives us a great flexibility. It allows us to define rules such as:

     new ValidationRule
     {
         ErrorMessage = "Either one of email address or phone number is required",
         ValidWhen = new Any(
             new Predicate[]
             {
                 new NonEmpty(emailAddress.CaseFieldIdentifier),
                 new NonEmpty(telephone.CaseFieldIdentifier)
             }
         ),
         InvalidFields = new [] {emailAddress.CaseFieldIdentifier, telephone.CaseFieldIdentifier }
     } 

Sharing our rules

Unfortunately we ran into some challenges with serialising and deserialising our form definitions. We only realised this late in the Dev Camp and didn’t resolve them completely.

The first issue related to the serialisation of subclasses. This is important for us, because we need to know, for example, which subclass of Predicate is defining our ValidationRule. By default Json.NET doesn’t allow you to specify in the json which subclass to deserialise to. You can change this, by setting the TypeNameHandling setting. However there are security issues to take into account.

The other issue was related to this. Our server and client side were targeting different .NET frameworks. For Blazor, we were using the preview .NET Standard. For our server side, due to other components we wanted to use, we were using .NET Core. We found when we ran these projects, the fully qualified type of primitives (e.g. strings) was different between the two. This meant that our naïve serialisation couldn’t be deserialised in the other project.

It was frustrating to only realise this at a late stage, and this prevented us joining us frontend and backend together. However it was not all bad news, we were able to test the two separately, e.g. using non-serialised form definitions to test the rendering of forms in the browser. It was exciting to see the validation rules and invisibility rules giving real time feedback as data was entered in our forms. We were running C# validation in the browser. Although we didn’t quite get there, running that same validation server-side was within arm’s reach and we’ll get it running at our next opportunity.