Out of the box, Blazor gives us some great components to get building forms quickly and easily. The EditForm
component allows us to manage forms, coordinating validation and submission events. There's also a range of built-in input components which we can take advantage of:
InputText
InputTextArea
InputSelect
InputNumber
InputCheckbox
InputDate
And of course, we wouldn't get very far without being able to validate form input, and Blazor has us covered there as well. By default, Blazor uses the data annotations method for validating forms, which if you've had any experience developing ASP.NET MVC or Razor Page applications, will be quite familiar.
Out of the many things I love about Blazor, one is the ability to customise things which don't quite suit your tastes or needs - Forms is no exception. I've previously blogged about how you can swap out the default data annotations validation for FluentValidation. In this post, I'm going to show you how you can create your own input components using InputBase
as a starting point.
Some issues when building real-world apps
The Blazor team have provided us with some great components to use out of the box, which cover many scenarios. But when building real-world applications, we start to hit little problems and limitations.
Verbose code
Most applications, especially line of business applications, require quite a few forms. These often have a set style and layout throughout the application. When using the built-in input components, this means things can get verbose and repetitive quite quickly.
<EditForm Model="NewPerson" OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<div class="form-group">
<label for="firstname">First Name</label>
<InputText @bind-Value="NewPerson.FirstName" class="form-control" id="firstname" />
<ValidationMessage For="NewPerson.FirstName" />
</div>
<div class="form-group">
<label for="lastname">Last Name</label>
<InputText @bind-Value="NewPerson.LastName" class="form-control" id="lastname" />
<ValidationMessage For="NewPerson.LastName" />
</div>
<div class="form-group">
<label for="occupation">Occupation</label>
<InputText @bind-Value="NewPerson.Occupation" class="form-control" id="occupation" />
<ValidationMessage For="NewPerson.Occupation" />
</div>
<button type="submit">Save</button>
</EditForm>
The more significant issue, however, is maintenance. This code is using Bootstrap for layout and styling, but what happens if that changed and we moved to a different CSS framework? Or a new design was developed, and all the styling and layout had to be rebuilt from scratch? We'd have to go everywhere we had a form in the application and update the code. Having experienced this first hand, I can safely say this isn't fun.
Limitations in the standard input components
There are some limitations in the standard components as well. For example, the InputSelect
component can't bind to int
values, or Guid
s, infact it can only bind to string
or enum
types. Which leads to having to write code in getters and setters to convert string values back and forth.
<InputSelect @bind-Value="CatalogueId">
<option>-- Please Select --</option>
<option values="1">General</option>
<option values="2">Plumbing</option>
<option values="3">Electrical</option>
</InputSelect>
@code {
private int _catalogueId;
public string CatalogueId
{
get
{
return _catalogueId.ToString();
}
set
{
_catalogueId = int.Parse(value);
}
}
}
A solution: Building custom input components
The approach my team and I have taken at work is to create custom input components which suit our applications needs. By doing this, we've greatly reduce the amount of code we write, while also making updates to styling and functionality much quicker and simpler.
All our form components can have an optional label, input control and validation message. If we didn't use our custom components, the code would look like this.
<!-- Control with label -->
<div class="form-control-wrapper">
<label class="form-control-label" for="catalogue">Catalogue</label>
<InputText class="form-control" id="catalogue" @bind-Value="Form.Catalogue" />
<div class="form-control-validation">
<ValidationMessage For="@(() => Form.Catalogue)" />
</div>
</div>
<!-- Control without label -->
<div class="form-control-wrapper">
<InputText class="form-control" id="client" @bind-Value="Form.Client" />
<div class="form-control-validation">
<ValidationMessage For="@(() => Form.Client)" />
</div>
</div>
But with our custom components the same functionality is achieved using far less code.
<!-- Control with label -->
<SwInputText Label="Catalogue" @bind-Value="Form.Catalogue" ValidationFor="@(() => Form.Catalogue)" />
<!-- Control without label -->
<SwInputText @bind-Value="Form.Client" ValidationFor="@(() => Form.Client)" />
Now if we want to update the styling of the SwInputText
component, we can do it in one place, and the whole of our app is updated.
How do we do this?
All of the standard input components in Blazor inherit from a single base class called InputBase
. This class handles all of the heavy lifting when it comes to validation by integrating with EditContext
. It also manages the value binding boilerplate by exposing a Value
parameter of type T
. Hence whenever you use one of the build-in form controls you bind to it like this, @bind-Value="myForm.MyValue"
.
Building on InputBase
We didn't want to recreate all the integration with the built-in form component. So we took InputBase
as a starting point and built our own components on top of it. This is what the code looks like for our SwInputText
component.
public class SwInputTextBase : InputBase<string>
{
[Parameter] public string Id { get; set; }
[Parameter] public string Label { get; set; }
[Parameter] public Expression<Func<string>> ValidationFor { get; set; }
protected override bool TryParseValueFromString(string value, out string result, out string validationErrorMessage)
{
result = value;
validationErrorMessage = null;
return true;
}
}
<!--kg-card-end: markdown--><!--kg-card-begin: markdown-->
@inherits SwInputTextBase
<div class="form-control-wrapper">
@if (!string.IsNullOrWhiteSpace(Label))
{
<label class="form-control-label" for="@Id">@Label</label>
}
<input class="form-control @CssClass" id="@Id" @bind="@CurrentValue" />
<div class="form-control-validation">
<ValidationMessage For="@ValidationFor" />
</div>
</div>
The SwInputTextBase
class inherits from InputBase
and the only real work we have to do is provide an implementation for the TryParseValueFromString
method and a few additional parameters.
Because all but one (InputCheckbox) of the built-in input components bind to string
representations of the bound value internally. This method is required to convert the string value back to whatever the original type was. In our case, we're only binding to string
s so it's just a case of setting the result
parameter to equal the value
parameter and we're done.
The majority of the effort has gone into the markup side of the component. This is where we're encapsulating our UI design and the logic for showing a label or not.
Binding to additional types
One of the issues we identified earlier was the inability for InputSelect
to bind to values other than string
s and enum
s. We can solve this limitation by adding logic to the TryParseValueFromString
method. This is the default implementation for the InputSelect
component.
protected override bool TryParseValueFromString(string value, out TValue result, out string validationErrorMessage)
{
if (typeof(TValue) == typeof(string))
{
result = (TValue)(object)value;
validationErrorMessage = null;
return true;
}
else if (typeof(TValue).IsEnum)
{
var success = BindConverter.TryConvertTo<TValue>(value, CultureInfo.CurrentCulture, out var parsedValue);
if (success)
{
result = parsedValue;
validationErrorMessage = null;
return true;
}
else
{
result = default;
validationErrorMessage = $"The {FieldIdentifier.FieldName} field is not valid.";
return false;
}
}
throw new InvalidOperationException($"{GetType()} does not support the type '{typeof(TValue)}'.");
}
Using the same technique as with the SwInputText
component above, we can start with InputBase
and create our own version of the select component and extend the logic above to allow more types to be bound to the component.
protected override bool TryParseValueFromString(string value, out T result, out string validationErrorMessage)
{
if (typeof(T) == typeof(string))
{
result = (T)(object)value;
validationErrorMessage = null;
return true;
}
else if (typeof(T) == typeof(int))
{
int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsedValue);
result = (T)(object)parsedValue;
validationErrorMessage = null;
return true;
}
else if (typeof(T) == typeof(Guid))
{
Guid.TryParse(value, out var parsedValue);
result = (T)(object)parsedValue;
validationErrorMessage = null;
return true;
}
else if (typeof(T).IsEnum)
{
try
{
result = (T)Enum.Parse(typeof(T), value);
validationErrorMessage = null;
return true;
}
catch (ArgumentException)
{
result = default;
validationErrorMessage = $"The {FieldIdentifier.FieldName} field is not valid.";
return false;
}
}
throw new InvalidOperationException($"{GetType()} does not support the type '{typeof(T)}'.");
}
In the above code, we've added some additional code to allow int
s and Guid
s to be bound to the component.
Summary
In this post, we've talked about some of the limitations of Blazor's built-in input components when building real world applications. We talked about the issue of maintance and maintainability as well as some limitations with binding.
We then explored some ways to deal with the issues we identified. We did this by building custom components on top of InputBase
, and replacing the existing markup with our own implementation. This allowed us to encapsulate our UI design in a single place. We then looked at how we can provide our own implementation for the TryParseValueAsString
method, from InputBase
. This allowed us to bind to additional types which the default components cannot.
Top comments (0)