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

BlazorForms 低代码开源框架。第三部分:CrmLight 潜在客户看板

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.17/5 (7投票s)

2023 年 1 月 27 日

CPOL

9分钟阅读

viewsIcon

18275

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

本系列中所有 BlazorForms 文章

我们在此处部署了一个可用的解决方案

https://crmlight.platz.app

请记住,它没有数据库,并且将所有数据存储在内存中,所有用户共享,并且在一段时间不活动后,容器将关闭,从而丢失所有数据。

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

引言

本文继续介绍 PRO CODERS PTY LTD 开发并以 MIT 许可证开源的 BlazorForms 框架系列文章。

在上一篇文章中,我介绍了 CrmLight 种子项目,该项目展示了如何使用 BlazorForms(一个简化 UI 开发并允许您构建简单且可维护的 C# 解决方案的框架)来实现、编辑和列出表单。

BlazorForms 范式

我们在这个框架中采用的主要范式是**将解决方案的逻辑部分与物理 UI 渲染分离**。BlazorForms 鼓励开发人员首先规划解决方案,考虑实体、关系、数据访问、模型和业务逻辑,而不是 UI 控件和事件编码。

从 GitHub 下载此博客文章代码

GitHub 上的 BlazorForms 项目

CrmLight 潜在客户看板

我们继续扩展 `CrmLight` 项目,以展示框架的不同区域和组件。在这篇文章中,我们将展示如何实现 CRM 潜在客户看板。

如果您运行结果解决方案,您可以在导航菜单中看到“**潜在客户看板**”。点击它将导航到该屏幕

Picture 1

在这里,您可以看到系统中所有已开放的潜在客户。

您可以垂直排序卡片,并将它们向左或向右移动到代表潜在客户看板卡片状态的桶中。

当您点击卡片时,将显示编辑对话框。在这里,用户可以更新卡片详细信息、添加评论并跟踪评论历史

Picture 2

历史评论对于评论所有者也是可编辑的

图 3

当卡片被移动到 `Won` 桶时,系统会要求用户提供更多详细信息以创建客户记录(这将在 `Clients` 页面中显示)

Picture 4

当用户在下拉搜索栏中选择一个值时,例如**客户经理**,也可以添加新记录或编辑现有项目之一

Picture 5

Picture 6

如何实现

如果您查看 Visual Studio 解决方案,您会看到 Flow 文件夹中添加了新的子文件夹

图 7

它包含潜在客户看板的业务逻辑(流程、表单和规则)。

UI 部分将由 `FlowBoard` 控件渲染,稍后将讨论。目前,我应该提到 `FlowBoard` 对其使用的流程和模型有一些要求。

流程

让我们从控制潜在客户看板状态和转换的 *LeadBoardStateFlow.cs* 开始。

using BlazorForms.Flows;
using BlazorForms.Flows.Definitions;
using CrmLightDemoApp.Onion.Domain.Repositories;
using CrmLightDemoApp.Onion.Services.Model;

namespace CrmLightDemoApp.Onion.Services.Flow.LeadBoard
{
    public class LeadBoardStateFlow : StateFlowBase<LeadBoardCardModel>
    {
        // Board Columns
        public state Lead;
        public state Contacted;
        public state MeetingScheduled = new state("Meeting Scheduled");
        public state ProposalDelivered = new state("Proposal Delivered");
        public state Won;

        // Board Card Transitions
        public override void Define()
        {
            this
                .SetEditForm<FormLeadCardEdit>()
                .State(Lead)
                    .TransitionForm<FormContactedCardEdit>
                     (new UserActionTransitionTrigger(), Contacted)
                .State(Contacted)
                    .Transition<UserActionTransitionTrigger>(Lead)
                    .Transition(new UserActionTransitionTrigger(), MeetingScheduled)
                .State(MeetingScheduled)
                    .Transition<UserActionTransitionTrigger>(Contacted)
                    .Transition<UserActionTransitionTrigger>(ProposalDelivered)
                .State(ProposalDelivered)
                    .Transition<UserActionTransitionTrigger>(MeetingScheduled)
                    .TransitionForm<FormCardCommit>
                     (new UserActionTransitionTrigger(), Won)
                .State(Won)
                    .Transition<UserActionTransitionTrigger>(Lead)
                    .Transition<UserActionTransitionTrigger>(Contacted)
                    .Transition<UserActionTransitionTrigger>(MeetingScheduled)
                    .Transition<UserActionTransitionTrigger>(ProposalDelivered)
                    .End();
        }
    }
}

您可以看到顶部声明的所有五个状态,当我们需要的状态描述与状态名称不同时,我们使用 `public state MeetingScheduled = new state("Meeting Scheduled");`

接下来,我们定义所有可能的状态转换,这行代码

.State(Lead)
    .TransitionForm<FormContactedCardEdit>(new UserActionTransitionTrigger(), Contacted)

表示在 `Lead` 和 `Contacted` 状态之间存在转换。在转换期间,当转换发生时显示表单 `FormContactedCardEdit`,转换只能由用户操作 `UserActionTransitionTrigger` 触发器完成——通过拖放或上下文菜单。

如果未定义两个状态之间的转换,潜在客户看板将不允许移动卡片。

模型

该流程指定了一个特定的模型类,您可以将其视为 `StateFlowBase` 流程基类的模板参数 - `LeadBoardCardModel`。如果您点击此模型类并按 **F12**,Visual Studio 将导航到包含该类代码的 *LeadBoardCardModel.cs*

public class LeadBoardCardModel : BoardCard, IFlowBoardCard
{
    public virtual string? Comments { get; set; }

    // for dropdowns
    public virtual List<PersonModel> AllPersons { get; set; } = new();
    public virtual List<CompanyModel> AllCompanies { get; set; } = new();
    public virtual List<LeadSourceType> AllLeadSources { get; set; } = new();

    // for ClientCompany
    public virtual ClientCompany ClientCompany { get; set; } = new();

    public virtual List<CardHistoryModel>? CardHistory { get; set; } = new();

    // properties
    public string SalesPersonFullName
    {
        get
        {
            var sp = AllPersons.FirstOrDefault(p => p.Id == SalesPersonId);

            if (sp != null)
            {
                return sp.FullName;
            }

            return null;
        }
    }
}

模型类继承 `BoardCard` 实体的所有属性,并实现 `IFlowBoardCard` 接口,这对于 `FlowBoard` UI 使用的模型是强制性的。

`LeadBoardCardModel` 类也用于流中定义的所有表单。

表单

流程行 `SetEditForm()` 定义了一个默认编辑表单,当用户想要编辑卡片时显示。

如果您在特定状态下指定 `SetEditForm<>()` 函数,则指定的表单将仅用于编辑此状态下的卡片。

public class FormLeadCardEdit : FormEditBase<LeadBoardCardModel>
{
    protected override void Define(FormEntityTypeBuilder<LeadBoardCardModel> f)
    {
        f.DisplayName = "Lead Card";
        f.Rule(typeof(FormLeadCard_RefreshSources), FormRuleTriggers.Loaded);
        f.Confirm(ConfirmType.ChangesWillBeLost, 
        "If you leave before saving, your changes will be lost.", 
         ConfirmButtons.OkCancel);
        f.Layout = FormLayout.TwoColumns;

        f.Group("left");

        f.Property(p => p.State).IsReadOnly();
        f.Property(p => p.Title).IsRequired();
        f.Property(p => p.Description);

        f.Property(p => p.SalesPersonId).DropdownSearch
        (p => p.AllPersons, m => m.Id, m => m.FullName).Label
                                       ("Sales person").IsRequired()
            .ItemDialog(typeof(PersonDialogFlow));

        f.Property(p => p.RelatedCompanyId).DropdownSearch
        (p => p.AllCompanies, m => m.Id, m => m.Name).Label("Lead company")
            .ItemDialog(typeof(CompanyDialogFlow));

        f.Property(p => p.RelatedPersonId).DropdownSearch
        (p => p.AllPersons, m => m.Id, m => m.FullName).Label("Lead contact")
            .ItemDialog(typeof(PersonDialogFlow));

        f.Property(p => p.LeadSourceTypeId).Dropdown
        (p => p.AllLeadSources, m => m.Id, m => m.Name).Label("Lead source");

        f.Property(p => p.Phone);
        f.Property(p => p.Email);
        f.Property(p => p.ContactDetails).Label("Other contact info");

        f.Group("right");

        f.Property(p => p.Comments).Control(ControlType.TextArea);

        f.CardList(p => p.CardHistory, e =>
        {
            e.DisplayName = "Comment history";
            e.Card(p => p.TitleMarkup, p => p.Text, p => p.AvatarMarkup);

            e.Rule(typeof(FormLeadCardEdit_ItemChangedRule));
            e.Rule(typeof(FormLeadCardEdit_ItemDeletingRule), 
                   FormRuleTriggers.ItemDeleting);
            e.Confirm(ConfirmType.DeleteItem, 
                      "Delete this comment?", ConfirmButtons.YesNo);

            e.Button(ButtonActionTypes.Edit);
            e.Button(ButtonActionTypes.Delete);
        });

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

表单加载时将执行规则 `FormLeadCard_RefreshSources`。

表单使用 `FormLayout.TwoColumns` 将表单内容分成两列,将所有编辑控件放在左列,将评论和历史记录放在右列。

对于 `DropdownSearch` 控件,我们使用 `ItemDialog` 函数来指定用于添加或查看项目详细信息的对话框。

为了实现评论历史,我们使用了 `CardList` 控件,它包含

  • 绑定到用于渲染每个历史项的模型属性,
  • **编辑**和**删除**按钮以编辑项目,
  • 以及当历史项被编辑或删除时执行的规则,其中包含 `DeleteItem` 操作的确认。

规则

FormLeadCard_RefreshSources

每次显示(加载)表单时都会执行此规则

public class FormLeadCard_RefreshSources : FlowRuleAsyncBase<LeadBoardCardModel>
{
    private readonly ICompanyRepository _companyRepository;
    private readonly IPersonRepository _personRepository;
    private readonly IBoardCardHistoryRepository _boardCardHistoryRepository;
    private readonly IAppAuthState _appAuthState;

    public override string RuleCode => "BRD-4";

    public FormLeadCard_RefreshSources(ICompanyRepository companyRepository, 
                                       IPersonRepository personRepository,
        IBoardCardHistoryRepository boardCardHistoryRepository, 
                                    IAppAuthState appAuthState)
    {
        _companyRepository = companyRepository;
        _personRepository = personRepository;
        _boardCardHistoryRepository = boardCardHistoryRepository;
        _appAuthState = appAuthState;
    }

    public override async Task Execute(LeadBoardCardModel model)
    {
        // refresh drop down sources
        model.AllPersons = (await _personRepository.GetAllAsync())
            .Select(x =>
            {
                var item = new PersonModel();
                x.ReflectionCopyTo(item);
                item.FullName = $"{x.FirstName} {x.LastName}";
                return item;
            }).OrderBy(x => x.FullName).ToList();

        model.AllCompanies = (await _companyRepository.GetAllAsync())
            .Select(x =>
            {
                var item = new CompanyModel();
                x.ReflectionCopyTo(item);
                return item;
            }).OrderBy(x => x.Name).ToList();

        // refresh comments
        if (model.Id > 0)
        {
            model.CardHistory = 
                  (await _boardCardHistoryRepository.GetListByCardIdAsync(model.Id))
                .Select(x =>
                {
                    var item = new CardHistoryModel();
                    x.ReflectionCopyTo(item);
                    return item;
                }).ToList();
        }

        // refresh card buttons - display buttons only for comment owners
        for (int i = 0; i < model.CardHistory.Count; i++)
        {
            var isCurrentUser = _appAuthState.GetCurrentUser().Id == 
                                model.CardHistory[i].PersonId;
            Result.Fields[FindField(m => m.CardHistory, 
                          ModelBinding.EditButtonBinding, i)].Visible = isCurrentUser;
            Result.Fields[FindField(m => m.CardHistory, 
                          ModelBinding.DeleteButtonBinding, i)].Visible = isCurrentUser;
        }
    }
}

它使用依赖注入来接收所需的存储库和 `IAppAuthState`,后者用于读取当前登录到系统的用户。

在 `Execute` 方法中,它填充用于 `DropdownSearch` 控件的集合,并填充 `CardHistory` 集合,读取此卡片的所有历史项目。

然后,它更新**编辑**和**删除**按钮的 `Visible` 属性,只允许评论历史项目所有者进行修改。

FormLeadCardEdit_ItemChangedRule

当用户修改评论卡片时,将执行此规则。它设置 `EditedDate` 并使用 `_boardCardHistoryRepository` 保存评论。

public class FormLeadCardEdit_ItemChangedRule : FlowRuleAsyncBase<LeadBoardCardModel>
{
    private readonly IBoardCardHistoryRepository _boardCardHistoryRepository;

    public override string RuleCode => "BRD-5";

    public FormLeadCardEdit_ItemChangedRule
           (IBoardCardHistoryRepository boardCardHistoryRepository)
    {
        _boardCardHistoryRepository = boardCardHistoryRepository;
    }

    public override async Task Execute(LeadBoardCardModel model)
    {
        var changedCard = model.CardHistory[RunParams.RowIndex];
        changedCard.EditedDate = DateTime.Now;
        await _boardCardHistoryRepository.UpdateAsync(changedCard);
        Result.SkipThisChange = true;
    }
}

FormLeadCardEdit_ItemDeletingRule

当用户点击**删除**按钮时,此规则将简单地删除项目。

public class FormLeadCardEdit_ItemDeletingRule : FlowRuleAsyncBase<LeadBoardCardModel>
{
    private readonly IBoardCardHistoryRepository _boardCardHistoryRepository;
    private readonly IAppAuthState _appAuthState;

    public override string RuleCode => "BRD-6";

    public FormLeadCardEdit_ItemDeletingRule
    (IBoardCardHistoryRepository boardCardHistoryRepository, IAppAuthState appAuthState)
    {
        _boardCardHistoryRepository = boardCardHistoryRepository;
        _appAuthState = appAuthState;
    }

    public override async Task Execute(LeadBoardCardModel model)
    {
        await _boardCardHistoryRepository.SoftDeleteAsync
                               (model.CardHistory[RunParams.RowIndex]);
        Result.SkipThisChange = true;
    }
}

其他表单和流程

FormContactedCardEdit

此表单用于从 `Lead` 状态到 `Contacted` 状态的转换,并且每次用户将卡片从 `Lead` 移动到 `Contacted` 桶时都会显示。

它显示了用户在转换期间可以更新的几个字段,以及一个必填字段“`潜在客户联系人`”,用户必须在提交转换之前输入该字段。

public class FormContactedCardEdit : FormEditBase<LeadBoardCardModel>
{
    protected override void Define(FormEntityTypeBuilder<LeadBoardCardModel> f)
    {
        f.DisplayName = "Lead Contacted Card";

        f.Property(p => p.RelatedCompanyId).DropdownSearch
        (p => p.AllCompanies, m => m.Id, m => m.Name).Label("Lead company")
            .ItemDialog(typeof(CompanyDialogFlow));

        f.Property(p => p.RelatedPersonId).DropdownSearch
        (p => p.AllPersons, m => m.Id, m => m.FullName).Label("Lead contact")
            .ItemDialog(typeof(PersonDialogFlow)).IsRequired();

        f.Property(p => p.Phone);
        f.Property(p => p.Email);
        f.Property(p => p.ContactDetails).Label("Other contact info");

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

FormCardCommit

此表单用于最终过渡到 `Won` 状态。它只有一个必填字段“`客户公司`”,用户可以在其中使用对话框 `CompanyDialogFlow` 选择或添加新公司

public class FormCardCommit : FormEditBase<LeadBoardCardModel>
{
    protected override void Define(FormEntityTypeBuilder<LeadBoardCardModel> f)
    {
        f.DisplayName = "Congrats with another win! 
                         Click 'Save' to create client record.";
        f.Property(p => p.Title).IsReadOnly();
        f.Property(p => p.ClientCompany.StartContractDate).Label("Start contract date");

        f.Property(p => p.ClientCompany.ClientManagerId).DropdownSearch
                  (p => p.AllPersons, m => m.Id, m => m.FullName)
            .Label("Client manager").ItemDialog(typeof(PersonDialogFlow));
            
        f.Property(p => p.ClientCompany.AlternativeClientManagerId).DropdownSearch
                  (p => p.AllPersons, m => m.Id, m => m.FullName)
            .Label("Alternative client manager").ItemDialog(typeof(PersonDialogFlow));

        f.Property(p => p.RelatedCompanyId).DropdownSearch
                  (p => p.AllCompanies, m => m.Id, m => m.Name)
            .Label("Client company").ItemDialog
                  (typeof(CompanyDialogFlow)).IsRequired();

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

您可能已经注意到,我们在定义 `DropdownSearch` 控件时多次使用了 `CompanyDialogFlow` 和 `PersonDialogFlow`。

这些是简化流程,它们继承自 `DialogFlowBase<`>` 基类,引用模型和表单,并且这些流程具有在显示对话框时加载数据和提交对话框时保存数据的方法。

重要的是使用特定 `DropdownSearch` 所使用的集合中项目的模型类型,例如,在这行代码中

f.Property(p => p.RelatedCompanyId).DropdownSearch(p => p.AllCompanies, 
           m => m.Id, m => m.Name)
            .Label("Client company").ItemDialog
            (typeof(CompanyDialogFlow)).IsRequired();

`DropdownSearch` 引用 `AllCompanies` 集合,该集合定义为 `List`,这意味着 `CompanyDialogFlow` 应该引用 `CompanyModel`。

CompanyDialogFlow

此流程使用依赖注入来接收 `ICompanyRepository`,该存储库用于在 `LoadDataAsync` 中通过 `Id` 读取模型数据并在 `SaveDataAsync` 中保存它。

表单 `FormCompanyDialogEdit` 定义了字段控件和按钮。

public class CompanyDialogFlow : DialogFlowBase<CompanyModel, FormCompanyDialogEdit>
{
    private readonly ICompanyRepository _companyRepository;

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

    public override async Task LoadDataAsync()
    {
        if (GetId() > 0)
        {
            var record = await _companyRepository.GetByIdAsync(GetId());
            record.ReflectionCopyTo(Model);
        }
        else
        {
            Model.Name = Params["Name"];
        }
    }

    public override async Task SaveDataAsync()
    {
        if (GetId() > 0)
        {
            await _companyRepository.UpdateAsync(Model);
        }
        else
        {
            Model.Id = await _companyRepository.CreateAsync(Model);
        }
    }
}

public class FormCompanyDialogEdit : FormEditBase<CompanyModel>
{
    protected override void Define(FormEntityTypeBuilder<CompanyModel> f)
    {
        f.DisplayName = "Add new company";
        f.Property(p => p.Name).Label("Name").IsRequired();
        f.Property(p => p.RegistrationNumber).Label("Reg. No.");
        f.Property(p => p.EstablishedDate).Label("Established date");
        f.Button(ButtonActionTypes.Cancel, "Cancel");
        f.Button(ButtonActionTypes.Submit, "Save");
    }
}

PersonDialogFlow

此表单与上一个非常相似,但使用了在第二部分:CrmLight 项目中创建的 `FormPersonEdit` 表单。

public class PersonDialogFlow : DialogFlowBase<PersonModel, FormPersonEdit>
{
    private readonly IPersonRepository _personRepository;

    public PersonDialogFlow(IPersonRepository personRepository)
    {
        _personRepository = personRepository;
    }

    public override async Task LoadDataAsync()
    {
        if (GetId() > 0)
        {
            var record = await _personRepository.GetByIdAsync(GetId());
            record.ReflectionCopyTo(Model);
        }

        var fullName = Params["Name"];

        if (fullName != null)
        {
            var split = fullName.Split(' ');
            Model.FirstName = split[0];

            if (split.Count() > 1)
            {
                Model.LastName = split[1];
            }
        }
    }

    public override async Task SaveDataAsync()
    {
        // we need full name for drop down option
        Model.FullName = $"{Model.FirstName} {Model.LastName}";

        if (GetId() > 0)
        {
            await _personRepository.UpdateAsync(Model);
        }
        else
        {
            Model.Id = await _personRepository.CreateAsync(Model);
        }
    }
}

现在我们完成了所有抽象的定义,可以专注于如何在 UI 中渲染它们。

UI 渲染

现在我们需要查看解决方案页面文件夹中的 *LeadBoard.razor*。

我们使用 `FlowBoard` 控件并为其提供一些参数。

<FlowBoard TFlow=LeadBoardStateFlow TItem=LeadBoardCardModel Caption="Lead Board" 
 Items=@_items ItemsChanged=@ItemsChanged
           CardTitleBackColor="lightblue" Options="GlobalSettings.BoardFormOptions" 
           EditFormOptions="GlobalSettings.EditFormOptions">
    <CardAvatar>
        <MudIcon Icon="@Icons.Material.TwoTone.Savings" />
    </CardAvatar>
    <CardTitle>
        <MudText Typo="Typo.body1" Color="Color.Info">@context.Title</MudText>
    </CardTitle>
    <CardBody>
        <MudText Typo="Typo.body2">@context.Description</MudText>
        <MudText Typo="Typo.caption" Color="Color.Primary">
                       @context.SalesPersonFullName</MudText>
    </CardBody>
</FlowBoard>

首先,我们需要指定 `TFlow` 和 `TItem` 参数,将控件指向我们上面已经讨论过的 `LeadBoardStateFlow` 及其模型。

第二件事是定义板卡的外观,我们可以定义 `CardAvatar`、`CardTitle` 和 `CardBody`。

最后一点是提供 `Items` – 要在板上显示的卡片列表,以及 `ItemsChanged` 事件,该事件将在卡片状态或卡片详细信息发生变化时执行,以便我们可以将这些更改保存到数据库中。

@code {
    @inject IBoardService _boardService

    private List<LeadBoardCardModel> _items = new();

    protected override async Task OnParametersSetAsync()
    {
        await LoadItems();
    }

    private async Task LoadItems()
    {
        _items = await _boardService.GetBoardCardsAsync();
    }

    private async Task ItemsChanged
            (List<BoardCardChangedArgs<LeadBoardCardModel>> list)
    {
        // you can save in transaction to make sure that
        // changes are saved all or nothing
        //_boardService.BeginUnitOfWork();

        var creating = list.Where(x => x.Type == ItemChangedType.Creating).ToList();
        creating.ForEach(async a => await _boardService.CreatingBoardCardAsync(a.Item));

        var deleted = list.Where(x => x.Type == ItemChangedType.Deleted).ToList();
        deleted.ForEach(async a => await _boardService.DeleteBoardCardAsync(a.Item));

        var added = list.Where(x => x.Type == ItemChangedType.Added).ToList();
        added.ForEach(async a => await _boardService.CreateBoardCardAsync(a.Item));

        // if card moved to Won state - create ClientCompany record
        var closing = list.FirstOrDefault(x => x.ChangedToTargetState("Won"));

        if (closing != null)
        {
            await CreateClientRecordAsync(closing.Item);
        }

        // save all changed board cards
        var changed = list.Where(x => x.Type == ItemChangedType.Changed 
            || x.Type == ItemChangedType.State
            || x.Type == ItemChangedType.Order).ToList();

        changed.ForEach(async a => await _boardService.UpdateBoardCardAsync(a.Item));

        //_boardService.CommitUnitOfWork();

        await LoadItems();
        StateHasChanged();
    }

    private async Task CreateClientRecordAsync(LeadBoardCardModel item)
    {
        // save Client Company
        item.ClientCompany.Id = item.ClientCompanyId ?? 0;
        item.ClientCompany.CompanyId = item.RelatedCompanyId.Value;
        var existing = await _boardService.FindClientCompanyAsync
                       (item.ClientCompany.CompanyId);

        if (existing != null)
        {
            // use existing ClientCompany, don't create duplicate
            item.ClientCompany.Id = existing.Id;
        }

        if (item.ClientCompany.Id > 0)
        {
            await _boardService.UpdateClientCompanyAsync(item.ClientCompany);
        }
        else
        {
            item.ClientCompanyId = 
                 await _boardService.CreateClientCompanyAsync(item.ClientCompany);
        }
    }
}

您可以看到我们通过依赖注入注入了 `IBoardService`,并在 `LoadItems` 方法中用于读取卡片,在 `ItemsChanged` 事件处理程序中用于保存卡片。

事件处理程序接收一个更改的项目列表,每个记录都有一个 `ItemChangedType` 属性,它告诉我们发生了哪种类型的更改。例如,如果它是 `ItemChangedType.Creating`,我们需要执行 `_boardService.CreatingBoardCardAsync`。

我们之所以将加载/保存逻辑保留在 razor 页面而不是 `LeadBoardStateFlow` 中,有一个特殊的原因。这是因为 `LeadBoardStateFlow` 一次只操作一张卡片,并且流程用于检查转换是否可能、触发器是什么以及在转换期间要做什么。然而,在看板上,我们操作的是卡片集合,我们必须从外部提供卡片,最好的方法是将卡片项目作为 `FlowBoard` 控件的参数提供。

对于 `Save` 操作,再次更好地操作卡片集合,例如,当用户重新排序卡片并且同时更改了几个卡片顺序时,我们可能希望在一次数据库事务中保存所有这些卡片。

BoardService

我想在这篇文章中考虑的最后一件事是实现 `IBoardService` 接口的 `BoardService`。

我们需要它,因为 UI 直接使用存储库和实体不是一个好的做法——最好有一个服务层来处理业务对象。

因此,`BoardService` 通过依赖注入接收一些存储库,并封装将存储库实体转换为 UI 中使用的更专业业务对象的逻辑。

它还提供高级业务对象操作,而不是存储库更细粒度和低级的操作。

using BlazorForms.Shared;
using CrmLightDemoApp.Onion.Domain;
using CrmLightDemoApp.Onion.Domain.Repositories;
using CrmLightDemoApp.Onion.Infrastructure;
using CrmLightDemoApp.Onion.Services.Abstractions;
using CrmLightDemoApp.Onion.Services.Model;

namespace CrmLightDemoApp.Onion.Services
{
    public class BoardService : IBoardService
    {
        private readonly IBoardCardRepository _repo;
        private readonly IPersonRepository _personRepository;
        private readonly ICompanyRepository _companyRepository;
        private readonly IClientCompanyRepository _clientCompanyRepository;
        private readonly IRepository<LeadSourceType> _leadSourceTypeRepository;
        private readonly IBoardCardHistoryRepository _boardCardHistoryRepository;
        private readonly IAppAuthState _appAuthState;

        public BoardService(IBoardCardRepository repo, 
                            IPersonRepository personRepository, 
                            IClientCompanyRepository clientCompanyRepository,
            ICompanyRepository companyRepository, 
            IRepository<LeadSourceType> leadSourceTypeRepository,
            IBoardCardHistoryRepository boardCardHistoryRepository, 
                                        IAppAuthState appAuthState) 
        { 
            _repo = repo;
            _personRepository = personRepository;
            _clientCompanyRepository = clientCompanyRepository;
            _companyRepository = companyRepository;
            _leadSourceTypeRepository = leadSourceTypeRepository;
            _boardCardHistoryRepository = boardCardHistoryRepository;
            _appAuthState = appAuthState;
        }

        public async Task<int> CreateBoardCardAsync(LeadBoardCardModel card)
        {
            var item = new BoardCard();
            card.ReflectionCopyTo(item);
            card.Id = await _repo.CreateAsync(item);
            return card.Id;
        }

        public async Task CreatingBoardCardAsync(LeadBoardCardModel card)
        {
            card.AllPersons = await GetAllPersons();
            card.AllCompanies = await GetAllCompanies();
            card.AllLeadSources = await GetAllLeadTypes();
        }

        public async Task DeleteBoardCardAsync(LeadBoardCardModel card)
        {
            await _repo.SoftDeleteAsync(card);
        }

        private async Task<List<LeadSourceType>> GetAllLeadTypes()
        {
            return await _leadSourceTypeRepository.GetAllAsync();
        }

        private async Task<List<CompanyModel>> GetAllCompanies()
        {
            return (await _companyRepository.GetAllAsync())
                .Select(x =>
                {
                    var item = new CompanyModel();
                    x.ReflectionCopyTo(item);
                    return item;
                }).OrderBy(x => x.Name).ToList();
        }

        private async Task<List<PersonModel>> GetAllPersons()
        {
            return (await _personRepository.GetAllAsync())
                .Select(x =>
                {
                    var item = new PersonModel();
                    x.ReflectionCopyTo(item);
                    item.FullName = $"{x.FirstName} {x.LastName}";
                    return item;
                }).OrderBy(x => x.FullName).ToList();
        }

        public async Task<List<LeadBoardCardModel>> GetBoardCardsAsync()
        {
            var persons = await GetAllPersons();
            var companies = await GetAllCompanies();
            var leadTypes = await GetAllLeadTypes();

            var items = (await _repo.GetAllAsync()).Select(x =>
            {
                var item = new LeadBoardCardModel();
                x.ReflectionCopyTo(item);
                item.AllPersons = persons;
                item.AllCompanies = companies;
                item.AllLeadSources = leadTypes;
                return item;
            }).OrderBy(x => x.Order).ToList();

            return items;
        }

        public async Task UpdateBoardCardAsync(LeadBoardCardModel card)
        {
            var item = new BoardCard();
            card.ReflectionCopyTo(item);
            await _repo.UpdateAsync(item);

            if (!string.IsNullOrWhiteSpace(card.Comments))
            {
                var comment = new BoardCardHistory
                {
                    BoardCardId = card.Id,
                    Title = "Comment",
                    Text = card.Comments,
                    PersonId = _appAuthState.GetCurrentUser().Id,
                    Date = DateTime.Now,
                };

                await _boardCardHistoryRepository.CreateAsync(comment);
            }
        }

        public async Task<int> CreateCompanyAsync(Company company)
        {
            return await _companyRepository.CreateAsync(company);
        }

        public async Task<int> CreateClientCompanyAsync(ClientCompany clientCompany)
        {
            return await _clientCompanyRepository.CreateAsync(clientCompany);
        }

        public async Task UpdateClientCompanyAsync(ClientCompany clientCompany)
        {
            await _clientCompanyRepository.UpdateAsync(clientCompany);
        }

        public async Task<ClientCompany> FindClientCompanyAsync(int companyId)
        {
            return await _clientCompanyRepository.FindByCompanyIdAsync(companyId);
        }
    }
}

如您所见,`GetBoardCardsAsync` 等某些操作使用多个存储库,并且需要了解如何与每个存储库以及数据库实体是什么。

因此,这种封装允许我们简化 UI 代码,使其对数据访问层的依赖性降低,从而提高了代码的可维护性和可扩展性。

考虑可维护性/可扩展性非常重要,因为许多项目由于添加新功能或更改现有功能时产生的错误数量不断增加而无法完成。

摘要

在这篇文章中,我介绍了使用 BlazorForms 开源框架实现的 `CrmLight` 种子项目潜在客户看板功能。它展示了如何连接数据库存储库、应用程序业务逻辑和用户界面的直接方法。

BlazorForms 的主要范式是**将解决方案的逻辑部分与物理 UI 渲染分离**。BlazorForms 鼓励开发人员首先规划解决方案,考虑实体、关系、数据访问、模型和业务逻辑,而不是 UI 控件和事件编码。

当前版本的 BlazorForms 0.8.2 包含这些更改,您可以将 `CrmLight` 种子项目作为您项目的基础或代码示例。

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

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

历史

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