其他分享
首页 > 其他分享> > 第二章-数据绑定

第二章-数据绑定

作者:互联网

快速了解 Razor

Blazor 是 Browser + Razor 的组合(具有很大的艺术自由度)。 因此,要了解 Blazor,我们需要了解浏览器和 Razor 语言。 我假设您了解什么是浏览器,因为互联网已经非常流行了几十年。 但是 Razor(作为一种计算机语言)可能还不是那么清楚。 Razor 是一种标记语法,可让您在模板中嵌入代码。 Razor 可用于动态生成 HTML,但您也可以使用它来生成代码和其他格式。

Razor 出现在 ASP.NET MVC 中。 在 ASP.NET Core MVC 中,razor 在服务器端执行以生成发送到浏览器的 HTML。 但在 Blazor 中,此代码在您的浏览器中执行(使用 Blazor WebAssembly),并将动态更新网页,而无需返回服务器。

还记得我们从上一章的模板生成的 MyFirstBlazor 解决方案吗? 使用 Visual Studio 或 Code 再次打开它,查看 SurveyPrompt.razor,如清单所示。

清单2-1 SurveyPrompt.razor

//SurveyPrompt.razor

<div class="alert alert-secondary mt-4" role="alert">
    <span class="oi oi-pencil mr-2" aria-hidden="true"></span>
    <strong>@Title</strong>
    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold"
           href="https://go.microsoft.com/fwlink/?linkid=2148851">brief survey</a>
    </span>
    and tell us what you think.
</div>
@code {
    // Demonstrates how a parent component can supply parameters
    [Parameter]
    public string Title { get; set; }
}

如您所见,razor 主要由 HTML 标记组成。 但是如果你想拥有一些 C# 属性或方法,你可以将它们嵌入到 Razor 文件的 @code 部分。 这是因为 razor 文件用于生成 .NET 类,并且 @code 中的所有内容都嵌入到该类中。

例如,SurveyPrompt 组件允许您设置 Title 属性,该属性在 Index.razor 中设置,如清单所示。

清单2-2 Index.razor

//Index.razor

<SurveyPrompt Title="How is Blazor working for you?" />

因为可以在另一个组件中设置公共 Title 属性,所以该属性成为一个参数,因此,您需要应用 [Parameter] 属性,如清单 2-1 所示。 然后,SurveyPrompt 可以使用 @ 语法将 Title 属性的内容嵌入到其 HTML 标记中(清单 2-1 中的第三行)。 此语法告诉 razor 切换到 C#,这会将属性作为表达式获取并将其值嵌入到标记中。

单向数据绑定

单向数据绑定是数据从组件流向 DOM 的地方,反之亦然,但仅在一个方向上。从组件到 DOM 的数据绑定是需要显示一些数据(如客户姓名)的地方。 从 DOM 到组件的数据绑定是 DOM 事件发生的地方,例如用户单击按钮,我们希望运行一些代码。

单向数据绑定语法

让我们看一个 razor 中单向数据绑定的示例。 打开我们在第 1 章中构建的解决方案(MyFirstBlazor.sln),然后打开 Counter.razor,在清单 2-3 中重复此处。

清单 2-3 使用 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++;
    }
}

在这个页面上,你会得到一个简单的计数器,你可以通过点击按钮来增加它,如图所示。

image

让我们看看这个页面的工作原理。 currentCount 字段在 Counter.razor@code 部分中定义。 这不是可以从外部设置的字段,因此不需要 [Parameter] 属性,我们可以将其保密。

为了在 razor 中显示计数器的值,我们使用 @currentCount razor 语法,如清单 2-4 所示。

清单 2-4 从组件到 DOM 的数据绑定

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

每次单击该按钮时,Blazor 运行时都会看到 currentCount 可能已更新,它会使用 currentCount 的最新值自动更新 DOM。

属性绑定

打开您可以在 wwwroot/css 文件夹中找到的 app.css 并添加清单 2-5 中的这两个 CSS 类。

清单 2-5 一些简单的样式

.red-background {
    background: red;
    color: white;
}
.yellow-background {
    background: yellow;
    color: black;
}

currentCount 包裹在 <span> 中,如清单 2-6 所示。 每次通过单击按钮更改 currentCount 的值时,都会更改 currentCount 的背景颜色。

清单 2-6 绑定 HTML 属性

@page "/counter"
<h1>Counter</h1>
<p>Current count: <span class="@BackgroundColor">@currentCount</span></p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
    private int currentCount = 0;
    private void IncrementCount()
    {
    currentCount++;
    }
    private string BackgroundColor
    => (currentCount % 2 == 0) ? "red-background" : "yellow-background";
}

条件属性

有时您可以通过向 DOM 元素添加一些属性来控制浏览器。 例如,在清单 2-7 中,要禁用一个按钮,您可以简单地使用 disabled 属性。

清单 2-7 使用 disabled 属性禁用按钮

<button disabled>Disabled Button</button>

使用 Blazor,您可以将属性数据绑定到布尔表达式(例如,字段、属性或 bool 类型的方法),如果表达式的计算结果为 false(或 null),Blazor 将隐藏该属性,并在以下情况下显示该属性 它评估为真。 回到 Counter.razor 并添加清单 2-8 中的代码。

清单 2-8 禁用单击我按钮

<button class="btn btn-primary"
    disabled="@(currentCount > 10)"
    @onclick="IncrementCount">
    Click me
</button>

试试看。 单击按钮直到 currentCount 变为 10 将通过向按钮添加 disabled 属性来禁用按钮。 当 currentCount 低于 10 时,该按钮将再次启用(除非您暂时无法执行此操作)。

事件处理和数据绑定

我们使用清单 2-3 中的 IncrementCount() 方法更新 currentCount。 通过单击“单击我”按钮调用此方法。 这又是一种单向数据绑定,但在另一个方向,从按钮到您的组件。 Blazor 允许您以这种方式对 DOM 事件(如 DOM 的单击事件)做出反应,而不是使用 JavaScript。 您还可以构建自己的具有事件的组件,您可以在其中使用相同的语法对它们做出反应。

事件绑定语法

请看清单 2-9。 现在我们使用@on<event> 语法; 在这种情况下,我们想要绑定到按钮的 click DOM 事件,所以我们在按钮元素上使用 @onclick 属性,并将我们想要调用的方法的名称传递给它。

清单 2-9 从 DOM 到组件的数据绑定

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

点击按钮会触发 DOM 的点击事件,然后会调用 IncrementCount 方法,这会导致 UI 更新为 currentCount 字段的新值。 每当用户与站点交互时,例如,通过单击按钮,Blazor 假定该事件将产生一些副作用,因为调用了一个方法,因此它将使用最新值更新 UI。 简单地调用方法不会导致 Blazor 更新 UI。 我们将在本章后面讨论这个问题。

事件参数

在常规 .NET 中,EventHandler 类型的事件处理程序可以使用 senderEventArgs 参数找到有关事件的更多信息。 在 Blazor 中,事件处理程序不遵循 .NET 中严格的事件模式,但您可以声明事件处理程序方法以获取从 EventArgs 派生的某种类型的参数,例如 MouseEventArgs,如清单 2-10 所示。 在这里,我们使用 MouseEventArgs 实例来查看是否按下了 Ctrl 键,如果是,则减少 currentCount 字段。

清单 2-10 接受参数的 Blazor 事件处理程序

private void IncrementCount(MouseEventArgs e)
{
    if (e.CtrlKey)
    {
        currentCount--;
    }
    else
    {
        currentCount++;
    }
}

使用 C# Lambda 函数

与事件的数据绑定并不总是需要您编写方法。 您还可以通过示例 2-11 中的示例使用 C# lambda 函数语法。

清单 2-11 使用 Lambda 语法的事件数据绑定

<button class="btn btn-primary"
        disabled="@(currentCount > 10)"
        @onclick="@(() => currentCount++)">
    Click me
</button>

如果要使用 lambda 函数来处理事件,则需要将其包裹在圆括号中。

双向数据绑定

双向数据绑定语法

使用双向数据绑定,我们将在组件更改时更新 DOM,但组件也会因为 DOM 中的修改而更新。 最简单的示例是使用 <input> HTML 元素。

让我们尝试一下。 通过使用@bind 属性添加一个增量字段和一个 <input> 元素来修改 Counter.razor,如清单 2-12 所示。 还要修改 IncrementCount 方法以在单击按钮时使用增量。

清单 2-12 添加增量和输入

@page "/counter"
<h1>Counter</h1>
<p>Current count: <span class="@BackgroundColor">@currentCount</span></p>
<p>
    <input type="number" @bind="@increment" />
</p>
<button class="btn btn-primary"
        disabled="@(currentCount > 10)"
        @onclick="IncrementCount">
    Click me
</button>

@code {
    private int currentCount = 0;
    private int increment = 1;
    private void IncrementCount(MouseEventArgs e)
    {
        if (e.CtrlKey)
        {
        currentCount -= increment;
        }
        else
        {
        currentCount += increment;
        }
    }
    private string BackgroundColor
    => (currentCount % 2 == 0) ? "red-background" : "yellow-background";
}

构建并运行。
更改输入的值,例如 3。您现在应该能够使用其他值递增 currentCount,如图所示。

image

看看你刚刚添加的 <input> 元素,在清单 2-13 中重复了这里。

清单 2-13 使用@bind 语法的双向数据绑定

<input type="number" @bind="@increment" />

在这里,我们使用 @bind 语法,它等效于两个不同的单向绑定,如清单 2-14 所示。

在这里,我们使用单向数据绑定 (value="@increment") 将输入的 value 属性设置为增量变量。 当用户修改输入元素的内容时,更改事件 (@onchange) 将触发并将增量变量设置为输入的值 (increment = int.Parse($"{e.Value}"))。 因此,当一侧更改时,另一侧将更新。

清单 2-14 双向数据绑定

<input type="number"
       value="@increment"
       @onchange="@((ChangeEventArgs e)=> increment = int.Parse($"{e.Value}"))" />

这种替代语法非常冗长,使用起来并不方便。 使用@bind 更实用。 但是,不要忘记这种技术; 使用更详细的语法有时可能是更优雅的解决方案!

绑定到其他事件:@bind:{event}

当 DOM 的 onchange 事件发生时,Blazor 将更新双向数据绑定中的值。 这意味着当用户将焦点更改为另一个元素(例如按钮)时,Counter 组件的增量字段将被更新。 但也许这对你来说为时已晚。 让我们看看如何更改触发数据绑定的事件。

通过复制清单 2-14 中的行来添加第二个输入。 运行此示例并通过在其中输入一个数字来更改一个输入的值(不要使用浏览器为数字输入添加的递增/递减按钮)。 另一个输入的值不会立即更新。 单击其他输入将更新它。 这是因为我们使用了 onchange 事件,当输入失去焦点时触发! 如果您希望数据绑定立即发生,您可以使用显式 @bind:event 语法绑定到 oninput 事件。oninput 事件在输入的每次更改后触发。 更新第二个输入元素以匹配示例 2-15。 键入第二个输入将在每次击键后更新第一个输入。

清单 2-15 显式绑定到事件

<input type="number" @bind="@increment" @bind:event="oninput" />

防止默认操作

在 Blazor 中,您可以对事件做出反应,浏览器也会对这些做出反应。 例如,当您按下一个焦点在 <input> 元素上的键时,浏览器将通过将击键添加到 <input> 来做出反应。

但是,如果您不希望浏览器正常运行怎么办? 假设您想允许用户通过按“+”或“-”来增加和减少输入的值。 将示例 2-12 中的 <input> 更改为对按键事件做出反应,如示例 2-16 和 2-17 所示。

清单 2-16 处理按键事件

<p>
    <input type="number" @bind="@increment" @onkeypress="KeyHandler" />
</p>

清单 2-17 KeyHandler 方法

private void KeyHandler(KeyboardEventArgs e)
{
    if (e.Key == "+")
    {
        increment += 1;
    }
    else if (e.Key == "-")
    {
        increment -= 1;
    }
}

构建并运行。 按“+”和“-”将增加和减少输入中的值,但您还会看到您刚刚按下的任何键都添加到 <input> HTML 元素,因为这是输入的默认行为。 要停止这种默认行为,我们可以添加 @{event}:preventDefault ,如清单 2-18 所示。 在这里,我们使用 bool 字段 shouldPreventDefault(设置为 true)来停止输入的默认行为,但您可以使用任何布尔表达式。

清单 2-18 停止输入的默认行为

<p>
    <input type="number"
           @bind="@increment"
           @onkeypress="KeyHandler"
           @onkeypress:preventDefault="@shouldPreventDefault" />
</p>
// add this next to the KeyHandler method
private bool shouldPreventDefault = true;

再次构建并运行。 现在按“+”将按预期增加输入的值。
您也可以省略 preventDefault 的值,然后它将始终阻止默认操作,如清单 2-19 所示。

清单 2-19 更短的符号

<p>
    <input type="number"
           @bind="@increment"
           @onkeypress="KeyHandler"
           @onkeypress:preventDefault />
</p>

停止事件传播

在浏览器中,事件传播到父元素,然后传播到该父元素的父元素,等等。同样,通常这是可取的,但并非总是如此。

让我们看一个例子。 首先向 Counter 组件添加两个嵌套的 div 元素,每个元素都处理 @onmousemove 事件,如清单 2-20 所示。

清单 2-20 事件传播示例

@page "/counter"
<h1>Counter</h1>
<p>Current count:
    <span class="@BackgroundColor">
        @currentCount
    </span>
</p>
<p>
    <input type="number"
           @bind="@increment"
           @onkeypress="KeyHandler"
           @onkeypress:preventDefault="@shouldPreventDefault" />
</p>
<div style="width: 400px; height: 400px; background: yellow"
     @onmousemove="OuterMouseMove">
    @outerPos
    <div style="width: 300px; height: 300px;
                background: green; margin:50px"
         @onmousemove="InnerMouseMove">
        @innerPos
    </div>
</div>
<br/>
<button class="btn btn-primary"
        disabled="@(currentCount > 10)"
        @onclick="IncrementCount">
    Click me
</button>

同时添加示例 2-21 中的代码。 这些事件处理程序仅显示元素中的鼠标位置。

清单 2-21 事件处理程序

private void KeyHandler(KeyboardEventArgs e)
{
    if (e.Key == "+")
    {
        increment += 1;
    }
    else if (e.Key == "-")
    {
        increment -= 1;
    }
}
private string outerPos = "Nothing yet";
private void OuterMouseMove(MouseEventArgs e)
    => outerPos = $"Mouse at {e.ClientX}x{e.ClientY}";
private string innerPos = "Nothing yet";
private void InnerMouseMove(MouseEventArgs e)
    => innerPos = $"Mouse at {e.ClientX}x{e.ClientY}";

构建并运行。
在黄色方块中移动鼠标指针。 现在对绿色矩形做同样的事情。 然而,在绿色方块中移动鼠标也会更新黄色方块!这是因为 mousemove 事件(和其他事件)被发送到事件发生的元素以及它的父元素一直到根元素! 如果你想避免这种情况,你可以通过添加 {event}:stopPropagation 属性来停止这种传播。 如示例 2-22 所示,将其添加到内部正方形。 从现在开始,在内部方块中移动鼠标不会更新外部方块。

清单 2-22 阻止事件传播到父级

<div style="width: 400px; height: 400px; background: yellow"
     @onmousemove="OuterMouseMove">
    @outerPos
    <div style="width: 300px; height: 300px;
                background: green; margin:50px"
         @onmousemove="InnerMouseMove"
         @onmousemove:stopPropagation>
        @innerPos
    </div>
</div>

如果您希望能够从代码中打开和关闭它,请为此属性分配一个布尔表达式,就像 preventDefault 一样。

格式化日期

绑定到 DateTime 值的数据可以使用 @bind:format 属性进行格式化,如清单 2-23 所示。

清单 2-23 格式化日期

<p>
	<input @bind="@Today" @bind:format="yyyy-MM-dd" />
</p>
@code {
	private DateTime Today { get; set; } = DateTime.Now;
}

目前,DateTime 值是唯一支持 @bind:format 属性的值。

变化检测

只要 Blazor 运行时认为您的数据发生了更改,它就会更新 DOM。 一个例子是当一个事件执行你的一些代码时,它假设你已经修改了一些值作为副作用并呈现 UI。 但是,Blazor 并不总是能够检测到所有更改,在这种情况下,您必须告诉 Blazor 将更改应用到 DOM。 一个典型的例子是后台线程,所以让我们看一个例子。

打开 Counter.razor 并添加另一个按钮,该按钮会在按下时自动增加计数器,如清单 2-24 所示。 AutoIncrement 方法使用 .NET Timer 实例每秒递增 currentCount。 计时器实例将在后台线程上运行,每隔一段时间执行回调委托(就像 JavaScript 中的 setInterval 一样)。

清单 2-24 添加另一个按钮

@page "/counter"
<h1>Counter</h1>
<p>Current count: <span class="@BackgroundColor">@currentCount</span></p>
<button class="btn btn-primary"
        disabled="@(currentCount > 10)"
        @onclick="IncrementCount">
    Click me
</button>
<button class="btn btn-secondary"
        @onclick="AutoIncrement">
    Auto Increment
</button>

@code {
    private int currentCount = 0;
    private void IncrementCount()
    {
        currentCount += 1;
        Console.WriteLine("++");
    }
    private string BackgroundColor
    => (currentCount % 2 == 0) ? "red-background": "yellow-background";
    private void AutoIncrement()
    {
        var timer = new System.Threading.Timer(
        callback: (_) => IncrementCount(),
        state: null,
        dueTime: TimeSpan.FromSeconds(1),
        period: TimeSpan.FromSeconds(1));
    }
}

您可能会发现 Timer 的构造函数中的 lambda 函数参数有点奇怪。 当我需要命名一个未在 lambda 函数体中使用的参数时,我会使用下划线。 随便你怎么称呼它,例如,忽略——没关系。 我只是喜欢使用下划线,因为这样我就不必为参数想一个好名字。

运行此页面。 单击“Auto Increment”按钮将启动计时器,但 currentCount 不会在屏幕上更新。 为什么? 尝试单击“增量”按钮。currentCount 已更新,因此是 UI 问题。 如果您打开浏览器的调试器,您将在控制台选项卡中看到每秒出现一个 ++,因此计时器可以正常工作! 那是因为我添加了一个 Console.Writeline,它将输出发送到调试器的控制台。 有时是一种简单的方法来查看事情是否正常。

每当发生事件时,Blazor 都会重新呈现页面。 它还将在异步操作的情况下重新呈现页面。 但是,无法自动检测到某些更改。 在这种情况下,由于我们在后台线程上进行了一些更改,因此您需要通过调用每个 Blazor 组件从其基类继承的 StateHasChanged 方法来告诉 Blazor 更新页面。

回到 AutoIncrement 方法并添加对 StateHasChanged 的调用,如清单 2-25 所示。 StateHasChanged 告诉 Blazor 某些状态已更改(谁会想到!)并且它需要重新呈现页面。

清单 2-25 添加 StateHasChanged

private void AutoIncrement()
{
    var timer = new System.Threading.Timer(
    callback: (_) => { IncrementCount(); StateHasChanged(); },
    state: null,
    dueTime: TimeSpan.FromSeconds(1),
    period: TimeSpan.FromSeconds(1));
}

再次运行。 现在按“自动增量”将起作用。
如您所见,有时我们需要手动告诉 Blazor 更新 DOM。 通常,Blazor 运行时会检测何时更新 UI。 当用户与您的应用程序交互时,会触发事件,从而进行更改检测。 当异步方法完成时,将发生更改检测。 只有当我们超出 Blazor 运行时(例如,使用 .NET 计时器)时,我们才需要自己触发更改检测。 当我们在接下来的两章中查看构建组件时,会详细介绍这一点。

PizzaPlace 单页应用程序

使用 Visual Studio 或 dotnet CLI 创建一个新的 Blazor 托管项目。 如果您不记得如何创建项目,请参阅第一章中有关创建项目的说明。 调用项目 PizzaPlace。 您将获得与 MyFirstBlazor 项目类似的项目。

首先,为每个项目启用可空引用类型功能(您可能会发现 Blazor 模板已启用可空引用类型):

<PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
</PropertyGroup>

使用 Visual Studio,您还可以打开项目的属性,如图所示。

image

开箱即用,Blazor 使用流行的 Bootstrap 4 布局框架
(https://getbootstrap.com/),包括开放标志性字体。 期望在代码示例中看到 bootstrap 和 open-iconic(oi) CSS 类。 但是,您可以使用任何其他布局框架,因为 Blazor 使用标准 HTML 和 CSS。 这本书是关于 Blazor 的,而不是花哨的布局,所以我们不会花很多时间来选择漂亮的颜色并使网站看起来很棒。 重点!

在服务器项目中,丢弃 WeatherForecastController.cs。 我们不需要天气预报来订购比萨饼。 在共享项目中,删除 WeatherForecast.cs。 一样。 在客户端项目中,丢弃 Pages 文件夹中的 Counter.razorFetchData.razor 文件以及 Shared 文件夹中的 SurveyPrompt.razor 文件。

添加共享类来表示数据

在 Blazor 中,最好将保存数据的类添加到共享项目(除非您正在构建没有后端服务器的 Blazor 应用程序)。 这些类用于将数据从服务器发送到客户端,然后再将数据发回。 您可能将这些类称为模型或数据传输对象 (DTO)。

我们需要什么? 由于我们将围绕PizzaPlace建立一个网站,因此创建一个类来表示它是有意义的。

从表示 Pizza 的类开始,以及它的辣度,如清单 2-26 和 2-27 所示。

清单 2-26 Spiciness Class

namespace PizzaPlace.Shared
{
    public enum Spiciness
    {
        None,
        Spicy,
        Hot
    }
}

清单 2-27 Pizza类

namespace PizzaPlace.Shared
{
    public class Pizza
    {
        public Pizza(int id, string name, decimal price,
                     Spiciness spiciness)
        {
            this.Id = id;
            this.Name = name;
            this.Price = price;
            this.Spiciness = spiciness;
        }
        public int Id { get; }
        public string Name { get; }
        public decimal Price { get; }
        public Spiciness Spiciness { get; }
    }
}

我们的应用程序还不是要编辑比萨饼,所以我让这个类不可变,也就是说,一旦创建了比萨饼对象,就无法更改任何内容。 在 C# 中,这很容易通过创建仅使用 getter 的属性来完成。 您仍然可以设置这些属性,但只能在构造函数中设置。

接下来,我们需要一个代表我们提供的菜单的类。 使用清单 2-28 中的实现向 Shared 项目添加一个名为 Menu 的新类。

清单 2-28 Menu 类

using System.Collections.Generic;
using System.Linq;
namespace PizzaPlace.Shared
{
    public class Menu
    {
        public List<Pizza> Pizzas { get; set; }= new List<Pizza>();
        public void Add(Pizza pizza) => Pizzas.Add(pizza);
        public Pizza? GetPizza(int id)
            => Pizzas.SingleOrDefault(pizza => pizza.Id == id);
    }
}

就像在现实生活中一样,餐厅的菜单是一份餐食清单,在这种情况下,是一份披萨餐。
Shared 项目中,我们还需要一个 Customer 类,实现清单 2-29。 在这种情况下,Customer 类是一个普通的、可变的类,与 Pizza 类不同。 用户将输入一些我们将存储在客户实例中的信息。 而且因为我们使用的是可为空的引用类型,所以当我们不初始化我们的属性时,我们需要删除编译器的警告。 这很容易通过分配默认值来完成! 给他们。

清单 2-29 Customer 类

namespace PizzaPlace.Shared
{
    public class Customer
    {
        public int Id { get; set; }
        public string Name { get; set; } = default!;
        public string Street { get; set; } = default!;
        public string City { get; set; } = default!;
    }
}

每个客户都有一个购物篮,因此将 Basket 类添加到 Shared 项目中,如清单 2-30 所示。

清单 2-30 Basket 类,代表客户的订单

using System.Collections.Generic;
namespace PizzaPlace.Shared
{
    public class ShoppingBasket
    {
        public Customer Customer { get; set; } = new Customer();
        public List<int> Orders { get; set; } = new List<int>();
        public bool HasPaid { get; set; }
        public void Add(int pizzaId)
            => Orders.Add(pizzaId);
        public void RemoveAt(int pos)
            => Orders.RemoveAt(pos);
    }
}

请注意,我们只是将披萨 ID 保留在 Orders 集合中。 稍后您将了解原因。
在我们将它们全部分组之前再上一堂课。 我们将使用一个 UI 类来跟踪一些 UI 选项,因此将这个类添加到 Shared 项目中,如清单 2-31 所示。

清单 2-31 UI 选项类

namespace PizzaPlace.Shared
{
    public class UI
    {
        public bool ShowBasket { get; set; } = true;
    }
}

最后,我们将所有这些类组合成一个 State 类,同样在 Shared 项目中,实现如清单 2-32 所示。

清单 2-32 State 类

namespace PizzaPlace.Shared
{
    public class State
    {
        public Menu Menu { get; } = new Menu();
        public ShoppingBasket Basket { get; } = new ShoppingBasket();
        public UI UI { get; set; } = new UI();
    }
}

将所有这些类放入 Shared 项目中还有另一个很好的理由。 Blazor 的调试有限。 通过将这些类放入 Shared 项目中,我们可以对共享类应用单元测试最佳实践,因为它是一个常规的 .NET 项目,甚至可以使用 Visual Studio 调试器来检查奇怪的行为。 共享项目也可以被其他项目使用,因为它是一个 .NET 标准项目,例如,Windows 或 MAUI 客户端!

构建 UI 以显示菜单

有了这些类来表示数据,下一步就是构建显示菜单的用户界面。 我们将从向用户显示菜单开始,然后我们将增强 UI 以允许用户订购一个或多个比萨饼。

显示菜单的问题有两个:首先,您需要显示数据列表。 菜单可以被认为是一个列表,就像任何其他列表一样。 其次,在我们的应用程序中,我们需要将辣度选择从它们的数值转换为指向用于指示不同辣度级别的图标的 URL。

打开 Index.razor。 删除 <SurveyPrompt> 元素。 通过初始化 State 实例,添加 @code 部分以使用清单 2-33 中的代码保存我们餐厅的(有限)菜单。 我们还重写了 OnInitialized 方法以将我们的菜单项添加到我们的状态菜单中。 此方法允许您在首次渲染组件之前对其进行一些更改。

清单 2-33 构建我们的应用程序菜单

@page "/"
<h1>Hello, world!</h1>
Welcome to your new app.
@code {
    private State State { get; } = new State();
    protected override void OnInitialized()
    {
        State.Menu.Add(
        new Pizza(1, "Pepperoni", 8.99M, Spiciness.Spicy ));
        State.Menu.Add(
        new Pizza(2, "Margarita", 7.99M, Spiciness.None));
        State.Menu.Add(
        new Pizza(3, "Diabolo", 9.99M, Spiciness.Hot));
    }
}

如果你现在编译,你会得到一堆编译器错误。 这些会告诉你编译器找不到类State。 如果这是一个 C# 文件,你会怎么做? 您将在顶部添加一个 using 语句。 我们可以在 razor 文件中执行相同的操作,示例如清单 2-34 所示。

清单 2-34 向 Razor 组件添加 using 语句

@page "/"
@using PizzaPlace.Shared
<h1>Hello, world!</h1>

但是,有了剃须刀,我们可以做得更好。 我们可以一次将这个 using 语句添加到所有组件中!
打开~ 文件并添加一个 @using ,如清单 2-35 所示。 _Imports.razor 目录(和子目录)中的所有 razor 文件现在将自动识别 PizzaPlace.Shared 命名空间。

清单 2-35 将 using 语句添加到 _Imports.razor

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using PizzaPlace.Client
@using PizzaPlace.Client.Shared
@using PizzaPlace.Shared

PizzaPlace 菜单与任何其他列表一样是一个列表。 您可以通过在 Index.razor 中添加一些 razor 标记来显示它,以将菜单生成为 HTML,如清单 2-36 所示。 我喜欢使用评论来显示页面上每个部分的开头和结尾。 这使我稍后返回页面时更容易找到页面的某个部分。

我们在这里所做的是遍历菜单中的每个比萨饼并生成一行四列,一列用于名称,一列用于价格,一列用于辣度,最后一列用于订购按钮。 还有一些编译器错误,我们将在接下来修复。

清单 2-36 使用 Razor 生成 HTML

@page "/"
<!-- Menu -->
<h1>Our selection of pizzas</h1>
@foreach (var pizza in State.Menu.Pizzas)
{
    <div class="row">
        <div class="col">
            @pizza.Name
        </div>
        <div class="col text-right">
            @($"{pizza.Price:0.00}")
        </div>
        <div class="col"></div>
        <div class="col">
            <img src="@SpicinessImage(pizza.Spiciness)"
                 alt="@pizza.Spiciness" />
        </div>
        <div class="col">
            <button class="btn btn-success pl-4 pr-4"
                    @onclick="@(() => AddToBasket(pizza))">
                Add
            </button>
        </div>
    </div>
}

<!-- End menu -->
@code {
	private State State { get; } = new State();
    protected override void OnInitialized()
    {
        State.Menu.Add(
        new Pizza(1, "Pepperoni", 8.99M, Spiciness.Spicy ));
        State.Menu.Add(
        new Pizza(2, "Margarita", 7.99M, Spiciness.None));
        State.Menu.Add(
        new Pizza(3, "Diabolo", 9.99M, Spiciness.Hot));
    }
}

转换值

我们还有一个小问题。 我们需要将 spiciness 值转换为 URL,这是通过 SpicinessImage 方法完成的,如清单 2-37 所示。 将此方法添加到 Index.razor 文件的 @code 区域。

清单 2-37 使用转换器函数转换值

private string SpicinessImage(Spiciness spiciness)
=> $"images/{spiciness.ToString().ToLower()}.png";

这个转换器函数只是将清单 2-26 中的枚举值的名称转换为可以在 Blazor 项目的图像文件夹中找到的图像文件的 URL,如图 所示。 将此文件夹)添加到 wwwroot 文件夹。

image

将比萨饼添加到购物篮

让菜单正常运行自然会导致将比萨饼添加到购物篮中。 当您单击 Add 按钮时,AddToBasket 方法将与所选的比萨一起执行。 您可以在清单 2-38 中找到 AddToBasket 方法的实现,它是 Index.razor 的一部分。

清单 2-38 订购比萨饼

@code {
    private State State { get; } = new State();
    protected override void OnInitialized()
    {
        State.Menu.Add(
            new Pizza(1, "Pepperoni", 8.99M, Spiciness.Spicy ));
        State.Menu.Add(
            new Pizza(2, "Margarita", 7.99M, Spiciness.None));
        State.Menu.Add(
            new Pizza(3, "Diabolo", 9.99M, Spiciness.Hot));
    }
    private string SpicinessImage(Spiciness spiciness)
        => $"images/{spiciness.ToString().ToLower()}.png";
    private void AddToBasket(Pizza pizza)
        => State.Basket.Add(pizza.Id);
}

我们的 ShoppingBasket 类现在需要一个 Add 方法,如清单 2-39 所示。

清单 2-39 篮子的添加方法

using System.Collections.Generic;
namespace PizzaPlace.Shared
{
    public class ShoppingBasket
    {
        public Customer Customer { get; set; } = new Customer();
        public List<int> Orders { get; set; } = new List<int>();
        public bool HasPaid { get; set; }
        public void Add(int pizzaId)
            => Orders.Add(pizzaId);
    }
}

查看清单 2-36 中按钮的 @onclick 事件处理程序(@onclick="@(() => AddToBasket(pizza))")。 为什么这个事件处理程序使用 lambda? 当您订购披萨时,您当然希望将您选择的披萨添加到购物篮中。 那么我们如何将示例 2-38 中的比萨传递给 AddToBasket 呢? 通过使用 lambda 函数,我们可以简单地将 @foreach 循环中使用的 Pizza 变量传递给它。 使用普通方法是行不通的,因为没有简单的方法可以发送选定的比萨饼。 这也称为闭包(非常类似于 JavaScript 闭包)并且非常实用!

运行应用程序。 您应该看到。

image

当您单击“Add”按钮时,您正在将披萨添加到购物篮中。 但是我们怎么能确定(因为我们还没有显示购物篮)?

我们可以使用调试器,就像任何其他 .NET 项目一样! 如图所示,在 AddToBasket 方法中添加一个断点,然后使用调试器运行您的项目。等待浏览器显示 PizzaPlace 页面并单击其中一个“添加”按钮。调试器应该停止断点。 现在您可以检查 AddToBasket 方法的参数,它应该是选定的比萨饼。 大多数常见的调试内容都适用于 Blazor!

image

显示购物篮

菜单上的下一件事(一些双关语)是显示购物篮。 我们将使用 C# 中称为元组的功能。 我稍后会解释元组。
在清单 2-36 的菜单之后添加清单 2-40(注释应该很容易找到)。

清单 2-40 显示购物篮

<!-- End menu -->
<!-- Shopping Basket -->
@if (State.Basket.Orders.Any())
{
    <h1 class="">Your current order</h1>
    @foreach (var (pizza, pos) in State.Basket.Orders.Select(
    			(id, pos) => (State.Menu.GetPizza(id), pos)))
    {
        <div class="row mb-2">
            <div class="col">
                @pizza.Name
            </div>
            <div class="col text-right">
                @($"{pizza.Price:0.00}")
            </div>
            <div class="col"></div>
            <div class="col"></div>
            <div class="col">
                <button class="btn btn-danger"
                        @onclick="@(() => RemoveFromBasket(pos))">
                    Remove
                </button>
            </div>
        </div>
    }
    <div class="row">
        <div class="col"></div>
        <div class="col"><hr /></div>
        <div class="col"> </div>
        <div class="col"> </div>
    </div>
    <div class="row">
        <div class="col"> Total:</div>
        <div class="col text-right font-weight-bold"> @($"{State.TotalPrice:0.00}") </div>
        <div class="col"> </div>
        <div class="col"> </div>
        <div class="col"> </div>
    </div>
}
<!-- End shopping basket -->

其中大部分内容非常相似,但现在我们正在迭代一个元组列表(继续阅读,C# https://docs.microsoft.com/dotnet/csharp/tuples 中的一个非常方便的新功能)。

元组与 C# 中的匿名类型非常相似,因为它们允许您存储和返回中间多部分结果,而无需构建帮助程序类。
让我们更详细地看一下这段代码:

@foreach (var (pizza, pos) in State.Basket.Orders.Select(
(id, pos) => (State.Menu.GetPizza(id), pos)))

我们正在使用 LINQ 的 Select 来遍历订单列表(其中包含披萨 ID)。 为了在购物篮中显示披萨,我们需要一个披萨,所以我们使用 Menu 中的 GetPizza 方法将 id 转换为披萨。
让我们看一下 Select 中使用的 lambda 函数:

(id, pos) => (State.Menu.GetPizza(id), pos))

LINQ Select 方法有两个重载,我们使用重载从集合中获取元素 (id) 和在集合中的位置 (pos)。 我们使用这些来创建元组。 每个元组代表篮子中的一个比萨饼及其在篮子中的位置! 我们也可以这样做,用比萨饼和位置创建一个小助手类,但现在我们已经完成了! 而且它是有效的,比类使用更少的内存,因为它是一个值类型!

披萨用于显示其名称和价格,而位置用于删除按钮。 这个按钮调用清单 2-41 中的 RemoveFromBasket 方法。

清单 2-41 从购物篮中取出物品

@code {
    private State State { get; } = new State();
    protected override void OnInitialized()
    {
        State.Menu.Add(
            new Pizza(1, "Pepperoni", 8.99M, Spiciness.Spicy ));
        State.Menu.Add(
            new Pizza(2, "Margarita", 7.99M, Spiciness.None));
        State.Menu.Add(
            new Pizza(3, "Diabolo", 9.99M, Spiciness.Hot));
    }
    private string SpicinessImage(Spiciness spiciness)
        => $"images/{spiciness.ToString().ToLower()}.png";
    private void AddToBasket(Pizza pizza)
        => State.Basket.Add(pizza.Id);
    private void RemoveFromBasket(int pos)
        => State.Basket.RemoveAt(pos);
}

当然,我们需要将 RemoveAt 方法添加到 ShoppingBasket 类中,如清单 2-42 所示。

清单 2-42 Basket类的 RemoveAt 方法

using System.Collections.Generic;
namespace PizzaPlace.Shared
{
    public class ShoppingBasket
    {
        public Customer Customer { get; set; } = new Customer();
        public List<int> Orders { get; set; } = new List<int>();
        public bool HasPaid { get; set; }
        public void Add(int pizzaId)
            => Orders.Add(pizzaId);
        public void RemoveAt(int pos)
            => Orders.RemoveAt(pos);
    }
}

在购物篮的底部,显示了总订单金额。 这是由 State 类计算的。 将清单 2-43 中的 TotalPrice 方法添加到 State 类中。 请注意使用 null-forgiving 运算符 (!),因为我假设 ShoppingBasket 将始终包含有效的比萨饼 ID。

清单 2-43 计算State 类中的总价格

using System.Linq;
namespace PizzaPlace.Shared
{
    public class State
    {
        public Menu Menu { get; } = new Menu();
        public ShoppingBasket Basket { get; } = new ShoppingBasket();
        public UI UI { get; set; } = new UI();
        public decimal TotalPrice
            => Basket.Orders.Sum(id => Menu.GetPizza(id)!.Price);
    }
}

运行应用程序并订购一些比萨饼。 您应该会看到类似于下图的当前订单。

image

输入客户信息

当然,要完成订单,我们需要知道一些关于客户的事情,尤其是我们需要知道客户的姓名和地址,因为我们需要交付订单。
首先将以下 razor 添加到您的 Index.razor 页面,如清单 2-44 所示。

清单 2-44 为数据输入添加表单元素

<!-- End shopping basket -->
<!-- Customer entry -->
<h1>Please enter your details below</h1>
<fieldset>
    <div class="row mb-2">
        <label class="col-2" for="name">Name:</label>
        <input class="col-6" id="name"
               @bind="State.Basket.Customer.Name" />
    </div>
    <div class="row mb-2">
        <label class="col-2" for="street">Street:</label>
        <input class="col-6" id="street"
               @bind="State.Basket.Customer.Street" />
    </div>
    <div class="row mb-2">
        <label class="col-2" for="city">City:</label>
        <input class="col-6" id="city"
               @bind="State.Basket.Customer.City" />
    </div>
    <button @onclick="PlaceOrder">Checkout</button>
</fieldset>
<!-- End customer entry -->

这添加了三个 <label> 和它们各自的 <input> 用于 name、street 和 city。
您还需要将 PlaceOrder 方法添加到您的 @code 中,如清单 2-45 所示。

清单 2-45 PlaceOrder 方法

@code {
    private State State { get; } = new State();
    protected override void OnInitialized()
    {
        State.Menu.Add(
            new Pizza(1, "Pepperoni", 8.99M, Spiciness.Spicy));
        State.Menu.Add(
            new Pizza(2, "Margarita", 7.99M, Spiciness.None));
        State.Menu.Add(
            new Pizza(3, "Diabolo", 9.99M, Spiciness.Hot));
    }
    private string SpicinessImage(Spiciness spiciness)
        => $"images/{spiciness.ToString().ToLower()}.png";
    private void AddToBasket(Pizza pizza)
        => State.Basket.Add(pizza.Id);
    private void RemoveFromBasket(int pos)
        => State.Basket.RemoveAt(pos);
    private void PlaceOrder()
    {
        Console.WriteLine("Placing order");
    }
}

PlaceOrder 方法还没有做任何有用的事情; 我们稍后会将订单发送到服务器。 然而,这确实在 Blazor 中展示了一种有效的调试技术,我们在其中放置 Console.WriteLine 语句以查看执行了什么。
运行应用程序并输入您的详细信息,例如,如图 2-8 所示。

image

调试提示

即使使用现代调试器,您也希望看到 State 对象,因为它在您与应用程序交互时包含客户的详细信息和订单。 当我们按下 Checkout 按钮时,我们会向服务器发送正确的信息吗? 为此,我们将使用一个简单的技巧,在我们的页面上显示状态,以便您随时查看它。

首先向 Blazor 客户端项目添加一个新的静态类 DebuggingExtensions,如清单 2-46 所示。

清单 2-46 DebuggingExtensions

using System.Text.Json;
namespace PizzaPlace.Client
{
    public static class DebuggingExtensions
    {
        private static JsonSerializerOptions options = new
            JsonSerializerOptions { WriteIndented = true };
        public static string ToJson(this object obj)
            => JsonSerializer.Serialize(obj, options);
    }
}

Index.razor 的底部,添加一个简单的段落,如示例 2-47 所示。

清单 2-47 显示状态

<!-- End customer entry -->
@State.ToJson()
@code {

运行你的项目。 当您与页面交互时,您会看到状态发生变化,示例如图所示。

image

很明显,我们在页面准备就绪时删除了此调试功能

标签:绑定,private,State,添加,清单,Blazor,第二章,数据,public
来源: https://www.cnblogs.com/King2019Blog/p/16654960.html