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