天天看点

在Blazor中构建数据库应用程序——第1部分——项目结构和框架介绍存储库和数据库设计理念解决方案结构界面结构Blazor.Database项目Blazor.Database.Web项目总结

目录

介绍

存储库和数据库

设计理念

数据

UI

解决方案结构

界面结构

页面

路由视图

布局

表单

控件

Blazor.Database项目

Program.cs

ServiceCollectionExtensions.cs

CSS

Blazor.Database.Web项目

CSS

页面

_Host.cshtml

_WASM.cshtml

Startup.cs

ServiceCollectionExtensions.cs

总结

介绍

这组文章描述了在Blazor中构建和构建数据库应用程序的框架。

这只是一个框架。我不提出任何建议:使用它或滥用它。这是我在我的项目中使用的。轻轻的固执己见:尽可能使用开箱即用的Blazor/Razor/DotNetCore系统和工具包。CSS框架是BootStrap的轻量定制版本。  

有5篇文章描述了所使用的框架和编码模式的各个方面:

  1. 项目结构和框架——一些介绍。
  2. 服务——构建CRUD数据层。
  3. 查看组件——UI中的CRUD编辑和查看操作。
  4. UI组件——构建 HTML/CSS控件。
  5. 查看组件——UI中的CRUD列表操作。

这些文章自最初发布以来发生了巨大变化:

  1. 整个框架不那么固执己见。对于Blazor/SPA中的某些问题,我已经放弃了许多更激进的方法。
  2. 随着我对如何使Server和WASM项目共存的理解不断增长,这些库已经重新组织。
  3. 一切都已更新到Net5。
  4. Repo home 已经搬走了。
  5. 服务器和WASM SPA现在托管并从同一站点运行。

他们不是:

  1. 试图定义最佳实践。
  2. 成熟产品。

第一篇文章概述了框架和解决方案架构。

存储库和数据库

文章的存储库已移至Blazor.Database存储库。所有以前的存储库都已过时,很快就会被删除。

存储库中的/SQL中有一个用于构建数据库的SQL脚本。

演示站点已更改,现在服务器和WASM已合并。该站点以服务器模式启动——https://cec-blazor-server.azurewebsites.net/。

设计理念

数据

该项目的数据方面有三层——数据层,逻辑层和表示层——模型上松散地构建。数据层实现标准CRUDL——创建/读取/更新/删除/列表——针对数据库实体的操作。

应用程序库包含两个DbContext类:

  1. LocalWeatherDbContext使用标准SQL数据库,连接字符串在AppSettings中定义。
  2. InMemoryWeatherDbContext 使用内存中SQLite数据库进行测试和演示。

这些DbContext服务是通过AddDBContextFactory服务扩展实现的 DBContextFactory创建的。数据服务使用它的IDbContextFactory<TDbContext>接口。

基础数据层由IFactoryDataService接口定义,以及接口的FactoryDataService抽象实现。有三个通用数据服务实现了大部分样板代码:

  1. FactoryServerDataService对于普通的SQL数据库。这都是Async,并为每个事务使用IDbContextFactory来获得DbContext的实例。
  2. FactoryServerInMemoryDataService。 SQLite In-Memory数据库只能存在于单个DbContext。此数据服务在启动时创建单个DbContext实例并将其用于所有事务。
  3. FactoryWASMDataService用于WASM SPA。此数据服务对API服务器控制器进行远程API调用。

为了演示通用服务中实现的样板化级别,本地数据库数据服务的声明如下所示:

public class LocalDatabaseDataService : FactoryServerDataService<LocalWeatherDbContext>
{
    public LocalDatabaseDataService(IConfiguration configuration, IDbContextFactory<LocalWeatherDbContext> dbContext) : base(configuration, dbContext) {}
}
           

和WeatherForecast数据类的控制器服务:

public class WeatherForecastControllerService : FactoryControllerService<WeatherForecast>
{
    public WeatherForecastControllerService(IFactoryDataService factoryDataService) : base(factoryDataService) { }
}
           

我们将在第二篇文章中详细介绍这些,以及Weather应用程序的具体实现。

UI界面层,我称之为控制器服务,是由IFactoryControllerService和FactoryControllerService中的基本抽象实现定义的。我们将在第二篇和第三篇文章中再次详细讨论这些。

数据服务是通过依赖注入直接或通过它们的接口访问的。

UI

我在SPA中使用Pages这个词有点问题。我认为这是Microsoft(和其他SPA框架)可以引入一些新术语的一个领域。我只将Pages目录用于真实的网页。SPA不是网站,因此要了解它们的工作原理,我们需要跳出网页范式。Blazor UI是基于组件的;认为它包含页面会使范式永久化。唯一的网页是服务器上的启动页面。SPA启动后,应用程序会更改组件以在视图之间进行转换。我构建了一个没有路由器或URL的SPA——就像桌面应用程序一样。

我将在这些文章中使用以下术语:

  1. Page——网站上的启动网页。SPA中唯一的页面。
  2. RouteView/Routed Compnent。这些都是描述伪页面的各种人使用的所有术语。我使用术语RouteViews。这是布局的内容部分中显示的内容,通常由定义的路由确定。我们将在本文后面更详细地介绍这些。
  3. Forms。表单是显示在视图或模式对话框中的控件的逻辑集合。列表、查看表单、编辑表单都是经典的表单。表单包含控件而不是HTML。
  4. Controls。控件是显示某些内容的组件:它们发出HTML代码。例如,一个编辑框、一个下拉菜单、一个按钮……一个表单是一个控件的集合。

该应用程序被配置为构建和部署SPA的服务器和WASM版本,并将两者托管在同一网站上。基本解决方案架构是:

  1. 核心Razor库——包含可以部署到任何应用程序的代码。这些可以作为Nuget包构建和部署。
  2. Web Assembly Razor库——包含SPA的应用程序特定代码以及Web Assembly特定代码。
  3. ASPNetCore Razor Web项目。包含WASM和服务器SPA的启动页面、Blazor服务器中心的服务和WASM SPA的服务器端API控制器的宿主项目。

解决方案结构

我使用Visual Studio,因此Github存储库包含一个包含五个项目的解决方案。这些都是:

  1. Blazor.SPA——包含可以在任何项目中进行样板化和重用的所有内容的核心库。
  2. Blazor.Database——这是其他项目共享的 WASM/Server 库。几乎所有的项目代码都在这里。示例是EF DB上下文、模型类、特定于模型的CRUD组件、Bootstrap SCSS、视图、表单……
  3. Blazor.Database.Web——主机ASPNetCore服务器。

此时您可能已经注意到没有Server项目。你不需要一个。

界面结构

该应用程序使用结构化的UI方法。这使得停止在应用程序中重复相同的旧Razor/Html标记、构建可重用组件以及将代码从应用程序移动到库中变得更加容易。

页面

页面是充当应用程序宿主的网页。每个应用程序有一个。

路由视图

RouteViews是加载到根App组件中的组件,通常由Router通过Layout加载。他们不必是。您可以编写自己的视图管理器,而不必使用布局。视图的唯一两个要求是:

  1. 它必须声明为Razor组件。
  2. 它必须使用@page指令声明一个或多个路由。

该FetchData视图被声明为如下所示。Razor makrup是最小的,只是WeatherForecastListForm.。该代码处理在列表表单中的各种操作上发生的事情。  

@page "/fetchdata"
<WeatherForecastListForm EditRecord="this.GoToEditor" ViewRecord="this.GoToViewer" NewRecord="this.GoToNew" ExitAction="Exit"></WeatherForecastListForm>

@code {
    [Inject] NavigationManager NavManager { get; set; }
    private bool _isWasm => NavManager?.Uri.Contains("wasm", StringComparison.CurrentCultureIgnoreCase) ?? false;

    public void GoToEditor(int id)
    => this.NavManager.NavigateTo($"weather/edit/{id}");

    public void GoToNew()
    => this.NavManager.NavigateTo($"weather/edit/-1");

    public void GoToViewer(int id)
    => this.NavManager.NavigateTo($"weather/view/{id}");

    public void Exit()
    {
        if (_isWasm)
            this.NavManager.NavigateTo($"/wasm");
        else
            this.NavManager.NavigateTo($"/");
    }
}
           

RouteView的目的是声明Router组件在SPA启动时可以找到的路由。根组件App如下所示,它声明了Router组件。AppAssembly="@typeof(WeatherApp).Assembly"将路由器指向它浏览的程序集以查找路由声明。在这种情况下,它指向包含根组件的程序集。

<Router AppAssembly="@typeof(WeatherApp).Assembly" PreferExactMatches="@true">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(WASMLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(WASMLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>
           

请注意,显示组件在RouteViews来自的地方被RouteView调用。

布局

布局是开箱即用的Blazor布局。路由器以RouteView为子内容呈现布局。定义中Router定义了一个默认Layout。我将跳过这里的布局,我们已经拥有它们很长时间了,并且它们在其他地方得到了充分的介绍。

表单

表单是组件层次结构中的中级单元。RouteViews包含一个或多个表单。

表单是显示在视图或模式对话框中的控件的逻辑集合。列表、查看表单、编辑表单都是经典的表单。表单包含控件而不是Html。

控件

控件是低级组件。它是构建Html代码的地方。控件可以包含其他控件以构建更复杂的控件。

您在Razor组件中重复相同的Html代码的频率如何。您在Razor中所做的事情是您梦想在C#代码中做不到的。你会写一个辅助方法。为什么不在组件中做同样的事情。

// mylist.razor
<td class="px-1 py-2">xxxx</td>
....  10 times
           

可能看起来更复杂:

// UiListRow.razor
<td class="px-1 py-2">@childContent</td>
           

并且:

// mylist.razor
<UiListRow>xxxx</UiListRow>
....  10 times
           

但是在整个应用程序中更改填充在组件方法中很简单,并且在标记中很痛苦。

// UiListRow.razor
<td class="px-1 py-1">@childContent</td>
           

Blazor.Database项目

Blazor.Database项目包含所有项目特定的Blazor代码以及WASM应用程序的启动代码和Web程序集代码。

Program.cs

Program 是WASM应用程序的入口点,包含服务定义和对根组件的引用。

public static async Task Main(string[] args)
{
    var builder = WebAssemblyHostBuilder.CreateDefault(args);
    builder.RootComponents.Add<WeatherApp>("#app");

    builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
    builder.Services.AddWASMApplicationServices();

    await builder.Build().RunAsync();
}
           

每个项目/库的服务在IServiceCollection扩展中指定。

ServiceCollectionExtensions.cs

加载的站点特定服务是作为IFactoryDataService接口加载的控制器服务WeatherForecastControllerService和数据服务FactoryWASMDataService。

public static class ServiceCollectionExtensions
{
    public static IServiceCollection AddWASMApplicationServices(this IServiceCollection services)
    {
        services.AddScoped<IFactoryDataService, FactoryWASMDataService>();
        services.AddScoped<WeatherForecastControllerService>();
        return services;
    }
}
           

WASM项目StaticWebAssetBasePath的最终设置是在项目文件中设置。这将让我们在Web项目上一起运行WASM和服务器版本。

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

CSS

所有CSS都是共享的,因此存在于Blazor.Database.Web。我使用Bootstrap,用SASS进行了一些定制。我在Visual Studio中安装了WEB编译器扩展来即时编译SASS文件。

Blazor.Database.Web项目

CSS

该项目使用SCSS来构建自定义版本的Bootstrap,具有一些颜色和小的格式差异。

页面

我们有两个真实页面——启动Blazor服务器SPA的标准问题_Host.cshtml和启动WASM SPA的_WASM.cshtml。

_Host.cshtml

标准Blazor服务器启动页面。注意:

  1. 样式表引用自定义CSS和组件CSS。
  2. blazor.server.js文件脚本引用。
  3. component对根组件的引用——在本例中是Blazor.Database.Server.Components.WeatherApp。根组件位于Blazor.Database库中。  
@page "/"
@namespace Blazor.Database.Web.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Layout = null;
}

<!DOCTYPE html>
<html >
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Blazor.Database.Web</title>
    <base href="~/" target="_blank" rel="external nofollow"  />
    <link rel="stylesheet" href="css/site.min.css" target="_blank" rel="external nofollow"  />
    <link href="Blazor.Database.Web.styles.css" target="_blank" rel="external nofollow"  rel="stylesheet" />
    <link href="/wasm/Blazor.Database.styles.css" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  rel="stylesheet" />
</head>
<body>
    <component type="typeof(Blazor.Database.Components.WeatherApp)" render-mode="ServerPrerendered" />

    <div id="blazor-error-ui">
        <environment include="Staging,Production">
            An error has occurred.  This application may no longer respond until reloaded.
        </environment>
        <environment include="Development">
            An unhandled exception has occurred.  See browser dev tools for details.
        </environment>
        <a href="" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>

    <script src="/wasm/site.js"></script>
    <script src="_framework/blazor.server.js"></script>
</body>
</html>
           

_WASM.cshtml

这只是WASM *index.html的服务器版本。

  1. 相同的CSS引用和服务器文件。
  2. 相同的site.js。
  3. 该<base href>组的WASM子目录。
  4. blazor.webassembly.js引用到子目录。
@page "/WASM"
@namespace Blazor.Database.Web.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <title>Blazor.DataBase.WASM</title>
    <base href="/wasm/" target="_blank" rel="external nofollow"  />
    <link rel="stylesheet" href="/css/site.min.css" target="_blank" rel="external nofollow"  />
    <link href="/Blazor.Database.Web.styles.css" target="_blank" rel="external nofollow"  rel="stylesheet" />
    <link href="/wasm/Blazor.Database.styles.css" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  rel="stylesheet" />
</head>
<body>
    <div id="app">
        <div class="mt-4" style="margin-right:auto; margin-left:auto; width:100%;">
            <div class="loader"></div>
            <div style="width:100%; text-align:center;"><h4>Web Application Loading</h4></div>
        </div>
    </div>
    <div id="blazor-error-ui">
        An unhandled error has occurred.
        <a href="" target="_blank" rel="external nofollow"  target="_blank" rel="external nofollow"  class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    <script src="/wasm//site.js"></script>
    <script src="/wasm/_framework/blazor.webassembly.js"></script>
</body>
</html>
           

Startup.cs

添加了本地服务和Blazor.SPA库服务。它:

  1. 添加Blazor服务器端服务
  2. 根据Url配置两个中间件路径。
public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services){
        services.AddRazorPages();
        services.AddServerSideBlazor();
        services.AddControllersWithViews();

        // services.AddApplicationServices(this.Configuration);
        services.AddInMemoryApplicationServices(this.Configuration);
            
        // Server Side Blazor doesn't register HttpClient by default
        // Thanks to Robin Sue - Suchiman https://github.com/Suchiman/BlazorDualMode
        if (!services.Any(x => x.ServiceType == typeof(HttpClient)))
        {
            // Setup HttpClient for server side in a client side compatible fashion
            services.AddScoped<HttpClient>(s =>
            {
                // Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it.
                var uriHelper = s.GetRequiredService<NavigationManager>();
                return new HttpClient
                {
                    BaseAddress = new Uri(uriHelper.BaseUri)
                };
            });
        }
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/WASM"), app1 =>
        {
            app1.UseBlazorFrameworkFiles("/wasm");
            app1.UseRouting();
            app1.UseEndpoints(endpoints =>
            {
                endpoints.MapFallbackToPage("/wasm/{*path:nonfile}", "/_WASM");
            });
        });

        app.UseRouting();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllers();
            endpoints.MapBlazorHub();
            endpoints.MapRazorPages();
            endpoints.MapFallbackToPage("/Server/{*path:nonfile}","/_Host");
            endpoints.MapFallbackToPage("/_Host");
        });
    }
}
           

ServiceCollectionExtensions.cs

有两种服务集合扩展方法。一个用于普通SQL数据库,第二个用于测试内存数据库。  

public static IServiceCollection AddApplicationServices(this IServiceCollection services, IConfiguration configuration)
{

    // Local DB Setup
    var dbContext = configuration.GetValue<string>("Configuration:DBContext");
    services.AddDbContextFactory<LocalWeatherDbContext>(options => options.UseSqlServer(dbContext), ServiceLifetime.Singleton);
    services.AddSingleton<IFactoryDataService, LocalDatabaseDataService>();

    services.AddScoped<WeatherForecastControllerService>();

    return services;
}

public static IServiceCollection AddInMemoryApplicationServices(this IServiceCollection services, IConfiguration configuration)
{

    // In Memory DB Setup
    var memdbContext = "Data Source=:memory:";
    services.AddDbContextFactory<InMemoryWeatherDbContext>(options => options.UseSqlite(memdbContext), ServiceLifetime.Singleton);
    services.AddSingleton<IFactoryDataService, TestDatabaseDataService>();

    services.AddScoped<WeatherForecastControllerService>();

    return services;
}
           

总结

本节到此结束。这是一个概述,稍后会提供更多详细信息。希望它展示了您可以使用Blazor项目实现的抽象级别。下一部分着眼于服务和实现数据层。

需要注意的一些关键点:

  1. 您可以使用Server和WASM项目的通用代码构建您的代码。您可以小心地编写一个可以像本项目一样部署的应用程序。
  2. WASM和Server都可以在同一个网站上运行,你可以在两者之间切换。
  3. 对术语要非常小心。了解“页面”的不同含义。

如果您在未来阅读本文,请查看存储库中的自述文件以获取文章集的最新版本。

Building a Database Application in Blazor - Part 1 - Project Structure and Framework - CodeProject

继续阅读