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.
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.
Table of Contents
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.
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 é 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:
1 2 3 4 5 6 | <div class = "col-md-4 mb-3" > <label>User:</label> <TextBox Id= "tbxUser" CssClass= "teste" @bind-Text= "@user" ></TextBox> </div> |
TextBox Number:
1 2 3 4 5 6 | <div class = "col-md-4 mb-3" > <label>Password MaxLength:</label> <TextBox Id= "tbxMax" @bind-Text= "@max" TextMode= "TextBoxMode.Number" ></TextBox> </div> |
TextBox Password:
1 2 3 4 5 6 7 | <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):
1 2 3 4 5 6 7 8 | <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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | <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:
1 2 3 4 5 6 7 8 | <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.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | 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:
1 | <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.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | @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.
1 2 3 4 5 6 7 8 9 | 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:
1 2 | [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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 | @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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | <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:
1 2 | [Parameter] public RenderFragment ChildContent { get ; set ; } |
E também é ncessário chamar as seguintes tags na View do DropDownList:
1 2 3 | <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.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 | 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.
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 | 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:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | 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. |