Blazor + MVVM = Silverlight ataca porque o mal antigo é invencível

Olá Habr!

Então, sim, o net core 3.0 estará disponível em breve e haverá um modelo de projeto com o Blazor como um dos padrões. O nome da estrutura, na minha opinião, é semelhante ao nome de alguns Pokémon. Blazor entra na batalha! Eu decidi olhar para que tipo de animal é e com o que é comido, então fiz uma folha de Todo nela. Bem, no Vue.js também, para comparação com o assunto, porque, na minha opinião, eles são semelhantes ao sistema de componentes em ambos e reatividade, e isso é tudo. Mais deusas deuses deuses! Na verdade, este é um guia para mentes jovens, não fortes, com preguiça de aprender TypeScript ou JavaScript e que desejam criar botões e entradas no site. Como naquele meme - "O técnico queria escrever um livro, mas a instrução acabou." Quem está interessado nas minhas aventuras no front-end ou descobre que tipo de Blazor você é bem-vindo ao gato.

1. Introdução


A Microsoft teve a idéia de trabalhar com C # em um navegador e chamou essa idéia de Silverlight. Não decolou. Esses tyrnets eram diferentes dos navegadores. Por que acho que está decolando agora? Porque agora os assemblies da Web estão em todos os navegadores modernos por padrão. Não há necessidade de instalar uma extensão separada. Outra questão é o tamanho dos aplicativos. Se o Vue.js SPA pesar 1,7 megabytes, exatamente o mesmo no Blazor 21 megabytes. Agora, a Internet se tornou mais rápida e confiável do que durante o Silverlight e você precisa fazer o download do aplicativo uma vez e depois há o cache e todo o trabalho. Em geral, Blazor parecia muito semelhante ao Vue.js. E assim, como uma homenagem a Silverligtht, WPF e UWP, e apenas por ser tão comum entre os afiadores, decidi usar o padrão MVVM para o meu projeto. Então, para referência - geralmente sou um back-end e gostei do Blazor. Eu aviso os fracos de coração - O design e o layout nos meus exemplos são terríveis e, no projeto com o Vue.js, um front-end experiente pode ver muito govnokod. Bem, com ortografia e pontuação, as coisas também são mais ou menos.

Referências


Todo exemplo no Vue + Vuex
Todo exemplo no Blazor

Modelos de posicionamento


  1. No lado do cliente. Um SPA padrão que pode ser distribuído de várias maneiras. No meu exemplo, usei um modelo no qual os arquivos do aplicativo são enviados para o servidor do navegador no núcleo do asp.net. A desvantagem dessa abordagem está nos 21 megabytes que você precisa baixar para o navegador.
  2. No lado do servidor. Tudo acontece no servidor e o DOM finalizado é passado para o cliente através de soquetes. O navegador não precisa baixar nada no começo, mas, em vez disso, baixe constantemente o DOM atualizado em partes. Bem, toda a carga no código do cliente cai repentinamente no servidor.

Pessoalmente, gosto mais da primeira opção e pode ser usado em todos os casos em que você não precisa se preocupar com as conversões do usuário. Por exemplo, esse é algum tipo de sistema interno de informações da empresa ou uma solução B2B especializada, porque o Blazor faz o download há muito tempo pela primeira vez. Se seus usuários fizerem login constantemente em seu aplicativo, eles não perceberão nenhuma diferença com a versão JS. Se um usuário clicar em um link de publicidade, basta olhar para que tipo de site existe, provavelmente ele não vai esperar muito tempo para carregar o site e simplesmente sair. Nesse caso, é melhor usar a segunda opção de canal, ou seja, Blazor do lado do servidor

Criação de projeto


Faça o download do net core 3.0 dotnet.microsoft.com/download/dotnet-core/3.0
Execute o comando no terminal que carregará os modelos necessários para você.

dotnet new -i Microsoft.AspNetCore.Blazor.Templates 

Para criar um lado do servidor

 dotnet new blazorserverside -o MyWebApp 

Para o lado do cliente cujos arquivos serão distribuídos pelo servidor principal do asp.net

 dotnet new blazorhosted -o MyWebApp 

Se você queria exotismo e de repente decidiu não usar o núcleo do asp.net como servidor, mas outra coisa (você precisa disso?) Você pode criar apenas um cliente sem um servidor com este comando.

 dotnet new blazor -o MyWebApp 

Ligações


Suporta ligação unidirecional e bidirecional. Portanto, sim, você não precisa de nenhum OnPropertichanged como no WPF. Ao alterar o modelo de exibição, o layout muda automaticamente.

 <label>One way binding:</label> <br /> <input type="text" value=@Text /> <br /> <label>Two way binding:</label> <br /> <input type="text" @bind=@Text /> <br /> <label>Two way binding         Text   oninput:</label> <br /> <input type="text" @bind=@Text @bind:event="oninput" /> //ViewModel @code{ string Text; async Task InpuValueChanged() { Console.WriteLine("Input value changed"); } } 

E assim, aqui temos um ViewModel (anônimo) que possui um campo de texto.

Na primeira entrada, por meio de "value = @ Text", fizemos a ligação unidirecional. Agora, quando alterarmos o texto no código, o texto dentro da entrada mudará imediatamente. Somente para que não imprimamos em nossa entrada isso afeta de alguma forma nossa VM. Na segunda entrada, através de "@ bind = @ Text", fizemos a ligação bidirecional. Agora, se escrevermos algo novo em nossa entrada, nossa VM mudará imediatamente, e o contrário também será verdadeiro, ou seja. se alterarmos o campo Texto no código, nossa entrada exibirá imediatamente o novo valor. Há um MAS - por padrão, as alterações estão vinculadas ao evento onchange de nossa entrada, portanto a VM mudará apenas quando concluirmos a entrada. Na terceira entrada "@bind: event =" oninput "", alteramos o evento para transferir dados da VM para oninput agora, toda vez que imprimimos algum caractere, um novo valor é imediatamente transferido para a nossa VM. Você também pode especificar um formato para DateTime, por exemplo, como este.

 <input @bind=@Today @bind:format="yyyy-MM-dd" /> 

Ver modelo


Você pode torná-lo anônimo e precisará interrompê-lo dentro do bloco "@code {}"

 @page "/todo" <p>  @UserName </p> @code{ public string UserName{get; set;} } 

ou você pode colocá-lo em um arquivo separado. Em seguida, ele deve ser herdado do ComponentBase e, na parte superior da página, especificar um link para nossa VM usando "@inherits"

Por exemplo

TodoViewModel.cs:

 public class TodoViewModel: ComponentBase{ public string UserName{get; set;} } 

Todo.razor:

 @page "/todo" @inherits MyWebApp.ViewModels.TodoViewModel <p>  @UserName </p> 

Encaminhamento


As rotas às quais a página responderá são indicadas no início da página usando "@page". Além disso, pode haver vários. O primeiro será selecionado exatamente de acordo com a ordem, de cima para baixo. Por exemplo:

 @page "/todo" @page "/todo/delete" <h1> Hello!</h1> 

Esta página será aberta em "/ todo" ou "todo / delete"

Layouts


Em geral, as coisas que são iguais para várias páginas geralmente são colocadas aqui. Como uma barra lateral e muito mais.

Para usar o layout em primeiro lugar, você precisa criá-lo. Ele deve ser herdado de LayotComponentBase usando "@inherits". Por exemplo

 @inherits LayoutComponentBase <div class="sidebar"> <NavMenu /> </div> <div class="main"> <div class="top-row px-4"> <a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a> </div> <div class="content px-4"> @Body </div> </div> 

Em segundo lugar, ele precisa ser importado. Para fazer isso, no diretório com as páginas que o usarão, você precisa criar o arquivo _imports.razor e, em seguida, adicionar a linha "@layout" a este arquivo

 @layout MainLayout @using System 

Em terceiro lugar, você pode indicar na página qual layout ele usa diretamente

 @layout MainLayout @page "/todo" @inherits BlazorApp.Client.Presentation.TodoViewModel <h3>Todo</h3> 

Em geral, _imports.razor e o uso nele atuam em todas as páginas que estão na mesma pasta.

Opções de Rota


Primeiramente, indique o parâmetro e seu tipo entre colchetes em nossa rota (sem distinção entre maiúsculas e minúsculas). Tipos padrão são suportados. Então, sim, não há parâmetros opcionais, ou seja, O valor sempre deve ser passado.

O valor em si pode ser obtido criando em nosso ViewModel uma propriedade com o mesmo nome que o parâmetro e com o atributo BTV [Parameter] - executando antes - os dados e eventos nos componentes pai também são transmitidos usando o atributo [Parameter] como parâmetros em cascata. Eles são transmitidos do componente pai para todos os seus componentes filhos e seus componentes filhos. Eles são usados ​​principalmente para estilos, mas é melhor usar apenas estilos em CSS. Por que não se importar?

 @page "/todo/delete/{id:guid}" <h1> Hello!</h1> @code{ [Parameter] public Guid Id { get; set; } } 

DI


Tudo está registrado no Startup.cs, como em um aplicativo principal do asp.net comum. Nada de novo aqui. Mas a implementação de dependências para nossa VM ainda ocorre através de propriedades públicas e não através do construtor. A propriedade só precisa ser decorada com o atributo [Inject]

  public class DeleteTodoViewModel : ComponentBase { [Parameter] private Guid Id { get; set; } [Inject] public ICommandDispatcher CommandDispatcher { get; set; } 

Por padrão, existem 3 serviços já conectados. HttpClient - Bem, você sabe o porquê. IJSRuntime - Chame o código JS de C #. IUriHelper - não é possível redirecionar para outras páginas.

Exemplo de aplicação


Todo Spreadsheet


TodoTableComponent.razor:

 //1) <table class="table table-hover"> <thead> <th> </th> <th></th> <th> </th> <th></th> </thead> <tbody> //2) @foreach (var item in Items) { //3) <tr @onclick=@(()=>ClickRow(item.Id)) class="@(item.Id == Current?"table-primary":null)"> <td><input type="checkbox" checked="@item.IsComplite" disabled="disabled" /></td> <td>@item.Name</td> <td>@item.Created.ToString("dd.MM.yyyy HH:mm:ss")</td> <td><a href="/todo/delete/@item.Id" class="btn btn-danger"></a></td> </tr> } </tbody> </table> @code { //4) [Parameter] private List<BlazorApp.Client.Presentation.TodoDto> Items { get; set; } [Parameter] private EventCallback<UIMouseEventArgs> OnClick { get; set; } [Parameter] private Guid Current { get; set; } private async Task ClickRow(Guid id) { //5 await OnClick.InvokeAsync(CreateArgs(id)); } private ClickTodoEventArgs CreateArgs(Guid id) { return new ClickTodoEventArgs { Id = id }; } //6) public class ClickTodoEventArgs : UIMouseEventArgs { public Guid Id { get; set; } } } 

  1. Como esse componente, não precisamos de "@page" e "@layout" porque ele não participará do roteamento e usará o layout do componente pai
  2. O código C # começa com o símbolo @. Na verdade, o mesmo que no Razor
  3.  @onclick=@(()=>ClickRow(item.Id)) 
    Vincula um evento de clique de linha ao método ClickRow do nosso ViewModel
  4. Especifique quais parâmetros serão transferidos do componente pai ou da página para a nossa usando o atributo [Parameter]
  5. Chamamos a função de retorno de chamada recebida do componente pai. Portanto, o componente pai descobre que algum evento ocorreu na criança. As funções só podem ser passadas agrupadas em EventCallback <> EventArgs com parâmetros. Uma possível lista de EventArgs pode ser encontrada aqui - docs.microsoft.com/ru-ru/aspnet/core/blazor/components?view=aspnetcore-3.0#event-handling
  6. Como a lista de possíveis tipos de EventArgs é limitada e precisamos passar uma propriedade de ID adicional para o manipulador de eventos no lado do componente pai, criamos nossa própria classe de parâmetros herdada da base e a passamos para o evento. Então, sim - no componente pai, o UIMouseEventArgs regular passará para a função do manipulador de eventos e precisará ser convertido para o nosso tipo, por exemplo, usando o operador as

Exemplo de uso:

 <TodoTableComponent Items=@Items OnClick=@Select Current=@(Selected?.Id??Guid.Empty)></TodoTableComponent> 

Página de remoção de Todo


Nosso ViewModel, também conhecido como VM, é o DeleteTodoViewModel.cs:

 public class DeleteTodoViewModel : ComponentBase { //1) [Parameter] private Guid Id { get; set; } //2) [Inject] public ICommandDispatcher CommandDispatcher { get; set; } [Inject] public IQueryDispatcher QueryDispatcher { get; set; } [Inject] public IUriHelper UriHelper { get; set; } //3) public TodoDto Todo { get; set; } protected override async Task OnInitAsync() { var todo = await QueryDispatcher.Execute<GetById,TodoItem>(new GetById(Id)); if (todo != null) Todo = new TodoDto { Id = todo.Id, IsComplite = todo.IsComplite, Name = todo.Name, Created = todo.Created }; await base.OnInitAsync(); } //4) public async Task Delete() { if (Todo != null) await CommandDispatcher.Execute(new Remove(Todo.Id)); Todo = null; //5) UriHelper.NavigateTo("/todo"); } } 

  1. O parâmetro de rota "/ todo / delete / {id: guid}" é passado para Guid aqui se formos, por exemplo, para localhost / todo / delete / ae434aae44 ...
  2. Injete serviços do contêiner DI em nossa VM.
  3. Apenas uma propriedade da nossa VM. Nós mesmos estabelecemos seu valor, como queremos.
  4. Este método é chamado automaticamente quando a página é inicializada. Aqui, definimos os valores necessários para as propriedades da nossa VM
  5. O método da nossa VM. Podemos vinculá-lo, por exemplo, ao evento de clicar em qualquer botão da nossa Visualização
  6. Indo para outra página localizada no endereço "/ todo", ou seja, ela tem no início a linha "@page" / todo ""
    Nossa visão é DeleteTodo.razor:

     //1) @page "/todo/delete/{id:guid}" @using BlazorApp.Client.TodoModule.Presentation @using BlazorApp.Client.Shared; //2) @layout MainLayout //3) @inherits DeleteTodoViewModel <h3> Todo </h3> @if (Todo != null) { <div class="row"> <div class="col"> <input type="checkbox" checked=@Todo.IsComplite disabled="disabled" /> <br /> <label>@Todo.Name</label> <br /> //4) <button class="btn btn-danger" onclick=@Delete></button> </div> </div> } else { <p><em> Todo  </em></p> } 

    1. Indicamos que este país estará disponível no endereço {endereço raiz do nosso site} + "/ todo / delete /" + {algum tipo de guia}. Por exemplo localhost / todo / delete / ae434aae44 ...
    2. Especifique que nossa página será renderizada dentro de MainLayout.razor
    3. Especifique que nossa página utilizará as propriedades e métodos da classe DeleteTodoViewModel
    4. Refinamos que, quando você clica nesse botão, o método Delete () da nossa VM será chamado

    Todo Home


    TodoViewModel.cs:

      public class TodoViewModel : ComponentBase { [Inject] public ICommandDispatcher CommandDispatcher { get; set; } [Inject] public IQueryDispatcher QueryDispatcher { get; set; } //1) [Required(ErrorMessage = "  Todo")] public string NewTodo { get; set; } public List<TodoDto> Items { get; set; } public TodoDto Selected { get; set; } protected override async Task OnInitAsync() { await LoadTodos(); await base.OnInitAsync(); } public async Task Create() { await CommandDispatcher.Execute(new Add(NewTodo)); await LoadTodos(); NewTodo = string.Empty; } //2) public async Task Select(UIMouseEventArgs args) { //3) var e = args as TodoTableComponent.ClickTodoEventArgs; if (e == null) return; var todo = await QueryDispatcher.Execute<GetById, TodoItem>(new GetById(e.Id)); if (todo == null) { Selected = null; return; } Selected = new TodoDto { Id = todo.Id, IsComplite = todo.IsComplite, Name = todo.Name, Created = todo.Created }; } public void CanselEdit() { Selected = null; } public async Task Update() { await CommandDispatcher.Execute(new Update(Selected.Id, Selected.Name, Selected.IsComplite)); Selected = null; await LoadTodos(); } private async Task LoadTodos() { var todos = await QueryDispatcher.Execute<GetAll, List<TodoItem>>(new GetAll()); Items = todos.Select(t => new TodoDto { Id = t.Id, IsComplite = t.IsComplite, Name = t.Name, Created = t.Created }) .ToList(); } } 

    1. Os atributos de validação padrão de System.ComponentModel.DataAnnotations são suportados. Especificamente, aqui indicamos que esse campo é obrigatório e o texto que será exibido se o usuário não especificar um valor na entrada que será associada a esse campo.
    2. Método para manipular um evento com um parâmetro Este método manipulará o evento do componente filho.
    3. Lançamos o argumento para o tipo que criamos no componente filho

    Todo.razor:

     @layout MainLayout @page "/todo" @inherits BlazorApp.Client.Presentation.TodoViewModel <h3>Todo</h3> <h4></h4> <div class="row"> <div class="col"> @if (Items == null) { <p><em>...</em></p> } else if (Items.Count == 0) { <p><em>   .    .</em></p> } else { //1) <TodoTableComponent Items=@Items OnClick=@Select Current=@(Selected?.Id??Guid.Empty)></TodoTableComponent> } </div> </div> <br /> <h4> Todo</h4> <div class="row"> <div class="col"> @if (Items != null) { //2) <EditForm name="addForm" Model=@this OnValidSubmit=@Create> //3) <DataAnnotationsValidator /> //4) <ValidationSummary /> <div class="form-group"> //5) <InputText @bind-Value=@NewTodo /> //6) <ValidationMessage For="@(() => this. NewTodo)" /> //7) <button type="submit" class="btn btn-primary"></button> </div> </EditForm> } </div> </div> <br /> <h4> Todo</h4> <div class="row"> <div class="col"> @if (Items != null) { @if (Selected != null) { <EditForm name="editForm" Model=@Selected OnValidSubmit=@Update> <DataAnnotationsValidator /> <ValidationSummary /> <div class="form-group"> <InputCheckbox @bind-Value=@Selected.IsComplite /> <InputText @bind-Value=@Selected.Name /> <button type="submit" class="btn btn-primary"></button> <button type="reset" class="btn btn-warning" @onclick=@CanselEdit></button> </div> </EditForm> } else { <p><em>     </em></p> } } </div> </div> 

    1. Chamamos o componente filho e passamos as propriedades e métodos da nossa VM como parâmetros.
    2. Componente de formulário interno com validação de dados. Indicamos que, como modelo, ele usará nossa VM e, ao enviar dados válidos, chamará o método Create ()
    3. A validação será realizada usando atributos de modelo como [Requared], etc.
    4. Aqui vou mostrar os erros gerais de validação
    5. Criará entrada com validação. A lista de tags possíveis é InputText, InputTextArea, InputSelect, InputNumber, InputCheckbox, InputDate
    6. Erros de validação para a propriedade pública da sequência NewTodo {get; set;} serão exibidos aqui
    7. Quando você clica nesse botão, o evento OnValidSubmit do nosso formulário é gerado

    Arquivo Startup.cs


    Aqui registramos nossos serviços

     public class Startup { public void ConfigureServices(IServiceCollection services) { // LocalStorage  SessionStorage       //    //     Nuget  Blazor.Extensions.Storage services.AddStorage(); services.AddSingleton<ITodoRepository, TodoRepository>(); services.AddSingleton<ICommandDispatcher, CommandDispatcher>(); services.AddSingleton<IQueryDispatcher, QueryDispatcher>(); services.AddSingleton<IQueryHandler<GetAll, List<TodoItem>>, GetAllHandler>(); services.AddSingleton<IQueryHandler<GetById, TodoItem>, GetByIdHandler>(); services.AddSingleton<ICommandHandler<Add>, AddHandler>(); services.AddSingleton<ICommandHandler<Remove>, RemoveHandler>(); services.AddSingleton<ICommandHandler<Update>, UpdateHandler>(); } public void Configure(IComponentsApplicationBuilder app) { //       App.razor //        <app></app> app.AddComponent<App>("app"); } } 

    Epílogo


    Este artigo foi escrito para estimular o apetite e incentivar um estudo mais aprofundado de Blazor. Espero ter atingido meu objetivo. Bem, para estudar melhor, recomendo a leitura do manual oficial da Microsoft .

    Agradecimentos


    Agradecimentos a AndreyNikolin , win32nipuh , SemenPV pelos erros ortográficos e gramaticais encontrados no texto.

Source: https://habr.com/ru/post/pt463197/


All Articles