Blazor: Muito mais que um WebForms de Luxo

Disponível também em inglês

O desafio de converter uma aplicação WebForms para Blazor é tentar resolver todas as questões PostBack do Webforms usando os conceitos de Single Page Application do Blazor.

Blazor: Muito mais que um WebForms de Luxo

A resposta para essas questão é simples: Blazor Components.

Depois que escrevi o artigo Blazor: O Começo do Fim do Javascript?, recebi uma série de críticas dos amantes de javascript (assim como eu).

Como usei a palavra WebForms no título desse artigo, não estarei surpreso se aparecem mais alguns Haters aqui.

A comparação de Blazor com WebForms seria no mínimo irresponsável e com certeza eu não faria isso… em outra vida :D.

Algumas informações importantes para quem está chegando agora. Se você é um desenvolvedor .NET mas nunca viu nada de .NET Core, sugiro você ler antes o artigo .NET Core para DesenvolvedoreS .NET.

Benchmark: ASP.NET 4.8 vs ASP.NET CORE 3.0


O Blazor

Blazor é uma “nova” tecnologia da Microsoft para criação de frontends usando ASP.NET CORE e C# ao invés de javascript.

Existem basicamente dois tipos de aplicações Blazor: Uma que roda no servidor Blazor Server App e utiliza o SignalR para comunicação entre o client/server.

Blazor: Muito mais que um WebForms de Luxo

E o outro tipo de aplicação Blazor é o Blazor Web Assembly, ainda em versão preview.

Nesse caso, a aplicação web roda inteira no navegador, até as DLLs C#.

Blazor: Muito mais que um WebForms de Luxo

Blazor é considerado o SPA (Single Page Application) da Microsoft que poderia bater de frente e substituir Angular, Vue, React e outros frameworks javascript.


Blazor: MVVM – Model View View Model

Os conceitos e design patterns de MVVM são bem antigos por sinal.

Eu utilizo MVVM desde Silverlight, KnockoutJS, AngularJS até chegar nos dias de hoje como Angular, Vue, React e até Xamarin Forms.

Consiste em “escutar” mudanças realizadas na View (campos INPUT por exemplo) e/ou Model (javascript/C# por exemplo).

Se um valor mudar, todas as referências View e/ou Model são atualizadas.

Você que já desenvolveu em Angular ou AngularJS, na imagem acima, imagine aquela variável @currentCount como {{ @currentCount }}.

Exibirá o valor de @currentCount sempre que a variável mudar.


Blazor: POC – Prova de Conceito

Apesar do Blazor usar como base o ASP.NET CORE e Razor Views, é legal fazer POC para conhecer melhor o funcionamento/comportamento de algumas coisas, entre elas a troca de informação entre componentes.

Como era de se esperar, na aplicação WebForms que estou convertendo, possui vários campos TextBox e DropDownList.

Decidi fazer uma prova de conceito criando esses dois componentes no Blazor com o mesmo nome de propriedades/eventos dos respectivos no WebForms.

Assim, criei os seguintes campos e funcionalidades:

– Ao preencher o TextBox Password MaxLength, o campo TextBox Password conterá o máximo possíveis desses caracteres.
– Ao deixar em branco o campo TextBox Token, ele será grifado em vermelho como inválido.
– Ao clicar no botão Select Three irá selecionar o item 3 do DropDownList ItemsList.
– Ao clicar no botão Disable Week Day irá desabilitar o DropDownList Week Days.
– Ao clicar no botão Include Options irá incluir mais uma opção no DropDownList Dynamic Options.
– Toda alteração será refletida nos Labels da parte inferior da tela.

O uso e declaração das tags HTML dos Componentes nas Razor Views ficaram assim:

TextBox String:

    <div class="col-md-4 mb-3">
        <label>User:</label>
        <TextBox Id="tbxUser"
                 CssClass="teste"
                 @bind-Text="@user"></TextBox>
    </div>

TextBox Number:

    <div class="col-md-4 mb-3">
        <label>Password MaxLength:</label>
        <TextBox Id="tbxMax"
                 @bind-Text="@max"
                 TextMode="TextBoxMode.Number"></TextBox>
    </div>

TextBox Password:

    <div class="col-md-4 mb-3">
        <label>Password:</label>
        <TextBox Id="tbxPassword"
                 @bind-Text="@password"
                 MaxLength="@max"
                 TextMode="TextBoxMode.Password"></TextBox>
    </div>

TextBox Multiline (textarea):

    <div class="col-md-12 mb-3">
        <label>Token:</label>
        <TextBox Id="tbxToken"
                 @bind-Text="@token"
                 TextMode="TextBoxMode.MultiLine"
                 Required="true"
                 Rows="5"></TextBox>
    </div>

Os valores dos TextBox serão armazenados nas variáveis C# user, max, password e token respectivamente.

DropDownList com Items declarados:

<div class="row">
    <div class="col-md-4 mb-3">
        <label>Week Days:</label>
        <DropDownList Id="ddlWeekDays"
                      @bind-SelectedValue="@weekDay">
            <DropDownListItem Text="Sunday" Value="1"></DropDownListItem>
            <DropDownListItem Text="Monday"></DropDownListItem>
            <DropDownListItem Text="Tusday" Selected="true"></DropDownListItem>
            <DropDownListItem Text="Wednesday"></DropDownListItem>
            <DropDownListItem Text="Thursday"></DropDownListItem>
            <DropDownListItem Text="Friday"></DropDownListItem>
            <DropDownListItem Text="Saturday"></DropDownListItem>
        </DropDownList>
    </div>
</div>

O valor selecionado no DropDownList será armazenado na variável C# weekDay.

No caso do DropDownList, ainda há possibilidade de ao invés de declarar explicitamente os itens como no exemplo acima, podemos passar os itens ao um DataSource:

<div class="row">
    <div class="col-md-4 mb-3">
        <label>Items List:</label>
        <DropDownList Id="ddlItemsList"
                      DataSource="@items"
                      @bind-SelectedValue="@item"></DropDownList>
    </div>
</div>

No caso acima, o valor selecionado será armazenado na variável C# item.

Os nomes dos componentes e propriedades na declaração nas Views parece muito com o WebForms.


Blazor: Os Componentes – Model

Uma comparação do Blazor/Components do ASP.NET CORE é a questão do codebehind de um determinado Component ou Page.

Não é obrigatoriamente necessário, mas é parecido.

A arquitetura e herança codebehind (aqui chamadas de Model) dos Componentes ficaram assim:

No arquivo TextBox.razor fica tudo o que for HTML das tags INPUT e TEXTAREA.

O TextBox.razor herda da classe C# TextBoxComponent, que possui todas as propriedades e eventos de um TextBox, como MaxLength e Quantidade de Linhas.

    public class TextBoxComponent : ControlComponent
    {
        public TextBoxComponent()
        {
            MaxLength = "500";
        }

        [Parameter]
        public bool Required { get; set; }

        [Parameter]
        public string Text { get; set; }

        [Parameter]
        public string MaxLength { get; set; }

        [Parameter]
        public string Rows { get; set; }

        [Parameter]
        public TextBoxMode TextMode { get; set; }

        [Parameter]
        public EventCallback<string> TextChanged { get; set; }

        [Parameter]
        public EventCallback<string> MaxLengthChanged { get; set; }

        [Parameter]
        public EventCallback<string> TextValueChanged { get; set; }

        protected async Task OnChangeAsync(
            ChangeEventArgs e)
        {
            Text = e.Value as string;
            IsValid = !(Required && string.IsNullOrEmpty(Text));

            await TextChanged.InvokeAsync(Text);
            await TextValueChanged.InvokeAsync(Text);
        }
    }

Aquelas propriedades que possuem atributo [Parameter], significa que será possível usá-los na declaração no HTML, vide exemplo da propriedade CssClass abaixo:

<TextBox Id="tbxUser" CssClass="teste" @bind-Text="@user"></TextBox>

A classe TextBoxComponent herda de uma mais básica ControlComponent, que possui propriedade básicas a qualquer Control (campo de tela) como Id, Disabled, CssClass entre outros.

    public class ControlComponent : ComponentBase
    {
        public ControlComponent()
        {
            IsValid = true;
        }

        [Parameter]
        public string Id { get; set; }

        [Parameter]
        public string CssClass { get; set; }

        [Parameter]
        public bool Disabled { get; set; }

        [Parameter]
        public bool IsValid { get; set; }

        public string ValidCssClass => IsValid ? "" : "is-invalid";

        public string AllCssClass => $"form-control {CssClass ?? ""} {ValidCssClass}";

        public void ToggleDisabled()
        {
            Disabled = !Disabled;
        }
    }

Por fim, ControlComponent herda da classe básica do ASP.NET CORE ComponentBase.


Blazor: Os Componentes – View

A View do Component Textbox.razor ficou assim:

@inherits TextBoxComponent

@if (TextMode == TextBoxMode.MultiLine)
{
    <textarea id="@(Id ?? "tbx1")"
              class="@AllCssClass"
              maxlength="@MaxLength"
              rows="@Rows"
              disabled="@Disabled"
              required="@Required"
              @onchange="OnChangeAsync">@Text</textarea>
}
else
{
    <input type="@TextMode.ToString().ToLower()"
           id="@(Id ?? "tbx1")"
           class="@AllCssClass"
           value="@Text"
           maxlength="@MaxLength"
           disabled="@Disabled"
           required="@Required"
           @onchange="OnChangeAsync" />
}

Os atributos HTML de INPUT/TEXTAREA que usam as propriedades C# AllCssClass, Text, MaxLength, Disabled, Id e Required estão bem claras no código fonte.

A única coisa diferente nesse código que chama atenção é o atributo @onchange definido em ambos campos.

É uma palavra-chave interna para o evento HTML onchange que chama uma função C# OnChangeAsync.

        protected async Task OnChangeAsync(
            ChangeEventArgs e)
        {
            Text = e.Value as string;
            IsValid = !(Required && string.IsNullOrEmpty(Text));

            await TextChanged.InvokeAsync(Text);
            await TextValueChanged.InvokeAsync(Text);
        }

Basicamente a função assíncrona pega o valor do INPUT/TEXTAREA e atribui para a propriedade Text.

Fazendo isso, todas as referências que usam a propriedade Text serão atualizadas, ou seja, se Text estiver sendo usado em um Label por exemplo, após sair do INPUT/TEXTAREA, esse Label será atualizado com o novo valor.

Esse conceito é chamado de Two-Way-Binding, ou seja, o componente tem uma entrada de dados que modifica o componente e o componente modifica sua referência fora dele.

Isso é possível devido a existência do método TextChanged chamado dentro de OnChangeAsync.

Para esse tipo de binding, a regra é: Crie uma propriedade (com atributo Parameter) do tipo EventCallback com o mesmo nome da propriedade que queira fazer o binding (no caso Text) + sufixo Changed:

[Parameter]
public EventCallback<string> TextChanged { get; set; }

E por fim chame esse evento em algum momento na sua Model.


Blazor: DropDownList e DropDownListItem

A diferença do DropDownList para o TextBox é a existência de um DataSource, ou seja uma lista de items/options representados por foreach.

DropDownList.razor:

    @inherits DropDownListComponent

    <select @onchange="OnChangeAsync" 
            id="@Id" 
            class="@AllCssClass"
            disabled="@Disabled">
        @foreach (var data in DataSource)
        {
            var value = data.Value ?? data.Text;
            <option value="@value" selected="@(value == SelectedValue)">@data.Text</option>
        }
    </select>

<CascadingValue Value=this>
    @ChildContent
</CascadingValue>

A Model DropDowListComponent tem a mesma lógica do TextBoxComponent, com uma pequena ressalva.

O DropDownList pode receber outras tags HTML/Componentes em seu Body.

Vamos lembrar:

<div class="row">
    <div class="col-md-4 mb-3">
        <label>Week Days:</label>
        <DropDownList Id="ddlWeekDays"
                      @bind-SelectedValue="@weekDay">
            <DropDownListItem Text="Sunday" Value="1"></DropDownListItem>
            <DropDownListItem Text="Monday"></DropDownListItem>
            <DropDownListItem Text="Tusday" Selected="true"></DropDownListItem>
            <DropDownListItem Text="Wednesday"></DropDownListItem>
            <DropDownListItem Text="Thursday"></DropDownListItem>
            <DropDownListItem Text="Friday"></DropDownListItem>
            <DropDownListItem Text="Saturday"></DropDownListItem>
        </DropDownList>
    </div>
</div>

Veja que estou passando outro componente DropDownListItem (filho) dentro do componente DropDownList (pai).

Isso é possível por essa simples propriedade na Model DropDowListComponent:

[Parameter]
public RenderFragment ChildContent { get; set; }

E também é ncessário chamar as seguintes tags na View do DropDownList:

<CascadingValue Value=this>
    @ChildContent
</CascadingValue>

A lógica é, quando o componente filho DropDownListItem for executado, esse terá uma referência ao pai DropDownList, que por sua vez irá inserir a opção/item correspondente na propriedade DataSource do pai.

    public class DropDownListItemComponent : ComponentBase
    {
        [Parameter]
        public string Text { get; set; }

        [Parameter]
        public string Value { get; set; }

        [Parameter]
        public bool Selected { get; set; }

        [CascadingParameter]
        public DropDownListComponent ParentDropDownList { get; set; }

        protected override void OnInitialized()
        {
            ParentDropDownList?.AddItem(this);

            base.OnInitialized();
        }
    }

Ao usar as tags CascadingValue no DropDownList (pai) e que contenha uma propriedade com atributo [CascadingParameter] no DropDownListItem (filho), podemos acessar o componente pai.

Repare no evento interno OnInitialized do DropDownListItem.

É através do método AddItem que o item/option é inserido no componente pai.

        internal void AddItem(
            DropDownListItemComponent item)
        {
            DataSource.Add(new Models.Item
            {
                Text = item.Text,
                Value = item.Value ?? item.Text
            });

            if (item.Selected)
            {
                SelectedValue = item.Value ?? item.Text;
                SelectedValueChanged.InvokeAsync(SelectedValue).GetAwaiter();
            }
        }

Baixe o código fonte no meu github e veja as customizações dos componentes.


Blazor: Observações

Em uma aplicação Blazor:

O arquivo _Imports.razor funciona como se fosse um arquivo web.config localizado dentro da pasta Views no ASP.NET MVC tradicional.

No arquivo Startup.cs, está a configuração e inicialização da aplicação ASP.NET CORE / Blazor, vide:

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddRazorPages();
            services.AddServerSideBlazor(); // here
            services.AddSingleton<WeatherForecastService>();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
            }

            app.UseStaticFiles();

            app.UseRouting();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapBlazorHub(); // here
                endpoints.MapFallbackToPage("/_Host"); // here
            });
        }

A última configuração, a mais interessante, é aquela que mapeaia client/server para utilização do SignalR como forma de escutar/propagar as alterações entre View e/ou Model.

Quando criar seus Components ou Pages, se você criar a Model (classe C#) com o mesmo nome do arquivo da View, por exemplo DropDownListItem.razor (View) e DropDownListItem.razor.cs (Model), o Visual Studio irá agrupá-los na Solution facilitando bastante a manutenção do código fonte.

As Razor Views e Tag Helpers ainda funcionam pois é uma aplicação comum ASP.NET CORE.

Por fim, para conhecimento geral, a mágica do Blazor só funciona com o arquivo Pages/_Host.cshtml, que chama a App.razor e o arquivo de script blazor.server.js, responsável pelo gerenciamento do SignalR.

O Blazor é muito mais poderoso do que imaginava, e com certeza irá revolucionar o desenvolvimento web, ainda mais após a conclusão da versão Blazor Web Assembly.

E aí? Já usa Blazor em seus projetos? Comenta aí!

Obrigado 🙂

Artigos sobre Blazor e ASP.NET CORE:

Crie seu Framework em ASP.NET CORE 3 e Blazor
Blazor: O Começo do Fim do Javascript?
Benchmark: ASP.NET 4.8 vs ASP.NET CORE 3.0
AppSettings: 6 Formas de Ler o Config no ASP.NET CORE 3.0
JWT: Customizando o Identity no ASP.NET CORE 3.0
Crie um Gerenciador de Arquivos do Zero em .NET Core e VueJS
IIS: Como Hospedar Aplicação .NET Core em 10 Passos
.NET Core para Desenvolvedores .NET

Faça download completo do código fonte no github.
Sobre o Autor:
Trabalha como arquiteto de soluções e desenvolvedor, tem mais de 18 anos de experiência em desenvolvimento de software em diversas plataformas sendo mais de 16 anos somente para o mercado de seguros.
Revisado por:
Apaixonado por tecnologia e sempre disposto a encarar novos desafios, atualmente trabalho focado em aplicações web e mobile com a plataforma .NET, e me aventurando nas diversas linguagens, desafios e experiências que a área nos proporciona.


Paulistano, 21 anos apaixonado por tecnologia, trabalho com desenvolvimento de software atuando com .NET / Xamarin / AngularJS / ASP.NET CORE / MVC.