Blazor + MVVM = Silverlight反击,因为远古邪恶无敌

哈Ha!

因此,是的,net core 3.0即将推出,并且将有一个项目模板,其中Blazor是默认模板之一。 在我看来,该框架的名称类似于某些Pokemon的名称。 开拓者开始战斗! 我决定看一下它是哪种动物,以及它被吃什么,所以我在上面制作了一个待办事项表。 好吧,也可以在Vue.js上与主题进行比较,因为在我看来,它们在组件和反应性方面都类似于组件系统,仅此而已。 更多女神神灵! 实际上,这是一个指南,面向那些懒于学习TypeScript或JavaScript并且想要在网站上进行按钮和输入的懒惰的年轻人。 就像那个模因一样-“这位技术人员想写一本书,但指示却出来了。” 谁对我在前端的冒险感兴趣,或者发现欢迎您使用哪种Blazor。

引言


微软曾经有过在浏览器中使用C#的想法,并将其称为Silverlight。 它没有起飞。 然后,您的这些tyrnet与实际的浏览器有所不同。 我为什么认为它现在正在起飞? 因为现在默认情况下,Web程序集在所有现代浏览器中都可用。 无需安装单独的扩展。 另一个问题是应用程序大小。 如果Vue.js SPA的重量为1.7兆字节,那么Blazor的21兆字节完全相同。 现在,Internet变得比Silverlight时代更快,更可靠,您需要下载一次应用程序,然后就可以拥有缓存和所有内容。 通常,Blazor看起来与Vue.js非常相似。 因此,为了向Silverligtht,WPF和UWP致敬,并且因为在Sharper中如此普遍,我决定在项目中使用MVVM模式。 因此,仅供参考-我通常是后端,我喜欢Blazor。 我警告我-例子中的设计和布局非常糟糕,在使用Vue.js的项目中,经验丰富的前端作家可以看到很多govnokod。 好吧,有了拼写和标点符号,事情也就这样。

参考文献


Vue + Vuex上的Todo示例
Blazor上的Todo示例

展示位置模型


  1. 在客户端。 可以以多种方式分发的标准SPA。 在我的示例中,我使用了一个模板,在其中将应用程序文件发送到asp.net核心上的浏览器服务器。 这种方法的缺点是需要下载到浏览器的那21兆字节。
  2. 在服务器端。 一切都在服务器上发生,完成的DOM通过套接字传递给客户端。 浏览器一开始根本不需要下载任何东西,而是不断地分段下载更新的DOM。 好吧,客户端代码的全部负载突然落到了服务器上。

我个人更喜欢第一种选择,并且可以在不需要担心用户转换的所有情况下使用。 例如,这是公司的某种内部信息系统或专用的B2B解决方案,因为Blazor首次下载已很长时间。 如果您的用户不断登录到您的应用程序,那么他们将不会注意到与JS版本的任何区别。 如果用户单击广告链接,则只需查看那里的网站类型,很可能他不会等待很长时间就可以加载并离开网站。 在这种情况下,最好使用第二个放置选项,即 服务器端西装外套

项目创建


下载Net Core 3.0 dotnet.microsoft.com/download/dotnet-core/3.0
在终端中运行命令,该命令将为您加载必要的模板。

dotnet new -i Microsoft.AspNetCore.Blazor.Templates 

创建服务器端

 dotnet new blazorserverside -o MyWebApp 

对于客户端,其文件将由asp.net核心服务器分发

 dotnet new blazorhosted -o MyWebApp 

如果您想了解异国情调并突然决定不将asp.net core用作服务器,则可以使用其他命令(是否完全需要它?),使用此命令只能创建一个没有服务器的客户端。

 dotnet new blazor -o MyWebApp 

绑定


支持单向和双向绑定。 因此,是的,您不需要像WPF中的任何OnPropertichanged。 更改视图模型时,布局会自动更改。

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

因此,这里有一个具有Text字段的ViewModel(匿名)。

在第一个输入中,通过“ value = @ Text”,我们进行了单向绑定。 现在,当我们在代码中更改文本时,输入中的文本将立即更改。 只有这样我们才能在输入中不进行打印,这才以任何方式影响我们的VM。 在第二个输入中,通过“ @ bind = @ Text”,我们进行了双向绑定。 现在,如果我们在输入中编写新内容,我们的VM将立即更改,反之亦然,即 如果我们更改代码中的“文本”字段,那么我们的输入将立即显示新值。 有一个BUT-默认情况下,更改与输入的onchange事件相关,因此VM仅在完成输入后才会更改。 在第三个输入“ @bind:event =” oninput“”中,我们现在每次将某些字符打印到新值时,都会将用于将VM数据传输到oninput的事件更改为oninput。 例如,您还可以指定DateTime的格式。

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

查看模型


您可以将其设为匿名,然后需要在“ @code {}”块中将其停止

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

或者您可以将其放在单独的文件中。 然后必须从ComponentBase继承它,并在页面顶部使用“ @inherits”指定指向我们的VM的链接

举个例子

TodoViewModel.cs:

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

Todo.razor:

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

路由选择


在页面的开头使用“ @page”指示页面将响应的路由。 而且,可能有几个。 将按照从上到下的顺序选择完全匹配的第一个。 例如:

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

此页面将以“ / todo”或“ todo / delete”打开

版面


通常,通常在几页上放置相同的内容。 像侧边栏,等等。

为了首先使用布局,您需要创建它。 必须使用“ @inherits”从LayotComponentBase继承。 举个例子

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

其次,它需要导入。 为此,在包含使用它的页面的目录中,您需要创建_imports.razor文件,然后在该文件中添加“ @layout”行

 @layout MainLayout @using System 

第三,您可以在页面上指示其直接使用哪种布局

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

通常,_imports.razor及其在其中使用会作用于与它位于同一文件夹中的所有页面。

路线选项


首先,在我们的路线中,使用大括号表示参数及其类型(不区分大小写)。 支持标准类型。 所以是的,没有可选参数,即 值必须始终传递。

可以通过在ViewModel中创建一个与参数同名的属性并使用[Parameter] BTB属性来获取值本身-之前已经遇到过-父组件中的数据和事件也可以使用[Parameter]属性以及级联参数从父组件中传输。 它们从父组件传递到其所有子组件及其子组件。 它们主要用于样式,但是最好只使用CSS样式,所以为什么不在乎。

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

DI


与常规asp.net核心应用程序一样,所有内容都在Startup.cs中注册。 这里没有新内容。 但是,我们的VM依赖关系的实现仍然通过公共属性而不是通过构造函数进行。 该属性只需用[Inject]属性装饰

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

默认情况下,已经连接了3个服务。 HttpClient-好吧,你知道为什么。 IJSRuntime-从C#调用JS代码。 IUriHelper-使用它无法重定向到其他页面。

应用实例


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. 由于此组件,我们不需要“ @page”和“ @layout”,因为它不会参与路由,并且会使用父组件的布局
  2. C#代码以@符号开头。 其实和剃刀一样
  3.  @onclick=@(()=>ClickRow(item.Id)) 
    将行单击事件绑定到我们的ViewModel的ClickRow方法
  4. 使用[Parameter]属性指定哪些参数将从父组件或页面转移到我们的组件或页面
  5. 我们调用从父组件收到的回调函数。 因此,父组件得知孩子中发生了一些事件。 函数只能包装在EventCallback <>参数化的EventArgs中传递。 可在此处找到EventArgs的可能列表-docs.microsoft.com/ru-ru/aspnet/core/blazor/components?view=aspnetcore-3.0#event-handling
  6. 由于EventArgs可能类型的列表是有限的,并且我们需要将附加的Id属性传递给父组件一侧的事件处理程序,因此我们创建了从基类继承的自己的参数类,并将其传递给事件。 是的,在父组件中,常规UIMouseEventArgs将进入事件处理程序的函数,并且需要将其转换为我们的类型,例如,使用as运算符

用法示例:

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

待办事项删除页面


我们的ViewModel aka VM是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. 路由参数“ / todo / delete / {id:guid}”在此处传递给Guid,例如,如果传递到localhost / todo / delete / ae434aae44 ...
  2. 将服务从DI容器注入到我们的VM中。
  3. 只是我们VM的一个属性。 我们可以根据需要自行设置其值。
  4. 初始化页面后,将自动调用此方法。 在这里,我们为VM的属性设置必要的值
  5. 我们的VM的方法。 例如,我们可以将其绑定到单击视图的任何按钮的事件上
  6. 转到位于“ / todo”地址的另一页,即 她在行首有“ @page” /“ todo”
    我们的视图是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. 我们指示该国家/地区的地址为{我们网站的根地址} +“ / todo / delete /” + {某种Guid}。 例如localhost / todo / delete / ae434aae44 ...
    2. 指定我们的页面将在MainLayout.razor中呈现
    3. 指定我们的页面将使用DeleteTodoViewModel类的属性和方法
    4. 我们缩小范围,当您单击此按钮时,将调用我们虚拟机的Delete()方法

    待办事项首页


    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. 支持System.ComponentModel.DataAnnotations中的标准验证属性。 具体来说,在这里我们指示此字段是必填字段,并且如果用户未在输入中指定与该字段关联的值,则将显示该文本。
    2. 使用参数处理事件的方法。 此方法将处理子组件中的事件。
    3. 我们将参数转换为我们在子组件中创建的类型

    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. 我们调用子组件并将其VM的属性和方法作为参数传递给它。
    2. 带有数据验证功能的内置表单组件。 我们在其中指出,作为模型,他将使用我们的VM,并在发送有效数据时将调用其Create()方法
    3. 将使用[Requared]等模型属性执行验证。
    4. 在这里,我将显示验证的一般错误
    5. 将创建带有验证的输入。 可能的标签列表为InputText,InputTextArea,InputSelect,InputNumber,InputCheckbox,InputDate
    6. 公共字符串属性NewTodo {get; set;}的验证错误将在此处显示
    7. 当您单击此按钮时,将引发我们表单的OnValidSubmit事件

    Startup.cs文件


    在这里我们注册我们的服务

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

    结语


    写这篇文章是为了胃口,并鼓励进一步研究Blazor。 我希望我实现了我的目标。 好吧,为了更好地研究它,我建议阅读Microsoft官方手册

    致谢


    感谢AndreyNikolinwin32nipuhSemenPV在文本中发现的拼写和语法错误。

Source: https://habr.com/ru/post/zh-CN463197/


All Articles