65.9K
CodeProject 正在变化。 阅读更多。
Home

Excelsior!在没有安全网的情况下构建应用程序 - 第 3 部分

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2021年5月20日

CPOL

14分钟阅读

viewsIcon

6638

该系列文章的第三部分,我们将构建一个应用程序,展示编写整个应用程序的思考过程。

引言

我记得,作为一名年轻的开发人员,我曾敬畏那些似乎毫不费力就能坐下来写代码的人。系统似乎从他们的指尖流淌出来,轻松地被制作出来,优雅而精致。我感觉我看到了米开朗琪罗在西斯廷教堂的创作,或者莫扎特坐在崭新的五线谱前。当然,随着经验的积累,我现在知道我看到的是开发人员在做他们应该做的事情。有些人做得很好,真正理解开发的工艺,而另一些人则创作出不太优雅、不太好的作品。多年来,我有幸向一些出色的开发人员学习,但我一遍又一遍地回到同一个基本问题,即……

如果我能听到其他开发者在编写应用程序时的思考过程,我现在作为开发者会好多少?

在本系列文章中,我将带您了解我在开发应用程序时所思考的过程。文章附带的代码将以“优缺点全暴露”的方式编写,以便您了解我如何将某项内容从最初的需求阶段处理到我乐意让其他人使用的程度。这意味着文章将展示我所做的每一个错误以及我在充实想法时所采取的捷径。我不会声称自己是一名伟大的开发人员,但我有足够的胜任和经验,这应该能帮助新进入该领域的人更早地克服他们的敬畏感,并建立自信。

场景设定

本文是 第一部分第二部分 的续篇,我们在其中介绍了 HTTP GET 操作的 MVP。现在我们将着眼于创建用户界面的决策过程,并创建基本界面。

源代码

本文的代码可以从 https://github.com/ohanlon/goldlight.xlcr/tree/article3 下载。

选择技术

第二部分 中,我提到还没有为我们的应用程序选择用户界面技术。我已经到了一个自然的节点来纠正这种情况,并选择一种我将用于该应用程序的技术。我必须承认,我决策过程的一部分必须包括您,读者,作为目标受众。我不能假设您了解我所知道的相同的接口堆栈,因此我选择的任何技术都将依赖于我介绍和教授该堆栈的足够基础知识,以便您能够跟上我的步伐。

我有一些要求,这些要求将有助于我选择堆栈。

  1. 由于许多人将使用该应用程序,并且他们可能在 Windows 以外的平台上运行该应用程序,因此我不能选择一种限制我只能在 Windows 上使用的技术。
  2. 我知道许多阅读本文的读者非常不喜欢基于 XAML 的应用程序开发。我不想疏远这些读者,所以必须避开基于 XAML 的技术。
  3. 考虑到我们 C# 的功能,该技术必须与 .NET 兼容。
  4. 该技术现在必须可用。

这些要求立即排除了 WPF(仅限 Windows)、Windows Forms(仅限 Windows)、Xamarin Forms(XAML)、.NET Multi-platform App UI(.NET 6 中提供)、WinUI(仅限 Windows)和 QT(.NET)。

多平台特性表明我们需要使用某种描述性的基于 Web 的技术。我想使用我熟悉的一种,所以这使我有 ASP.NET Core、Angular、Vue、React 和 Blazor 的选择。其中大多数技术都需要我创建一个 Web 服务器来访问我已编写的 C# 代码,所以在这方面它们都是平等的。纯粹基于 Web 的技术将要求我创建一套 API,前端将调用这些 API,然后与现有的 C# 代码进行交互。如果我使用服务器端 Blazor,那么我就可以在不添加这种间接层的情况下使用 C# 代码。如果我使用客户端 Blazor,那么我就可以将所有代码直接托管在浏览器中。由此,我似乎已经做出了决定。我将使用 Blazor 来提供用户界面,但我仍然需要决定是使用服务器端 Blazor 还是客户端 Blazor。

根据 Microsoft

引用

“Blazor 允许您使用 C# 而不是 JavaScript 来构建交互式 Web UI。Blazor 应用程序由使用 C#、HTML 和 CSS 实现的可重用 Web UI 组件组成。客户端和服务器代码都用 C# 编写,这使您可以共享代码和库。”

实际上,我们有两种类型的 Blazor 应用程序可用。我可以构建一个客户端 Blazor 应用程序,它将 .NET 程序集下载到托管在浏览器中的 Mono 运行时,因此所有内容都完全在客户端运行。或者,我可以编写一个服务器端 Blazor 应用程序,其中服务器是一个标准的 Web 服务器,客户端通过 SignalR 与服务器进行交互。

虽然我喜欢应用程序的 WebAssembly 版本这个想法,但在这里我脑子里想的是,实际的 GET 请求等将被视为客户端请求,因此它们将受到 CORS 策略的约束,因为 HTTP 请求是由浏览器作为 fetch 请求实现的。这使得我只能选择服务器端 Blazor。由于请求是从服务器端发出的,因此不受 CORS 约束。

注意:我可以通过引入一个代理服务器在应用程序和远程服务器之间充当中介来解决 CORS 问题,但为了简单起见,我选择不这样做。

潜在的增强

我心里想着,如果我有时间来处理这个应用程序,我可以将 Blazor 服务器应用程序封装成一个 Electron 应用程序。这超出了本文的范围,但如果需要,我以后肯定可以使用它。

添加 Blazor 应用程序

如果我使用 Visual Studio 2019 或 JetBrains,我可以通过“添加新项目”并选择 Blazor 服务器模板来将 UI 组件添加到我的项目中。如果我这样做,我会确保关闭 HTTPS 支持,因为我现在不需要它。

或者,我可以使用我在本系列介绍中介绍的技术,并使用以下命令创建服务器。

dotnet new Goldlight.Xlcr.Ui -o BlazorApp --no-https

如果您采用此方法,请不要忘记将项目添加到解决方案中。

在开始添加功能之前,我想看看我们的 Blazor 应用程序“开箱即用”的样子。快速构建并运行,我看到的就是这样(如果您在 IDE 中,让 UI 应用程序成为启动对象会很有帮助)。

这是 Blazor 应用程序的样板代码,展示了如何添加和引用组件。

连接代码

显然,我需要在我们的 UI 应用程序中添加对 goldlight.xlcr.core 项目的引用。我的项目文件添加此项后看起来是这样的。

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\goldlight.xlcr.core\goldlight.xlcr.core.csproj" />
  </ItemGroup>

</Project>

当我编写 GetRequest 类时,我总是想着请求将在每个需要它的页面中创建。为了做到这一点,我需要在Startup.cs 中的 ConfigureServices 方法中注册 GetRequest 类。为此,我只需添加以下一行:

services.AddScoped<GetRequest>();

我将使用主页来测试 GetRequest。我需要更改的文件位于Pages 文件夹内,名为 Index.razor

这是当前页面的样子

@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

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

我将删除此页面中的所有内容,只保留 @page "/" 指令。Blazor 路由引擎使用它来确定我们将使用什么路由来访问此页面。随着应用程序的成型,我将更深入地回顾路由。

我要添加的第一件事将是我们用于执行基本 HTTP GET 操作的 HTML 元素。我将添加一个 textbox 和一个 button 来触发 get 请求。这是我首先要添加的内容。

@page "/"

<input type="text" placeholder="Enter address to search for" />
<button>Go</button>

如果我现在运行该应用程序,我得到的,嗯,我得到的不是我所期望的。我得到以下异常。

我看到这个的原因是因为我忘了 HttpClient 在 Blazor 应用程序中不会自动注册。我现在必须做的是添加 http 客户端。回到我的 Startup 类,我将增强我的 GetRequest 注册,以包含相关的注册,如下所示:

services.AddHttpClient().AddScoped<GetRequest>();

我现在应该能够运行应用程序并看到我期望看到的 textboxbutton

既然我知道页面可以显示,我就准备将注意力转向实际调用 GetRequest 类。由于我使用依赖注入来注入 GetRequest 操作,因此我还必须将其添加到页面中。我将通过使用另一个 @ 指令来执行某个操作来实现这一点。在页面中,我只需添加以下内容即可注入相关类:

@inject Goldlight.Xlcr.Core.GetRequest Get

这个 @inject 语句的意思是,我已经将 GetRequest 的一个实例注入到我的页面中,存储在一个名为 Get 的变量中。

显然,现在,我没有什么可以将“Go”按钮连接到 Execute 方法。在 razor 页面中,我将添加一个被 @code { } 包围的部分。这将允许我直接在网页上添加 C# 代码;这是 Blazor 的一项相当智能的功能。但在添加任何代码之前,我必须考虑如何进行搜索。我有一个 textbox,所以如果我能将 textbox 的值直接绑定到一个属性,那将很有用。我还想创建一个由按钮单击触发的异步操作。

这就是这个 @code 部分的外观

@code {
  private string searchAddress;
  private async Task ExecuteGet()
  {
      HttpResponseMessage result = await Get.Execute(searchAddress);
  }
}

显然,我想将它连接到 textbox 和 button。为了将 searchAddress 连接到 textbox,我将使用 @bind 声明来告诉 Blazor 我想将 textbox 输入绑定到什么。类似地,我将使用 @onclick 声明将按钮的 click 事件绑定到 ExecuteGet 方法。

整个 razor 页面现在看起来是这样的:

@page "/"

@inject Goldlight.Xlcr.Core.GetRequest Get

<input type="text" placeholder="Enter address to search for" @bind="searchAddress" />
<button @onclick="ExecuteGet">Go</button>

@code {
  private string searchAddress;
  private async Task ExecuteGet()
  {
      HttpResponseMessage result = await Get.Execute(searchAddress);
  }
}

我现在应该能够运行它并测试功能。目前我还看不到页面上的任何输出,但我可以在 HttpResponseMessage 行设置一个断点,以确保调用正常工作。

显示搜索结果对我们很有用。为此,我首先在我的代码部分中添加一个属性。由于这是搜索结果,我将称之为 searchResult。为了填充它,我将从 result 变量中获取内容作为 string,如下所示:

searchResult = await result.Content.ReadAsStringAsync();

最后,我需要一个地方来显示结果。为此,我将使用 div 语句,并使用如下所示的语法将 searchResult 绑定进来:

<div>@searchResult</div>

现在当我运行应用程序并搜索时,我看到以下内容:

看起来似乎我已经掌握了将 GetRequest 连接到页面的基本知识,但现在它不太友好。用户可以点击“Go”,即使 textbox 是空的,所以我想为此添加一些简单的检查。为此,我可以将按钮的 disabled 属性连接到 textbox 是否为空或包含空格,如下所示:

<button @onclick="ExecuteGet" disabled="@string.IsNullOrWhiteSpace(searchAddress)">Go</button>

现在让我来测试一下。太棒了,当我运行它时,按钮是禁用的。啊,有个问题。无论我在这个 textbox 中输入什么,按钮都保持禁用状态,而且我确信我的逻辑是正确的。当我将焦点从 textbox 移开时会发生什么?啊,按钮现在启用了。

发生这种情况的原因是绑定连接到 onchange 事件,该事件在 textbox 失去焦点之前不会触发。我实际想要的是在文本更改时进行评估,所以我必须将绑定连接到 oninput 事件。幸运的是,Blazor 提供了一个 bind:event 属性,可以让我做到这一点。有了这个调整,我的代码现在看起来是这样的:

@page "/"

@inject Goldlight.Xlcr.Core.GetRequest Get

<input type="text" placeholder="Enter address to search for" 
 @bind="searchAddress" @bind:event="oninput" />
<button @onclick="ExecuteGet" 
 disabled="@string.IsNullOrWhiteSpace(searchAddress)">Go</button>
<div>@searchResult</div>

@code {
  private string searchAddress;
  private string searchResult;
  private async Task ExecuteGet()
  {
      HttpResponseMessage result = await Get.Execute(searchAddress);
      searchResult = await result.Content.ReadAsStringAsync();
  }
}

现在,当我运行应用程序时,只要 textbox 中有一个字符,按钮就会启用,当它为空时,按钮就会禁用。我知道这是一个非常简单的实现,因为这个框里可能存在非常糟糕的内容,但目前来说足够了。

添加样式

我现在不太满意的是 textbox 和它旁边的按钮的布局。我希望它们能填满内容区域的宽度。我想做的是为布局的这一部分应用 CSS,使其工作方式符合我的意愿。Blazor 使用 FlexBox 布局,所以我可以利用这一点来控制页面的外观。我需要做的第一件事是将输入和按钮字段包装到 div 元素中。我将添加一个具有 d-flex 类的外部 div,这会创建一个简单的 flexbox 容器。我之所以想这样做,是因为我希望 textbox 和按钮并排显示。

在放置了简单的容器后,我将把按钮包装到它自己的 div 中,并将输入包装到另一个 div 中。输入 div 需要“flex”来填充空间。由于这是唯一需要增长以填充容器的项,我将使用一个带有 flex-grow-1 类的技巧。这个快捷方式基本上说明了容器会以什么比例与其他灵活项相比而增长。默认情况下,flex 是 0,因此,由于我们没有为按钮设置显式的 flex 大小,我们将填充可用空间。

我运行了应用程序,并意识到输入文本框仍然没有拉伸。虽然输入框的大小似乎是正确的,但实际上它只和输入文本一样大。我现在想做的是覆盖输入框的宽度。我不是特别喜欢直接在元素上添加样式,所以我将创建一个 CSS 文件来存放样式。在添加样式表之前,我将为输入元素添加一个名为 searchclass,以便我可以在 CSS 文件中添加一个匹配的值。

为了将本地样式表添加到此视图,我需要添加一个 CSS 文件,该文件名称与 razor 文件名匹配,只是末尾带有 .css。我知道这句话听起来很令人困惑,但它非常简单。如果我添加一个名为 Index.razor.css 的文件,那么当我们点击 Index 页面时,它会自动作为资源添加。我只需要在这个文件中添加一个简单的样式,所以样式表看起来是这样的:

input.search {
  width: 100%;
}

现在,当我运行应用程序时,textbox 会如我所料地拉伸以填充可用空间。

实际的 razor 文件现在看起来是这样的:

@page "/"

@inject Goldlight.Xlcr.Core.GetRequest Get

<div class="d-flex">
  <div class="flex-grow-1">
    <input class="search" type="text" 
     placeholder="Enter address to search for" @bind="searchAddress" @bind:event="oninput" />
  </div>
  <div>
    <button @onclick="ExecuteGet" 
     disabled="@string.IsNullOrWhiteSpace(searchAddress)">Go</button>
  </div>
</div>
<div>@searchResult</div>

@code {
  private string searchAddress;
  private string searchResult;
  private async Task ExecuteGet()
  {
    HttpResponseMessage result = await Get.Execute(searchAddress);
    searchResult = await result.Content.ReadAsStringAsync();
  }
}

结论

现在我们有了一个非常基本的页面,可以让我们获取简单的 API。我选择在此处结束本文,以便您有时间消化 Blazor 并尝试这个简单的项目。在下一篇文章中,我将更深入地探讨 Blazor,并研究如何使用选项卡来允许我添加多个 HTTP 请求。我真心希望您和我一样享受这个系列,并且它能让您洞察我的思考过程。

历史

  • 2021年5月20日:初始版本
© . All rights reserved.