Telerik blogs

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 oninputevent 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:afteris 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.


Jon Hilton
About the Author

Jon Hilton

Jon spends his days building applications using Microsoft technologies (plus, whisper it quietly, a little bit of JavaScript) and his spare time helping developers level up their skills and knowledge via his blog, courses and books. He's especially passionate about enabling developers to build better web applications by mastering the tools available to them. Follow him on Twitter here.

Related Posts

Comments

Comments are disabled in preview mode.