Blazor + MVVM = Silverlight menyerang balik karena kejahatan kuno tidak terkalahkan

Halo, Habr!

Jadi ya, net core 3.0 akan segera hadir dan akan ada template proyek dengan Blazor sebagai salah satu yang default. Nama kerangka, menurut saya, mirip dengan nama beberapa Pokemon. Blazor memasuki pertempuran! Saya memutuskan untuk melihat jenis binatang apa itu dan apa yang dimakannya, jadi saya membuat selembar kertas Todo. Nah, di Vue.js juga, untuk perbandingan dengan subjek karena menurut pendapat saya mereka mirip dengan sistem komponen dalam keduanya dan reaktivitas, dan itu saja. Lebih banyak dewi dewa dewa! Bahkan, ini adalah Panduan untuk anak muda, bukan yang kuat yang terlalu malas untuk belajar TypeScript atau JavaScript dan ingin membuat tombol dan input di situs. Seperti dalam meme itu - "Teknisi itu ingin menulis buku tetapi instruksi ternyata." Siapa yang tertarik dengan petualangan saya di ujung depan atau cari tahu Blazor seperti apa yang Anda sukai.

Pendahuluan


Microsoft pernah memiliki ide untuk bekerja C # di browser dan menyebut ide ini Silverlight. Itu tidak lepas landas. Ini tyrnets Anda kemudian berbeda sebagai browser yang sebenarnya. Mengapa saya pikir itu lepas landas sekarang? Karena sekarang web assemblies ada di semua browser modern secara default. Tidak perlu menginstal ekstensi terpisah. Masalah lainnya adalah ukuran aplikasi. Jika Vue.js SPA memiliki berat 1,7 megabita, maka persis sama pada Blazor 21 megabita. Sekarang Internet telah menjadi lebih cepat dan lebih dapat diandalkan daripada pada saat Silverlight, dan Anda perlu mengunduh aplikasi satu kali, dan kemudian ada cache dan semuanya. Secara umum, Blazor tampak sangat mirip dengan Vue.js. Jadi, sebagai penghargaan untuk Silverligtht, WPF dan UWP, dan hanya karena itu sangat umum di kalangan penajam, saya memutuskan untuk menggunakan pola MVVM untuk proyek saya. Jadi untuk referensi - Saya umumnya back-end dan saya suka Blazor. Saya memperingatkan orang yang lemah hati - Desain dan tata letak dalam contoh saya sangat buruk, dan dalam proyek dengan Vue.js seorang penulis front-end yang berpengalaman dapat melihat banyak govnokod. Nah, dengan ejaan dan tanda baca, semuanya juga begitu-begitu.

Referensi


Contoh Todo pada Vue + Vuex
Contoh Todo di Blazor

Model Penempatan


  1. Di sisi klien. SPA standar yang dapat didistribusikan dengan berbagai cara. Dalam contoh saya, saya menggunakan templat di mana file aplikasi dikirim ke server browser di asp.net core. Kerugian dari pendekatan ini adalah 21 megabyte yang harus Anda unduh ke browser.
  2. Di sisi server. Segala sesuatu terjadi di server, dan DOM yang sudah selesai diteruskan ke klien melalui soket. Peramban tidak perlu mengunduh apa pun di awal, melainkan mengunduh unduhan DOM yang baru secara terus-menerus. Nah, seluruh beban pada kode klien tiba-tiba jatuh ke server.

Saya pribadi lebih suka opsi pertama dan dapat digunakan dalam semua kasus ketika Anda tidak perlu khawatir tentang konversi pengguna. Misalnya, ini adalah semacam sistem informasi internal perusahaan atau solusi B2B khusus karena Blazor telah mengunduh sejak lama untuk pertama kalinya. Jika pengguna Anda terus-menerus masuk ke aplikasi Anda, maka mereka tidak akan melihat adanya perbedaan dengan versi JS. Jika pengguna mengklik tautan iklan, lihat saja situs apa yang ada di sana, kemungkinan besar ia tidak akan menunggu lama untuk memuat situs dan pergi begitu saja. Dalam hal ini, lebih baik menggunakan opsi penempatan kedua yaitu Server Side Blazor

Pembuatan proyek


Unduh net core 3.0 dotnet.microsoft.com/download/dotnet-core/3.0
Jalankan perintah di terminal yang akan memuat template yang diperlukan untuk Anda.

dotnet new -i Microsoft.AspNetCore.Blazor.Templates 

Untuk membuat sisi server

 dotnet new blazorserverside -o MyWebApp 

Untuk Sisi Klien yang file-nya akan didistribusikan oleh server inti asp.net

 dotnet new blazorhosted -o MyWebApp 

Jika Anda menginginkan eksotisme dan tiba-tiba memutuskan untuk tidak menggunakan inti asp.net sebagai server, tetapi sesuatu yang lain (Apakah Anda memerlukannya?) Anda hanya dapat membuat klien tanpa server dengan perintah ini.

 dotnet new blazor -o MyWebApp 

Binding


Mendukung pengikatan satu arah dan dua arah. Jadi ya, Anda tidak perlu OnPropertichanged seperti pada WPF. Saat mengubah Model Tampilan, tata letak berubah secara otomatis.

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

Jadi, di sini kita memiliki ViewModel (anonim) yang memiliki bidang Teks.

Pada input pertama, melalui β€œvalue = @ Text” kami membuat pengikatan satu arah. Sekarang ketika kita mengubah Teks dalam kode, teks di dalam input akan segera berubah. Hanya agar kami tidak mencetak dalam input kami, apakah hal ini mempengaruhi VM kami. Pada input kedua, melalui "@ bind = @ Text" kami membuat penjilidan dua arah. Sekarang jika kita menulis sesuatu yang baru di input kita, VM kita akan segera berubah, dan yang sebaliknya juga benar yaitu. jika kita mengubah bidang Teks dalam kode, maka input kita akan segera menampilkan nilai baru. Ada satu TETAPI - secara default, perubahan terkait dengan acara pertukaran input kami, sehingga VM hanya akan berubah ketika kami menyelesaikan input. Pada input ketiga "@bind: event =" oninput "" kami mengubah acara untuk mentransfer data VM menjadi oninput sekarang setiap kali kami mencetak beberapa karakter, nilai baru segera ditransfer ke VM kami. Anda juga dapat menentukan format untuk DateTime, misalnya, seperti ini.

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

Lihat model


Anda dapat menjadikannya anonim sehingga Anda harus menghentikannya di dalam blok "@code {}"

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

atau Anda bisa meletakkannya di file terpisah. Maka itu harus diwarisi dari ComponentBase dan di bagian atas halaman tentukan tautan ke VM kami menggunakan "@inherits"

Sebagai contoh

TodoViewModel.cs:

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

Todo.razor:

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

Routing


Rute ke mana halaman akan merespons ditunjukkan di awal halaman menggunakan "@page". Apalagi mungkin ada beberapa. Yang pertama akan dipilih dengan tepat sesuai urutan dari atas ke bawah. Sebagai contoh:

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

Halaman ini akan terbuka di "/ todo" atau "todo / delete"

Tata letak


Secara umum, hal-hal yang sama untuk beberapa halaman biasanya ditempatkan di sini. Seperti bilah sisi, dan banyak lagi.

Untuk menggunakan tata letak di tempat pertama, Anda harus membuatnya. Itu harus diwarisi dari LayotComponentBase menggunakan "@inherits". Sebagai contoh

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

Kedua, perlu diimpor. Untuk melakukan ini, di direktori dengan halaman yang akan menggunakannya, Anda perlu membuat file _imports.razor dan kemudian menambahkan baris "@layout" ke file ini

 @layout MainLayout @using System 

Ketiga, Anda dapat menunjukkan pada halaman mana tata letak yang digunakannya secara langsung

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

Secara umum, _imports.razor dan penggunaannya berfungsi pada semua halaman yang ada di folder yang sama dengannya.

Opsi Rute


Pertama, tentukan parameter dan jenisnya dalam kurung kurawal di rute kami (tidak sensitif huruf). Jenis standar didukung. Jadi ya, tidak ada parameter opsional yaitu nilai harus selalu dilewati.

Nilai itu sendiri dapat diperoleh dengan membuat properti ViewModel kami dengan nama yang sama dengan parameter dan dengan [Parameter] atribut BTB - berjalan sebelum - data dan peristiwa dalam komponen induk juga ditransmisikan dari komponen induk menggunakan atribut [Parameter] serta parameter cascading. Mereka diteruskan dari komponen induk ke semua komponen turunannya dan komponen turunannya. Mereka digunakan terutama untuk gaya, tetapi lebih baik melakukan gaya dalam CSS, jadi mengapa tidak peduli.

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

DI


Semuanya terdaftar di Startup.cs, seperti dalam aplikasi inti asp.net biasa. Tidak ada yang baru di sini. Tetapi implementasi dependensi untuk VM kami masih terjadi melalui properti publik dan bukan melalui konstruktor. Properti hanya perlu didekorasi dengan atribut [Suntikkan]

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

Secara default, ada 3 layanan yang sudah terhubung. HttpClient - Ya, Anda tahu sebabnya. IJSRuntime - Panggil kode JS dari C #. IUriHelper - menggunakannya tidak mungkin untuk mengarahkan ulang ke halaman lain.

Contoh aplikasi


Spreadsheet Todo


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. Karena komponen ini kita tidak perlu "@ halaman" dan "@ buka" karena tidak akan berpartisipasi dalam perutean dan akan menggunakan tata letak dari komponen induk
  2. Kode C # dimulai dengan simbol @. Sebenarnya sama seperti di Razor
  3.  @onclick=@(()=>ClickRow(item.Id)) 
    Mengikat acara klik baris ke metode ClickRow dari ViewModel kami
  4. Tentukan parameter mana yang akan ditransfer dari komponen induk atau halaman ke kita menggunakan atribut [Parameter]
  5. Kami memanggil fungsi panggilan balik yang diterima dari komponen induk. Jadi komponen induk mengetahui bahwa beberapa peristiwa telah terjadi pada anak. Fungsi hanya dapat dilewatkan dengan dibungkus dalam EventCallback <> EventArgs yang diparameterisasi. Daftar EventArgs yang memungkinkan dapat ditemukan di sini - docs.microsoft.com/ru-ru/aspnet/core/blazor/components?view=aspnetcore-3.0#event-handling
  6. Karena daftar kemungkinan jenis EventArgs terbatas dan kami perlu memberikan properti ID tambahan ke pengendali event di sisi komponen induk, kami membuat kelas parameter kami sendiri yang diwarisi dari basis dan meneruskannya ke acara. Jadi ya - dalam komponen induk, UIMouseEventArgs biasa akan terbang ke fungsi event handler dan itu perlu dikonversi ke tipe kami, misalnya menggunakan

Contoh penggunaan:

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

Halaman Penghapusan Todo


ViewModel kami alias VM adalah 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. Parameter rute "/ todo / delete / {id: guid}" diteruskan ke Guid di sini jika kita pergi, misalnya, ke localhost / todo / delete / ae434aae44 ...
  2. Suntikkan layanan dari wadah DI ke dalam VM kami.
  3. Hanya milik VM kami. Kami menetapkan nilainya sendiri, seperti yang kami inginkan.
  4. Metode ini dipanggil secara otomatis ketika halaman diinisialisasi. Di sini kita menetapkan nilai yang diperlukan untuk properti VM kami
  5. Metode VM kami. Kami dapat mengikatnya, misalnya, ke acara mengklik tombol apa pun dari Tampilan kami
  6. Pergi ke halaman lain yang terletak di alamat "/ todo" yaitu dia memiliki di awal baris "@ halaman" / todo ""
    Pandangan kami adalah 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. Kami mengindikasikan bahwa negara ini akan tersedia di alamat {alamat root situs kami} + "/ todo / delete /" + {semacam Guid}. Misalnya localhost / todo / delete / ae434aae44 ...
    2. Tentukan bahwa halaman kami akan dirender di dalam MainLayout.razor
    3. Tentukan bahwa halaman kami akan menggunakan properti dan metode kelas DeleteTodoViewModel
    4. Kami mempersempit bahwa ketika Anda mengklik tombol ini, metode Delete () dari VM kami akan dipanggil

    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. Atribut validasi standar dari System.ComponentModel.DataAnnotations didukung. Secara khusus, di sini kami menunjukkan bahwa bidang ini diperlukan dan teks yang akan ditampilkan jika pengguna tidak menentukan nilai dalam input yang akan dikaitkan dengan bidang ini.
    2. Metode untuk menangani suatu peristiwa dengan parameter. Metode ini akan menangani acara dari komponen anak.
    3. Kami melemparkan argumen ke tipe yang kami buat di komponen anak

    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. Kami memanggil komponen turunan dan mengirimkannya properti dan metode VM kami sebagai parameter.
    2. Komponen formulir bawaan dengan validasi data. Kami menunjukkan di dalamnya bahwa sebagai model ia akan menggunakan VM kami dan saat mengirim data yang valid ia akan memanggil metode Create ()
    3. Validasi akan dilakukan menggunakan atribut model seperti [Dibutuhkan kembali], dll.
    4. Di sini saya akan menampilkan kesalahan umum validasi
    5. Akan membuat input dengan validasi. Daftar kemungkinan tag adalah InputText, InputTextArea, InputSelect, InputNumber, InputCheckbox, InputDate
    6. Kesalahan validasi untuk properti string publik NewTodo {get; set;} akan ditampilkan di sini
    7. Ketika Anda mengklik tombol ini, acara OnValidSubmit dari formulir kami akan dimunculkan

    File startup.cs


    Di sini kami mendaftarkan layanan kami

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

    Epilog


    Artikel ini ditulis untuk membangkitkan selera dan mendorong studi lebih lanjut tentang Blazor. Saya berharap bahwa saya telah mencapai tujuan saya. Nah, untuk mempelajarinya lebih baik, saya sarankan membaca manual resmi dari Microsoft .

    Ucapan Terima Kasih


    Terima kasih kepada AndreyNikolin , win32nipuh , SemenPV untuk kesalahan pengejaan dan tata bahasa yang ditemukan dalam teks.

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


All Articles