Microsoft Blazor - 使用开源 Platz.SqlForms 快速开发 SQL 表单
如何使用开源 Platz.SqlForms 动态开发 Blazor 服务器应用程序并从 Entity Framework 生成 UI,包括主-明细数据录入
如果您有兴趣,可以阅读我关于 Blazor 动态表单的其他文章
即将发布的新博文:
Microsoft Blazor 开源 Platz.SqlForms - 使用 Schema Builder 设计和维护 SQL Server 数据库
Platz.SqlForms 官方路线图
### Release 0.3.0 - 当前版本
- SqlForms 自定义规则,用于更改字段属性(隐藏、必填、只读等)
SchemaDesigner
原型,允许设计数据库实体并将架构保存为 json- T4 模板,用于生成
SchemaDesigner
实体和数据访问层 - 升级
ObjectBuilder
以支持SchemaDesigner
实体
### Release 0.4.0 - 21 年 5 月
- UI SQL 表单,用于输入/编辑业务对象
ObjectBuilder
业务对象定义 - 实体映射和 CRUD 操作ObjectBuilder
T4 模板,用于生成业务对象 CRUD 操作的 C# 代码ObjectBuilder
Select
函数ObjectBuilder
Group
By
查询ObjectBuilder
子查询
### Release 0.5.0 - 21 年 6 月
- 构建器和设计器的可用性和错误数据恢复
- Bug 修复
### Release 1.0.0 - 21 年 8 月
- 支持文档
- 教程
- Bug 修复
1. 创建演示项目
1.1 DemoSqlForms.App
首先,使用 Visual Studio 2019 的“创建新项目”链接创建一个 Blazor Server App .NET 5.0 项目 DemoSqlForms.App
。
然后找到“Blazor App”模板,选择它,然后单击“下一步”按钮。
在下一个屏幕上,指定项目名称:DemoSqlForms.App
,解决方案名称:DemoSqlForms
,然后单击“创建”按钮。
现在选择“.NET 5.0”和“Blazor Server App”模板,然后单击“创建”按钮。
Visual Studio 将创建包含项目的解决方案。
我喜欢花一些时间删除示例页面(Counter
和 FetchData
)及其相关代码,但这并非必需。
1.2 Platz.SqlForms NuGet 包
现在我们需要添加 Platz.SqlForms
NuGet 包,右键单击解决方案项目,然后单击“管理 NuGet 程序包…”菜单,然后在 **浏览** 选项卡中,键入“Platz
”搜索模式,您将看到 Platz 程序包。选择 **Platz.SqlForms**,然后单击“安装”按钮。版本 0.2.0 和 0.2.1 包含错误,因此请使用 0.2.2 或更高版本。
安装后,您会看到一个 readme.txt 文件,其中包含简单的说明,请按照这些说明操作。
重要步骤是在 ConfigureServices
方法中添加 Platz.SqlForms
初始化逻辑
services.AddPlatzSqlForms();
1.3 数据库项目
为了演示如何使用 Platz.SqlForms
,我们需要创建一个数据库项目。
右键单击“DemoSqlForms”解决方案(解决方案资源管理器中的第一行),单击“添加”,然后单击“新建项目…”。
在“添加新项目”向导中,找到“类库 (.NET Core)”模板,选择它,然后单击“下一步”。
在“项目名称”中键入“DemoSqlForms.Database
”,然后单击“创建”。
Visual Studio 将创建新的类库项目并将其添加到解决方案中。
我们需要确保目标框架为“.NET 5.0”,右键单击项目“DemoSqlForms.Database”,然后单击“属性”。
选择目标框架“.NET 5.0”,然后按 <Ctrl+S> 保存更改。
2. 设置演示数据库
如何设置演示数据库,您可以在本文的附录中查看 - 它与我们演示的方法无关,而且许多人知道如何使用 Entity Framework,所以我不想占用您在这方面的时间。
我只需要说,对于此演示,我们需要 SchoolContext
数据库上下文以及具有一些测试数据的以下实体
3. SqlForms 动态页面
SqlForms 的主要思想是为开发人员提供一种工具,使他们能够以类型安全的方式定义 UI。拥有 Entity Framework 实体或自己的 POCO 对象意味着您可以定义要显示的特定属性、使用的 UI 控件、使其成为必需或可选提交,还可以附加业务规则来验证输入。
3.1 CourseEditForm 和 CourseEdit.razor 页面
让我们从 [Course
] 实体开始,在“DemoSqlForms.App
”项目中添加一个名为“Forms”的新文件夹,并创建一个 CourseEditForm
类。
using DemoSqlForms.Database.Model;
using Platz.SqlForms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DemoSqlForms.App.Forms
{
public class CourseEditForm : DynamicEditFormBase<SchoolContext>
{
protected override void Define(DynamicFormBuilder builder)
{
builder.Entity<Course>(e =>
{
e.Property(p => p.CourseID).IsPrimaryKey().IsUnique();
e.Property(p => p.Title).IsRequired();
e.Property(p => p.Credits).IsRequired();
e.DialogButton(ButtonActionTypes.Cancel).DialogButton
(ButtonActionTypes.Submit);
e.DialogButtonNavigation("CourseList", ButtonActionTypes.Cancel,
ButtonActionTypes.Delete, ButtonActionTypes.Submit);
});
}
}
}
您可以看到 [CourseEditForm
] 继承自 [DynamicEditFormBase<SchoolContext>
],它有一个类型参数 [SchoolContext
] – 这是我们告知 SqlForms 引擎使用哪个 DbContext
的方式。
我们重写 [Define
] 方法,并在其中提供表单定义。
代码 [builder.Entity<Course>
] 指定了 [Course
] 类型参数,因此我们告知 SqlForms
引擎使用哪个实体。
现在我们需要指定如何显示每个属性
e.Property(p => p.CourseID).IsPrimaryKey().IsUnique();
这意味着 CourseID
是主键并具有唯一约束。IsRequired()
表示如果此属性的值为空,表单将不会提交。
方法 DialogButton
用于指定要显示的按钮。
方法 DialogButtonNavigation
用于将导航操作分配给一组按钮。因此,下一行...
e.DialogButtonNavigation("CourseList", ButtonActionTypes.Cancel,
ButtonActionTypes.Delete, ButtonActionTypes.Submit);
表示当单击 **Cancel**、**Delete** 或 **Submit** 按钮时,应用程序将重定向到链接 /CourseList。
表单定义的完整规范可以在项目 wiki 页面上找到
现在,当表单已定义后,我们可以在 Pages 文件夹中添加一个新的 razor 页面 CourseEdit.razor。
@page "/CourseEdit/{CourseId:int}"
@page "/CourseEdit"
<h1>Course Edit</h1>
<FormDynamicEditComponent TForm="CourseEditForm" Id="@CourseId" />
@code {
[Parameter]
public int CourseId { get; set; }
}
<FormDynamicEditComponent TForm="CourseEditForm" Id="@CourseId" />
组件需要 TForm
参数,该参数指向表单定义 CourseEditForm
,以及映射到页面参数 CourseEdit
的实体的 Id
。
现在,如果您运行应用程序并在浏览器路径中添加 /CourseEdit
,您将看到从定义渲染的编辑页面。因为我们没有提供 Id
值,它将在数据库中创建一个新的 Course
记录。
如果您单击“Submit”,您将看到 CourseID
和 Title*
的验证失败。
因为 CourseID
是主键,但它不是自动递增的,所以您可以指定任何非 0
且尚未使用的整数值。对于自动递增的主键,输入始终是只读的。
如果您用值(100、C#、4)填充表单并单击 **Submit**,表单将在数据库中创建一个新记录并重定向到 /CourseList,该页面尚未实现。
3.2 CourseListForm 和 CourseList.razor 页面
列表表单的定义方式略有不同,但我们使用类似的方法,顺便说一句,这种方法我们借鉴自 Entity Framework 实体定义,请看 SchoolContext.cs 中的这段代码
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Enrollment>(entity =>
{
entity.HasOne(d => d.Course)
.WithMany(p => p.Enrollments)
.HasForeignKey(d => d.CourseID)
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("FK_Enrollment_Course");
entity.HasOne(d => d.Student)
.WithMany(p => p.Enrollments)
.HasForeignKey(d => d.StudentID)
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("FK_Enrollment_Student");
});
}
因此,课程列表表单将如下所示
using DemoSqlForms.Database.Model;
using Platz.SqlForms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DemoSqlForms.App.Forms
{
public class CourseListForm : DataServiceBase<SchoolContext>
{
protected override void Define(DataServiceFormBuilder builder)
{
builder.Entity<Course>(e =>
{
e.ExcludeAll();
e.Property(p => p.CourseID).IsPrimaryKey();
e.Property(p => p.Title);
e.Property(p => p.Credits);
// Parameter {0} is always PrimaryKey, parameters {1} and above - Filter Keys
// {0} = AddressId {1} = CustomerId
e.ContextButton("Edit", "CourseEdit/{0}").ContextButton
("Delete", "CourseDelete/{0}");
e.DialogButton("CourseEdit/0", ButtonActionTypes.Add);
});
builder.SetListMethod(GetCourseList);
}
public List<Course> GetCourseList(params object[] parameters)
{
using (var db = GetDbContext())
{
var query =
from s in db.Course
select new Course
{
CourseID = s.CourseID,
Title = s.Title,
Credits = s.Credits
};
var result = query.ToList();
return result;
}
}
}
}
类 CourseListForm
现在继承自 DataServiceBase<SchoolContext>
,我们再次需要重写 Define
方法,在该方法中放置表单定义。
首先,我们使用 e.ExcludeAll();
来移除定义中的所有属性,当我们不想显示所有内容时这样做。
其次,我们按所需顺序指定所有要显示的列。
接下来,我们在这一行定义上下文菜单
e.ContextButton("Edit", "CourseEdit/{0}").ContextButton("Delete", "CourseDelete/{0}");
我们在此提供按钮文本和导航链接。链接部分“{0}
”是记录主键的占位符,当用户单击某行上的此按钮时,主键值将从行中提取并放入占位符,例如,对于主键值 17
,我们将获得导航链接“CourseEdit/17”。
然后,我们使用 DialogButton
来显示带有链接“CourseEdit/0”的“Add”按钮,“0
”表示编辑页面执行以创建新记录。
最后,我们需要指定返回页面上要显示的数据的方法(“SetListMethod
”)。GetCourseList
使用 LINQ 从数据库返回所有课程。
定义准备就绪后,我们可以添加 razor 页面
@page "/CourseList"
<h1>Courses</h1>
<FormDataServiceListComponent TForm="CourseListForm"/>
@code {
}
我们使用 FormDataServiceListComponent
并将我们的定义设置为 TForm
参数。
我们还需要修改 Shared 文件夹中的 NavMenu.razor
,并在左侧菜单中包含 CourseList
页面,我还包含了一个指向 StudentList
页面的链接,我们将在下一个实现。
<li class="nav-item px-3">
<NavLink class="nav-link" href="StudentList">
<span class="oi oi-people" aria-hidden="true"></span> Student List
</NavLink>
</li>
<li class="nav-item px-3">
<NavLink class="nav-link" href="CourseList">
<span class="oi oi-bell" aria-hidden="true"></span> Course List
</NavLink>
</li>
如果您现在运行应用程序,您将看到
如果您单击 **Course List**,您将看到
您可以使用“Add”按钮向数据库添加更多课程,或使用“Actions”上下文菜单编辑记录。
如果我们添加 CourseDelete.razor
页面,我们也可以删除课程记录。
@page "/CourseDelete/{CourseId:int}"
<h1>Delete Course</h1>
<FormDynamicEditComponent TForm="CourseEditForm" Id="@CourseId" ForDelete="true" />
@code {
[Parameter]
public int CourseId { get; set; }
}
此页面具有路由 [@page "/CourseDelete/{CourseId:int}"
],它重用了 CourseEditForm
,但我们也提供了 ForDelete="true"
,此参数告诉 SqlForms 表单应为只读并包含“Delete
”按钮。
如您所见,所有 insert
、update
和 delete
操作都由 SqlForms 为我们完成,我们只需要创建选择课程记录的查询。
3.3 StudentListForm 和 StudentList.razor 页面
学生列表的 Form
定义与 CourseList
非常相似。
using DemoSqlForms.Database.Model;
using Platz.SqlForms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DemoSqlForms.App.Forms
{
public class StudentListForm : DataServiceBase<SchoolContext>
{
protected override void Define(DataServiceFormBuilder builder)
{
builder.Entity<StudentDetails>(e =>
{
e.ExcludeAll();
e.Property(p => p.ID).IsPrimaryKey();
e.Property(p => p.FirstMidName);
e.Property(p => p.LastName);
e.Property(p => p.EnrollmentDate).Format("dd-MMM-yyyy");
e.Property(p => p.EnrollmentCount);
// Parameter {0} is always PrimaryKey, parameters {1} and above - Filter Keys
// {0} = AddressId {1} = CustomerId
e.ContextButton("Edit", "StudentEdit/{0}").ContextButton
("Delete", "StudentDelete/{0}").ContextButton
("Enrollments", "EnrollmentList/{0}");
e.DialogButton("StudentEdit/0", ButtonActionTypes.Add);
});
builder.SetListMethod(GetStudentList);
}
public class StudentDetails : Student
{
public int EnrollmentCount { get; set; }
}
public List<StudentDetails> GetStudentList(params object[] parameters)
{
using (var db = GetDbContext())
{
var query =
from s in db.Student
select new StudentDetails
{
ID = s.ID,
FirstMidName = s.FirstMidName,
LastName = s.LastName,
EnrollmentDate = s.EnrollmentDate,
EnrollmentCount = (db.Enrollment.Where
(e => e.StudentID == s.ID).Count())
};
var result = query.ToList();
return result;
}
}
}
}
注意 Format("dd-MMM-yyyy")
格式,它指定了如何显示 EnrollmentDate
属性。
另外,有时您需要显示比实体更多的列,这时我们需要创建一个业务对象 - 一个包含所有必需属性的类。我创建了 StudentDetails
类,它继承了 Student
的所有属性,我还添加了 EnrollmentCount
属性。
GetStudentList
返回所有学生数据并计算每个 student
的入学人数。
razor 页面将如下所示
@page "/StudentList"
<h1>Students</h1>
<FormDataServiceListComponent TForm="StudentListForm"/>
@code {
}
如果您运行应用程序并单击 **Student List** 菜单项,您将看到
要使 **Add**、**Edit**、**Delete** 生效,我们需要添加 StudentEditForm
。
3.4 StudentEditForm 和 StudentEdit.razor 页面
StudentEditForm
的定义与 CourseEditForm
非常相似,但我添加了业务规则,以便在输入或编辑新 student
时进行附加验证。
using DemoSqlForms.Database.Model;
using Platz.SqlForms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DemoSqlForms.App.Forms
{
public class StudentEditForm : DynamicEditFormBase<SchoolContext>
{
protected override void Define(DynamicFormBuilder builder)
{
builder.Entity<Student>(e =>
{
e.Property(p => p.ID).IsReadOnly();
e.Property(p => p.FirstMidName).IsRequired();
e.Property(p => p.LastName).IsRequired();
e.Property(p => p.EnrollmentDate).Rule
(DefaultDate, FormRuleTriggers.Create).Rule(CheckDate);
e.DialogButton(ButtonActionTypes.Cancel).DialogButton
(ButtonActionTypes.Validate).DialogButton(ButtonActionTypes.Submit);
e.DialogButtonNavigation("StudentList", ButtonActionTypes.Cancel,
ButtonActionTypes.Delete, ButtonActionTypes.Submit);
});
}
public FormRuleResult DefaultDate(Student model)
{
model.EnrollmentDate = new DateTime(DateTime.Now.Year, 9, 1);
return null;
}
public FormRuleResult CheckDate(Student model)
{
if (model.EnrollmentDate < new DateTime(2015, 1, 1))
{
return new FormRuleResult("EnrollmentDate is incorrect");
}
return null;
}
}
}
规则 Rule(DefaultDate, FormRuleTriggers.Create)
表示当创建新的学生记录时,将执行 DefaultDate
方法,此方法将 EnrollmentDate
设置为当前年份的 9 月 1 日。
规则 CheckDate
将在 EnrollmentDate
属性更改或表单提交时执行。当输入的值早于 2015 年 1 月 1 日时,此规则将触发验证错误。
StudentEdit.razor
页面一如既往地非常简单
@page "/StudentEdit/{Id:int}"
@page "/StudentEdit"
<h1>Student Edit</h1>
<FormDynamicEditComponent TForm="StudentEditForm" Id="@Id" />
@code {
[Parameter]
public int Id { get; set; }
}
如果您现在运行应用程序,选择 **Student List** 页面,然后单击“Add”按钮。您可以尝试默认和验证规则。
对于删除功能,我们需要添加 StudentDelete.razor
页面。
@page "/StudentDelete/{Id:int}"
<h1>Delete Student</h1>
<FormDynamicEditComponent TForm="StudentEditForm" Id="@Id" ForDelete="true" />
@code {
[Parameter]
public int Id { get; set; }
}
当您运行应用程序时,删除页面将如下所示
现在我们需要创建 Enrollment 页面,我想演示列表表单创建如何简化。
4. Platz.ObjectBuilder
Platz.ObjectBuilder
可用于可视化构建具有联接、子查询、条件等的复杂 LINQ 查询,并为查询和查询返回的业务对象生成 C# 代码。
要演示如何使用 Platz.ObjectBuilder
,我们需要创建另一个目标框架为 .NET 5.0 的 Blazor Server 应用程序,并将其命名为“DemoSqlForms.ObjectBuilder.App
”。
然后我们需要安装 Platz.ObjectBuilder
NuGet 包,并按照 readm.txt 文件中的说明进行操作。
要使用 SchoolContext
,我们需要添加一个项目引用到 DemoSqlForms.Database
项目,并将连接字符串添加到“appsettings.json”文件中。
现在让我们修改 Index.razor
页面。
@page "/"
@using Platz.ObjectBuilder
<QueryComponent DbContextType="typeof(DemoSqlForms.Database.Model.SchoolContext)"
StoreDataPath="StoreData" DataService="MyDataService" Namespace="Default" />
右键单击“DemoSqlForms.ObjectBuilder.App
”项目,选择“Debug”,然后选择“Start New Instance”。
您将看到允许我们可视化构建查询的应用程序。
选择 **Enrollment** 实体,然后选择 **Course** 实体。您将看到两个对象已添加到“From”面板。现在在“Select”面板中,“Filter”输入“@p1
”到“e.StudentID”列。您应该会看到一个类似以下的查询窗口
现在单击 **Settings** 面板上的“…
”,在“Query Return Type name”控件中输入“EnrollmentDetails
”,然后单击“Save”,并关闭应用程序。
我们创建了保存为“DemoSqlForms.ObjectBuilder.App\StoreData”文件夹中 json 文件的查询定义。我们可以使用 t4 模板从该 json 定义生成代码。
4.1 代码生成
让我们回到“DemoSqlForms.App
”项目。如果您打开项目文件夹“Platz.Config.Link”,您会看到“CopyMe.PlatzDataService.tt.txt”文件。双击此文件,选择所有代码(<Ctrl+A>),然后复制到剪贴板(<Ctrl+C>)。
现在在“Forms”文件夹中,创建一个名为“DataServices”的子文件夹。
在“DataServices”文件夹中,创建一个名为“SchoolDataService.tt”的文件,并将剪贴板中的内容粘贴进去(<Ctrl+V>)。
您需要更改第 12 行,使其指向“DemoSqlForms.ObjectBuilder.App
”项目中保存查询的“StoreData”文件夹。
<# var JsonStorePath = @"DemoSqlForms.ObjectBuilder.App\StoreData"; #>
现在,当您保存文件时,Visual Studio 将为您生成代码并将其放在“SchoolDataService.cs”中。
// ******************************************************************************************
// This code is auto generated by Platz.ObjectBuilder template,
// any changes made to this code will be lost
// ******************************************************************************************
using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using Platz.SqlForms;
using DemoSqlForms.Database.Model;
namespace Default
{
#region Interface
public partial interface IMyDataService
{
List<EnrollmentDetails> GetEnrollmentDetailsList(params object[] parameters);
}
#endregion
#region Data Service
public partial class MyDataService : DataServiceBase<SchoolContext>, IMyDataService
{
public List<EnrollmentDetails> GetEnrollmentDetailsList(params object[] parameters)
{
var p1 = (Int32)parameters[0];
using (var db = GetDbContext())
{
var query =
from c in db.Course
join e in db.Enrollment on c.CourseID equals e.CourseID
where e.StudentID == p1
select new EnrollmentDetails
{
EnrollmentID = e.EnrollmentID,
CourseID = e.CourseID,
Grade = e.Grade,
StudentID = e.StudentID,
Credits = c.Credits,
Title = c.Title,
};
var result = query.ToList();
return result;
}
}
}
#endregion
#region Entities
public partial class EnrollmentDetails
{
public Int32 EnrollmentID { get; set; }
public Int32 CourseID { get; set; }
public Grade? Grade { get; set; }
public Int32 StudentID { get; set; }
public Int32 Credits { get; set; }
public String Title { get; set; }
}
#endregion
}
生成的 T4 文件包含 EnrollmentDetails
业务对象类和 MyDataService:: GetEnrollmentDetailsList
方法,该方法返回 Enrollment
和 Course
实体的联接数据。它还接受参数 p1
,数据将按 StudentID
字段进行筛选。
4.2 EnrollmentListForm 和 EnrollmentList.razor 页面
现在我们添加 EnrollmentListForm
代码
using Default;
using Platz.SqlForms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DemoSqlForms.App.Forms
{
public class EnrollmentListForm : MyDataService
{
protected override void Define(DataServiceFormBuilder builder)
{
builder.Entity<EnrollmentDetails>(e =>
{
e.ExcludeAll();
e.Property(p => p.EnrollmentID).IsPrimaryKey();
e.Property(p => p.StudentID).IsFilter().IsReadOnly();
e.Property(p => p.CourseID);
e.Property(p => p.Grade);
e.Property(p => p.Title);
e.Property(p => p.Credits);
// Parameter {0} is always PrimaryKey, parameters {1} and above - Filter Keys
// {0} = EnrollmentID {1} = StudentID
e.ContextButton("Edit", "EnrollmentEdit/{0}/{1}").ContextButton
("Delete", "EnrollmentDelete/{0}/{1}");
e.DialogButton("StudentList", ButtonActionTypes.Custom, "Back");
e.DialogButton("EnrollmentEdit/0/{1}", ButtonActionTypes.Add);
});
builder.SetListMethod(GetEnrollmentDetailsList);
}
}
}
我们继承类 EnrollmentListForm
自生成的 MyDataService
,并使用 SetListMethod
来指定生成的 GetEnrollmentDetailsList
。
我们像往常一样定义了属性,但导航链接现在有两个占位符:“EnrollmentEdit/{0}/{1}
”和“EnrollmentDelete/{0}/{1}
”。
原因是 EnrollmentListForm
是 StudentListForm
的依赖表单。当我们选择一个学生并单击“Enrollments
”上下文菜单按钮时,我们需要将 StudentID
主键提供给 EnrollmentListForm
,这个 StudentID
将被传递到 EnrollmentEditForm
作为“{1}
”占位符,而“{0}
”保留给 EnrollmentEditForm
的主键 - EnrollmentID
。
EnrollmentList.razor
页面将如下所示
@page "/EnrollmentList/{StudentId:int}"
<h1>Student Enrollments</h1>
<FormDynamicEditComponent TForm="StudentHeaderForm" Id="@StudentId" />
<FormDataServiceListComponent TForm="EnrollmentListForm"
ServiceParameters="@(new object[] { StudentId })"/>
@code {
[Parameter]
public int StudentId { get; set; }
}
页面路由现在接受 StudentId
参数,我们使用 ServiceParameters
将 StudentId
提供给 FormDataServiceListComponent
。引擎将使用 ServiceParameters
生成导航链接来填充以“{1}
”及以上开始的占位符。
我们还添加了 FormDynamicEditComponent
,它显示了 StudentHeaderForm
,该表单的所有字段都设置为只读。
using DemoSqlForms.Database.Model;
using Platz.SqlForms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DemoSqlForms.App.Forms
{
public class StudentHeaderForm : DynamicEditFormBase<SchoolContext>
{
protected override void Define(DynamicFormBuilder builder)
{
builder.Entity<Student>(e =>
{
e.ExcludeAll();
e.Property(p => p.ID).IsReadOnly();
e.Property(p => p.FirstMidName).IsReadOnly();
e.Property(p => p.LastName).IsReadOnly();
});
}
}
}
如果我们现在运行应用程序,在 **Student List** 中选择一个 **student** 并单击“Enrollments”上下文菜单按钮,我们将看到
您可以在 header
中看到**student** 的只读详细信息,并在下方的 enrollments
表格中看到。
如果我们单击“Back”按钮,我们将返回到 **Student List** 页面。
4.3 Enrollment 编辑和删除
最后一步是创建一个 EnrollmentEditForm
定义,这与我们之前做的很简单。
using DemoSqlForms.Database.Model;
using Platz.SqlForms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DemoSqlForms.App.Forms
{
public class EnrollmentEditForm : DynamicEditFormBase<SchoolContext>
{
protected override void Define(DynamicFormBuilder builder)
{
builder.Entity<Enrollment>(e =>
{
e.Property(p => p.EnrollmentID).IsPrimaryKey().IsReadOnly();
e.Property(p => p.StudentID).IsFilter().IsHidden();
e.Property(p => p.CourseID).IsRequired().Dropdown<Course>().Set
(c => c.CourseID, c => c.Title);
e.Property(p => p.Grade).IsRequired().Rule
(DefaultGrade, FormRuleTriggers.Create).Dropdown<Grade>().Set(g => g, g => g);
e.DialogButton(ButtonActionTypes.Cancel).DialogButton
(ButtonActionTypes.Submit);
// {0} always reserved for Primary Key (EnrollmentID in this case)
// but EnrollmentList accepts StudentId as parameter
e.DialogButtonNavigation("EnrollmentList/{1}",
ButtonActionTypes.Cancel, ButtonActionTypes.Delete, ButtonActionTypes.Submit);
});
}
public FormRuleResult DefaultGrade(Enrollment model)
{
model.Grade = Grade.A;
return null;
}
}
}
在这里,我们使用了 Dropdown
定义。对于 CourseID
属性,我们使用 Course
实体,并指定 [value]
为 Course.CourseID
,[name]
为 Course.Title
。对于“Grade
”属性,我们指定“Grade
” enum
,下拉列表的 [value] 和 [name] 将是“Grade
” enum 项目(A、B、C 等)。
然后我们需要为 Edit
添加 razor 页面。
@page "/EnrollmentEdit/{EnrollmentId:int}/{StudentId:int}"
<h1>Student Enrollment Edit</h1>
<FormDynamicEditComponent TForm="StudentHeaderForm" Id="@StudentId" ReadOnly="true" />
<FormDynamicEditComponent TForm="EnrollmentEditForm" Id="@EnrollmentId"
ServiceParameters="new object[] { StudentId }" />
@code {
[Parameter]
public int EnrollmentId { get; set; }
[Parameter]
public int StudentId { get; set; }
}
然后为 Delete
添加。
@page "/EnrollmentDelete/{EnrollmentId:int}/{StudentId:int}"
<h1>Student Enrollment Delete</h1>
<FormDynamicEditComponent TForm="StudentHeaderForm" Id="@StudentId" />
<FormDynamicEditComponent TForm="EnrollmentEditForm" Id="@EnrollmentId"
ServiceParameters="new object[] { StudentId }" ForDelete="true" />
@code {
[Parameter]
public int EnrollmentId { get; set; }
[Parameter]
public int StudentId { get; set; }
}
在这两个页面中,我们都将 StudentHeaderForm
显示为标题,并在 ServiceParameters
中提供 StudentId
。
现在应用程序已准备好进行测试,单击 Student Enrollments 的“Edit”操作,您将看到
如果我们单击“Delete”操作,将显示此页面
所有数据库操作,包括 **Insert**、**Update** 和 **Delete**,都将由 SqlForms
引擎使用我们提供的表单定义来执行。
5. 总结
在本文中,我们演示了一种使用 C# 中的类型安全定义来构建 Blazor UI 应用程序的方法。这项技术可以为使用 Platz.SqlForms
进行原型开发或低成本应用程序的开发人员节省大量时间。
这种方法有几个优点
- 中级或初级开发人员可以轻松使用,无需前端经验
- 代码结构会非常好,业务逻辑仅允许在业务规则中
- 业务逻辑可以轻松进行单元测试
- 生成的代码库要小得多,并且不需要昂贵的维护
- 复杂的查询和业务对象可以在可视化工具中生成
但是,也有一些缺点
- SqlForms 动态组件有局限性,无法生成您想要的任何 UI
- 不支持复合主键
- 目前只有一个 Bootstrap 演示可用
我们还考虑了 Platz.ObjectBuilder
工具,它可以节省定义业务对象和将它们映射到 LINQ 查询结果的大量时间。尽管 Object Builder 目前不支持复杂查询,但我们演示了一个概念,即可视化工具的输出如何被 t4 模板使用来生成无需维护的代码:任何时候您需要更改内容,只需修改查询并重新生成代码。
Platz.SqlForms
项目是开源的,由 Pro Coders 团队开发。
您可以在 Github 上找到所有详细信息。
要提交错误或功能请求,请使用此链接
Issues · ProCodersPtyLtd/MasterDetailsDataEntry (github.com)
5.1 下一步
我的下一篇文章将是关于嵌入式数据库设计器:
Microsoft Blazor 开源 Platz.SqlForms - 使用 Schema Builder 设计和维护 SQL Server 数据库
附录
设置演示数据库
您可以在此处详细阅读如何设置第一个 Entity Framework 模型数据库
教程:在 ASP.NET MVC Web 应用中入门 EF Core | Microsoft Docs.
SchoolContext
我们在“DemoSqlForms.Database
”项目中创建一个名为“Model”的文件夹,并添加 SchoolContext.cs 文件。
using Microsoft.EntityFrameworkCore;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Configuration;
using Microsoft.Extensions.Configuration;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DemoSqlForms.Database.Model
{
public class SchoolContext : DbContext
{
public SchoolContext()
{
}
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
if (!optionsBuilder.IsConfigured)
{
IConfigurationRoot configuration =
new ConfigurationBuilder().AddJsonFile
("appsettings.json", optional: false).Build();
optionsBuilder.UseSqlServer
(configuration.GetConnectionString("DefaultConnection"));
}
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Enrollment>(entity =>
{
entity.HasOne(d => d.Course)
.WithMany(p => p.Enrollments)
.HasForeignKey(d => d.CourseID)
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("FK_Enrollment_Course");
entity.HasOne(d => d.Student)
.WithMany(p => p.Enrollments)
.HasForeignKey(d => d.StudentID)
.OnDelete(DeleteBehavior.Restrict)
.HasConstraintName("FK_Enrollment_Student");
});
}
public DbSet<Course> Course { get; set; }
public DbSet<Enrollment> Enrollment { get; set; }
public DbSet<Student> Student { get; set; }
}
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
public enum Grade
{
A, B, C, D, F
}
public class Enrollment
{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
public Grade? Grade { get; set; }
public Course Course { get; set; }
public Student Student { get; set; }
}
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
public DateTime EnrollmentDate { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
我将简要提及此文件包含 Entity Framework DbContext
和我们演示数据库的实体。SchoolContext
从“appsettings.json”读取连接字符串,我们将在“DemoSqlForms.App
”项目中添加该文件。
实体将看起来像
DbInitializer
为了用测试数据初始化我们的数据库,我们添加 DbInitializer.cs 文件。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DemoSqlForms.Database.Model
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
context.Database.EnsureCreated();
// Look for any students.
if (context.Student.Any())
{
return; // DB has been seeded
}
var students = new Student[]
{
new Student{FirstMidName="Carson",LastName="Alexander",
EnrollmentDate=DateTime.Parse("2005-09-01")},
new Student{FirstMidName="Meredith",LastName="Alonso",
EnrollmentDate=DateTime.Parse("2002-09-01")},
new Student{FirstMidName="Arturo",LastName="Anand",
EnrollmentDate=DateTime.Parse("2003-09-01")},
new Student{FirstMidName="Gytis",LastName="Barzdukas",
EnrollmentDate=DateTime.Parse("2002-09-01")},
new Student{FirstMidName="Yan",LastName="Li",
EnrollmentDate=DateTime.Parse("2002-09-01")},
new Student{FirstMidName="Peggy",LastName="Justice",
EnrollmentDate=DateTime.Parse("2001-09-01")},
new Student{FirstMidName="Laura",LastName="Norman",
EnrollmentDate=DateTime.Parse("2003-09-01")},
new Student{FirstMidName="Nino",LastName="Olivetto",
EnrollmentDate=DateTime.Parse("2005-09-01")}
};
foreach (Student s in students)
{
context.Student.Add(s);
}
context.SaveChanges();
var courses = new Course[]
{
new Course{CourseID=1050,Title="Chemistry",Credits=3},
new Course{CourseID=4022,Title="Microeconomics",Credits=3},
new Course{CourseID=4041,Title="Macroeconomics",Credits=3},
new Course{CourseID=1045,Title="Calculus",Credits=4},
new Course{CourseID=3141,Title="Trigonometry",Credits=4},
new Course{CourseID=2021,Title="Composition",Credits=3},
new Course{CourseID=2042,Title="Literature",Credits=4}
};
foreach (Course c in courses)
{
context.Course.Add(c);
}
context.SaveChanges();
var enrollments = new Enrollment[]
{
new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
new Enrollment{StudentID=3,CourseID=1050},
new Enrollment{StudentID=4,CourseID=1050},
new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
new Enrollment{StudentID=6,CourseID=1045},
new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
};
foreach (Enrollment e in enrollments)
{
context.Enrollment.Add(e);
}
context.SaveChanges();
}
}
}
现在我们需要对“DemoSqlForms.App
”项目进行更改。
连接字符串
将连接字符串添加到“appsettings.json”,该文件将如下所示
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;
Database=DemoSqlForms1;Trusted_Connection=True;MultipleActiveResultSets=true"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
连接字符串指定了 SQL Server LocalDB
。LocalDB
是 SQL Server Express 数据库引擎的轻量级版本,专为应用程序开发设计,而非生产使用。LocalDB
按需启动并在用户模式下运行,因此无需复杂配置。默认情况下,LocalDB
在 C:/Users/<user> 目录中创建 .mdf 数据库文件。
Program.cs
在 Program.cs 文件中,我们删除行
CreateHostBuilder(args).Build().Run();
并添加创建数据库的逻辑,代码将如下所示
using DemoSqlForms.Database.Model;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DemoSqlForms.App
{
public class Program
{
public static void Main(string[] args)
{
//CreateHostBuilder(args).Build().Run();
var host = CreateHostBuilder(args).Build();
CreateDbIfNotExists(host);
host.Run();
}
private static void CreateDbIfNotExists(IHost host)
{
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<SchoolContext>();
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
CreateDbIfNotExists
方法仅执行 DbInitializer
,它将在第一次运行时创建数据库并填充测试数据。
Startup.cs
在这里,在 ConfigureServices
方法中,我们需要添加 DbContext
初始化逻辑
services.AddDbContext<SchoolContext>
(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
我们已经添加了 Platz.SqlForms
初始化逻辑
services.AddPlatzSqlForms();
代码将如下所示
using DemoSqlForms.Database.Model;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Platz.SqlForms;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace DemoSqlForms.App
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime.
// Use this method to add services to the container.
// For more information on how to configure your application,
// visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddDbContext<SchoolContext>(options => options.UseSqlServer
(Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddPlatzSqlForms();
}
// This method gets called by the runtime. Use this method to configure
// the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days.
// You may want to change this for production scenarios,
// see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
}
}
历史
- 2021 年 1 月 11 日:初始版本