├── .gitignore ├── LICENSE ├── README.md ├── facade-pattern ├── app.json └── src │ ├── CustomerListExt.PageExt.al │ ├── ItemResourceSearch.Codeunit.al │ ├── ItemSearchImpl.Codeunit.al │ └── ResourceSearchImpl.Codeunit.al ├── generic-validator-method ├── README.md ├── app.json └── src │ ├── GenericMethodPattern.Codeunit.al │ └── GenericValidatorMethodPattern.Codeunit.al └── the-rules-design-pattern ├── app.json └── src ├── DiscountCalculator.Codeunit.al ├── DiscountEvaluator.Codeunit.al ├── DiscountRule.Interface.al ├── DiscountRuleSubscriber.Codeunit.al ├── GoldCustomerDiscountRule.Codeunit.al └── SilverCustomerPriceRule.Codeunit.al /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .snapshots 3 | .alpackages 4 | *.app -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 MSN Raju 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Design Patterns for Business Central 2 | 3 | This repository contains various Design Pattern implementations in AL Language. 4 | 5 | ## Design Patterns 6 | 7 | * [Generic Validator Method Pattern](https://www.msnjournals.com/post/generic-validator-method-pattern) 8 | * [The Rules Design Pattern](https://www.msnjournals.com/post/the-rules-pattern-in-business-central-al-language) 9 | * [The Facade Pattern](https://www.msnjournals.com/post/facade-pattern-in-business-central-al-language) 10 | 11 | 12 | -------------------------------------------------------------------------------- /facade-pattern/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "4d3a53e8-dc3c-4750-8784-296c760b5855", 3 | "name": "facade-pattern", 4 | "publisher": "Default publisher", 5 | "version": "1.0.0.0", 6 | "brief": "", 7 | "description": "", 8 | "privacyStatement": "", 9 | "EULA": "", 10 | "help": "", 11 | "url": "", 12 | "logo": "", 13 | "dependencies": [ 14 | { 15 | "id": "63ca2fa4-4f03-4f2b-a480-172fef340d3f", 16 | "publisher": "Microsoft", 17 | "name": "System Application", 18 | "version": "16.0.0.0" 19 | }, 20 | { 21 | "id": "437dbf0e-84ff-417a-965d-ed2bb9650972", 22 | "publisher": "Microsoft", 23 | "name": "Base Application", 24 | "version": "16.0.0.0" 25 | } 26 | ], 27 | "screenshots": [], 28 | "platform": "16.0.0.0", 29 | "idRanges": [ 30 | { 31 | "from": 50100, 32 | "to": 50149 33 | } 34 | ], 35 | "contextSensitiveHelpUrl": "https://facade-pattern.com/help/", 36 | "showMyCode": true, 37 | "runtime": "5.0" 38 | } -------------------------------------------------------------------------------- /facade-pattern/src/CustomerListExt.PageExt.al: -------------------------------------------------------------------------------- 1 | pageextension 50130 CustomerListExt extends "Customer List" 2 | { 3 | trigger OnOpenPage(); 4 | var 5 | ItemSearchImpl: Codeunit "Item Search Impl"; 6 | Items: List of [Text[100]]; 7 | begin 8 | ItemSearchImpl.SearchItems('A', Items); 9 | Message('Count: %1', Items.Count); 10 | end; 11 | } -------------------------------------------------------------------------------- /facade-pattern/src/ItemResourceSearch.Codeunit.al: -------------------------------------------------------------------------------- 1 | codeunit 50132 "Item / Resource Search" 2 | { 3 | Access = Public; 4 | 5 | procedure SearchItems(SearchText: Text[100]; Items: List of [Text[100]]) 6 | var 7 | ItemSearchImpl: Codeunit "Item Search Impl"; 8 | begin 9 | ItemSearchImpl.SearchItems(SearchText, Items); 10 | end; 11 | 12 | procedure SearchResource(SearchText: Text[100]; Resources: List of [Text[100]]) 13 | var 14 | ResourceSearchImpl: Codeunit "Resource Search Impl"; 15 | begin 16 | ResourceSearchImpl.SearchResource(SearchText, Resources); 17 | end; 18 | } -------------------------------------------------------------------------------- /facade-pattern/src/ItemSearchImpl.Codeunit.al: -------------------------------------------------------------------------------- 1 | codeunit 50130 "Item Search Impl" 2 | { 3 | Access = Internal; 4 | 5 | procedure SearchItems(SearchText: Text[100]; Items: List of [Text[100]]) 6 | var 7 | Item: Record Item; 8 | begin 9 | Item.SetFilter(Description, '@*' + SearchText + '*'); 10 | if Item.FindSet() then 11 | repeat 12 | Items.Add(Item.Description); 13 | until Item.Next() = 0; 14 | end; 15 | } -------------------------------------------------------------------------------- /facade-pattern/src/ResourceSearchImpl.Codeunit.al: -------------------------------------------------------------------------------- 1 | codeunit 50131 "Resource Search Impl" 2 | { 3 | Access = Internal; 4 | 5 | procedure SearchResource(SearchText: Text[100]; Resources: List of [Text[100]]) 6 | var 7 | Resource: Record Resource; 8 | begin 9 | Resource.SetFilter(Name, '@*' + SearchText + '*'); 10 | if Resource.FindSet() then 11 | repeat 12 | Resources.Add(Resource.Name); 13 | until Resource.Next() = 0; 14 | end; 15 | } -------------------------------------------------------------------------------- /generic-validator-method/README.md: -------------------------------------------------------------------------------- 1 | # Design Patterns for Business Central 2 | 3 | ## Generic Validator Method Pattern 4 | 5 | ## The Rules Design Pattern -------------------------------------------------------------------------------- /generic-validator-method/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "db0c65a3-9882-441d-9a38-ab82d04d6505", 3 | "name": "validation-pattern", 4 | "publisher": "Default publisher", 5 | "version": "1.0.0.0", 6 | "brief": "", 7 | "description": "", 8 | "privacyStatement": "", 9 | "EULA": "", 10 | "help": "", 11 | "url": "", 12 | "logo": "", 13 | "dependencies": [ 14 | { 15 | "id": "63ca2fa4-4f03-4f2b-a480-172fef340d3f", 16 | "publisher": "Microsoft", 17 | "name": "System Application", 18 | "version": "16.0.0.0" 19 | }, 20 | { 21 | "id": "437dbf0e-84ff-417a-965d-ed2bb9650972", 22 | "publisher": "Microsoft", 23 | "name": "Base Application", 24 | "version": "16.0.0.0" 25 | } 26 | ], 27 | "screenshots": [], 28 | "platform": "16.0.0.0", 29 | "idRanges": [ 30 | { 31 | "from": 50100, 32 | "to": 50149 33 | } 34 | ], 35 | "contextSensitiveHelpUrl": "https://validation-pattern.com/help/", 36 | "showMyCode": true, 37 | "runtime": "5.0" 38 | } -------------------------------------------------------------------------------- /generic-validator-method/src/GenericMethodPattern.Codeunit.al: -------------------------------------------------------------------------------- 1 | // Generic Method Pattern 2 | codeunit 50100 CreateSalesOrderMethod 3 | { 4 | procedure CreateSalesOrder(Customer: Record Customer; HideDialog: Boolean) 5 | var 6 | Handled: Boolean; 7 | begin 8 | if not PreCreateSalesOrder(Customer, HideDialog) then 9 | exit; 10 | 11 | OnBeforeCreateSalesOrder(Customer, Handled); 12 | DoCreateSalesOrder(Customer, Handled); 13 | OnAfterCreateSalesOrder(Customer); 14 | PostCreateSalesOrder(Customer, HideDialog); 15 | end; 16 | 17 | local procedure PreCreateSalesOrder(Customer: Record Customer; HideDialog: Boolean): Boolean 18 | begin 19 | // UI Code 20 | exit(true); 21 | end; 22 | 23 | local procedure DoCreateSalesOrder(Customer: Record Customer; Handled: Boolean) 24 | var 25 | SalesHeader: Record "Sales Header"; 26 | CreateSalesOrderValidator: Codeunit CreateSalesOrderValidator; 27 | begin 28 | // Code 29 | CreateSalesOrderValidator.Validate(Customer); 30 | 31 | SalesHeader.Init(); 32 | SalesHeader."Document Type" := SalesHeader."Document Type"::Order; 33 | SalesHeader.Insert(true); 34 | 35 | SalesHeader.Validate("Sell-to Customer No.", Customer."No."); 36 | SalesHeader.Modify(); 37 | end; 38 | 39 | 40 | local procedure PostCreateSalesOrder(Customer: Record Customer; HideDialog: Boolean) 41 | begin 42 | // UI Code 43 | end; 44 | 45 | [BusinessEvent(false)] 46 | local procedure OnBeforeCreateSalesOrder(Customer: Record Customer; Handled: Boolean) 47 | begin 48 | end; 49 | 50 | [BusinessEvent(false)] 51 | local procedure OnAfterCreateSalesOrder(Customer: Record Customer) 52 | begin 53 | end; 54 | } -------------------------------------------------------------------------------- /generic-validator-method/src/GenericValidatorMethodPattern.Codeunit.al: -------------------------------------------------------------------------------- 1 | // Generic Validator Method Pattern 2 | codeunit 50101 CreateSalesOrderValidator 3 | { 4 | procedure Validate(Customer: Record Customer) 5 | var 6 | Errors: List of [Text]; 7 | begin 8 | if HasErrors(Customer, Errors) then 9 | Error(ErrorsToText(Errors)); 10 | end; 11 | 12 | procedure HasErrors(Customer: Record Customer; Errors: List of [Text]): Boolean 13 | var 14 | Handled: Boolean; 15 | begin 16 | OnBeforeCreateSalesOrderValidator(Customer, Handled); 17 | DoCreateSalesOrder(Customer, Errors, Handled); 18 | OnAfterCreateSalesOrderValidator(Customer); 19 | 20 | if Errors.Count() > 0 then 21 | exit(true); 22 | end; 23 | 24 | local procedure ErrorsToText(Errors: List of [Text]): Text 25 | var 26 | ErrText: Text; 27 | TxtBuffer: TextBuilder; 28 | begin 29 | foreach ErrText in Errors do 30 | TxtBuffer.AppendLine(ErrText); 31 | 32 | exit(TxtBuffer.ToText()); 33 | end; 34 | 35 | local procedure DoCreateSalesOrder(Customer: Record Customer; Errors: List of [Text]; Handled: Boolean) 36 | begin 37 | if Handled then 38 | exit; 39 | 40 | ValidateGenBusinessPostingGroup(Customer, Errors); 41 | ValidateVATBusPostingGroup(Customer, Errors); 42 | ValidateVATRegistrationNo(Customer, Errors); 43 | end; 44 | 45 | local procedure ValidateGenBusinessPostingGroup(Customer: Record Customer; Errors: List of [Text]) 46 | var 47 | GenBusPostingGroupErr: Label 'Gen. Bus. Posting Group should not be empty.'; 48 | begin 49 | if Customer."Gen. Bus. Posting Group" = '' then 50 | Errors.Add(GenBusPostingGroupErr); 51 | end; 52 | 53 | local procedure ValidateVATBusPostingGroup(Customer: Record Customer; Errors: List of [Text]) 54 | var 55 | GenBusPostingGroupErr: Label 'VAT Bus. Posting Group should not be empty.'; 56 | begin 57 | if Customer."VAT Bus. Posting Group" = '' then 58 | Errors.Add(GenBusPostingGroupErr); 59 | end; 60 | 61 | local procedure ValidateVATRegistrationNo(Customer: Record Customer; Errors: List of [Text]) 62 | var 63 | GenBusPostingGroupErr: Label 'Customer should have a valid VAT Registration No.'; 64 | begin 65 | if Customer."VAT Registration No." = '' then 66 | Errors.Add(GenBusPostingGroupErr); 67 | end; 68 | 69 | [BusinessEvent(false)] 70 | local procedure OnBeforeCreateSalesOrderValidator(Customer: Record Customer; var Handled: Boolean) 71 | begin 72 | end; 73 | 74 | [BusinessEvent(false)] 75 | local procedure OnAfterCreateSalesOrderValidator(Customer: Record Customer) 76 | begin 77 | end; 78 | } -------------------------------------------------------------------------------- /the-rules-design-pattern/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "a0b6d224-51cd-4e6f-83e3-ab5070f5992b", 3 | "name": "the-rules-design-pattern", 4 | "publisher": "Default publisher", 5 | "version": "1.0.0.0", 6 | "brief": "", 7 | "description": "", 8 | "privacyStatement": "", 9 | "EULA": "", 10 | "help": "", 11 | "url": "", 12 | "logo": "", 13 | "dependencies": [ 14 | { 15 | "id": "63ca2fa4-4f03-4f2b-a480-172fef340d3f", 16 | "publisher": "Microsoft", 17 | "name": "System Application", 18 | "version": "16.0.0.0" 19 | }, 20 | { 21 | "id": "437dbf0e-84ff-417a-965d-ed2bb9650972", 22 | "publisher": "Microsoft", 23 | "name": "Base Application", 24 | "version": "16.0.0.0" 25 | } 26 | ], 27 | "screenshots": [], 28 | "platform": "16.0.0.0", 29 | "application": "16.0.0.0", 30 | "idRanges": [ 31 | { 32 | "from": 50100, 33 | "to": 50149 34 | } 35 | ], 36 | "contextSensitiveHelpUrl": "https://the-rules-design-pattern.com/help/", 37 | "showMyCode": true, 38 | "runtime": "5.0" 39 | } -------------------------------------------------------------------------------- /the-rules-design-pattern/src/DiscountCalculator.Codeunit.al: -------------------------------------------------------------------------------- 1 | codeunit 50105 DiscountCalculator 2 | { 3 | procedure Execute(CustomerNo: Code[20]): Decimal 4 | var 5 | DiscountEvaluator: Codeunit DiscountEvaluator; 6 | Discount: Decimal; 7 | begin 8 | OnExecute(DiscountEvaluator, CustomerNo, Discount); 9 | exit(Discount); 10 | end; 11 | 12 | [BusinessEvent(false)] 13 | local procedure OnExecute(DiscountEvaluator: Codeunit DiscountEvaluator; CustomerNo: Code[20]; var Discount: Decimal) 14 | begin 15 | end; 16 | } -------------------------------------------------------------------------------- /the-rules-design-pattern/src/DiscountEvaluator.Codeunit.al: -------------------------------------------------------------------------------- 1 | codeunit 50106 DiscountEvaluator 2 | { 3 | procedure Evaluate(DiscountRule: Interface DiscountRule; CustomerNo: Code[20]; var Discount: Decimal) 4 | var 5 | NewDiscount: Decimal; 6 | begin 7 | if not DiscountRule.CanProcess(CustomerNo) then 8 | exit; 9 | 10 | NewDiscount := DiscountRule.Process(CustomerNo); 11 | if NewDiscount > Discount then 12 | Discount := NewDiscount; 13 | end; 14 | } -------------------------------------------------------------------------------- /the-rules-design-pattern/src/DiscountRule.Interface.al: -------------------------------------------------------------------------------- 1 | interface DiscountRule 2 | { 3 | procedure CanProcess(CustomerNo: Code[20]): Boolean; 4 | procedure Process(CustomerNo: Code[20]): Decimal; 5 | } -------------------------------------------------------------------------------- /the-rules-design-pattern/src/DiscountRuleSubscriber.Codeunit.al: -------------------------------------------------------------------------------- 1 | codeunit 50112 DiscountRuleSubscriber 2 | { 3 | [EventSubscriber(ObjectType::Codeunit, Codeunit::DiscountCalculator, 'OnExecute', '', false, false)] 4 | local procedure OnExecute(DiscountEvaluator: Codeunit DiscountEvaluator; CustomerNo: Code[20]; var Discount: Decimal) 5 | var 6 | GoldCustomerDiscountRule: Codeunit GoldCustomerDiscountRule; 7 | SilverCustomerDiscountRule: Codeunit SilverCustomerDiscountRule; 8 | begin 9 | DiscountEvaluator.Evaluate(GoldCustomerDiscountRule, CustomerNo, Discount); 10 | DiscountEvaluator.Evaluate(SilverCustomerDiscountRule, CustomerNo, Discount); 11 | end; 12 | } -------------------------------------------------------------------------------- /the-rules-design-pattern/src/GoldCustomerDiscountRule.Codeunit.al: -------------------------------------------------------------------------------- 1 | codeunit 50110 GoldCustomerDiscountRule implements DiscountRule 2 | { 3 | procedure CanProcess(CustomerNo: Code[20]): Boolean; 4 | var 5 | Customer: Record Customer; 6 | begin 7 | Customer.Get(CustomerNo); 8 | if Customer."Customer Price Group" = 'GOLD' then 9 | exit(true); 10 | end; 11 | 12 | procedure Process(CustomerNo: Code[20]): Decimal; 13 | var 14 | Customer: Record Customer; 15 | begin 16 | Customer.Get(CustomerNo); 17 | case Customer."Gen. Bus. Posting Group" of 18 | 'DOM': 19 | exit(0.015); 20 | else 21 | exit(0.025); 22 | end; 23 | end; 24 | } -------------------------------------------------------------------------------- /the-rules-design-pattern/src/SilverCustomerPriceRule.Codeunit.al: -------------------------------------------------------------------------------- 1 | codeunit 50111 SilverCustomerDiscountRule implements DiscountRule 2 | { 3 | procedure CanProcess(CustomerNo: Code[20]): Boolean; 4 | var 5 | Customer: Record Customer; 6 | begin 7 | Customer.Get(CustomerNo); 8 | if Customer."Customer Price Group" = 'SILVER' then 9 | exit(true); 10 | end; 11 | 12 | procedure Process(CustomerNo: Code[20]): Decimal; 13 | var 14 | Customer: Record Customer; 15 | begin 16 | Customer.Get(CustomerNo); 17 | case Customer."Gen. Bus. Posting Group" of 18 | 'DOM': 19 | exit(0.01); 20 | else 21 | exit(0.015); 22 | end; 23 | end; 24 | } --------------------------------------------------------------------------------