目錄
介紹
存儲庫和資料庫
設計理念
資料
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篇文章描述了所使用的架構和編碼模式的各個方面:
- 項目結構和架構——一些介紹。
- 服務——建構CRUD資料層。
- 檢視元件——UI中的CRUD編輯和檢視操作。
- UI元件——建構 HTML/CSS控件。
- 檢視元件——UI中的CRUD清單操作。
這些文章自最初釋出以來發生了巨大變化:
- 整個架構不那麼固執己見。對于Blazor/SPA中的某些問題,我已經放棄了許多更激進的方法。
- 随着我對如何使Server和WASM項目共存的了解不斷增長,這些庫已經重新組織。
- 一切都已更新到Net5。
- Repo home 已經搬走了。
- 伺服器和WASM SPA現在托管并從同一站點運作。
他們不是:
- 試圖定義最佳實踐。
- 成熟産品。
第一篇文章概述了架構和解決方案架構。
存儲庫和資料庫
文章的存儲庫已移至Blazor.Database存儲庫。所有以前的存儲庫都已過時,很快就會被删除。
存儲庫中的/SQL中有一個用于建構資料庫的SQL腳本。
示範站點已更改,現在伺服器和WASM已合并。該站點以伺服器模式啟動——https://cec-blazor-server.azurewebsites.net/。
設計理念
資料
該項目的資料方面有三層——資料層,邏輯層和表示層——模型上松散地建構。資料層實作标準CRUDL——建立/讀取/更新/删除/清單——針對資料庫實體的操作。
應用程式庫包含兩個DbContext類:
- LocalWeatherDbContext使用标準SQL資料庫,連接配接字元串在AppSettings中定義。
- InMemoryWeatherDbContext 使用記憶體中SQLite資料庫進行測試和示範。
這些DbContext服務是通過AddDBContextFactory服務擴充實作的 DBContextFactory建立的。資料服務使用它的IDbContextFactory<TDbContext>接口。
基礎資料層由IFactoryDataService接口定義,以及接口的FactoryDataService抽象實作。有三個通用資料服務實作了大部分樣闆代碼:
- FactoryServerDataService對于普通的SQL資料庫。這都是Async,并為每個事務使用IDbContextFactory來獲得DbContext的執行個體。
- FactoryServerInMemoryDataService。 SQLite In-Memory資料庫隻能存在于單個DbContext。此資料服務在啟動時建立單個DbContext執行個體并将其用于所有事務。
- 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——就像桌面應用程式一樣。
我将在這些文章中使用以下術語:
- Page——網站上的啟動網頁。SPA中唯一的頁面。
- RouteView/Routed Compnent。這些都是描述僞頁面的各種人使用的所有術語。我使用術語RouteViews。這是布局的内容部分中顯示的内容,通常由定義的路由确定。我們将在本文後面更詳細地介紹這些。
- Forms。表單是顯示在視圖或模式對話框中的控件的邏輯集合。清單、檢視表單、編輯表單都是經典的表單。表單包含控件而不是HTML。
- Controls。控件是顯示某些内容的元件:它們發出HTML代碼。例如,一個編輯框、一個下拉菜單、一個按鈕……一個表單是一個控件的集合。
該應用程式被配置為建構和部署SPA的伺服器和WASM版本,并将兩者托管在同一網站上。基本解決方案架構是:
- 核心Razor庫——包含可以部署到任何應用程式的代碼。這些可以作為Nuget包建構和部署。
- Web Assembly Razor庫——包含SPA的應用程式特定代碼以及Web Assembly特定代碼。
- ASPNetCore Razor Web項目。包含WASM和伺服器SPA的啟動頁面、Blazor伺服器中心的服務和WASM SPA的伺服器端API控制器的宿主項目。
解決方案結構
我使用Visual Studio,是以Github存儲庫包含一個包含五個項目的解決方案。這些都是:
- Blazor.SPA——包含可以在任何項目中進行樣闆化和重用的所有内容的核心庫。
- Blazor.Database——這是其他項目共享的 WASM/Server 庫。幾乎所有的項目代碼都在這裡。示例是EF DB上下文、模型類、特定于模型的CRUD元件、Bootstrap SCSS、視圖、表單……
- Blazor.Database.Web——主機ASPNetCore伺服器。
此時您可能已經注意到沒有Server項目。你不需要一個。
界面結構
該應用程式使用結構化的UI方法。這使得停止在應用程式中重複相同的舊Razor/Html标記、建構可重用元件以及将代碼從應用程式移動到庫中變得更加容易。
頁面
頁面是充當應用程式宿主的網頁。每個應用程式有一個。
路由視圖
RouteViews是加載到根App元件中的元件,通常由Router通過Layout加載。他們不必是。您可以編寫自己的視圖管理器,而不必使用布局。視圖的唯一兩個要求是:
- 它必須聲明為Razor元件。
- 它必須使用@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伺服器啟動頁面。注意:
- 樣式表引用自定義CSS群組件CSS。
- blazor.server.js檔案腳本引用。
- 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的伺服器版本。
- 相同的CSS引用和伺服器檔案。
- 相同的site.js。
- 該<base href>組的WASM子目錄。
- 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庫服務。它:
- 添加Blazor伺服器端服務
- 根據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項目實作的抽象級别。下一部分着眼于服務和實作資料層。
需要注意的一些關鍵點:
- 您可以使用Server和WASM項目的通用代碼建構您的代碼。您可以小心地編寫一個可以像本項目一樣部署的應用程式。
- WASM和Server都可以在同一個網站上運作,你可以在兩者之間切換。
- 對術語要非常小心。了解“頁面”的不同含義。
如果您在未來閱讀本文,請檢視存儲庫中的自述檔案以擷取文章集的最新版本。
Building a Database Application in Blazor - Part 1 - Project Structure and Framework - CodeProject