使用Blazor构建CRUD项目 Blazor CRUD 使用 构建 项目
在小公司中,往往没有一个前后端分离的大型团队,去各司其职的负责构建web应用程序。面对比较简单的需求,可能所谓团队只有一个人,既要开发前端又要开发后端。
如果能有一项技术,能够前后端通吃,并且具备非常高的开发效率,那就非常适合小公司的小型项目的小型甚至一人团队来使用了。
aspdotnet就是这样高效的后端开发框架,而有了blazor后,C#前端也可以通吃了,真正做到了一套框架,一种语言,前后端通吃。
本文使用aspdotnet + blazor,快速构建了一个CRUD项目。
1. 新建项目
新的Blazor Web App,可以同时使用Blazor Server和Blazor WebAssembly两种渲染模式
勾上sample pages
在生成的解决方案中,有两个项目
后面.Client的,就是WebAssembly的部分,这一部分只需要关注Pages里的页面。当用户访问这个页面时,就是WebAssembly,于是就可以离线操作页面。
如果页面功能不涉及前后台数据交互,则可以使用WebAssembly模式。
例如,问卷调查、考试,从后台获取数据,前提渲染出题目后,就是答题的过程。知道用户提交答案之前,都不需要与后台又交互。这时候整个作答页面可以使用WebAssembly。
2. 添加数据库支持
给项目添加sqlite数据库支持
- 引入nuget包
- 编写DbContext类
- 编写Model类
- 运行Package Manager Console命令
新建DefaultDbContext.cs文件
using Microsoft.EntityFrameworkCore; using QuickCRUD.Models; namespace QuickCRUD; public class DefaultDbContext : DbContext { protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlite("Data Source=quick_crud.sqlite"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.ApplyConfigurationsFromAssembly(GetType().Assembly); } public DbSet<WeatherForecast> WeatherForecasts { get; set; } }
新建WeatherForecast.cs文件。里面除了模型类,还有一个Configuration类,用来模型与配置数据库中表和表字段的对应关系。
删除自动生成的实例代码里,Pages/Weather.razor中的相关内容。
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; namespace QuickCRUD.Models; public class WeatherForecast { public int Id { get; set; } public DateOnly Date { get; set; } public int TemperatureC { get; set; } public string? Summary { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); } public class WeatherForecastConfig : IEntityTypeConfiguration<WeatherForecast> { public void Configure(EntityTypeBuilder<WeatherForecast> builder) { builder.ToTable("weather_forcast"); builder.Property("Id").HasColumnName("obj_id").ValueGeneratedOnAdd(); builder.Property("Date").HasColumnName("ddate").HasColumnType("Text"); builder.Property("TemperatureC").HasColumnName("temp_c"); builder.Property("Summary").HasColumnName("summary"); } }
Package Manger Console,运行命令
Add-Migration Init
Update-Database
此时,将自动生成数据库与表结构
3. 编写Repo代码
Repo是直接与数据库打交道的代码,提供了基本的对数据库表的CRUD操作
为了操作数据库,注入了DbContext类
新建WeatherForecastRepo.cs文件,里面利用DbContext对象,编写增删改查数据库的基本操作方法:
using Microsoft.EntityFrameworkCore; using QuickCRUD.Models; namespace QuickCRUD.Repos; public class WeatherForecastRepo { private readonly DefaultDbContext _context; public WeatherForecastRepo(DefaultDbContext context) { _context = context; } public async Task<int> Add(WeatherForecast entity) { _context.WeatherForecasts.Add(entity); return await _context.SaveChangesAsync(); } public async Task<int> DeleteAll() { _context.WeatherForecasts.RemoveRange( _context.WeatherForecasts.Take(_context.WeatherForecasts.Count()) ); return await _context.SaveChangesAsync(); } public async Task<int> DeleteById(int id) { var w = await GetById(id); if (w != null) { _context.WeatherForecasts.Remove(w); } return await _context.SaveChangesAsync(); } public async Task<List<WeatherForecast>?> GetAll() { return await _context.WeatherForecasts.ToListAsync(); } public async Task<WeatherForecast?> GetById(int id) { return await _context.WeatherForecasts.FindAsync(id); } public async Task<int> Update(WeatherForecast entity) { var w = await GetById(entity.Id); if (w != null) { _context.WeatherForecasts.Update(entity); } return await _context.SaveChangesAsync(); } }
4. 编写前端list代码
- 引入QuickGrid包
- 编写前端list展示页面的component
- 编写add页面component
- 根据需要编写service文件
编写展示数据的list页面,在Pages文件夹下建立Weather.razor文件
@page "/weather" @rendermode InteractiveServer @inject WeatherForecastService weatherForecastService @inject NavigationManager nav <PageTitle>Weather</PageTitle> <h1>Weather</h1> <button class="btn btn-sm btn-outline-success" @onclick="BtnNew">New</button> <button class="btn btn-sm btn-danger" @onclick="BtnDeleteAll">Delete All</button> <button class="btn btn-sm btn-outline-info" @onclick="BtnGenerateRandomDate">Generate Random Date</button> @if (forecasts == null) { <p><em>Loading...</em></p> } else { <QuickGrid class="table" Items="forecasts.AsQueryable()"> <PropertyColumn Property="@(f=>f.Id)" /> <PropertyColumn Property="@(f=>f.Date)" /> <PropertyColumn Title="Temp.(C)" Property="@(f=>f.TemperatureC)" /> <PropertyColumn Title="Temp.(F)" Property="@(f=>f.TemperatureF)" /> <PropertyColumn Property="@(f=>f.Summary)" /> <TemplateColumn Context="f"> <button class="btn btn-sm btn-outline-info" @onclick="_=>BtnEdit(f.Id)">Edit</button> <button class="btn btn-sm btn-outline-danger" @onclick="_=>BtnDelete(f.Id)">Delete</button> </TemplateColumn> </QuickGrid> } @code { private List<WeatherForecast>? forecasts; protected override async Task OnInitializedAsync() { forecasts = await weatherForecastService.AllForecast(); } private void BtnNew() { nav.NavigateTo("/weather/add", true, true); } private void BtnEdit(int id) { nav.NavigateTo($"/weather/edit/{id}", true, true); } private async Task BtnDelete(int id) { await weatherForecastService.DeleteForecast(id); nav.Refresh(true); } private async Task BtnDeleteAll() { await weatherForecastService.DeleteAllForecast(); nav.Refresh(true); } private async Task BtnGenerateRandomDate() { await weatherForecastService.GenerateRandom(); nav.Refresh(true); } }
注入了WeatherForecastService
需要注意页面上方的@rendermode InteractiveServer,这个标注将使得页面在服务端进行渲染,这是必不可少的,因为我们使用的是service,里面注入了repo,而repo中使用的是EF,这就意味着service的代码必须在服务端运行,所以这个页面必须在服务端渲染完毕后,再在前端展示。如果我们的service选择使用HttpClient获取后端api接口数据,则可以使用Wasm模式,就像Count.razor页面。
5. 编写Service
前端页面当需要使用数据时,将注入service,service如果需要向数据库请求数据,则在service中注入repo
编写WeatherForecastService.cs文件
using QuickCRUD.Models; using QuickCRUD.Repos; namespace QuickCRUD.Services; public class WeatherForecastService { private readonly WeatherForecastRepo _repo; public WeatherForecastService(WeatherForecastRepo repo) { _repo = repo; } public async Task<WeatherForecast> GetById(int id) { var f = await _repo.GetById(id); if (f == null) return new(); return f; } public async Task<List<WeatherForecast>> AllForecast() { var result = await _repo.GetAll(); if (result == null) { return []; } else { return result; } } public async Task<int> NewForecast(WeatherForecast forecast) { if (forecast == null) return 0; return await _repo.Add(forecast); } public async Task<int> UpdateForecast(WeatherForecast forecast) { if (forecast == null) { return 0; } return await _repo.Update(forecast); } public async Task<int> DeleteForecast(int id) { return await _repo.DeleteById(id); } public async Task<int> DeleteAllForecast() { return await _repo.DeleteAll(); } public async Task<int> GenerateRandom() { var startDate = DateOnly.FromDateTime(DateTime.Now); var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; var forecasts = Enumerable.Range(1, 10).Select(index => new WeatherForecast { Date = startDate.AddDays(index), TemperatureC = Random.Shared.Next(-20, 55), Summary = summaries[Random.Shared.Next(summaries.Length)] }).ToList(); foreach (var forecast in forecasts) await _repo.Add(forecast); return forecasts.Count; } }
6. 编写add和edit子页面
新建WeatherAdd.razor文件
@page "/weather/add" @rendermode InteractiveServer @inject WeatherForecastService weatherForecastService @inject NavigationManager nav <h1>New Weather Forecast</h1> <EditForm Model="forecast" OnValidSubmit="SubmitForecast"> <p> <label> Date: <InputDate @bind-Value="forecast.Date" /> </label> </p> <p> <label> Temperature C: <InputNumber @bind-Value="forecast.TemperatureC" /> </label> </p> <p> <label> Summary: <InputText @bind-Value="forecast.Summary" /> </label> </p> <p> <button type="submit" class="btn btn-primary">Submit</button> </p> </EditForm> <button class="btn btn-outline-primary" @onclick="BtnCancel">Cancel</button> @code { private WeatherForecast forecast { get; set; } = new() { Date = DateOnly.FromDateTime(DateTime.Today) }; private async Task SubmitForecast() { await weatherForecastService.NewForecast(forecast); nav.NavigateTo("/weather", true, true); } private void BtnCancel() { nav.NavigateTo("/weather", true, true); } }
新建WeatherEdit.razor文件
@page "/weather/edit/{id:int}" @rendermode InteractiveServer @inject WeatherForecastService weatherForecastService @inject NavigationManager nav <h1>Edit Weather Forecast</h1> <h3>Id: @Id</h3> <EditForm Model="forecast" OnValidSubmit="SubmitForecast"> <p> <label> Date: <InputDate @bind-Value="forecast.Date" /> </label> </p> <p> <label> Temperature C: <InputNumber @bind-Value="forecast.TemperatureC" /> </label> </p> <p> <label> Summary: <InputText @bind-Value="forecast.Summary" /> </label> </p> <p> <button type="submit" class="btn btn-primary">Submit</button> </p> </EditForm> <button class="btn btn-outline-primary" @onclick="BtnCancel">Cancel</button> @code { [Parameter] public int Id { get; set; } private WeatherForecast forecast { get; set; } = new(); protected override async Task OnParametersSetAsync() { forecast = await weatherForecastService.GetById(Id); } private void BtnCancel() { nav.NavigateTo("/weather", true, true); } private async Task SubmitForecast() { await weatherForecastService.UpdateForecast(forecast); nav.NavigateTo("/weather", true, true); } }
edit页面与add页面的不同在于,需要传入id参数
7. 检查依赖注入
检查一下Program.cs文件中,是否将dbcontext,repo和service都配置了依赖注入
builder.Services.AddDbContext<DefaultDbContext>(); builder.Services.AddScoped<WeatherForecastRepo>(); builder.Services.AddScoped<WeatherForecastService>();
8. 效果展示
9. 发布
- 将sqlite数据库的文件编译属性调整为复制到输出目录
- publish参数
最终生成
至此,最简单的CRUD完成了
10. 利用泛型的Repo
目前的Repo需要逐个编写操作数据库的方法,如果新增了一个model,则需要对应添加一个repo类,并再次重新编写所有的CRUD方法。但是因为都是CRUD的标准化方法,可以通过接口和泛型,实现新的model类继承全部CRUD方法。
首先编写一个接口,新建ICRUD.cs
namespace QuickCRUD.Repos; public interface ICRUD<T, T_ID> { public int GetCount(); public Task<List<T>?> GetAll(); public Task<List<T>?> GetLimit(int num); public Task<T?> GetById(T_ID id); public Task<int> Add(T entity); public Task<int> Update(T entity, T_ID id); public Task<int> DeleteById(T_ID id); public Task<int> DeleteAll(); }
然后,编写一个抽象类,AbstractRepo.cs,再抽象类中,同泛型,实现全部接口
using Microsoft.EntityFrameworkCore; namespace QuickCRUD.Repos; public abstract class AbstractRepo<T, T_ID>(DefaultDbContext context) : ICRUD<T, T_ID> where T : class { public async Task<int> Add(T entity) { context.Set<T>().Add(entity); return await context.SaveChangesAsync(); } public async Task<int> DeleteAll() { context.Set<T>().RemoveRange( context.Set<T>().Take(context.Set<T>().Count()) ); return await context.SaveChangesAsync(); } public async Task<int> DeleteById(T_ID id) { var w = await GetById(id); if (w != null) { context.Set<T>().Remove(w); } return await context.SaveChangesAsync(); } public async Task<List<T>?> GetAll() { return await context.Set<T>().ToListAsync(); } public async Task<T?> GetById(T_ID id) { return await context.Set<T>().FindAsync(id); } public int GetCount() { return context.Set<T>().Count(); } public async Task<List<T>?> GetLimit(int num) { var result = context.Set<T>().Take(num); return await result.ToListAsync(); } public async Task<int> Update(T entity, T_ID id) { var w = await GetById(id); if (w != null) { context.Set<T>().Update(entity); } return await context.SaveChangesAsync(); } }
最后,修改WeatherForcastRepo.cs
using QuickCRUD.Models; namespace QuickCRUD.Repos; public class WeatherForecastRepo(DefaultDbContext context) : AbstractRepo<WeatherForecast, int>(context) { }
WeatherForcastRepo只需要继承抽象类,即可实现全部CRUD接口方法。如果有个性化的数据库操作方法,再在repo中添加方法即可。
如果有新的model,只需要创建一个新的repo,并继承AbstractRepo即可实现全部CRUD方法。
本文来自博客园,作者:HalloHerö,转载请注明原文链接:https://www.cnblogs.com/jimokelly/p/18142830