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

BlazorForms 低代码开源框架。第二部分:CrmLight 项目

starIconstarIconstarIconstarIconstarIcon

5.00/5 (7投票s)

2023年1月11日

CPOL

8分钟阅读

viewsIcon

21898

利用 C# 的类型安全优势,基于流程、表单和规则开发应用程序

本系列中所有 BlazorForms 文章

我们在这里部署了一个可工作的解决方案

https://crmlight.platz.app

请注意,它没有数据库,所有数据都保存在内存中,供所有用户共享,并且在一段时间不活动后,容器会关闭,所有数据都会丢失。

我们将其部署在具有最低 CPU 和内存要求的 Linux Docker 容器中,但运行速度非常快。

引言

本文继续介绍 PRO CODERS PTY LTD 开发的 BlazorForms 框架的系列文章,该框架已作为开源项目以 MIT 许可证共享。

在前一篇文章“BlazorForms 低代码开源框架介绍和种子项目”中,我介绍了这个框架,旨在简化 Blazor UI 开发,并允许创建简单且可维护的 C# 代码。

该框架的主要思想是提供一种将逻辑与 UI 分离的模式,并强制开发人员将逻辑保存在 Flows 和 Rules 中。Forms 仅包含 Model 和 UI 控件之间的绑定。

除非您需要高度自定义,否则无需直接操作 UI。这意味着 Flows 和 Rules 中的逻辑不依赖于 UI,并且 100% 可单元测试。

为了最大限度地减少初始工作量,我们创建了以下种子项目,这些项目在 GitHub 上可用,我将 CrmLight 项目版本 0.7.0 复制到了我的博客存储库中。

从 GitHub 下载此博客文章代码

GitHub 上的 BlazorForms 项目

CrmLight 项目

创建 CrmLightDemoApp 项目是为了演示如何实现比基本种子项目中介绍的更复杂的场景。CrmLight Flows 使用 Repositories,这些 Repositories 具有完整的 CRUD 操作实现以及一些扩展。

数据和 Repositories

应用程序处理多个实体和关系

  • Company
  • 人员
  • PersonCompanyLink
  • PersonCompanyLinkType

它们之间的关系可以在图表中显示

Picture 1

为了实现数据访问,我们使用了经典的 Repository 模式,这意味着每个实体都有一个专门的 Repository。但是,没有必要多次实现相同的 CRUD 操作,所以我们使用了泛型。

如果您查看解决方案资源管理器,您将看到简化的 Onion 架构文件夹结构

Picture 2

其中 IRepository.cs 为所有 Repositories 定义了通用接口

namespace CrmLightDemoApp.Onion.Domain.Repositories
{
    public interface IRepository<T>
        where T : class
    {
        Task<List<T>> GetAllAsync();
        IQueryable<T> GetAllQuery();
        Task<List<T>> RunQueryAsync(IQueryable<T> query);
        Task<T> GetByIdAsync(int id);
        Task<int> CreateAsync(T data);
        Task UpdateAsync(T data);
        Task DeleteAsync(int id);
        Task SoftDeleteAsync(int id);
        Task<List<T>> GetListByIdsAsync(IEnumerable<int> ids);
    }
}

LocalCacheRepository.cs 实现该接口

using BlazorForms.Shared;
using CrmLightDemoApp.Onion.Domain;
using CrmLightDemoApp.Onion.Domain.Repositories;

namespace CrmLightDemoApp.Onion.Infrastructure
{
    // this is repository emulator that stores all data in memory
    // it stores and retrieves object copies, like a real database
    public class LocalCacheRepository<T> : IRepository<T>
        where T : class, IEntity
    {
        protected int _id = 0;
        protected readonly List<T> _localCache = new List<T>();

        public async Task<int> CreateAsync(T data)
        {
            _id++;
            data.Id = _id;
            _localCache.Add(data.GetCopy());
            return _id;
        }

        public async Task DeleteAsync(int id)
        {
            _localCache.Remove(_localCache.Single(x => x.Id == id));
        }

        public async Task<T> GetByIdAsync(int id)
        {
            return _localCache.Single(x => x.Id == id).GetCopy();
        }

        public async Task<List<T>> GetAllAsync()
        {
            return _localCache.Where
                   (x => !x.Deleted).Select(x => x.GetCopy()).ToList();
        }

        public async Task UpdateAsync(T data)
        {
            await DeleteAsync(data.Id);
            _localCache.Add(data.GetCopy());
        }

        public async Task SoftDeleteAsync(int id)
        {
            _localCache.Single(x => x.Id == id).Deleted = true;
        }

        public async Task<List<T>> GetListByIdsAsync(IEnumerable<int> ids)
        {
            return _localCache.Where(x => ids.Contains(x.Id)).Select
                                    (x => x.GetCopy()).ToList();
        }

        public IQueryable<T> GetAllQuery()
        {
            return _localCache.AsQueryable();
        }

        public async Task<List<T>> RunQueryAsync(IQueryable<T> query)
        {
            return query.ToList();
        }
    }
}

如您所见,我们在项目中不使用任何数据库,而是将所有数据保存在内存模拟器中。这应该简化演示的运行体验,同时确保开发的 Repositories 可用于单元测试。

为了简化代码,我们使用了 BlazorForms.Shared 中的 GetCopy 扩展方法,它使用反射创建一个新实例并将所有 public 属性复制过去。
专门的 Repositories 会预填充一些数据,您可以在运行应用程序时看到这些数据。

using CrmLightDemoApp.Onion.Domain;
using CrmLightDemoApp.Onion.Domain.Repositories;

namespace CrmLightDemoApp.Onion.Infrastructure
{
    public class CompanyRepository : LocalCacheRepository<Company>, ICompanyRepository
    {
        public CompanyRepository()
        {
            // pre fill some data
            _localCache.Add(new Company { Id = 1, Name = "Mizeratti Pty Ltd", 
             RegistrationNumber = "99899632221", 
             EstablishedDate = new DateTime(1908, 1, 17) });
            _localCache.Add(new Company { Id = 2, Name = "Alpha Pajero", 
             RegistrationNumber = "89963222172", 
             EstablishedDate = new DateTime(1956, 5, 14) });
            _localCache.Add(new Company { Id = 3, Name = "Zeppelin Ltd Inc", 
             RegistrationNumber = "63222172899", 
             EstablishedDate = new DateTime(2019, 11, 4) });
            _localCache.Add(new Company { Id = 4, Name = "Perpetuum Automotives Inc", 
             RegistrationNumber = "22217289963", 
             EstablishedDate = new DateTime(2010, 1, 7) });
            _id = 10;
        }
    }
}

PersonCompanyRepository.cs 包含连接多个实体的方法,并返回 PersonCompanyLinkDetails 组合对象。

public async Task<List<PersonCompanyLinkDetails>> GetByCompanyIdAsync(int companyId)
{
    var list = _localCache.Where(x => !x.Deleted && 
               x.CompanyId == companyId).Select(x =>
    {
        var item = new PersonCompanyLinkDetails();
        x.ReflectionCopyTo(item);
        return item;
    }).ToList();

    var company = await _companyRepository.GetByIdAsync(companyId);
    var personIds = list.Select(x => x.PersonId).Distinct().ToList();
    var persons = (await _personRepository.GetListByIdsAsync
                  (personIds)).ToDictionary(x => x.Id, x => x);
    var linkIds = list.Select(x => x.LinkTypeId).Distinct().ToList();
    var links = (await _personCompanyLinkTypeRepository.
                 GetListByIdsAsync(linkIds)).ToDictionary(x => x.Id, x => x);

    foreach (var item in list)
    {
        item.LinkTypeName = links[item.LinkTypeId].Name;
        item.PersonFullName = $"{persons[item.PersonId].FirstName} 
                              {persons[item.PersonId].LastName}";
        item.PersonFirstName = persons[item.PersonId].FirstName;
        item.PersonLastName = persons[item.PersonId].LastName;
        item.CompanyName = company.Name;
    }

    return list;
} 

业务逻辑

Services 文件夹包含 BlazorForms 相关的代码——应用程序的业务逻辑。

图 3

Flow Model

在开始查看 Flows 之前,我想提一下,我们不使用领域实体作为 Model,而是使用业务 Model 类,这些类可能与领域实体具有相同的属性。这样做,我们可以用对 Flow 处理逻辑有用的额外属性来扩展 Flow Model。例如,CompanyModel 继承了 Company 实体的所有属性,同时还具有将在 Flow 逻辑中使用的特殊属性。

public class CompanyModel : Company, IFlowModel
{
    public virtual List<PersonCompanyLinkDetailsModel> 
    PersonCompanyLinks { get; set; } = new List<PersonCompanyLinkDetailsModel>();
    public virtual List<PersonCompanyLinkDetailsModel> 
    PersonCompanyLinksDeleted { get; set; } = new List<PersonCompanyLinkDetailsModel>();
    public virtual List<PersonCompanyLinkType> AllLinkTypes { get; set; }
    public virtual List<PersonModel> AllPersons { get; set; }
}

List Flow

List Flow 是一个简化的 Flow,用于在 UI 表格形式中显示记录列表。它没有 Define body,但它有一个 LoadDataAsync 方法来检索记录数据。

public class CompanyListFlow : ListFlowBase<CompanyListModel, FormCompanyList>
{
    private readonly ICompanyRepository _companyRepository;

    public CompanyListFlow(ICompanyRepository companyRepository) 
    {
        _companyRepository = companyRepository;
    }

    public override async Task<CompanyListModel> 
           LoadDataAsync(QueryOptions queryOptions)
    {
        var q = _companyRepository.GetAllQuery();

        if (!string.IsNullOrWhiteSpace(queryOptions.SearchString))
        {
            q = q.Where(x => x.Name.Contains
                (queryOptions.SearchString, StringComparison.OrdinalIgnoreCase) 
                || (x.RegistrationNumber != null && 
                x.RegistrationNumber.Contains(queryOptions.SearchString, 
                StringComparison.OrdinalIgnoreCase)) );
        }

        if (queryOptions.AllowSort && !string.IsNullOrWhiteSpace
        (queryOptions.SortColumn) && queryOptions.SortDirection != SortDirection.None)
        {
            q = q.QueryOrderByDirection(queryOptions.SortDirection, 
                                        queryOptions.SortColumn);
        }
                
        var list = (await _companyRepository.RunQueryAsync(q)).Select(x =>
        {
            var item = new CompanyModel();
            x.ReflectionCopyTo(item);
            return item;
        }).ToList();

        var result = new CompanyListModel { Data = list };
        return result;
    }
}

如您所见,CompanyListFlow 通过依赖注入在构造函数中接收 ICompanyRepository,并使用它来检索数据。

QueryOptions 参数可能包含搜索模式和/或排序信息,我们使用这些信息来组合查询,添加 WhereOrderBy 子句——这要归功于我们 Repository 的灵活性,它具有 GetAllQueryRunQueryAsync 方法。

运行查询后,我们遍历返回的记录,并使用扩展方法 ReflectionCopyTo 将返回的 Company 实体的所有属性复制到 CompanyModel 业务对象(继承自 Company)(顺便说一句,您也可以为此使用 AutoMapper,但我个人认为它的语法过于复杂)。

List Form

定义 Company UI 表的最后一部分是 List Form,它定义了列和导航。

public class FormCompanyList : FormListBase<CompanyListModel>
{
    protected override void Define(FormListBuilder<CompanyListModel> builder)
    {
        builder.List(p => p.Data, e =>
        {
            e.DisplayName = "Companies";

            e.Property(p => p.Id).IsPrimaryKey();
            e.Property(p => p.Name);
            e.Property(p => p.RegistrationNumber).Label("Reg. No.");
            e.Property(p => p.EstablishedDate).Label("Established date").
                       Format("dd/MM/yyyy");

            e.ContextButton("Details", "company-edit/{0}");
            e.NavigationButton("Add", "company-edit/0");
        });
    }
}

IsPrimaryKey() 标记了一个包含记录主键的列,该主键将作为参数提供给 ContextButton 导航链接格式字符串“company-edit/{0}”。

我们还为 DateTime 列提供了列标签和日期格式。

为了渲染表单,我们在 Pages 文件夹的 CompanyList.razor 中添加了 FlowListForm 控件。

@page "/company-list"

<FlowListForm FlowType="@typeof(CrmLightDemoApp.Onion.
 Services.Flow.CompanyListFlow).FullName" Options="GlobalSettings.ListFormOptions" />

@code {
}

您可以运行应用程序并搜索和排序公司。

Picture 4

如果您单击行,您将被导航到 Company 编辑页面,并将记录主键作为参数提供,但如果您单击 **Add** 按钮,Company 编辑页面将收到 0 作为主键参数,这意味着——添加新记录。

CompanyEdit.razor 接受参数 Pk 并将其提供给 FlowEditForm

@page "/company-edit/{pk}"

<FlowEditForm FlowName="@typeof(CrmLightDemoApp.Onion.Services.
                        Flow.CompanyEditFlow).FullName" Pk="@Pk"
              Options="GlobalSettings.EditFormOptions" 
              NavigationSuccess="/company-list" />

@code {
    [Parameter]
    public string Pk { get; set; }
}

Edit Flow

CompanyEditFlow 类定义了两种主要情况——对于非零 ItemKey(提供的 Pk),应执行 LoadData 方法,并向用户显示 FormCompanyViewFormCompanyView 具有 **Delete** 按钮,如果按下该按钮,则执行 DeleteData 方法。

第二种情况——零 ItemKey 或在 FormCompanyView 上按了 **Edit** 按钮——在这种情况下,将执行 LoadRelatedData 方法,并向用户显示 FormCompanyEdit

public override void Define()
{
    this
        .If(() => _flowContext.Params.ItemKeyAboveZero)
            .Begin(LoadData)
            .NextForm(typeof(FormCompanyView))
        .EndIf()
        .If(() => _flowContext.ExecutionResult.FormLastAction == 
                                               ModelBinding.DeleteButtonBinding)
            .Next(DeleteData)
        .Else()
            .If(() => _flowContext.ExecutionResult.FormLastAction == 
            ModelBinding.SubmitButtonBinding || !_flowContext.Params.ItemKeyAboveZero)
                .Next(LoadRelatedData)
                .NextForm(typeof(FormCompanyEdit))
                .Next(SaveData)
            .EndIf()
        .EndIf()
        .End();
}

LoadData 方法使用 Company 详细信息(包括 PersonCompanyLinks——CompanyPerson 实体之间的引用)填充 Flow Model。

public async Task LoadData()
{
    if (_flowContext.Params.ItemKeyAboveZero)
    {
        var item = await _companyRepository.GetByIdAsync(_flowContext.Params.ItemKey);
        // item and Model have different types - we use reflection 
        // to copy similar properties
        item.ReflectionCopyTo(Model);

        Model.PersonCompanyLinks = 
              (await _personCompanyRepository.GetByCompanyIdAsync(Model.Id))
            .Select(x =>
            {
                var item = new PersonCompanyLinkDetailsModel();
                x.ReflectionCopyTo(item);
                return item;
            }).ToList();
    }
}

DeleteData 方法仅通过 Repository 方法 SoftDeleteAsync 删除 Company,该方法将实体的 Deleted 标志更改为 true

LoadRelatedData 方法填充 AllLinkTypesAllPersons 集合,这些集合将用于 DropdownDropdownSearch 控件。

如果用户按下了 FormCompanyEdit 上的 **Submit** 按钮,则执行 SaveData 方法。该方法会更新 ID 大于零的 Company 记录,否则会在 ID 为零时插入(添加新 Company 的情况)。此方法还会遍历 PersonCompanyLinksDeletedPersonCompanyLinks 集合,删除用户删除的记录,并插入和更新用户添加和更改的记录。

public async Task SaveData()
{
    if (_flowContext.Params.ItemKeyAboveZero)
    {
        await _companyRepository.UpdateAsync(Model);
    }
    else
    {
        Model.Id = await _companyRepository.CreateAsync(Model);
    }

    foreach (var item in Model.PersonCompanyLinksDeleted)
    {
        if (item.Id != 0)
        {
            await _personCompanyRepository.SoftDeleteAsync(item.Id);
        }
    }

    foreach (var item in Model.PersonCompanyLinks)
    {
        if (item.Id == 0)
        {
            item.CompanyId = Model.Id;
            await _personCompanyRepository.CreateAsync(item);
        }
        else if (item.Changed)
        {
            await _personCompanyRepository.UpdateAsync(item);
        }
    }
}

编辑表单

FormCompanyView 定义了 Company 的只读表示,它以表格形式显示所有 Company 属性和 PersonCompanyLinks。我还为 **Delete** 按钮添加了确认消息。

public class FormCompanyView : FormEditBase<CompanyModel>
{
    protected override void Define(FormEntityTypeBuilder<CompanyModel> f)
    {
        f.DisplayName = "Company View";

        f.Property(p => p.Name).Label("Name").IsReadOnly();
        f.Property(p => p.RegistrationNumber).Label("Reg. No.").IsReadOnly();
        f.Property(p => p.EstablishedDate).Label("Established date").IsReadOnly();

        f.Table(p => p.PersonCompanyLinks, e => 
        {
            e.DisplayName = "Associations";
            e.Property(p => p.LinkTypeName).Label("Type");
            e.Property(p => p.PersonFullName).Label("Person");
        });

        f.Button(ButtonActionTypes.Close, "Close");

        f.Button(ButtonActionTypes.Delete, "Delete")
            .Confirm(ConfirmType.Continue, "Delete this Company?", ConfirmButtons.YesNo);

        f.Button(ButtonActionTypes.Submit, "Edit");
    }
}

Picture 5

FormCompanyEdit 包含用于编辑 Company 属性的输入控件,以及用于编辑 PersonCompanyLinks 的控件,这些控件在 Repeater 控件中定义,该控件呈现一个可编辑的网格,其中可以添加、更改或删除记录。

    public class FormCompanyEdit : FormEditBase<CompanyModel>
    {
        protected override void Define(FormEntityTypeBuilder<CompanyModel> f)
        {
            f.DisplayName = "Company Edit";
            f.Confirm(ConfirmType.ChangesWillBeLost, 
            "If you leave before saving, your changes will be lost.", 
             ConfirmButtons.OkCancel);

            f.Property(p => p.Name).Label("Name").IsRequired();
            f.Property(p => p.RegistrationNumber).Label("Reg. No.").IsRequired();
            f.Property(p => p.EstablishedDate).Label("Established date").IsRequired();

            f.Repeater(p => p.PersonCompanyLinks, e =>
            {
                e.DisplayName = "Associations";
                e.Property(p => p.Id).IsReadOnly().Rule(typeof
                (FormCompanyEdit_ItemDeletingRule), FormRuleTriggers.ItemDeleting);
                
                e.PropertyRoot(p => p.LinkTypeId).Dropdown
                (p => p.AllLinkTypes, m => m.Id, 
                 m => m.Name).IsRequired().Label("Type")
                    .Rule(typeof(FormCompanyEdit_ItemChangedRule), 
                     FormRuleTriggers.ItemChanged);

                e.PropertyRoot(p => p.PersonId).DropdownSearch
                (e => e.AllPersons, m => m.Id, m => 
                      m.FullName).IsRequired().Label("Person")
                    .Rule(typeof(FormCompanyEdit_ItemChangedRule), 
                                 FormRuleTriggers.ItemChanged);
            }).Confirm(ConfirmType.DeleteItem, "Delete this association?", 
                                                ConfirmButtons.YesNo);

            f.Button(ButtonActionTypes.Cancel, "Cancel");
            f.Button(ButtonActionTypes.Submit, "Save");
        }
    }

为不保存数据而离开表单定义了确认消息;为在 Repeater 中删除 PersonCompanyLinks 记录定义了另一个消息。

PropertyRoot() 函数与 Property() 相同,但可以在 Repeater 中使用,当您需要引用根 Model 类中的集合时。

表单还有两个 Rules。

FormCompanyEdit_ItemDeletingRule 在用户删除 Repeater 记录时触发,此 Rule 将已删除的记录存储在一个特殊的 Model 集合中,我们将在 SaveData 方法中使用它。

public class FormCompanyEdit_ItemDeletingRule : FlowRuleBase<CompanyModel>
{
    public override string RuleCode => "CMP-1";

    public override void Execute(CompanyModel model)
    {
        // preserve all deleted items
        model.PersonCompanyLinksDeleted.Add
              (model.PersonCompanyLinks[RunParams.RowIndex]);
    }
}

FormCompanyEdit_ItemChangedRule 在用户在 Repeater 中更改 LinkTypePerson 属性时触发,此规则会更新 Changed 标志。这也会在 Flow 的 SaveData 方法中使用。

public class FormCompanyEdit_ItemChangedRule : FlowRuleBase<CompanyModel>
{
    public override string RuleCode => "CMP-2";

    public override void Execute(CompanyModel model)
    {
        model.PersonCompanyLinks[RunParams.RowIndex].Changed = true;
    }
}

当用户单击 **Edit** 按钮时,他们将看到

Picture 6

我应该再次提到,Forms 不包含任何业务逻辑,它们只定义 Model 如何绑定到控件和 Rules。Forms 也不能加载或保存任何数据。当您需要保存/加载数据时,您应该使用 Flows。当您需要进行复杂的验证、标记已更改的记录或重新加载某些数据时,您应该使用 Rules。遵循这些建议,您的代码将易于理解和维护,没有任何问题。

PersonPersonCompanyType 的 Flows 和 Forms 遵循相同的方法,您可以在从 GitHub 下载的解决方案中看到它们的代码。

添加 SQL 数据库

为了完成这篇文章,我想用 SqlRepository 替换 LocalCacheRepository,它将数据存储在 SQL 数据库中。最终解决方案已添加到 CrmLightDemoApp.Sql\ 文件夹中。

为了开始使用 SQL,我添加了 Microsoft.EntityFrameworkCore.ToolsMicrosoft.EntityFrameworkCore.SqlServer 包,然后将 CrmContext.cs 添加到 Onion\Infrastructure\ 文件夹中。

using CrmLightDemoApp.Onion.Domain;
using Microsoft.EntityFrameworkCore;

namespace CrmLightDemoApp.Onion.Infrastructure
{
    public class CrmContext : DbContext
    {
        public DbSet<Company> Company { get; set; }
        public DbSet<Person> Person { get; set; }
        public DbSet<PersonCompanyLink> PersonCompanyLink { get; set; }
        public DbSet<PersonCompanyLinkType> PersonCompanyLinkType { get; set; }

        protected override void OnConfiguring(DbContextOptionsBuilder options)
            => options.UseSqlServer("Server = 
               (localdb)\\mssqllocaldb; Database=CrmLightDb1;
               Trusted_Connection=True;MultipleActiveResultSets=true");
    }
}

这是 SQL Express mssqllocaldb,它应该可以在任何 Windows 机器上运行,但如果您愿意,可以在连接字符串中指定您自己的实际 SQL Server 数据库。

然后,我需要修改我的 CompanyPersonPersonCompanyLinkType 实体,以包含对 PersonCompanyLink 表的引用,这对于 EF Model-First 方法是必需的,在该方法中,数据库架构是从您的实体生成的。

然后,我在程序包管理器控制台中创建了一个 EF 迁移,运行了命令

Add-Migration InitialCreate

接下来,如果您想运行该应用程序,则必须在程序包管理器控制台中运行以下命令。

Update-Database

此命令将在目标数据库中创建表和关系。

更改 Repository

创建了 SqlRepository.cs 代替 LocalCacheRepository。它使用 CrmContext 在 SQL 数据库中执行查询。

using CrmLightDemoApp.Onion.Domain;
using CrmLightDemoApp.Onion.Domain.Repositories;
using Microsoft.EntityFrameworkCore;

namespace CrmLightDemoApp.Onion.Infrastructure
{
    public class SqlRepository<T> : IRepository<T>
        where T : class, IEntity, new()
    {
        public async Task<int> CreateAsync(T data)
        {
            using var db = new CrmContext();
            db.Set<T>().Add(data);
            await db.SaveChangesAsync();
            return data.Id;
        }

        public async Task DeleteAsync(int id)
        {
            using var db = new CrmContext();
            var table = db.Set<T>();
            var entity = new T { Id = id };
            table.Attach(entity);
            table.Remove(entity);
            await db.SaveChangesAsync();
        }

        public async Task<T> GetByIdAsync(int id)
        {
            using var db = new CrmContext();
            return await db.Set<T>().SingleAsync(x => x.Id == id);
        }

        public async Task<List<T>> GetAllAsync()
        {
            using var db = new CrmContext();
            return await db.Set<T>().Where(x => !x.Deleted).ToListAsync();
        }

        public async Task UpdateAsync(T data)
        {
            using var db = new CrmContext();
            db.Set<T>().Update(data);
            await db.SaveChangesAsync();
        }

        public async Task SoftDeleteAsync(int id)
        {
            using var db = new CrmContext();
            var record = await db.Set<T>().SingleAsync(x => x.Id == id);
            record.Deleted = true;
            await db.SaveChangesAsync();
        }

        public async Task<List<T>> GetListByIdsAsync(IEnumerable<int> ids)
        {
            using var db = new CrmContext();
            return await db.Set<T>().Where(x => ids.Contains(x.Id)).ToListAsync();
        }

        public ContextQuery<T> GetContextQuery()
        {
            var db = new CrmContext();
            return new ContextQuery<T>(db, db.Set<T>().Where(x => !x.Deleted));
        }

        public async Task<List<T>> RunContextQueryAsync(ContextQuery<T> query)
        {
            return await query.Query.ToListAsync();
        }
    }
}

不幸的是,我需要更改 IRepository<> 接口,因为我最初的设计不允许在 Repository 外部组装搜索和排序查询,现在使用了 GetContextQueryRunContextQueryAsync 方法,它们使用可处置类 ContextQuery<>

现在,如果您运行应用程序,初始数据库将为空,您需要使用 UI 填充 PersonCompanyPersonCompanyLink 表。

摘要

在本文中,我介绍了 BlazorForms 开源框架的 CrmLight 种子项目。它展示了一种将数据库 Repositories、应用程序业务逻辑和用户界面连接在一起的直接方法。最后,我将解决方案从使用内存模拟 Repositories 转换为真实的 SQL 数据库,以便最终解决方案可以成为一些实际项目的良好起点。

在未来的文章中,我将重点介绍更复杂的场景以及其他类型的 Flows,并介绍更多可以使用 BlazorForms 实现的用例。

您可以在我的 GitHub 上找到完整的解决方案代码,文件夹为 Story-09-BlazorForms-CrmLight

感谢您的阅读,请记住,如果您在实施过程中需要任何帮助,可以随时与我们联系。

https://procoders.com.au/contact-us/

历史

  • 2023 年 1 月 12 日:初始版本
© . All rights reserved.