天天看點

在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

繼續閱讀