目录
3.1 CourseEditForm和CourseEdit.razor页面
3.2 CourseListForm和CourseList.razor页面
3.3 StudentListForm和StudentList.razor页面
3.4 StudentEditForm和StudentEdit.razor页面
4.2 EnrollmentListForm和EnrollmentList.razor页面
当您需要为客户构建有效的原型时,或者您的公司没有用于企业发展的预算时,您别无选择,并且需要使用一些捷径和生活技巧,通常是低代码或无代码方法。在这篇文章中,我提出了一种有趣的方法,它关于如何使用开源库Platz.SqlForms快速开发Blazor UI。SqlForms将提供SPA用户体验,并且无需您进行任何编码即可与数据库进行通信,您所要做的就是定义UI表单,为要作为UI控件显示的EF实体提供流畅的符号定义。
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软件包,右键单击解决方案项目,然后单击“Manage NuGet Packages…”菜单,然后在“浏览”选项卡中键入“Platz”搜索模式,您将看到Platz软件包。选择Platz.SqlForms并单击“安装”按钮。
安装后,您将看到一个带有简单说明的readme.txt文件,请遵循它们。
重要的步骤是在ConfigureServices 方法中添加Platz.SqlForms初始化逻辑:
services.AddPlatzSqlForms();
1.3数据库项目
为了演示如何使用Platz.SqlForms,我们将需要创建一个数据库项目。
右键单击“DemoSqlForms”解决方案(解决方案资源管理器中的第一行),单击“添加”,然后单击“新建项目…”。
在“添加新项目”向导中,找到“类库(.NET Core)”模板,将其选中并单击“下一步”。
在项目名称中键入“DemoSqlForms.Database”,然后单击“创建”。
Visual Studio将创建一个新的类库项目并将其添加到解决方案中。
我们需要确保目标框架是“.NET 5.0”,右键单击项目“DemoSqlForms.Database”,然后单击“Properties”。
选择目标框架“.NET 5.0”和<ctrl + s>以保存您的更改。
2.设置演示数据库
您可以在本文的附录中看到如何设置演示数据库——它与我们演示的方法无关,并且许多人都知道如何使用Entity Framework,因此,我不想在此花费您的时间。
我只应该说,对于此演示,我们需要SchoolContext数据库上下文以及以下带有一些测试数据的实体:
3. SqlForms动态页面
SqlForms的主要思想是为开发人员提供一个工具,使他们能够以C#类型安全的方式定义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]继承自具有类型参数[SchoolContext]的[DynamicEditFormBase<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);
表示单击“取消”、“删除”或“提交”按钮时,应用程序将重定向到/CourseList链接。
可以在项目Wiki页面上找到Form Definition的完整规范:
现在,在定义表单后,我们可以将新的razor页面添加到Pages文件夹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" />组件需要指向表单定义CourseEditForm的TForm参数,以及映射到页面参数CourseEdit的实体Id。
现在,如果您运行该应用程序并将其/CourseEdit添加到浏览器路径,您将看到从定义呈现的编辑页面。因为我们没有提供Id值,所以它将在数据库中创建一个新Course记录。
如果你点击“提交”,你会看到,验证了CourseID和Title*失败。
因为CourseID是主键,但不是自动递增的,所以您可以指定任何整数值,除了0和已经使用的整数值外,对于自动递增的主键,输入始终是只读的。
如果使用值(100, C#, 4) 填充表单,然后单击Submit,则表单将在数据库中创建新记录并重定向到/CourseList,该记录尚未实现。
3.2 CourseListForm和CourseList.razor页面
列表形式的定义有些不同,但是我们使用类似的方法BTW,这是我们在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”的“添加”按钮,并且“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>
如果立即运行该应用程序,您将看到:
如果单击“课程列表”,您将看到:
您可以使用“添加”按钮将更多课程添加到数据库中,也可以使用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属性的格式。
另外,有时您需要显示比实体更多的列,然后我们需要创建一个业务对象——一个将包含所有必需属性的类。我创建了一个继承自Student的所有属性的StudentDetails类,并且还添加了该EnrollmentCount属性。
GetStudentList返回所有学生数据,并计算每个student的入学人数。
razor页面如下所示:
@page "/StudentList"
<h1>Students</h1>
<FormDataServiceListComponent TForm="StudentListForm"/>
@code {
}
如果运行该应用程序并单击“学生列表”菜单项,则将看到:
为了使添加、编辑、删除工作,我们需要添加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为当年的01-Sep。
EnrollmentDate属性更改或提交表单后,将执行规则CheckDate。当输入的值早于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; }
}
如果现在运行该应用程序,请选择“学生列表”页面,然后单击“添加”按钮。您可以使用默认规则和验证规则。
对于删除功能,我们需要添加StudentDelete.razor页面。
@page "/StudentDelete/{Id:int}"
<h1>Delete Student</h1>
<FormDynamicEditComponent TForm="StudentEditForm" Id="@Id" ForDelete="true" />
@code {
[Parameter]
public int Id { get; set; }
}
运行应用程序时,删除页面将如下所示:
现在我们需要创建“注册”页面,我想演示如何简化列表表单的创建。
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”项目,然后选择“调试”,然后选择“启动新实例”。
您将看到该应用程序,它使我们可以直观地构建查询。
选择注册实体,然后选择Course实体。您将看到两个对象已添加到“From”面板。现在,在“选择”面板中,为“e.StudentID”列输入“@p1”到Filter。您应该看到一个查询窗口,如下所示:
现在,在“设置”面板上单击“…”,并在“查询返回类型名称”控件中输入 “EnrollmentDetails” ,然后单击“保存”,然后关闭应用程序。
我们创建了查询定义,该定义保存为json文件,位于文件夹“DemoSqlForms.ObjectBuilder.App\StoreData”中。我们可以使用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
}
生成的文件包含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);
}
}
}
我们从生成的MyDataService继承类EnrollmentListForm,并使用SetListMethod指定生成的GetEnrollmentDetailsList。
我们照常定义了属性,但是导航链接现在具有两个占位符:“EnrollmentEdit/{0}/{1}”和“EnrollmentDelete/{0}/{1}”。
原因是EnrollmentListForm是StudentListForm的附属形式。当我们选择学生,点击“Enrollments”上下文菜单按钮,我们需要向EnrollmentListForm提供StudentID主键,这个StudentID将作为“{1}”占位符被传播到EnrollmentEditForm,但“{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();
});
}
}
}
如果我们现在运行该应用程序,然后在“学生列表”中选择一个学生,然后单击“注册”上下文菜单按钮,我们将看到:
您可以在下面的header和enrollments表中看到学生只读详细信息。
如果单击“返回”按钮,我们将返回到“学生列表”页面。
4.3登记编辑和删除
最后一步是创建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”枚举项(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为header,并在ServiceParameters中提供StudentId。
现在,该应用程序可以进行测试了,单击“学生注册”的“编辑”操作,您将看到:
如果单击“删除”操作,将显示此页面:
SqlForms引擎将使用我们提供的表单定义来执行Insert、Update和Delete的所有数据库操作。
5.总结
在本文中,我们演示了一种使用C#中的类型安全定义来构建Blazor UI应用程序的方法。对于使用Platz.SqlForms进行原型或低预算应用程序的开发人员而言,该技术可以节省大量时间。
这种方法有几个优点:
- 中级或初级开发人员可以轻松使用它,不需要任何前端体验
- 代码将被很好地组织,并且仅在业务规则中允许业务逻辑
- 业务逻辑可以轻松地进行单元测试
- 生成的代码库要小得多,并且不需要昂贵的维护
- 可以在可视工具中生成复杂的查询和业务对象
但是,有一些缺点:
- SqlForms动态组件具有局限性,无法生成所需的任何UI
- 不支持复合主键
- 目前只有一个bootstrap演示文稿
我们还考虑了可以节省大量时间来定义业务对象并将其映射到LINQ查询结果的Platz.ObjectBuilder工具。尽管对象生成器目前不支持复杂的查询,但我们演示了一个概念,该概念涉及t4模板如何使用可视化工具输出来生成不需要维护的代码:任何时候您需要更改内容时,只需进行修改即可查询并重新生成代码。
该项目Platz.SqlForms是开源的,由Pro Coders团队开发。
您可以在Github上找到所有详细信息
要提交错误或功能请求,请使用以下链接:
Issues · ProCodersPtyLtd/MasterDetailsDataEntry (github.com)
5.1下一步
我的下一篇文章将介绍SqlForms内联编辑。
附录
设置演示数据库
您可以在此处详细了解如何设置实体框架模型优先数据库:
教程:在ASP.NET MVC Web应用程序中开始使用EF Core 微软文档。
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; }
}
}
我将简要提及该文件包含我们的演示数据库的实体框架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按需启动并以用户模式运行,因此没有复杂的配置。默认情况下,在C:/Users/<user>目录中LocalDB创建.mdf DB文件。
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");
});
}
}
}
https://www.codeproject.com/Articles/5291832/Microsoft-Blazor-Rapid-Development-with-SQL-Forms