Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web / Blazor

BlazorForms Low-Code Open-Source Framework. Part 3: CrmLight Lead Board

4.17/5 (7 votes)
4 Feb 2023CPOL9 min read 17.9K  
Developing applications based on Flows, Forms, and Rules using type-safe advantages of C#
CrmLight Lead Board continues the series of posts about BlazorForms framework developed by PRO CODERS PTY LTD and shared as an open-source with MIT license.

All BlazorForms articles in this series:

We deployed a working solution here:

https://crmlight.platz.app

Please remember it doesn't have a database and it keeps all data in memory, shared for all users, and after some time of inactivity, the container shuts down losing all data.

We deployed to a Linux docker container with minimum CPU and memory requirements, but it works very fast. 

Introduction

This post continues the series of posts about BlazorForms framework developed by PRO CODERS PTY LTD and shared as an open-source with MIT license.

In the previous post, I presented the CrmLight seed project that shows how to implement, edit and list forms using BlazorForms – a framework that simplifies UI development and allows you to build simple and maintainable C# solutions.

BlazorForms Paradigm

The main paradigm that we put in this framework is the separation of the logical part of the solution from the physical UI rendering. BlazorForms encourages developers to plan the solution first and think about entities, relationships, data access, models and business logic rather than UI controls and events coding.

Download this blog post code from GitHub:

BlazorForms project on GitHub:

CrmLight Lead Board

We continue extending the CrmLight project to demonstrate different areas and components of the framework. In this post, we will show how the CRM Lead Board can be implemented.

If you run the result solution, you can see ‘Lead Board’ in the navigation menu. Clicking on it will navigate you to the screen:

Image 1

Picture 1

Here, you can see all the opened leads in the system.

You can sort cards vertically and move them to the left or to the right into buckets that represent Lead Board card states.

When you click on a card, the edit dialog will be shown. Here, the user can update card details, add comments and track comments history:

Image 2

Picture 2

The history comments are also editable for the comment owner:

Image 3

Picture 3

When a card has moved to the Won bucket, the system asks the user to provide more details to create a client record (that will be shown in Clients page):

Image 4

Picture 4

When a user selects a value in the drop down search bar, for example Client manager, it is also possible to add a new record or edit one of the existing items:

Image 5

Picture 5

Image 6

Picture 6

How It is Implemented

If you look at the Visual Studio solution, you will see that new subfolder added to Flow folder:

Image 7

Picture 7

It contains the business logic (flows, forms and rules) of the Lead Board.

The UI part will be rendered by the FlowBoard control that will be discussed later. For now, I should mention that the FlowBoard has some requirements for flows and model it uses.

Flow

Let’s start with LeadBoardStateFlow.cs which controls Lead Board states and transitions.

C#
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();
        }
    }
}

You can see all five states declared at the top and when we need a state description that differs from the state name, we use public state MeetingScheduled = new state("Meeting Scheduled");

Next, we define all possible transitions between states, this line of code:

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

means that there is a transition between Lead and Contacted states. During the transition, show form FormContactedCardEdit when the transition happens, the transition can be made only by user action UserActionTransitionTrigger trigger – by drag and drop or context menu.

If the transition between the two states is not defined, the Lead Board will not allow moving the card.

Model

The flow specifies a particular model class that you can see as a template parameter of the StateFlowBase flow base class - LeadBoardCardModel. If you click on this model class and press F12, Visual Studio will navigate you to LeadBoardCardModel.cs which contains the class code:

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

The model class inherits all properties of BoardCard entity and implements the interface IFlowBoardCard, which is mandatory for the model used by FlowBoard UI.

LeadBoardCardModel class is also used by all forms defined in the flow.

Forms

The flow line .SetEditForm<FormLeadCardEdit>() defines a default edit form that is shown when a User wants to edit a card.

If you specify SetEditForm<>() function in a particular state, the specified form will be used only for edit cards in this state.

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

Rule FormLeadCard_RefreshSources will be executed when the form is loaded.

The form uses FormLayout.TwoColumns to split form content into two columns, keeping all edit controls on the left column and comments and history on the right.

For DropdownSearch controls, we used ItemDialog functions to specify dialogs for adding or viewing details of the item.

To implement comment history, we used CardList control, which contains

  • bindings to model properties used for rendering each history item,
  • Edit and Delete buttons to edit the item,
  • and rules that are executed when the history item is edited or deleted, with confirmation for DeleteItem operation.

Rules

FormLeadCard_RefreshSources

This rule is executed each time the form is shown (loaded):

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

It uses dependency injection to receive required repositories and IAppAuthState which is needed to read the current user logged in to the system.

In the Execute method, it populates collections that are used for DropdownSearch controls, and also populates CardHistory collection reading all history items for this card.

Then it updates Visible properties for Edit and Delete buttons allowing modification only for the comment history item owner.

FormLeadCardEdit_ItemChangedRule

This rule is executed when the user modified a comment card. It sets EditedDate and saves the comment using _boardCardHistoryRepository.

C#
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

This rule simply deletes the item when the user clicks Delete button.

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

Other Forms and Flows

FormContactedCardEdit

This form is used in transition from Lead to Contacted state, and it will be shown each time when a user moves a card from Lead to Contacted bucket.

It shows several fields that the user can update during the transition and one required field 'Lead contact' that the user must enter before the transition will be committed.

C#
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

This form is used for the final transition to Won state. It has only one required field 'Client company' in which the user can select or add new a company using dialog CompanyDialogFlow:

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

As you may have noticed, we used CompanyDialogFlow and PersonDialogFlow a few times when we defined DropdownSearch controls.

These are simplified flows that are inherited from DialogFlowBase<> base class referencing to model and form, and these flows have methods to Load data when a dialog is shown and Save data when the dialog is submitted.

It is important to use the model type of an item from the collection that a particular DropdownSearch uses, for example, in this line of code:

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

DropdownSearch referencing AllCompanies collection that is defined as List<CompanyModel>, it means that CompanyDialogFlow should reference to CompanyModel.

CompanyDialogFlow

This flow uses dependency injection to receive ICompanyRepository that is used to read model data by Id in LoadDataAsync and save it in SaveDataAsync.

The form FormCompanyDialogEdit defines field controls and buttons.

C#
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

This form is very similar to the previous one, but uses FormPersonEdit form previously created in Part 2: CrmLight Project.

C#
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);
        }
    }
}

Now we finished the definition of all abstractions and can focus on how to render them in UI.

UI Rendering

Now we need to have a look at LeadBoard.razor in solution Pages folder.

We use FlowBoard control and supply some parameters to it.

Razor
<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>

First of all, we need to specify TFlow and TItem parameters, pointing the control to LeadBoardStateFlow and its model that we already discussed above.

The second thing is to define how board cards will look, we can define CardAvatar, CardTitle , and CardBody.

The final thing is to provide Items – the list of cards to show on the board, and the ItemsChanged event that will be executed each time when the card state or card details are changed so that we can save these changes to the database.

C#
@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);
        }
    }
}

You can see that we inject IBoardService via dependency injection and use it for reading cards in LoadItems method, and to save cards in ItemsChanged event handler.

The event handler receives a list of changed items and each record has ItemChangedType property that informs us what type of change happened. For example, if it is ItemChangedType.Creating, we need to execute _boardService.CreatingBoardCardAsync.

There is a particular reason why we keep Load/Save logic in the razor page instead of keeping it in LeadBoardStateFlow. This is because LeadBoardStateFlow operates with only one card at a time, and the flow is used to check whether the transition is possible, what is the trigger, and what to do during the transition. However, on the board, we operate with a collection of cards, and we must supply the cards from outside, and the best way to do it is to supply the card items as a parameter for FlowBoard control.

For Save operation, it is again better to operate by a collection of cards, for example, when the user reorders cards and several card orders’ are changed simultaneously, we may want to save all of them in one database transaction.

BoardService

The last thing I would like to consider in this post is BoardService that implements the IBoardService interface.

We need it because it is not good practice when the UI uses repositories and entities directly – it is much better to have a service layer for that, that operates with business objects.

Thus BoardService receives a few repositories via dependency injection and encapsulates the logic of translation repository entities to more specialized business objects used in UI.

It also provides high-level business object operations instead of repositories' more granular and low-level.

C#
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);
        }
    }
}

As you can see, some operations like GetBoardCardsAsync use several repositories, and require knowledge of how to work with each repository and what are the database entities.

So, this encapsulation allows us to simplify UI code, make it less dependent on the data access layer, and as a result, it improves code maintainability and extendibility.

It is important to think about maintainability/extendibility as many projects simply cannot be finished because of the growing number of bugs created when new functionality is added or existing functionality is changed.

Summary

In this post, I presented the CrmLight seed project Lead Board feature implemented using the BlazorForms open-source framework. It shows a straightforward approach on how to connect database Repositories, application business logic, and a user interface.

The main paradigm of BlazorForms is the separation of the logical part of the solution from the physical UI rendering. BlazorForms encourages developers to plan the solution first and think about entities, relationships, data access, models, and business logic rather than UI controls and events coding.

The current version BlazorForms 0.8.2 contains these changes and you can use the CrmLight seed project as a foundation, or as code examples, for your projects.

Thank you for reading and remember that you can always reach out to us if you would like any help with your implementations.

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

History

  • 27th January, 2023: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)