Blazor + MVVM = Silverlight strikes back because ancient evil is invincible

Hello, Habr!



So yes, net core 3.0 is coming soon and there will be a project template with Blazor as one of the default ones. The name of the framework, in my opinion, is similar to the name of some Pokemon. Blazor enters the battle! I decided to look at what kind of animal it is and what it is eaten with, so I made a Todo sheet on it. Well, on Vue.js too, for comparison with the subject because in my opinion they are similar to the component system in both and reactivity, and that’s all. More goddesses god gods! In fact, this is a Guide for young, not strong minds who are too lazy to learn TypeScript or JavaScript and want to make buttons and inputs on the site. As in that meme - "The techie wanted to write a book but the instruction turned out." Who is interested in my adventures in the front end or find out what kind of Blazor you are welcome to cat.



Introduction



Microsoft once had the idea of ​​working C # in a browser and called this idea Silverlight. It didn’t take off. These your tyrnets were then different as actually browsers. Why do I think it’s taking off now? Because now web assemblies are in all modern browsers by default. There is no need to install a separate extension. Another issue is application sizes. If Vue.js SPA weighs 1.7 megabytes, then exactly the same on Blazor 21 megabytes. Now the Internet has become faster and more reliable than during Silverlight and you need to download the application once, and then there’s the cache and all the work. In general, Blazor seemed very similar to Vue.js. And so, as a tribute to Silverligtht, WPF and UWP, and just because it was so common among sharpers, I decided to use the MVVM pattern for my project. So for reference - I generally backend and I liked Blazor. I warn the faint of heart - The design and layout in my examples are terrible, and in the project with Vue.js an experienced front-end can see a lot of govnokod. Well, with spelling and punctuation, things are also so-so.



References



Todo example on Vue + Vuex

Todo example on Blazor



Placement Models



  1. On the client side. A standard SPA that can be distributed in a variety of ways. In my example, I used a template in which the application files are sent to the browser server on asp.net core. The disadvantage of this approach is in those 21 megabytes that you need to download to the browser.
  2. On the server side. Everything happens on the server, and the finished DOM is passed to the client through sockets. The browser doesn’t need to download anything at all at the beginning, but instead constantly download the updated DOM in pieces. Well, the entire load on the client code suddenly falls onto the server.


I personally like the first option more and can be used in all those cases when you do not need to worry about user conversions. For example, this is some kind of internal information system of the company or a specialized B2B solution because Blazor has been downloading for a long time for the first time. If your users constantly log into your application, then they will not notice any difference with the JS version. If a user clicks on an advertising link, just look at what kind of site is there, most likely he will not wait long for the site to load and just leave. In this case, it is better to use the second placement option i.e. Server Side Blazor



Project creation



Download net core 3.0 dotnet.microsoft.com/download/dotnet-core/3.0

Run the command in the terminal which will load the necessary templates for you.



dotnet new -i Microsoft.AspNetCore.Blazor.Templates
      
      





To create a server side



 dotnet new blazorserverside -o MyWebApp
      
      





For Client Side whose files will be distributed by asp.net core server



 dotnet new blazorhosted -o MyWebApp
      
      





If you wanted exoticism and suddenly decided not to use asp.net core as a server, but something else (Do you need it at all?) You can create only a client without a server with this command.



 dotnet new blazor -o MyWebApp
      
      





Bindings



Supports one-way and two-way binding. So yes, you do not need any OnPropertichanged as in WPF. When changing the View Model, the layout changes automatically.



 <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"); } }
      
      





And so, here we have a ViewModel (anonymous) that has a Text field.



In the first input, through “value = @ Text” we made one-way binding. Now when we change the Text in the code, the text inside the input will immediately change. Only so that we do not print in our input does this in any way affect our VM. In the second input, through "@ bind = @ Text" we made two-way binding. Now, if we write something new in our input, our VM will immediately change, and the opposite is also true i.e. if we change the Text field in the code, then our input will immediately display the new value. There is one BUT - by default, the changes are tied to the onchange event of our input, so the VM will change only when we finish the input. In the third input "@bind: event =" oninput "" we changed the event for transferring VM data to oninput now every time we print some character a new value is immediately transferred to our VM. Also for DateTime you can specify a format, for example like this.



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





View model



You can make it anonymous then you need to stop it inside the block "@code {}"



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





or you can put it in a separate file. Then it must be inherited from ComponentBase and at the top of the page specify a link to our VM using "@inherits"



for example



TodoViewModel.cs:



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





Todo.razor:



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





Routing



The routes to which the page will react are indicated at the beginning of the page using "@page". Moreover, there may be several. The first one will be selected exactly matching in order from top to bottom. For example:



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





This page will open at "/ todo" or "todo / delete"



Layouts



In general, things that are common for several pages are usually placed here. Like a sidebar, and more.



In order to use the layout in the first place, you need to create it. It must be inherited from LayotComponentBase using "@inherits". for example



 @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>
      
      





Secondly, it needs to be imported. To do this, in the directory with the pages that will use it, you need to create the _imports.razor file and then add the line "@layout" to this file



 @layout MainLayout @using System
      
      





Thirdly, you can indicate at the page which layout it uses directly



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





In general, _imports.razor and using in it act on all pages that are in the same folder with it.



Route Options



First, indicate the parameter and its type in curly brackets in our route (case insensitive). Standard types are supported. So yes, there are no optional parameters i.e. value must always be passed.



The value itself can be obtained by creating in our ViewModel a property with the same name as the parameter and with the [Parameter] BTB attribute - running into before - the data and events in the parent components are also transmitted from the parent components using the [Parameter] attribute as well as cascading parameters. They are passed from the parent component to all its child components and their child components. They are used mainly for styles, but it’s better to just do styles in CSS, so why not care.



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





DI



Everything is registered in Startup.cs, as in a regular asp.net core application. Nothing new here. But the implementation of dependencies for our VM still occurs through public properties and not through the constructor. The property just needs to be decorated with the [Inject] attribute



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





By default, there are 3 services already connected. HttpClient - Well, you know why. IJSRuntime - Call JS code from C #. IUriHelper - using it is not possible to redirect to other pages.



Application example



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. Since this component we don’t need "@page" and "@layout" because it will not participate in routing and it will use the layout from the parent component
  2. The C # code begins with the @ symbol. Actually the same as in Razor
  3.  @onclick=@(()=>ClickRow(item.Id))
          
          



    Binds a row click event to the ClickRow method of our ViewModel
  4. Specify which parameters will be transferred from the parent component or page to ours using the [Parameter] attribute
  5. We call the callback function that was received from the parent component. So the parent component finds out that an event has occurred in the child. Functions can only be passed wrapped in EventCallback <> parameterized EventArgs. A possible list of EventArgs can be found here - docs.microsoft.com/ru-ru/aspnet/core/blazor/components?view=aspnetcore-3.0#event-handling
  6. Since the list of possible types of EventArgs is limited and we need to pass an additional Id property to the event handler on the side of the parent component, we create our own parameter class inherited from the base and pass it to the event. So yes - in the parent component, the regular UIMouseEventArgs will fly into the function of the event handler and it will need to be converted to our type, for example, using the as operator


Usage example:



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





Todo Removal Page



Our ViewModel aka VM is 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. The route parameter "/ todo / delete / {id: guid}" is passed to Guid here if we go, for example, to localhost / todo / delete / ae434aae44 ...
  2. Inject services from the DI container into our VM.
  3. Just a property of our VM. We set its value ourselves, whatever we want.
  4. This method is called automatically when the page is initialized. Here we set the necessary values ​​for the properties of our VM
  5. The method of our VM. We can bind it, for example, to the event of clicking any button of our View
  6. Going to another page located at the address "/ todo" ie she has at the beginning the line "@page" / todo ""

    Our View is 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. We indicate that this country will be available at the address {root address of our site} + "/ todo / delete /" + {some kind of Guid}. For example localhost / todo / delete / ae434aae44 ...
    2. Specify that our page will be rendered inside MainLayout.razor
    3. Specify that our page will use the properties and methods of the DeleteTodoViewModel class
    4. We narrow down that when you click on this button, the Delete () method of our VM will be called


    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. The standard validation attributes from System.ComponentModel.DataAnnotations are supported. Specifically, here we indicate that this field is required and the text that will be displayed if the user does not specify a value in the input that will be associated with this field.
    2. Method for handling an event with a parameter. This method will handle the event from the child component.
    3. We give the argument to the type that we created in the child component


    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. We call the child component and pass to it as parameters the properties and methods of our VM.
    2. Built-in form component with data validation. We indicate in it that as a model he will use our VM and when sending valid data he will call its Create () method
    3. Validation will be performed using model attributes like [Requared], etc.
    4. Here I will display the general errors of validation
    5. Will create input with validation. The list of possible tags is InputText, InputTextArea, InputSelect, InputNumber, InputCheckbox, InputDate
    6. Validation errors for the public string property NewTodo {get; set;} will be displayed here
    7. When you click on this button, the OnValidSubmit event of our form will be raised


    Startup.cs file



    Here we register our services



     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"); } }
          
          





    Epilogue



    This article was written to play an appetite and encourage further study of Blazor. I hope that I have achieved my goal. Well, to study it better, I recommend reading the official manual from Microsoft .



    Acknowledgments



    Thanks to AndreyNikolin , win32nipuh , SemenPV for the found spelling and grammatical errors in the text.



All Articles