This content originally appeared on Telerik Blogs and was authored by Jon Hilton
If you need to run async code in response to use input, .NET 7 has your back with its new bind modifiers.
Have you ever needed to run some asynchronous code in response to user input in your Blazor application? If so, you probably realized this was no trivial task prior to .NET 7.
Here’s an example. Say you want users to enter their bio, then sync the entered value to local storage (in the browser).
You could of course store the value anywhere, including making an API call to a backend to store it in a database, but we’ll stick with local storage for now, as a handy way to explore how this all works.
First we need a text input of some sort.
@page "/BindModifiers"
<textarea @bind="bio" placeholder="Introduce yourself"></textarea>
<p>
@bio
</p>
@code {
string bio;
}
I’ve defined a textarea
input and bound it to a field called bio
.
With this, any changes to the textarea
value will be reflected in bio
and if bio
is changed directly (via code, in response to another event) the textarea
will automatically reflect the new value.
By default, this binding will run when the textarea
loses focus so, if we want the binding to take effect immediately, we can modify the bind event as follows:
<textarea @bind="bio" @bind:event="oninput" placeholder="Introduce yourself"></textarea>
@bind:event="oninput"
here ensures the bio
field is updated with the new value as soon as it changes.
Running Async Code After Binding (Pre .NET 7)
Now what about that requirement to store the entered value in local storage?
We want to ensure our two-way binding (to store the value in the bio
field) continues to work, and also store the entered value in local storage.
This was a non-trivial task with Blazor prior to .NET 7.
The problem, in trying to perform async tasks like this as part of the binding process, is that you can end up inadvertently slowing down the UI.
Any attempt to write to local storage during binding will cause issues (especially if we were to switch from local storage to a database or API call), and potentially result in UI updates being held back until the slower, async process completes.
It’s easy to imagine a user being frustrated if every keypress results in a visible delay before the value appears in the textarea
.
The better option would be to perform the async task after binding completes. That way the UI remains responsive, but you still have a way to trigger logic every time the binding process has occurred (in this case, every time the value changes).
Prior to .NET 7 this was tricky because of the way two-way binding works.
You see, under the hood, Blazor is using the oninput
event to handle its binding.
Although you define your Blazor components using Razor, .NET doesn’t actually run these components directly. Instead it converts them to pure C# code.
If we look at that generated code for our Blazor component (as it currently stands) we see something like this:
protected override void BuildRenderTree(RenderTreeBuilder __builder)
{
// some code omitted for brevity
__builder.OpenElement(0, "textarea");
__builder.AddAttribute(2, "value", BindConverter.FormatValue(this.bio));
__builder.AddAttribute<ChangeEventArgs>(3, "oninput", EventCallback.Factory.CreateBinder((object) this, (Action<string>) (__value => this.bio = __value), this.bio));
__builder.CloseElement();
}
Notice how Blazor has attached a handler to the oninput
event which sets this.bio
to the entered value.
The challenge, if we want to also perform actions for that same oninput
event, is that we can’t attach another handler to that same event.
The workaround in .NET 6 (and below) was to handle oninput
yourself, You could then write your own code to update the bio field, and run your async code.
But even then, you had to ensure your async code ran without blocking the UI in the meantime.
Happily, .NET 7 makes this much, much easier.
Running Async Code After Binding Completes (.NET 7 and Above)
.NET 7 addresses this requirement with a new bind:after
bind modifier.
With this, you can keep your existing bindings and attach a handler to run logic after the binding completes.
<textarea @bind="bio" @bind:event="oninput" @bind:after="Sync" placeholder="Introduce yourself"></textarea>
<p>
@bio
</p>
@code {
string bio;
private async Task Sync()
{
// write to local storage here!
Console.WriteLine("Syncing to local storage");
}
}
Notice how we’re using @bind:after
to run additional logic, in this case pointing it to our Sync
method.
With this, our UI will remain responsive, and the code to sync the value to local storage will be invoked as well.
Here’s the generated code which actually executes when we run this in the browser (note you don’t need to know or fully understand this to use @bind:after
but it can be useful to know what’s going on!)
namespace BlazorExamples.WASM.Pages
{
[Route("/BindModifiers")]
public class BindModifiers : ComponentBase
{
private
#nullable disable
string bio;
protected override void BuildRenderTree(RenderTreeBuilder __builder)
{
__builder.OpenElement(0, "textarea");
__builder.AddAttribute(1, "placeholder", "Introduce yourself");
__builder.AddAttribute(2, "value", BindConverter.FormatValue(this.bio));
__builder.AddAttribute<ChangeEventArgs>(3, "oninput", EventCallback.Factory.CreateBinder((object) this, RuntimeHelpers.CreateInferredBindSetter<string>((Func<string, Task>) (__value =>
{
this.bio = __value;
return RuntimeHelpers.InvokeAsynchronousDelegate(new Func<Task>(this.Sync));
}), this.bio), this.bio));
__builder.SetUpdatesAttributeName("value");
__builder.CloseElement();
// code omitted for brevity
}
private async Task Sync() => Console.WriteLine("Syncing to local storage");
}
}
Blazor is still handling oninput
but now it’s performing two tasks: updating the bio
field and invoking our Sync
method.
@bind:after
is super handy for this kind of requirement.
It provides a safe way to run async code after binding completes while still letting Blazor handle the binding itself (reading and updating the value) so we can’t accidentally break that functionality.
Blazor assigns the new value to bio
before our logic in Sync
is invoked, thereby ensuring existing safety mechanisms remain in place (for example, Blazor Server implements logic to ensure keystrokes aren’t accidentally
lost).
Store Values in Local Storage
For completeness, to actually sync with local storage we can use the handy Blazored Local Storage library.
@page "/BindModifiers"
@using Blazored.LocalStorage
@inject ILocalStorageService localStorage
<textarea @bind="bio" @bind:event="oninput" @bind:after="Sync" placeholder="Introduce yourself"></textarea>
<p>
@bio
</p>
@code {
string bio;
private async Task Sync()
{
await localStorage.SetItemAsStringAsync("bio", bio);
}
}
What If We Want to Modify the Entered Value?
Now this is all well and good if you just want to take the value and do something with it, but what if you need to also modify that value when binding takes place?
For example, let’s say for some reason we need to make sure the user doesn’t enter any email addresses in the bio and we want to block the @
symbol.
In this (admittedly contrived!) example, we’d want to add a step to the binding process that removes any @
symbols, maybe replacing them with the word “at” instead.
First let’s try a naïve approach whereby we use that Sync
method to change the value of bio
.
@code {
string bio;
private async Task Sync()
{
bio = bio.Replace("@", "at");
await localStorage.SetItemAsStringAsync("bio", bio);
}
}
This assumes that bio
has already been updated with the new value, reads that value and updates it with a “sanitized” version (with “@” symbols replaced with the word “at”).
With this in place, if our users try to enter an “@” in the textarea
, it will be replaced with “at”.
However, there is one problem with this approach. If we look at generated code for our component we can see how we’re essentially updating the value of bio
twice.
protected override void BuildRenderTree(RenderTreeBuilder __builder)
{
// code omitted for brevity
__builder.OpenElement(0, "textarea");
__builder.AddAttribute(2, "value", BindConverter.FormatValue(this.bio));
__builder.AddAttribute<ChangeEventArgs>(3, "oninput", EventCallback.Factory.CreateBinder((object) this, RuntimeHelpers.CreateInferredBindSetter<string>((Func<string, Task>) (__value =>
{
this.bio = __value;
return RuntimeHelpers.InvokeAsynchronousDelegate(new Func<Task>(this.Sync));
}), this.bio), this.bio));
__builder.CloseElement();
}
private async Task Sync()
{
this.bio = this.bio.Replace("@", "at");
await this.localStorage.SetItemAsStringAsync("bio", this.bio);
}
First the event handler for oninput
sets this.bio = __value
.
__builder.AddAttribute<ChangeEventArgs>(3, "oninput", EventCallback.Factory.CreateBinder((object) this, RuntimeHelpers.CreateInferredBindSetter<string>((Func<string, Task>) (__value =>
{
this.bio = __value;
return RuntimeHelpers.InvokeAsynchronousDelegate(new Func<Task>(this.Sync));
}), this.bio), this.bio));
Then, in the Sync
method, we have code to read that value, modify it and reassign it to the bio
field:
this.bio = this.bio.Replace("@", "at");
Finally we read the value of this.bio
again so we can store it in local storage.
await this.localStorage.SetItemAsStringAsync("bio", this.bio);
In practice this code works, but it feels a little redundant to read and change the value of our bio
field multiple times this way.
Take Control with bind:set and bind:get
If you find yourself wanting to control the binding process, perhaps to modify the bound value as described above, you may need to drop @bind:after
and use a couple of the other new modifiers introduced in .NET 7.
Where @bind:after
is a handy and simple way to run code after binding completes, @bind:get
and @bind:set
enable closer control over the binding process itself, while still making it possible to run asynchronous code
as part of that process.
With @bind:set
and @bind:get
we can specify both the field where our bound value should be stored, and the method which should handle the updating of that field when the user enters a different value.
Here’s our example modified to use @bind:set
and @bind:get
.
<input @bind:get="bio" @bind:set="OnInput" @bind:event="oninput" placeholder="Introduce yourself"></input>
@code {
string bio;
private async Task OnInput(string value)
{
var newValue = value.Replace("@", "at");
bio = newValue;
await localStorage.SetItemAsStringAsync("bio", newValue);
}
}
This will work very similarly to our @bind:after
example but if we look at the generated code, we can see the difference:
protected override void BuildRenderTree(RenderTreeBuilder __builder)
{
// code omitted for brevity
__builder.OpenElement(0, "input");
__builder.AddAttribute(1, "placeholder", "Introduce yourself");
__builder.AddAttribute(2, "value", BindConverter.FormatValue(this.bio));
__builder.AddAttribute<ChangeEventArgs>(3, "oninput", EventCallback.Factory.CreateBinder((object) this, RuntimeHelpers.CreateInferredBindSetter<string>(new Func<string, Task>(this.OnInput), this.bio), this.bio));
__builder.SetUpdatesAttributeName("value");
__builder.CloseElement();
__builder.AddMarkupContent(4, "\r\n");
}
private async Task OnInput(string value)
{
string newValue = value.Replace("@", "at");
this.bio = newValue;
await this.localStorage.SetItemAsStringAsync("bio", newValue);
newValue = (string) null;
}
Gone is the double assignment of this.bio
.
In this version, the text area’s oninput
event is handled via our OnInput
method which synchronously assigns a new value to the bio
field before running the async code to update local storage.
The order is important here. If we attempt to assign a value to bio
after running our async code we will run into issues:
private async Task OnInput(string value)
{
// running this async code first will cause issues
await localStorage.SetItemAsStringAsync("bio", value);
// this needs to happen first
var newValue = value.Replace("@", "at");
bio = newValue;
}
The UI will behave in unpredictable ways, with old values potentially being shown while the async code is executed (especially if it takes a little time to complete).
By using the explicit get and set modifiers, we’re responsible for assigning a new value to bio
, whereas with @bind:after
that was handled for us automatically.
For this reason, if you can, it’s generally safer (and easier) to use bind:after
which does that assignment for you automatically (and at the right time).
But if you want more control over the binding process, you can switch to the more specific @bind:set
and @bind:get
modifiers instead.
In Summary
Performing asynchronous tasks after binding is much easier with .NET 7. You can use the new @bind:after
modifier to point to a handler which will be invoked after binding completes.
This way you can keep your UI responsive and still perform asynchronous tasks at the same time.
If you need to take more control over the binding process, perhaps to modify the incoming value, you can drop to the lower level @bind:get
and @bind:set
modifiers instead.
This content originally appeared on Telerik Blogs and was authored by Jon Hilton
Jon Hilton | Sciencx (2023-01-17T09:05:01+00:00) Simplify Your Blazor Applications Using .NET 7’s New Bind Modifiers. Retrieved from https://www.scien.cx/2023/01/17/simplify-your-blazor-applications-using-net-7s-new-bind-modifiers/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.