首页 > 解决方案 > 剃刀页面内的 Blazor 页面和组件代码?(Blazor 图案)

问题描述

在 razor 页面中处理 Blazor 时是否有一些特定的模式来编写代码?我看到的所有关于 Blazor 的示例都将 HTML/Razor/CSS 和 C# 代码放在同一个文件中,例如:

<div class="text">@sometext</div>

<style> .text{ color:black; } </style>

@code{
   sometext = "text.."
}

是否有一些模式可以将 C# 和 HTML 分开,就像在 MVC 中一样?我将大部分逻辑从@code{}页面的单独服务类中移出。

我很想听听不同的方法,最佳实践是什么?

标签: c#.net-coreblazor

解决方案


实际问题提出了两个不同的问题,需要长的答案。

有几种模式可以使用,例如 MVVM、MVU。Blazor WebAssembly 大致是 React#,因此使用 React 中使用的相同组件逻辑有很大帮助。

两个框架都构建 SPA,因此关注点相同,因此解决方案也相似。就像所有的 SPA 框架一样,MVC 在大约十年前被 MVVM 取代,例如 Backbone。多个组件之间的状态管理作为一个单独的问题出现,因此 SPA 具有不同的模式和机制来管理状态(想想 React 的 Redux 和替代方案)。

甚至存储也很重要。ASP.NET Core Blazor 状态管理在多个地方描述存储:

  • 服务器数据库
  • 查询参数
  • 浏览器本地存储
  • 内存缓存

可以使用多种模式 - 类似 React 的组件,其主状态保存在“父”级别的 ViewModel 中,无论它在哪里。

在 Blazor 的数据绑定中,如 React,数据和状态通过组件Parameter属性从父级向下流动。对数据的更改通过EventCallbacks 向上流动。这使得 MVVM 和 MVU 等模式的实现变得容易。

组件级设计

在 React(和 Blazor)中,数据和状态从父级向下流动。数据的变化向上流动。Blazor 的数据绑定旨在促进这一点,尽管它不限制你:

  • 实际状态由父母持有,无论父母想要什么形式。它可能是多个属性、模型/DTO 类或跟随的 ViewModel。数据通过组件Parameter属性传递给嵌套组件。
  • 例如响应事件的变化通过EventCallbacks 发出信号。嵌套属性本身并不存储更改的数据。

在从文档复制的此示例中,ChildBind组件永远不会更改Year属性本身。它告诉其父级更改的年份,并将其留给父级(和数据绑定)告诉它要重绘什么:

Shared/ChildBind.razor

<div class="card bg-light mt-3" style="width:18rem ">
    <div class="card-body">
        <h3 class="card-title">ChildBind Component</h3>
        <p class="card-text">
            Child <code>Year</code>: @Year
        </p>
        <button @onclick="UpdateYearFromChild">Update Year from Child</button>
    </div>
</div>

@code {
    private Random r = new();

    [Parameter]
    public int Year { get; set; }

    [Parameter]
    public EventCallback<int> YearChanged { get; set; }

    private async Task UpdateYearFromChild()
    {
        await YearChanged.InvokeAsync(r.Next(1950, 2021));
    }
}

可以通过一些工作或使用级联值和参数来绑定超过 2 个组件

应用程序架构 - 概述

在 React 和 Blazor 中设计复杂 SPA 的明显方法是组合组件。这两个框架都是按照这种方式构建的。不太明显的是如何在这些组件之间移动数据/状态和事件,谁将存储数据,谁将更改它,更改最终如何修改 UI 等等。

而且由于我们谈论 SPA,即使是“位置”也没有明确的答案,状态保存在查询参数、浏览器存储或远程服务中。

选择架构取决于需要解决的问题。没有best practice。模式的定义是

在上下文中解决问题

这意味着在一种情况下是好的解决方案在另一种情况下不会是好的解决方案。

应用架构 0 - 组件组成

这几乎是使用 Blazor 的“默认”方式。使用包含组件的多个页面。数据下降,变化上升,试图横盘整理基本上是不可能的,因此开发人员必须顺其自然。

如果页面或组件变得太大,很容易将其提取到具有自己Parameter的 s 的单独组件中。

在某些情况下,组件组合就足够了,尤其是当不需要管理很多复杂的状态或者组件之间没有那么多依赖项时。这些组件可以在不增加复杂性的情况下调用自己的服务器端 API。不同的人可以在一定程度上轻松地独立处理自己的组件。

许多快速项目在变得足够大以需要更复杂的架构之前都是这样开始的。当这种情况发生时,使用组件可以更容易地采用另一种架构。

但是,在例如 ERP 或 CRM 应用程序中,即使是单个客户也需要管理大量复杂的数据。同一页面可能会以摘要或详细视图的形式显示来自多个系统的多个选项卡。单个更改可能会影响同一页面上多个组件的显示。

不要介意这样的应用程序通常需要组合来自不同应用程序的 UI 元素,并且仍然作为单个应用程序运行。15 年前,这些被称为Composite Applicationsor Mashups。现在人们谈论Micro-Frontends

应用架构1-MVVM

这种模式在 SPA 和 WPF 中被大量使用。在这个设计ViewModel中,每个都使用一个单独View的,实际状态存储在一个Model. Model可能是隐藏数据库或服务调用。实现自己的View/ViewModels 的多个组件可以使用相同的 s Model,例如在每个页面上以不同的方式显示当前用户数据。

很多关于 MVVM 的文章。我从Blazor For State Management – A Complete Guide 中的 MVVM Pattern 复制了图像。

在此处输入图像描述

View 和 ViewModel 之间的通信使用 Blazor 数据绑定。类ViewModel本身需要实现INotifyPropertyChanged,就像在 WPF 中一样,因此对其属性的更改可以触发视图更新。

应用架构 - MVU

这种模式起源于 Elm 语言,因此也被称为Elm Architecture. 它越来越受欢迎,.NET MAUI 可以在 MVU 的一个变体上运行。本文介绍了它在 F# 中的工作原理。

高级图像显示了它的主要特征 - 流程只是单向的。该Update部件生成用于更新Model. 听起来很像Redux,不是吗?

在此处输入图像描述

下图来自Comet,这是 .NET MAUI 的一种 MVU 模式,它更清楚地说明了这些概念如何映射到 Blazor 概念(如数据绑定):

在此处输入图像描述

在这种情况下,组件Parameters 提供单向绑定,但EventCallbacks 需要生成更新Model而不是通知父组件。

Redux中,Commands 和Updates 都是顶层概念,预先定义,类似于消息,因此应用程序状态和组件可以相互独立地更改。

将代码与标记分离

Blazor 组件概述页面中的部分类支持部分显示您可以将 C# 代码放在代码隐藏文件中,该文件包含名称与您的组件相同的部分类。在编译期间,来自所有分部类的代码被合并到一个单独的类中并编译为一个类。

如果您有一个名为的组件,请从文档中借用Pages/Counter.razor

page "/counter"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}

您可以将其拆分为一个.razor和一个.cs文件:

CounterPartialClass.razor

@page "/counter-partial-class"

<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

CounterPartialClass.razor.cs

namespace BlazorSample.Pages
{
    public partial class CounterPartialClass
    {
        private int currentCount = 0;

        void IncrementCount()
        {
            currentCount++;
        }
    }
}

推荐阅读