65.9K
CodeProject 正在变化。 阅读更多。
Home

Microsoft Blazor - 使用开源 Platz.SqlForms 快速开发 SQL 表单

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.98/5 (33投票s)

2021 年 1 月 11 日

CPOL

15分钟阅读

viewsIcon

49325

如何使用开源 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 将创建包含项目的解决方案。

我喜欢花一些时间删除示例页面(CounterFetchData)及其相关代码,但这并非必需。

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”,您将看到 CourseIDTitle* 的验证失败。

因为 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”按钮。

如您所见,所有 insertupdatedelete 操作都由 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 方法,该方法返回 EnrollmentCourse 实体的联接数据。它还接受参数 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}”。

原因是 EnrollmentListFormStudentListForm 的依赖表单。当我们选择一个学生并单击“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 参数,我们使用 ServiceParametersStudentId 提供给 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”属性,我们指定“Gradeenum,下拉列表的 [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 LocalDBLocalDB 是 SQL Server Express 数据库引擎的轻量级版本,专为应用程序开发设计,而非生产使用。LocalDB 按需启动并在用户模式下运行,因此无需复杂配置。默认情况下,LocalDBC:/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 日:初始版本
© . All rights reserved.