New opensource technology

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

Developing applications based of Flows, Forms and Rules using type safe advantages of C#

Please note this is part 3 of the series.
Please refer to previous posts below.

Part 1:

Part 2:

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 posts code from GitHub:

https://github.com/euklad/BlogCode/tree/main/Story-10-BlazorForms-CrmLight-LeadBoard

BlazorForms project on GitHub:

https://github.com/ProCodersPtyLtd/BlazorForms

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 navigation menu, clicking on it will navigate you to the screen:

CRMLight CRM Lead Board

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:

CRMLight CRM Lead Board

The history comments are also editable for the comment owner:

CRM CRM Light opensource

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):

CRMLight opensource

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:

CRM new record opensource
CRM Light opensource

How it is implemented

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

Visual Studio

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 from ‘LeadBoardStateFlow.cs’ that controls Lead Board states and transitions.

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 state name we use

[public state MeetingScheduled = new state("Meeting Scheduled");]

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

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

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

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

Model

The flow specifies 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 that contains the class code:

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 card in this state.

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 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, that contains

  • bindings to model properties used for rendering each history item,
  • ‘Edit’ and ‘Delete’ buttons to edit the item,
  • and rules that 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)

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 a dependency injection to receive required repositories and ‘IAppAuthState’ which is needed to read current user logged in to the system.

In the ‘Execute’ method it populates collections that 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 user modified a comment card. It sets ‘EditedDate’ and saves the comment using ‘_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

This rule simply deletes the item when user clicks ‘Delete’ button

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 transition will be committed.

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’

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.

This 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 model type of an item from the collection that a particular DropdownSearch uses, for example in this line of code

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

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 the ‘FormPersonEdit’ form previously created in “Part 2: CrmLight Project”

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

<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 like, 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, that we can save these changes to database.

@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 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 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 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 practise 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 logic of translation repository entities to more specialized business objects used in UI.

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

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 how to work with each repository and what are the database entities.

So this encapsulation allows us to simplify UI code, make it less depended on 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 straight forward 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.0 contains these changes and you can use the CrmLight seed project as a foundation, or as code examples, for your projects.

Thanks for reading.

Dont forget you can reach out to us with any questions or for help with any project.