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

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2021 年 8 月 3 日

CPOL

17分钟阅读

viewsIcon

6244

一系列文章的第四部分,我们将在其中展示编写应用程序的整个思考过程。

引言

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

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

在这一系列文章中,我将带您了解我在开发应用程序时所做的思考。文章附带的代码将以“优缺点全暴露”的方式编写,以便您可以看到我如何将某个东西从最初的需求阶段一直开发到我可以放心地让别人使用的程度。这意味着文章将展示我所犯的每一个错误以及在构思想法时所采取的捷径。我不会声称自己是一个伟大的开发者,但我足够称职和有经验,这应该能帮助那些刚进入这个领域的人更早地克服敬畏心理,并建立自信。

场景设定

本文是 第一部分第二部分 的续篇,在其中我们介绍了 HTTP GET 操作的 MVP,以及 第三部分,我们在其中开始用 Blazor 创建我们的用户界面。

源代码

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

第三部分 的结论中,我曾提到我将继续开发用户界面,通过添加标签页来支持我添加多个 GET 操作。我在这里要告诉您一个小秘密,我最初的想法是自己编写这个控件。我只需要创建几个 Blazor 组件,一个代表选项卡控件,一个选项卡页面组件,我将将其作为子组件渲染到控件中。虽然这似乎是一个有趣的挑战,但我停下来问自己,还有其他什么用例需要考虑这个选项卡?当选项卡的宽度超过屏幕宽度时,我将如何将选项卡保持在单行?我在脑海中设想,我将需要将选项卡控件包装在一个 span 中并自己管理滚动?我将如何显示用于滚动到屏幕外选项卡的按钮?我将如何使其响应式?这是专业开发者的“肮脏秘密”,我做的第一件事就是使用 Google 搜索看看别人是如何解决这个问题的。如果可以的话,我不想重新发明轮子。

在查看 Google 搜索结果时,有几家组件供应商列出了他们的商业选项卡控件。由于我希望大家都能跟着我一起工作,我不希望强迫人们付费购买控件,因此我才想自己写一个。StackOverflow 上有一些建议,我正卷起袖子准备开始编码时,我看到了一个指向 MudBlazor 的链接。这个名字引起了我的兴趣,所以我跟着链接点进去,发现 MudBlazor 是一个开源项目,它将为我提供一个 选项卡控件 来使用。

请注意,我还可以尝试其他选项,从 MatBlazorfast-blazor

最终结果是,我将使用这个控件而不是“自己动手”。通常我会创建一个测试项目来试用这样的控件,但在这种情况下,我将尝试在现有代码上试用 MudBlazor。我之所以愿意这样做,有两个原因:

  1. 我目前还没有多少代码,所以对代码进行调整以适应这个控件不会是很大的投入。
  2. 我在 GitHub 上使用一个新分支进行工作。如果需要,我可以直接回滚代码,不会比之前更糟。

安装 MudBlazor

MudBlazor 有 全面的文档,但我会在这里包含这些步骤,以便您可以跟着我一起操作。

第一步是安装 MudBlazor 组件。现在,我可以从包管理器安装它,也可以从命令行安装。在这种情况下,由于我已经打开了解决方案,我将直接使用 NuGet 包管理器安装 MudBlazor 包。安装完成后,我就可以开始修改代码以使用 MudBlazor 了。

根据 MudBlazor 文档,我应该在 _Imports.razor 中添加一个 using 语句。如果您不熟悉 razor,这个文件用于添加可以在整个代码库中使用的 using 语句。我本可以用它来简化我在 Index.razor 中添加的 @inject Goldlight.Xlcr.Core.GetRequest 语句。我没有理由不遵循 MudBlazor 的文档,所以我会在 imports 文件中添加以下语句。

@using MudBlazor

MudBlazor 不使用 Bootstrap,所以我要从 _Host.cshtml 中删除以下几行。

<link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
<link href="css/site.css" rel="stylesheet" />

我用以下内容替换了这些样式:

<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" 
 rel="stylesheet" />
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />

在这个文件中,我最后需要做的是在关闭 body 标签之前添加对 MudBlazor JavaScript 代码的引用。我添加了如下内容:

<script src="_content/MudBlazor/MudBlazor.min.js"></script>

在我可以使用 MudBlazor 之前,我需要在 Startup.csConfigureServices 方法中添加对其服务的引用。

services.AddMudServices();

我想看看应用程序现在看起来怎么样,所以我要启动它。

嗯,这对我来说看起来不太对。根据文档,我需要在 app.razor 中添加对 MudThemeProvider 的引用,如下所示。

<MudThemeProvider />

重新设计应用程序

现在我的应用程序中已成功运行 MudBlazor,我想真正开始利用它。在 Index.razor 文件中,我使用了标准的 HTML 按钮。我将用 MudButton 替换它,这将为我提供一个主题按钮。

<MudButton Variant="Variant.Filled" Color="Color.Primary"
 @onclick="ExecuteGet" Disabled="@string.IsNullOrWhiteSpace(searchAddress)">Go</MudButton>

与标准的 HTML 按钮相比,有几个变化。首先,我们可以通过使用 Variant 来设置按钮的外观类型,以定义我们希望按钮显示为 Outlined(边框)、Text(文本)还是 Filled(填充)。然后,我们可以设置按钮显示的 Color 类型,是希望按钮使用 Primary(主要)样式、Secondary(次要)还是其他几种。

我将用 MudBlazor 的等效组件替换 textbox。它提供了更丰富的视觉效果和用户体验,但需要比类型更重大的更改。为了复制我们现在的内容,我将我的代码修改如下。

<MudTextField T="string" @bind-Value="searchAddress" 
 Placeholder="Enter address to search for" FullWidth="true"
 Variant="Variant.Outlined" Immediate="true" Margin="Margin.Dense" AutoFocus="true" />

您可能会认为我使用了试错法来找出我想要的这些值的组合,但实际上是我使用了 文本字段文档 来选择我想要的外观。Dense(紧凑)值的示例对我来说是一个很好的起点,相关的代码使我很容易选择我需要设置的这些值。MudTextField 是一个泛型类型,所以 T="string" 告诉代码泛型的类型是什么。通过设置bind-value到绑定的值,我大大简化了绑定属性;我使用 Immediate 属性来说明我希望绑定实时更新,而不是在失去焦点时更新。

就在我编写这个按钮时,我意识到我在原始输入组件中犯了一个愚蠢的错误。我使用了 bind 关键字绑定到 searchAddress 属性,并使用 bind:event 将更改检测设置为 oninput。虽然这在技术上是正确的,但它错过了 bind 关键字的目的。这个关键字封装了 bind-valuebind-value:event 关键字,将事件设置为 onchangebind-value 告诉代码绑定到什么,所以我应该在我的原始代码中使用 bind-valuebind-value:event="oninput"

更改布局

对我来说有趣的是,我现在选择了将这些字段包装在 div 语句中。由于 MudBlazor 提供了自己的布局系统,所以我应该研究它推荐的应用程序布局方式。我将着手更改字段的布局。我首先要做的就是删除 div 元素,并添加一个 MudContainer,它将包装页面上的所有内容。我碰巧知道这个容器使用 flexbox 来包装内容,所以当我在里面布局时,我将使用 flex 布局来确定这些项目如何排列在一行中。

我布局容器内内容的方式是使用 MudGrid,并在网格内将每个项目添加到 MudItem 中,在这里我可以控制每个项目在显示时的大小。我想让 textbox 占据 12 分中的 11 分,让按钮占据最后 1 分。我这样做的方式是设置每个项目的 xssmmdlg 大小。这些属性是指定当 flex 项目移动到相应屏幕大小时的行为的简写方式(这是当它在不同最大屏幕尺寸的不同设备上查看时,布局如何变得响应式的一部分)。

没有我们的元素,页面代码看起来是这样的:

<MudContainer>
  <MudGrid>
    <MudItem lg="11" md="11" sm="11" xs="11">
      <!-- Text field goes here -->
    </MudItem>
    <MudItem lg="1" md="1" sm="1" xs="1">
      <!-- Button goes here --> 
    </MudItem>
    <MudItem lg="12" md="12" sm="12" xs="12">
      <!-- Search result goes here -->
    </MudItem>
  </MudGrid>
</MudContainer>

在将输入文本和按钮的标记添加到网格之前,我想谈谈我想要在搜索结果网格中看到的内容。我希望能够显示不仅仅是搜索内容。为了做到这一点,我将添加一个可展开的功能,使用 MudExpanders 布局来布局相关的 MudExpander 元素。我之所以有这个想法,是因为我想有一个地方可以显示发生的任何错误,而不仅仅是把错误记录到控制台,所以我的计划是有一个第二个展开器,显示错误和其他有用的日志信息。

我选择将结果内容显示在 readonly MudTextField 中,因为我想让搜索结果整齐地约束在包含的扩展面板中。我也可以选择在这里使用一个 div 元素,如果发生溢出就设置滚动条,但我在这里选择最简单的选项,因为这样我可以避免处理组件的样式。

几乎不费吹灰之力,我现在得到了以下布局。

<MudContainer>
  <MudGrid>
    <MudItem lg="11" md="11" sm="11" xs="11">
      <MudTextField T="string" @bind-Value="searchAddress" 
       Placeholder="Enter address to search for" FullWidth="true"
       Variant="Variant.Outlined" Immediate="true" 
       Margin="Margin.Dense" AutoFocus="true" />
    </MudItem>
    <MudItem lg="1" md="1" sm="1" xs="1">
      <MudButton Variant="Variant.Filled" Color="Color.Primary"
                 @onclick="ExecuteGet" 
                 Disabled="@string.IsNullOrWhiteSpace(searchAddress)">Go</MudButton>

    </MudItem>
    <MudItem lg="12" md="12" sm="12" xs="12">
      <MudExpansionPanels>
        <MudExpansionPanel Text="Search result">
          <MudTextField T="string" Text="@searchResult" 
           Lines="10" ReadOnly="true" Variant="Variant.Text" />
        </MudExpansionPanel>
      </MudExpansionPanels>
    </MudItem>
  </MudGrid>
</MudContainer>

这些不是我想做的唯一布局更改。接下来我想处理的是实际布局页面的容器。页面的主布局是从 Shared/MainLayout.razor 控制的,如下所示:

@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <div class="main">
        <div class="top-row px-4">
            <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
        </div>

        <div class="content px-4">
            @Body
        </div>
    </div>
</div>

这个布局使导航菜单占据了侧边栏的全部宽度,标题则填充其余部分。正如您现在已经发现的,当屏幕尺寸低于某个宽度时,侧边栏会折叠。我必须问自己的问题是,我将如何改变这个布局来使用 MudBlazor 作为容器系统?

由于我正在创建布局,文档很清楚地表明我需要使用 MudLayout 系统。我将根据 文档 中提供的布局模板之一来构建我的布局,特别是“带剪裁抽屉的顶部应用栏”。我之所以决定基于这个模板,是因为我更喜欢让标题始终拉伸整个宽度。

@inherits LayoutComponentBase

<MudLayout>
  <MudAppBar Elevation="2">
    <MudIconButton Icon="@Icons.Material.Filled.Menu" 
     Color="Color.Inherit" Edge="Edge.Start" 
                   OnClick="@((e) => drawerOpen = !drawerOpen)" />
    <MudText Typo="Typo.h6" Class="ml-3">Goldlight Xlcr</MudText>
  </MudAppBar>
  <MudDrawer @bind-Open="drawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2">
   <NavMenu />
  </MudDrawer>
  <MudMainContent>
    @Body
  </MudMainContent>
</MudLayout>

@code {
  bool drawerOpen = true;
}

我决定尝试应用栏的许多不同高度。这个值指的是组件在 z 轴上的高度,所以它会影响显示的阴影。我不会在每次进行更改时都停止和启动应用程序,而是将从命令行运行 Blazor 应用程序,并设置它来监视我所做的任何更改。这样做会触发重建和应用程序的重新显示。

最终,我选择了 6 作为 Elevation 属性的满意值。应用程序现在看起来像这样:

导航菜单现在需要一些调整,所以是时候转向那部分了,这意味着要更改 NavMenu.razor。我现在不打算列出这个文件看起来是什么样子,因为我将删除几乎所有内容,并用新的导航菜单替换它。作为我更改的一部分,我将简化条目,只留下“Home”链接。我的新导航菜单就像这样简单:

<MudNavMenu>
  <MudNavLink Match="NavLinkMatch.All">Home</MudNavLink>
</MudNavMenu>

更新后的应用程序现在看起来像这样:

在我看来,这比我原来的主题看起来好多了。我很高兴能以此作为添加选项卡栏的起点。现在,我的 Index.razor 文件承担了很多繁重的工作,包含了 get 功能的逻辑和布局。我现在想将这些逻辑从 Index 文件移出,放到它自己的组件中。我喜欢将我的组件放在一起,所以我要创建一个 components 文件夹来存放我创建的任何组件。我喜欢将我的组件放在一起的原因是,这有助于我一目了然地可视化我的代码在哪里。如果我在一个 razor 页面中看到对其中组件的引用,我就确切地知道在哪里找到它。

我要添加的组件类型是 Razor 组件,我将无趣地将其命名为 RequestLayout。在这个组件中,我将复制 Index.razor 文件中的大部分内容,因此布局文件最终看起来像这样:

@inject Goldlight.Xlcr.Core.GetRequest Get
<MudGrid>
  <MudItem lg="11" md="11" sm="11" xs="11">
    <MudTextField T="string" @bind-Value="searchAddress" 
     Placeholder="Enter address to search for" FullWidth="true"
     Variant="Variant.Outlined" Immediate="true" 
     Margin="Margin.Dense" AutoFocus="true" />
  </MudItem>
  <MudItem lg="1" md="1" sm="1" xs="1">
    <MudButton Variant="Variant.Filled" Color="Color.Primary"
               @onclick="ExecuteGet" 
               Disabled="@string.IsNullOrWhiteSpace(searchAddress)">Go</MudButton>

  </MudItem>
  <MudItem lg="12" md="12" sm="12" xs="12">
    <MudExpansionPanels>
      <MudExpansionPanel Text="Search result">
        <MudTextField T="string" Text="@searchResult" 
         Lines="10" ReadOnly="true" Variant="Variant.Text" />
      </MudExpansionPanel>
    </MudExpansionPanels>
  </MudItem>
</MudGrid>

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

为了让我在添加和引用组件时更轻松,我将用以下内容更新 _Imports.razor 文件:

@using Goldlight.Xlcr.Ui.Components

显然,我需要从 Index.razor 文件中删除我已复制到组件中的代码,这样我的 index 内容就变成这样了。

@page "/"
<MudContainer>
  <RequestLayout />
</MudContainer>

我不得不承认,我知道这并不是我最终想要的结果。我想在布局中添加选项卡,所以基本上我有两个选择。我要么在容器内添加选项卡,要么添加另一个组件,我将在其中引用。如果我选择另一个组件,那么 RequestLayout 将移动到那个新组件中。

向显示添加选项卡

我花了一些时间离开文章来仔细考虑我在这里想要采取的方法,我是否想将选项卡放在它自己的组件中。我现在脑海中想的是,我想让应用程序能够保存选项卡,这样用户就可以在应用程序再次打开时返回到打开的选项卡。我在这里考虑的是,我想将选项卡分割到它自己的组件中,以便我可以为选项卡应用单一责任。

我将添加一个名为 AppTabs 的组件。它将用于保存选项卡结构。最基本的结构将是这样的:

<MudTabs>
  <MudTabPanel Text="GET">
    <RequestLayout />
  </MudTabPanel>
</MudTabs>

由于我已经将 RequestLayout 组件移到了这里,所以我可以在 Index.razor 中显示选项卡组件。我现在有一个选项卡界面,即使它现在只显示一个选项卡。

现在我可以看到一个选项卡,如果我能动态添加选项卡就很有用了。为了实现这一点,我将用一个固定的选项卡替换现有的面板,该选项卡允许我添加选项卡。

<MudTabs>
  <MudTabPanel Text="Add request" Icon="@Icons.Material.Filled.Add" 
   ID='"AddRequestId"'></MudTabPanel>
</MudTabs>

目前,这实际上并没有添加任何选项卡,所以需要决定一个添加选项卡的方法。我在这里的想法是,我需要一种方法来增加选项卡的数量,并将它们渲染到面板中。最简单的方法是使用一个计数器来跟踪我需要显示的选项卡数量,并使用循环来渲染选项卡。在我看来,我只需要在我的页面上添加一个简单的变量,并在每次点击 **Add request** 按钮时将其递增。让我添加代码部分来递增我的选项卡计数。

@code {
  private int tabs = 0;

  private void AddTab()
  {
    tabs++;
  }
}

虽然我已经添加了递增选项卡数量的代码,但我还没有将其连接到 **Add Request** 按钮。要做到这一点,我只需要添加一个 OnClick 处理程序来告诉按钮在按下时触发什么操作。添加处理程序时,我必须记住,我只需要提供函数名,不需要括号。我的代码现在看起来像这样:

<MudTabs>
  <MudTabPanel OnClick="AddTab" Text="Add request" 
               Icon="@Icons.Material.Filled.Add" ID='"AddRequestId"'>
  </MudTabPanel>
</MudTabs>

我现在几乎完成了。我需要动态渲染选项卡,所以所有我需要做的就是添加一个 for 循环,使用 tabs 变量来控制我想要显示的选项卡数量。当我添加这个循环时,我的代码是这样的:

<MudTabs>
  <MudTabPanel OnClick="AddTab" Text="Add request" 
               Icon="@Icons.Material.Filled.Add" ID='"AddRequestId"'>
  </MudTabPanel>
  @for (int i = 0; i < tabs; i++)
  {
     <MudTabPanel Text="GET">
      <RequestLayout />
    </MudTabPanel>
  }
</MudTabs>

@code {
  private int tabs = 0;

  private void AddTab()
  {
    tabs++;
  }
}

是时候对应用程序进行一次试驾了。这张截图显示了应用程序启动时的样子。

如果我添加几个选项卡,如果我的逻辑是正确的,这应该会反映在显示中。

到目前为止,这似乎和我预期的一样。如果我选择一个选项卡,我就可以像预期的那样使用 GET 请求。

实际上,这并不完全正确。我还没有考虑到的是,当我切换到另一个选项卡时,该选项卡不会保持选项卡实例的活动状态。这意味着,我执行操作,然后得到结果。当我切换选项卡,然后回到我刚刚执行 get 操作的那个选项卡时,我丢失了所有细节。这显然不是我想要的行为,并且表明我在选择使用简单的计数器来跟踪选项卡时可能犯了错误。让我回去看看选项卡文档,看看他们是怎么说的。如果没有什么简单的方法可以在选项卡切换之间维护状态,那么我将不得不改变我添加和管理选项卡的方法。

好的,他们已经考虑到了这一点,我只需要在我的 MudTabs 声明中添加一个简单的属性。有一个 KeepPanelsAlive 属性,默认设置为 false。如果我将其设置为 true,选项卡在切换时将被保留。

<MudTabs KeepPanelsAlive="true">
  <MudTabPanel OnClick="AddTab" Text="Add request" 
               Icon="@Icons.Material.Filled.Add" ID='"AddRequestId"'>
  </MudTabPanel>
  @for (int i = 0; i < tabs; i++)
  {
     <MudTabPanel Text="GET">
      <RequestLayout />
    </MudTabPanel>
  }
</MudTabs>

@code {
  private int tabs = 0;

  private void AddTab()
  {
    tabs++;
  }
}

现在,当我重新运行应用程序时,get 操作将在选项卡更改之间得到保留。应用程序的可用性还没有达到我想要的高度,但我们在动态显示操作方面已经走了很长一段路。我本可以有很多不同的方法来渲染选项卡,但我在这里遵循了 TDD 的精神,使用最简单的代码来添加选项卡。我毫不怀疑这段代码会变得更复杂,但正如您在这里看到的,简单的解决方案是理想的起点,因为随着我们引入更复杂的需要,我可以逐渐增加复杂性。在下一篇文章中,我将退一步,看看如何处理更多 HTTP 动词,现在我已经知道我可以在我的 Blazor 应用程序中处理 GET 操作了。

历史

  • 2021 年 8 月 3 日:初始版本
© . All rights reserved.