BlazorForms低代码开源框架——第3部分:CrmLight Lead Board

目录

介绍

BlazorForms Paradigm

CrmLight Lead Board

如何实现

Flow

Model

Form

规则

FormLeadCard_RefreshSources

FormLeadCardEdit_ItemChangedRule

FormLeadCardEdit_ItemDeletingRule

其他Form和Flow

FormContactedCardEdit

FormCardCommit

CompanyDialogFlow

PersonDialogFlow

用户界面渲染

BoardService

总结


本系列中的所有BlazorForms文章:

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

https://crmlight.platz.app

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

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

介绍

这篇文章延续了由PRO CODERS PTY LTD开发的关于BlazorForms框架的系列文章,并作为带有MIT许可证的开源共享。

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

BlazorForms Paradigm

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

GitHub下载此博客文章代码:

GitHub上的BlazorForms项目:

CrmLight Lead Board

我们将继续扩展CrmLight项目,以展示框架的不同领域和组件。在这篇文章中,我们将展示如何实施CRM领导板。

如果运行结果解决方案,则可以在导航菜单中看到Lead Board。单击它将导航到屏幕:

1

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

您可以垂直对卡进行排序,并将它们向左或向右移动到表示Lead Board状态的存储桶中。

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

2

历史记录注释也可以为注释所有者编辑:

3

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

4

当用户在下拉搜索栏中选择一个值(例如客户端管理器)时,还可以添加新记录或编辑现有项目之一:

5

6

如何实现

如果您查看Visual Studio解决方案,您将看到新的子文件夹已添加到Flow文件夹:

7

它包含Lead Board的业务逻辑(flowform和规则)。

UI部分将由稍后将讨论的FlowBoard控件呈现。现在,我应该提到FlowBoard对它使用的flowmodel有一些要求。

Flow

让我们从 LeadBoardStateFlow.cs 开始它控制 Lead Board 状态和转换。

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)

表示LeadContacted状态之间存在过渡。在过渡期间,当过渡发生时显示formFormContactedCardEdit,只能通过用户操作触UserActionTransitionTrigger发器(通过拖放或上下文菜单)进行过渡。

如果未定义两种状态之间的转换,则Lead Board将不允许移动卡。

Model

flow指定一个特定的model类,您可以将其视为StateFlowBase flow基类——LeadBoardCardModel的模板参数。如果单击此模型类并按 F12Visual 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类也由flow中定义的所有form使用。

Form

flow线.SetEditForm<FormLeadCardEdit>()定义当用户想要编辑卡片时显示的默认编辑form

如果指定处于特定状态的SetEditForm<>()函数,则指定的form将仅用于编辑处于此状态的卡片。

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");
    }
}

加载form时将执行FormLeadCard_RefreshSources规则。

form FormLayout.TwoColumns用于将form内容拆分为两列,将所有编辑控件保留在左列,注释和历史记录保留在右列。

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

为了实现评论历史记录,我们使用了CardList控件,其中包含

  • 绑定到用于呈现每个历史项的模型属性,
  • 编辑和删除按钮以编辑项目,
  • 以及在编辑或删除历史记录项时执行的规则,并确认DeleteItem操作。

规则

FormLeadCard_RefreshSources

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

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;
    }
}

其他FormFlow

FormContactedCardEdit

此窗体用于从LeadContacted状态的转换,每次用户将卡从Lead存储桶移动到Contacted存储桶时都会显示此窗体。

它显示用户可以在转换期间更新的几个字段,以及用户在提交转换之前必须输入的一个必填字段Lead contact

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

form用于向Won状态的最终过渡。它只有一个必填字段Client company,用户可以在其中使用对话框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");
    }
}

您可能已经注意到,我们在定义控件时使用了几次。CompanyDialogFlowPersonDialogFlowDropdownSearch

这些是从基类引用模型和窗体继承的简化流,这些流具有在显示对话框时加载数据和在提交对话框时保存数据的方法。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引用定义为List<CompanyModel>AllCompanies集合,这意味着CompanyDialogFlow应该引用CompanyModel

CompanyDialogFlow

flow使用依赖关系注入来接收ICompanyRepository,该ICompanyRepository用于通过LoadDataAsync中的Id读取模型数据,并将其保存在SaveDataAsync中。

form 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

form与前一个form非常相似,但使用FormPersonEdit之前在第 2部分:CrmLight项目中创建的form

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中呈现它们。

用户界面渲染

现在我们需要看看解决方案页面文件夹中的 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>

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

第二件事是定义板卡的外观,我们可以定义CardAvatarCardTitleCardBody

最后一件事是提供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操作,并且flow用于检查转换是否可能、触发器是什么以及在转换期间要做什么。但是,在board上,我们使用一系列卡片进行操作,我们必须从外部提供卡片,而最好的方法是提供卡片项目作为FlowBoard控件参数。

对于Save操作,最好通过一组卡来操作,例如,当用户重新订购卡并且同时更改多个卡订单时,我们可能希望将它们全部保存在一个数据库事务中。

BoardService

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

我们需要它,因为当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种子项目用作项目的基础或代码示例。
Contact Us - Pro Coders

https://www.codeproject.com/Articles/5353131/BlazorForms-Low-Code-Open-Source-Framework-Part-3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值