Restructure for Docsy theme
18
content/docs/NAVPatterns/2-anti-patterns/_index.md
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
+++
|
||||
chapter = true
|
||||
title = "2. Anti-Patterns"
|
||||
weight = 130
|
||||
+++
|
||||
Some of the software development practices, had **not** stood the test of time. Despite that, some are still being used today by developers everywhere.
|
||||
|
||||
"An **anti-pattern** (or **antipattern**) is a common response to a recurring problem that is usually ineffective and risks being highly counterproductive" (from [Wikipedia][anchor0])
|
||||
|
||||
Since almost the beginning of the NAV Design Patterns project, we talked about documenting the anti-patterns - but never found the time. Until, spontaneously, the April 1st 2015 article had practically wrote itself in a couple of hours, with priceless contributions coming from Andreas, Elly, Nikola - and last but not least, waldo.
|
||||
|
||||
Best regards,
|
||||
|
||||
Bogdana Botez
|
||||
|
||||
|
||||
|
||||
[anchor0]: http://en.wikipedia.org/wiki/Anti-pattern
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
+++
|
||||
title = "Nav Upgrade"
|
||||
weight = 840
|
||||
+++
|
||||
## Anti-Patterns in NAV Upgrade
|
||||
|
||||
_By Carlos Raul Garcia and Bogdana Botez at Microsoft Development Center Copenhagen_
|
||||
|
||||
[![ ][image0]][anchor0]
|
||||
|
||||
**Context**: when NAV is upgraded, whether on-premises or in the cloud, developers have the chance to write upgrade code to move data across changing data structures. Writing good quality code will help successful upgrades.
|
||||
|
||||
### General on upgrade
|
||||
|
||||
**Problem:** assuming that the upgrade table contains data.
|
||||
|
||||
If the table is empty, it means that either the upgrade has run, or there was no data in the original tenant; in both cases, the upgrade code should exit immediately.
|
||||
|
||||
**Solution:** if using an upgrade table, always validate that the table contains data before doing anything.
|
||||
|
||||
### Upgrade code, can it be rerun safely?
|
||||
|
||||
**Problem**: if the upgrade code is not written in a way that makes it runnable twice ([idempotent][anchor1]), then several failures can happen, including something as critical as data corruption.
|
||||
|
||||
In on premise NAV installations, if something fails at upgrade, there is no way to run only the "remaining" tasks. You will need to run the whole upgrade again, and might end-up with data that you cannot trust.
|
||||
|
||||
What about the cloud? In Platform As A Service (PaaS), in some situations, the upgrade code needs to be run twice (for example, when moving tenants from a broken/frozen VM to a healthy one).
|
||||
|
||||
**Solution:** Make sure each of your upgrade procedures only kicks in if it didn't run before.
|
||||
|
||||
**Examples**
|
||||
|
||||
The examples below have happened in real live NAV PaaS upgrade:
|
||||
|
||||
**Table data overwrite**
|
||||
|
||||
* **Problem**: at upgrade, a new column has been added to a table and initialized with default values. In the meanwhile, during production, some of the default values are changed to production real life values. The second time the upgrade runs, those values will be overwritten with defaults, any personalization lost.
|
||||
* **Solution:** before initializing with default values, check if non-default values exist.
|
||||
|
||||
**Crash on math operations**
|
||||
|
||||
* **Problem:** one tenant upgrade managed to divide by zero, by assuming a non-zero value.
|
||||
* **Solution:** don't assume values can never be zero, always check before using them in divisions.
|
||||
|
||||
**Use of external components**
|
||||
|
||||
* **Problem:** a one-time registration through web services to an external service failed when attempting to register a second time.
|
||||
* **Solution:** check if already registered, before attempting again.
|
||||
|
||||
### Parallelism
|
||||
|
||||
**Problem**: Upgrade procedures can be run in parallel, causing issues when different procedures attempt to modify the same table at the same time.
|
||||
|
||||
When modifications to the same table are being made from two or more different procedures, there is no guarantee on sequential run, or on a certain order they will be run in.
|
||||
|
||||
**Solution**: if sequential or ordered execution is needed, make the affected upgrade procedures local and call them all, in the desired sequence, from a public upgrade procedure.
|
||||
|
||||
### Access to cloud machines
|
||||
|
||||
**Problem:** dependencies on manual installation steps do not fit in the cloud.
|
||||
|
||||
If Dynamics NAV is installed on-premises, then any additional setup (like dependencies of external dlls, manual configuration steps etc.) can be done manually or semi-manually by the IT admin, at first setup and upgrade.
|
||||
|
||||
In the cloud, NAV partners don't have access to the machines -- hence they cannot deploy and configure those external dependencies as they did in the old on-premises installations.
|
||||
|
||||
**Solution:** Don't assume you will have access to PaaS or SaaS machines. Build your solution in such a way that it doesn't depend on executing manual configurations on the host machine.
|
||||
|
||||
|
||||
|
||||
[anchor0]: upgrade.png
|
||||
[anchor1]: http://stackoverflow.com/questions/1077412/what-is-an-idempotent-operation#1077421
|
||||
|
||||
|
||||
[image0]: upgrade.png
|
||||
BIN
content/docs/NAVPatterns/2-anti-patterns/nav-upgrade/upgrade.png
Normal file
|
After Width: | Height: | Size: 2 KiB |
|
|
@ -0,0 +1,94 @@
|
|||
+++
|
||||
title = "Reusable Bugs"
|
||||
weight = 1020
|
||||
+++
|
||||
_By Bogdana Botez, Andreas Moth, Eric Wauters (waldo), Elly Nkya, Nikola Kukrika_
|
||||
|
||||
_April 1st, 2015_
|
||||
|
||||
## Abstract
|
||||
|
||||
It is difficult to come up with new and different bugs in each release, and it is a waste of resources to not use the ones which already had proven successful in the past. To avoid reinventing the wheel, we propose to come up with new innovative ways to create bugs that are reusable and generic enough to be used in various places of the application.
|
||||
|
||||
## Examples
|
||||
|
||||
**Option Strings**
|
||||
|
||||
One example of a reusable bug is to find a heavily used table, like table 36 Sales Header, and observe one of the more important fields of type Option, for example Document Type. The OptionString property has the following value: Quote,Order,Invoice,Credit Memo,Blanket Order,Return Order, which you should update to add one option, for example Transport Order in the beginning.
|
||||
|
||||
The main benefit of this reusable bug is that code changed in one place, has impact in multiple sides on the application: document creation and handling, posting etc.
|
||||
|
||||
**Application Management**
|
||||
|
||||
Codeunit 1, ApplicationManagement is a good place for creating reusable bugs. A simple INSERT on the RecRef on the first line of OnDatabaseInsert will create a sure and versatile bug that is reusable all over the application. Redundancy on database insertion ensures that data is surely conveyed to the data storage layer, twice.
|
||||
|
||||
**God objects**
|
||||
|
||||
We are considering to no longer make localization builds for countries. It consumes lab resources to keep running multiple country builds. Instead, we will merge everything into one single build. This is a bigger scale implementation of the ["God object"][anchor0]. Inside this unique build, we will use IF statements and sporadically CASE statements to select each country's behavior. For more help on how to use IFs, see also the IF .. THEN|ELSE C/AL Coding Guideline.
|
||||
|
||||
**Field length economy**
|
||||
|
||||
When you post a document, make sure to transfer data to a field that is smaller than the field you are assigning from. This will not immediately be caught and will only hurt a subset of the customers who uses Microsoft Dynamics NAV to its fullest.
|
||||
|
||||
## Guidelines
|
||||
|
||||
When bug addition is not entirely obvious, there is a second way to approach the problem. By following some general coding best practices like the ones described below, reliable bug innovation is facilitated and can come naturally as a collateral effect.
|
||||
|
||||
**Code structure**
|
||||
|
||||
Put everything in one function and only use comments to explain the structure of your code. And don't use functions - because this only complicates things... having to navigate from function to function, and completely lose track of where you are in the business logic.
|
||||
|
||||
On top of that .. put everything in one codeunit. Because also that will simplify and make your structure more readable.
|
||||
|
||||
Use Hungarian Notation on your variables, because at any time, you need to know what type, and what context your variable is on.
|
||||
|
||||
Declare all your functions and variables global, so they are available at any time.
|
||||
|
||||
**Don't do Unit Testing**
|
||||
|
||||
Unit testing adds complexity and extra time to the stuff you're doing. Also, it eats up extra codeunits which means: it costs money. You will never be able to foresee all scenarios possible, so you're destined to forget and not test everything. So you will save time in not doing unit testing.
|
||||
|
||||
**Never add images to actions**
|
||||
|
||||
Because there is an image by default. When you don't provide an image on an action on a page, the application will foresee a ball... and when you never do it, your application will have a very consistent way of showing your actions, by providing that picture of that ball. On top of that, you'll save time.
|
||||
|
||||
**Do not care about ControlIDs**
|
||||
|
||||
When you're doing development of your product, do not care about ControlIDs, and just leave the Offset ID to the default value of 0\. This way, when merging, you will receive nice notifications, saying both you and Microsoft have added functions in that objects. You can use this feature to document all these places.
|
||||
|
||||
**Hooks**
|
||||
|
||||
Never apply the hook pattern. Hooks will only reduce upgrade time. This means, you will only shortly enjoy using the AMU (Application Merge Utilities). The more you change in default application, the longer it takes to upgrade, the longer you will enjoy the toolkit
|
||||
|
||||
This can be taken one level higher. Simply you are not hardcore if you do not use notepad to resolve all of the merge issues.
|
||||
|
||||
**How to use RecRef**
|
||||
|
||||
Why fuss around declaring specific table variables, just generalize, all you need is one, two, or perhaps three RecRef variables, with a few IFs and CASEs here and there for reflection, to carry you all the way.
|
||||
|
||||
**Arguments**
|
||||
|
||||
Using only a few arguments on the functions is a sign of a weak developer. Stick in as many arguments as possible on the function, even if you are not using them, they could be useful in the future.
|
||||
|
||||
**Just another field / action**
|
||||
|
||||
Thinking of the design is overrated, each problem can be solved by adding an additional field or the table/page or with adding another action. We all know this has worked well in the past.
|
||||
|
||||
**Reusability**
|
||||
|
||||
We have decided that each time we fix a bug, we now also explain how it can be applied as a pattern. We then use anti-virus software to search for these patterns, to make sure we do not re-introduce these bugs anywhere else in NAV.
|
||||
|
||||
**Business logic placement**
|
||||
|
||||
As a best practice, we have also decided to move code into pages. Business logic should no longer be in tables and codeunits, but instead pages should know and be aware of the context and update it accordingly. As opposite to tables and codeunits, pages are aware of the context.
|
||||
|
||||
## Conclusion
|
||||
|
||||
Happy April Fools' Day.
|
||||
|
||||
Disclaimer: this is inspired from IETF documentation published on April 1st, like for example the revolutionizing [IP over Avian Carriers][anchor1] standard.
|
||||
|
||||
|
||||
|
||||
[anchor0]: http://en.wikipedia.org/wiki/God_object
|
||||
[anchor1]: http://en.wikipedia.org/wiki/IP_over_Avian_Carriers
|
||||
26
content/docs/NAVPatterns/3-cal-coding-guidelines/_index.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
+++
|
||||
chapter = true
|
||||
title = "3. CAL Coding Guidelines"
|
||||
weight = 150
|
||||
+++
|
||||
We've decided to publish our current C/AL coding guidelines. They are actual, as per January 2015 when this is published (but might fall out of sync as time goes by).
|
||||
|
||||
You can also [download the C/AL coding guidelines as pdf,][anchor0] all in one document. In contrast, on this wiki, the coding guidelines are published individually. The reason is: give you the chance to comment and share your oppinion on each one. Thanks to [waldo][anchor1] for this idea and for helping out.
|
||||
|
||||
The guidelines are debatable - and it is good when they are generating debate. There is variation of opinion on the rules at Microsoft too. The plan is to simply expose what we use now. And more important, to say that guidelines could be used. Debate on individual guidelines can become heated for any programming language, but the benefit of using some guidelines stays.
|
||||
|
||||
For us, those guidelines are enforced at check-in time - we are using a tool which verifies and only allows compliant check-ins. While this tool is internal and not ready to publish, we had anyways decided to open up and present the rules we use to the community, as inspiration.
|
||||
|
||||
Question: Since we're having the guidelines, how come there is still C/AL code in NAV which doesn't respect them?
|
||||
|
||||
Answer: all new C/AL code is bound to follow the guidelines (else it cannot be checked-in). However, the code that existed before the rules - it does not. We had done cleanup in a certain degree. Now we're gradually improving the old code base as we visit various objects in order to add new functionality, however chances are that code we didn't touch in a long time had remained in its old form.
|
||||
|
||||
We're looking forward to your comments. Where you can, do provide concrete examples of the alternatives, Good and Bad.
|
||||
|
||||
{{< youtube z6skKy0pkmU >}}
|
||||
|
||||
|
||||
|
||||
[anchor0]: https://blogs.msdn.microsoft.com/nav/2015/01/09/cal-coding-guidelines-used-at-microsoft-development-center-copenhagen "download the C/AL coding guidelines as pdf"
|
||||
[anchor1]: /members/waldo/default.aspx "waldo"
|
||||
[anchor2]: https://www.youtube.com/watch?v=z6skKy0pkmU&list=PLhZ3P-LY7CqmVszuvtJLujFyHpsVN0U_w&index=26
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
+++
|
||||
title = "Design"
|
||||
weight = 490
|
||||
+++
|
||||
## C/AL Coding Guidelines
|
||||
|
||||
## **Design**
|
||||
|
||||
Find the C/AL guidelines by expanding the menu in the left.
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
+++
|
||||
title = "By Reference Parameters"
|
||||
weight = 280
|
||||
+++
|
||||
Do not declare parameters by reference if their values are not intended to be changed.
|
||||
|
||||
Unintentional value changes might propagate. Also, it might lead people to believe that value changes are intended.
|
||||
|
||||
Bad code
|
||||
|
||||
LOCAL PROCEDURE ShowMessage@15(VAR Text@1000 : Text[250]);
|
||||
BEGIN
|
||||
Text := GetMessageText;
|
||||
IF (Text <> '') AND GenJnlLineInserted THEN
|
||||
MESSAGE(Text);
|
||||
END;
|
||||
|
||||
Good code
|
||||
|
||||
LOCAL PROCEDURE ShowMessage@15(Text@1000 : Text[250]);
|
||||
BEGIN
|
||||
Text := GetMessageText;
|
||||
IF (Text <> '') AND GenJnlLineInserted THEN
|
||||
MESSAGE(Text);
|
||||
END;
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
+++
|
||||
title = "Class Coupling"
|
||||
weight = 320
|
||||
+++
|
||||
Do not write functions that have high class coupling. This makes the code hard to maintain.
|
||||
|
||||
Bad code
|
||||
|
||||
Any procedure / trigger that has class coupling of > 30
|
||||
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
Any procedure / trigger that has class coupling of <= 30\.
|
||||
Class coupling is computed by summing the unique instances of the following in a code block:
|
||||
- every unique usage of a complex C/AL data type (table, codeunit, etc) as 1\.
|
||||
- every unique usage of a DotNet type as 1\.
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
+++
|
||||
title = "Cyclomatic Complexity"
|
||||
weight = 460
|
||||
+++
|
||||
Do not write functions that have high cyclomatic complexity. This makes the code hard to maintain.
|
||||
|
||||
Bad code
|
||||
|
||||
Any procedure / trigger that has a cyclomatic complexity > 25, using the CC3 version mentioned in [this article][anchor0].
|
||||
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
Any procedure / trigger that has a cyclomatic complexity <= 25, using the CC3 version.
|
||||
The CC3 version is computed by summing the following in a code block:
|
||||
- each IF statement as 1\.
|
||||
- each entire CASE as 1\.
|
||||
|
||||
|
||||
|
||||
[anchor0]: http://www.aivosto.com/project/help/pm-complexity.html
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
+++
|
||||
title = "Encapsulate Local Functionality"
|
||||
weight = 530
|
||||
+++
|
||||
Any function used local must be defined as local.
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
+++
|
||||
title = "FINDSET FINDFIRST FINDLAST"
|
||||
weight = 600
|
||||
+++
|
||||
FINDSET, FIND('+') or FIND('-') should only be used when NEXT is used and vice versa.
|
||||
|
||||
Bad code
|
||||
|
||||
IF Cust.FIND('-') THEN
|
||||
ERROR(CustIsBlockErr)
|
||||
|
||||
Good code
|
||||
|
||||
IF Cust.FINDFIRST THEN
|
||||
ERROR(CustIsBlockErr)
|
||||
|
||||
Bad code
|
||||
|
||||
IF Cust.FINDFIRST THEN
|
||||
REPEAT
|
||||
...
|
||||
UNTIL Cust.NEXT = 0;
|
||||
|
||||
Good code
|
||||
|
||||
IF Cust.FINDSET THEN
|
||||
REPEAT
|
||||
...
|
||||
UNTIL Cust.NEXT = 0;
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
+++
|
||||
title = "Initialized Variables"
|
||||
weight = 660
|
||||
+++
|
||||
Variables should always be set to a specific value, before they are used.
|
||||
|
||||
Bad code
|
||||
|
||||
PROCEDURE SetPurchLine@22(VAR CurrentPurchLine@1000 : Record 39);
|
||||
VAR
|
||||
Pegging@1001 : Boolean;
|
||||
BEGIN
|
||||
IF Pegging THEN
|
||||
CurrQuantity := CurrentPurchLine."Quantity (Base)"
|
||||
ELSE
|
||||
CurrQuantity := CurrentPurchLine."Outstanding Qty. (Base)";
|
||||
END;
|
||||
|
||||
Good code
|
||||
|
||||
PROCEDURE SetPurchLine@22(VAR CurrentPurchLine@1000 : Record 39);
|
||||
VAR
|
||||
Pegging@1001 : Boolean;
|
||||
BEGIN
|
||||
Pegging := IsPegging(CurrentPurchLine);
|
||||
IF Pegging THEN
|
||||
CurrQuantity := CurrentPurchLine."Quantity (Base)"
|
||||
ELSE
|
||||
CurrQuantity := CurrentPurchLine."Outstanding Qty. (Base)";
|
||||
END;
|
||||
|
||||
Bad code
|
||||
|
||||
// In the example below, the function will always return FALSE.
|
||||
PROCEDURE GetItemsToPlan@22() : Boolean;
|
||||
BEGIN
|
||||
SETRANGE("Document Type","Document Type"::Order);
|
||||
...
|
||||
FINDSET
|
||||
END;
|
||||
|
||||
Good code
|
||||
|
||||
PROCEDURE GetItemsToPlan@22() : Boolean;
|
||||
BEGIN
|
||||
SETRANGE("Document Type","Document Type"::Order);
|
||||
...
|
||||
EXIT(FINDSET)
|
||||
END;
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
+++
|
||||
title = "Maintainability Index"
|
||||
weight = 770
|
||||
+++
|
||||
[Maintainability Index][anchor0]: Do not write functions that have a very low maintainability index. This makes the code hard to maintain.
|
||||
|
||||
Bad code
|
||||
|
||||
Any procedure / trigger that has a maintainability index < 20
|
||||
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
Any procedure / trigger that has a maintainability index >= 20\.
|
||||
The Maintainability Index is computed as a function:
|
||||
- Lines Of Code (inverse proportional)
|
||||
- the Halstead Volume
|
||||
- Cyclomatic Complexity (inverse proportional).
|
||||
|
||||
More info
|
||||
|
||||
* [Halstead Volume][anchor1]
|
||||
* [Cyclomatic Complexity][anchor2]
|
||||
|
||||
Bad code
|
||||
|
||||
Any procedure / trigger that is > 100 lines of code
|
||||
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
Any procedure / trigger that is <= 100 lines of code.
|
||||
A full C/AL Statement counts as 1 line of code
|
||||
|
||||
|
||||
|
||||
[anchor0]: http://blogs.msdn.com/b/codeanalysis/archive/2007/11/20/maintainability-index-range-and-meaning.aspx
|
||||
[anchor1]: http://en.wikipedia.org/wiki/Halstead_complexity_measures
|
||||
[anchor2]: http://www.aivosto.com/project/help/pm-complexity.html
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
+++
|
||||
title = "Parameter Placeholders"
|
||||
weight = 920
|
||||
+++
|
||||
The number of parameters passed to a string must match the placeholders.
|
||||
|
||||
Bad code
|
||||
|
||||
CannotDeleteLineErr@1005 : TextConst 'ENU=You cannot delete this line because one or more rating values exists.';
|
||||
...
|
||||
ERROR(CannotDeleteLineErr,TABLECAPTION);
|
||||
|
||||
Good code
|
||||
|
||||
CannotDeleteLineErr@1005 : TextConst 'ENU=You cannot delete this line because one or more rating values exists.';
|
||||
...
|
||||
ERROR(CannotDeleteLineErr);
|
||||
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
CannotUseThisFieldErr@1020 : TextConst 'ENU=You cannot use this field for %2 fields.';
|
||||
...
|
||||
ERROR(CannotUseThisFieldErr,0,Field.Class);
|
||||
|
||||
Good code
|
||||
|
||||
CannotUseThisFieldErr@1020 : TextConst 'ENU=You cannot use this field for %1 fields.';
|
||||
...
|
||||
ERROR(CannotUseThisFieldErr,Field.Class);
|
||||
|
||||
###
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
+++
|
||||
title = "Static Object Invocation"
|
||||
weight = 1160
|
||||
+++
|
||||
Call objects statically whenever possible. It reduces extra noise and removes extra variables. Downside: changing the name of the object which is called statically will need a code update.
|
||||
|
||||
Bad code
|
||||
|
||||
LOCAL PROCEDURE Code@1();
|
||||
VAR
|
||||
CAJnlPostBatch@1001 : Codeunit 1103;
|
||||
BEGIN
|
||||
CAJnlPostBatch.Run(CostJnlLine);
|
||||
END;
|
||||
|
||||
Good code
|
||||
|
||||
LOCAL PROCEDURE Code@1();
|
||||
BEGIN
|
||||
CODEUNIT.RUN(CODEUNIT::"CA Jnl.-Post Batch",CostJnlLine);
|
||||
END;
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
+++
|
||||
title = "Unreachable Code"
|
||||
weight = 1310
|
||||
+++
|
||||
Do not write code that will never be hit.
|
||||
|
||||
It affects code readability and can lead to wrong assumptions.
|
||||
|
||||
Bad code
|
||||
|
||||
IF Type <> Type::FIELD THEN BEGIN
|
||||
...
|
||||
ERROR(...);
|
||||
RecRef.CLOSE;
|
||||
END;
|
||||
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
IF Type <> Type::FIELD THEN BEGIN
|
||||
...
|
||||
RecRef.CLOSE;
|
||||
ERROR(...);
|
||||
END;
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
+++
|
||||
title = "Unused Initialized Variables"
|
||||
weight = 1320
|
||||
+++
|
||||
The value assigned to a variable must be used. Else the variable is not necessary.
|
||||
|
||||
Bad code
|
||||
|
||||
PROCEDURE AddEntities@1(FilterStr@1000 : Text[250]);
|
||||
VAR
|
||||
Vendor@1001 : Record 23;
|
||||
Count@1002 : Integer;
|
||||
BEGIN
|
||||
Count := 0;
|
||||
Vendor.SETFILTER("No.",FilterStr);
|
||||
IF Vendor.FINDSET THEN
|
||||
REPEAT
|
||||
"User ID" := USERID;
|
||||
"Vendor No." := Vendor."No.";
|
||||
IF INSERT THEN
|
||||
Count += 1;
|
||||
UNTIL Vendor.NEXT = 0;
|
||||
END;
|
||||
|
||||
Good code
|
||||
|
||||
PROCEDURE AddEntities@1(FilterStr@1000 : Text[250]);
|
||||
VAR
|
||||
Vendor@1001 : Record 23;
|
||||
BEGIN
|
||||
Vendor.SETFILTER("No.",FilterStr);
|
||||
IF Vendor.FINDSET THEN
|
||||
REPEAT
|
||||
"User ID" := USERID;
|
||||
"Vendor No." := Vendor."No.";
|
||||
IF INSERT THEN;
|
||||
UNTIL Vendor.NEXT = 0;
|
||||
END;
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
+++
|
||||
title = "Unused Variables"
|
||||
weight = 1330
|
||||
+++
|
||||
Do not declare variables that are unused.
|
||||
|
||||
Unused variables affect readability.
|
||||
|
||||
Bad code
|
||||
|
||||
PROCEDURE CheckPostingDate@23(CaptionEntryNo@1005 : Text[50]);
|
||||
BEGIN
|
||||
IF GenJnlCheckLine.DateNotAllowed(PostingDate) THEN
|
||||
ERROR(DateNotAllowedErr,Caption,EntryNo)
|
||||
IF PostingDate > MaxPostingDate THEN
|
||||
MaxPostingDate := PostingDate;
|
||||
END
|
||||
|
||||
Good code
|
||||
|
||||
PROCEDURE CheckPostingDate@23();
|
||||
BEGIN
|
||||
IF GenJnlCheckLine.DateNotAllowed(PostingDate) THEN
|
||||
ERROR(DateNotAllowedErr,Caption,EntryNo);
|
||||
IF PostingDate > MaxPostingDate THEN
|
||||
MaxPostingDate := PostingDate;
|
||||
END;
|
||||
|
||||
Bad code
|
||||
|
||||
PROCEDURE IsReturned@14(EntryNo@1002 : Integer) : Decimal;
|
||||
VAR
|
||||
ItemEntry@1000 : Record 32;
|
||||
Quantity@1003 : Integer;
|
||||
BEGIN
|
||||
EXIT(-OutboundApplied(EntryNo,TRUE) - InboundApplied(EntryNo,TRUE));
|
||||
END;
|
||||
|
||||
Good code
|
||||
|
||||
PROCEDURE IsReturned@14(EntryNo@1002 : Integer) : Decimal;
|
||||
BEGIN
|
||||
EXIT(-OutboundApplied(EntryNo,TRUE) - InboundApplied(EntryNo,TRUE));
|
||||
END;
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
+++
|
||||
title = "Variable Capacity Mismatch"
|
||||
weight = 1410
|
||||
+++
|
||||
Do not assign a value to a variable whose capacity is smaller.
|
||||
|
||||
It will throw an error at runtime.
|
||||
|
||||
Bad code
|
||||
|
||||
FileName@1010 : Text[250];
|
||||
...
|
||||
UploadedFileName@1016 : Text[1024];
|
||||
...
|
||||
FileName := UploadedFileName;
|
||||
|
||||
Good code
|
||||
|
||||
FileName@1010 : Text[1024];
|
||||
...
|
||||
UploadedFileName@1016 : Text[1024];
|
||||
...
|
||||
FileName := UploadedFileName;
|
||||
|
||||
Bad code
|
||||
|
||||
FileName@1010 : Text[250];
|
||||
...
|
||||
UploadedFileName@1016 : Text[1024];
|
||||
...
|
||||
FileName := UploadedFileName;
|
||||
|
||||
Good code
|
||||
|
||||
FileName@1010 : Text[250];
|
||||
...
|
||||
UploadedFileName@1016 : Text[1024];
|
||||
...
|
||||
FileName := COPYSTR(UploadedFileName,1,250); // In case only the first 250 chars are needed. Similar for fields
|
||||
|
||||
Bad code
|
||||
|
||||
VAR
|
||||
ExceededNumberTxt@001 : 'ENU=Warning: Exceeded number of unsent documents/requests'
|
||||
Subject@1002 : Text[50];
|
||||
...
|
||||
BEGIN
|
||||
...
|
||||
Subject := ExceededNumberTxt;
|
||||
|
||||
Good code
|
||||
|
||||
VAR
|
||||
ExceededNumberTxt@001 : 'ENU=Warning: Exceeded number of unsent documents/requests'
|
||||
Subject@1002 : Text[100];
|
||||
...
|
||||
BEGIN
|
||||
...
|
||||
Subject := ExceededNumberTxt';
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
+++
|
||||
title = "WITH Scope Name Collision"
|
||||
weight = 1450
|
||||
+++
|
||||
Do not use the WITH scope when it has a variable whose name is the same as a local variable. This can lead to wrong code assumptions.
|
||||
|
||||
**Given that**
|
||||
"Contract Type" is a field on table ServiceContractHeader, then in the following example there is a parameter name clash with the field name. Which one will be used?
|
||||
|
||||
Bad code
|
||||
|
||||
PROCEDURE InsertData@1("Contract Type"@1000 : Option...);
|
||||
...
|
||||
BEGIN
|
||||
...
|
||||
WITH ServiceContractHeader DO BEGIN
|
||||
...
|
||||
DimMgt.InsertServContractDim(...,"Contract Type","Contract No.",0,...);
|
||||
END;
|
||||
|
||||
Good code
|
||||
|
||||
PROCEDURE InsertData@1(ContractType@1000 : Option...);
|
||||
...
|
||||
BEGIN
|
||||
...
|
||||
WITH ServiceContractHeader DO BEGIN
|
||||
...
|
||||
DimMgt.InsertServContractDim(...,ContractType,"Contract No.",0,...);
|
||||
END;
|
||||
|
|
@ -0,0 +1,206 @@
|
|||
+++
|
||||
title = "Internally used DotNet Types"
|
||||
weight = 690
|
||||
+++
|
||||
_(Dynamics NAV 2015)_
|
||||
|
||||
|
||||
**Dot Net Types**
|
||||
|
||||
'mscorlib'.System.Convert
|
||||
|
||||
'mscorlib'.System.Globalization.CultureInfo
|
||||
|
||||
'mscorlib'.System.Globalization.DateTimeStyles
|
||||
|
||||
'mscorlib'.System.Globalization.NumberStyles
|
||||
|
||||
'mscorlib'.System.Type
|
||||
|
||||
'mscorlib'.System.Array
|
||||
|
||||
'mscorlib'.System.EventArgs
|
||||
|
||||
'mscorlib'.System.Security.Cryptography.SHA512Managed
|
||||
|
||||
'mscorlib'.System.Security.Cryptography.HashAlgorithm
|
||||
|
||||
'mscorlib'.System.Text.Encoding
|
||||
|
||||
'mscorlib'.System.Text.UTF8Encoding
|
||||
|
||||
'mscorlib'.System.Environment
|
||||
|
||||
'mscorlib'.System.IO.Directory
|
||||
|
||||
'mscorlib'.System.IO.Path
|
||||
|
||||
'mscorlib'.System.IO.File
|
||||
|
||||
'mscorlib'.System.IO.FileAttributes
|
||||
|
||||
'mscorlib'.System.Collections.ArrayList
|
||||
|
||||
'mscorlib'.System.Collections.IEnumerator
|
||||
|
||||
'mscorlib'.System.Collections.Generic.IEnumerator\`1
|
||||
|
||||
'mscorlib'.System.TimeSpan
|
||||
|
||||
'mscorlib'.System.DateTime
|
||||
|
||||
'mscorlib'.System.DateTimeKind
|
||||
|
||||
'mscorlib'.System.DateTimeOffset
|
||||
|
||||
'mscorlib'.System.Decimal
|
||||
|
||||
'mscorlib'.System.String
|
||||
|
||||
'System'.System.Diagnostics.Process
|
||||
|
||||
'System'.System.Diagnostics.ProcessStartInfo
|
||||
|
||||
'System'.System.IO.Compression.CompressionMode
|
||||
|
||||
'System'.System.IO.Compression.GZipStream
|
||||
|
||||
'System'.System.Uri
|
||||
|
||||
'System'.System.UriPartial
|
||||
|
||||
'System.Data'.System.Data.DataColumn
|
||||
|
||||
'System.Data'.System.Data.DataTable
|
||||
|
||||
'System.Data'.System.Data.DataRow
|
||||
|
||||
'System.Web'.System.Web.HttpUtility
|
||||
|
||||
'System.Windows.Forms'.System.Windows.Forms.DialogResult
|
||||
|
||||
'System.Windows.Forms'.System.Windows.Forms.FileDialog
|
||||
|
||||
'System.Windows.Forms'.System.Windows.Forms.OpenFileDialog
|
||||
|
||||
'System.Windows.Forms'.System.Windows.Forms.SaveFileDialog
|
||||
|
||||
'System.Windows.Forms'.System.Windows.Forms.FolderBrowserDialog
|
||||
|
||||
'System.Xml'.\*
|
||||
|
||||
'DocumentFormat.OpenXml'.\*
|
||||
|
||||
'mscorlib'.System.IO.DirectoryInfo
|
||||
|
||||
'mscorlib'.System.IO.FileInfo
|
||||
|
||||
'Microsoft.Dynamics.Nav.Client.CodeViewerTypes'.Microsoft.Dynamics.Nav.Client.CodeViewerTypes.BreakpointCollection
|
||||
|
||||
'Microsoft.Dynamics.Nav.Client.CodeViewerTypes'.Microsoft.Dynamics.Nav.Client.CodeViewerTypes.VariableCollection
|
||||
|
||||
'Microsoft.Dynamics.Nav.SMTP'.Microsoft.Dynamics.Nav.SMTP.SmtpMessage
|
||||
|
||||
'Microsoft.Dynamics.Nav.Management.DSObjectPickerWrapper'.\*
|
||||
|
||||
'Microsoft.Dynamics.Nav.Timer'.\*
|
||||
|
||||
'Microsoft.Dynamics.Nav.DO.ClientProxyWrapper'.\*
|
||||
|
||||
'Microsoft.Dynamics.Nav.Client.BusinessChart'.\*
|
||||
|
||||
'Microsoft.Dynamics.Nav.Client.BusinessChart.Model'.\*
|
||||
|
||||
'Microsoft.Dynamics.Nav.Integration.Office'.\*
|
||||
|
||||
'Microsoft.Dynamics.Nav.Integration.Office.Mock'.\*
|
||||
|
||||
'Microsoft.Dynamics.Nav.EwsWrapper'.\*
|
||||
|
||||
'Microsoft.Dynamics.Nav.EwsWrapper.ALTestHelper'.\*
|
||||
|
||||
'Microsoft.Dynamics.NAV.OLSync.OLSyncSupplier'.\*
|
||||
|
||||
'Microsoft.Dynamics.Nav.OLSync.Common'.\*
|
||||
|
||||
'Microsoft.Dynamics.Nav.NavUserAccount'.\*
|
||||
|
||||
'Microsoft.Dynamics.Nav.OpenXml'.\*
|
||||
|
||||
'Microsoft.Dynamics.Nav.RapidStart'.\*
|
||||
|
||||
'Microsoft.Dynamics.Framework.RapidStart.Common'.\*
|
||||
|
||||
'Microsoft.Dynamics.Nav.Client.TimelineVisualization'.Microsoft.Dynamics.Nav.Client.TimelineVisualization.
|
||||
|
||||
VisualizationScenarios
|
||||
|
||||
'Microsoft.Dynamics.Framework.UI.WinForms.DataVisualization.Timeline'.Microsoft.Dynamics.Framework.UI.
|
||||
|
||||
WinForms.DataVisualization.TimelineVisualization.DataModel+TransactionChangesRow
|
||||
|
||||
'Microsoft.Dynamics.Framework.UI.WinForms.DataVisualization.Timeline'.Microsoft.Dynamics.Framework.UI.
|
||||
|
||||
WinForms.DataVisualization.TimelineVisualization.DataModel+TransactionChangesDataTable
|
||||
|
||||
'Microsoft.Dynamics.Framework.UI.WinForms.DataVisualization.Timeline'.Microsoft.Dynamics.Framework.UI.
|
||||
|
||||
WinForms.DataVisualization.TimelineVisualization.DataModel+TransactionRow
|
||||
|
||||
'Microsoft.Dynamics.Framework.UI.WinForms.DataVisualization.Timeline'.Microsoft.Dynamics.Framework.UI.
|
||||
|
||||
WinForms.DataVisualization.TimelineVisualization.DataModel+TransactionDataTable
|
||||
|
||||
'Microsoft.Office.Interop.Word'.\*
|
||||
|
||||
'Microsoft.Office.Interop.Excel'.\*
|
||||
|
||||
'Microsoft.Dynamics.BAPIWrapper'.\*
|
||||
|
||||
'Microsoft.Dynamics.Nav.Types'.Microsoft.Dynamics.Nav.Types.ConfigSettings
|
||||
|
||||
'Microsoft.Dynamics.Nav.DocumentService'.\*
|
||||
|
||||
'Microsoft.Dynamics.Nav.DocumentService.Types'.\*
|
||||
|
||||
'mscorlib'.System.IO.StreamWriter
|
||||
|
||||
'Microsoft.Dynamics.Nav.Client.TimelineVisualization'.Microsoft.Dynamics.Nav.Client.TimelineVisualization.
|
||||
|
||||
InteractiveTimelineVisualizationAddIn
|
||||
|
||||
'System'.System.ComponentModel.CancelEventArgs
|
||||
|
||||
'System'.System.Text.RegularExpressions.Regex
|
||||
|
||||
'System'.System.Text.RegularExpressions.RegexOptions
|
||||
|
||||
'mscorlib'.System.IO.StreamReader
|
||||
|
||||
'System.Windows.Forms'.System.Windows.Forms.Control
|
||||
|
||||
'System.Windows.Forms'.System.Windows.Forms.ControlEventArgs
|
||||
|
||||
'System.Windows.Forms'.System.Windows.Forms.DragEventArgs
|
||||
|
||||
'System.Windows.Forms'.System.Windows.Forms.GiveFeedbackEventArgs
|
||||
|
||||
'System.Windows.Forms'.System.Windows.Forms.HelpEventArgs
|
||||
|
||||
'System.Windows.Forms'.System.Windows.Forms.InvalidateEventArgs
|
||||
|
||||
'System.Windows.Forms'.System.Windows.Forms.KeyEventArgs
|
||||
|
||||
'System.Windows.Forms'.System.Windows.Forms.KeyPressEventArgs
|
||||
|
||||
'System.Windows.Forms'.System.Windows.Forms.LayoutEventArgs
|
||||
|
||||
'System.Windows.Forms'.System.Windows.Forms.MouseEventArgs
|
||||
|
||||
'System.Windows.Forms'.System.Windows.Forms.PaintEventArgs
|
||||
|
||||
'System.Windows.Forms'.System.Windows.Forms.PreviewKeyDownEventArgs
|
||||
|
||||
'System.Windows.Forms'.System.Windows.Forms.QueryAccessibilityHelpEventArgs
|
||||
|
||||
'System.Windows.Forms'.System.Windows.Forms.UICuesEventArgs
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
+++
|
||||
title = "Internationalization"
|
||||
weight = 700
|
||||
+++
|
||||
## C/AL Coding Guidelines
|
||||
|
||||
## **Internationalization**
|
||||
|
||||
|
||||
Find the C/AL guidelines by expanding the menu in the left.
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
+++
|
||||
title = "Using Calcdate"
|
||||
weight = 1370
|
||||
+++
|
||||
CALCDATE should only be used with DateFormula variables. Alternatively the string should be enclosed using the <> symbols.
|
||||
|
||||
Bad code
|
||||
|
||||
IF ReservEntry."Expected Receipt Date" >
|
||||
CALCDATE('-' + FORMAT("Dampener (Time)") + FirstDate)
|
||||
THEN
|
||||
|
||||
Good code
|
||||
|
||||
IF ReservEntry."Expected Receipt Date" >
|
||||
CALCDATE('<-' + FORMAT("Dampener (Time)") + FirstDate + '>')
|
||||
THEN
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
+++
|
||||
title = "Localizability"
|
||||
weight = 750
|
||||
+++
|
||||
## C/AL Coding Guidelines
|
||||
|
||||
## **Localizability**
|
||||
|
||||
Find the C/AL guidelines by expanding the menu in the left.
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
+++
|
||||
title = "CaptionML on System Pages"
|
||||
weight = 300
|
||||
+++
|
||||
CaptionML should always be specified on a page field for a system table. By default, system tables do not have captions, so if you need to use them in the UI then captions need to be added.
|
||||
|
||||
Bad code
|
||||
|
||||
...
|
||||
{ 2 ;2 ;Field ;
|
||||
SourceExpr=Name }
|
||||
...
|
||||
OBJECT Table 2000000000 User
|
||||
...
|
||||
{ 2 ; ;Name ;Text50 }
|
||||
|
||||
Good code
|
||||
|
||||
...
|
||||
{ 2 ;2 ;Field ;
|
||||
CaptionML=ENU=Name;
|
||||
SourceExpr=Name }
|
||||
...
|
||||
OBJECT Table 2000000000 User
|
||||
...
|
||||
{ 2 ; ;Name ;Text50 }
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
+++
|
||||
title = "FIELDCAPTION and TABLECAPTION"
|
||||
weight = 580
|
||||
+++
|
||||
For user messages, errors etc., use FIELDCAPTION not FIELDNAME and TABLECAPTION not TABLENAME.
|
||||
|
||||
Reason:
|
||||
|
||||
1. The correct translation will be automatically used.
|
||||
2. If the caption/name changes, then there will be a single point of change needed.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF NOT CONFIRM(UpdateLocationQst,TRUE,FIELDNAME("Location Code"),...)
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF NOT CONFIRM(UpdateLocationQst,TRUE,FIELDCAPTION("Location Code"),...)
|
||||
```
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
+++
|
||||
title = "Global Text Constants"
|
||||
weight = 610
|
||||
+++
|
||||
Declare Text Constant as global variables.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
PROCEDURE GetRequirementText@6(...) : Text[50];
|
||||
VAR
|
||||
RequirementOptionsTxt@1002 : TextConst 'ENU=Shipment,Receive,Pick,Put-Away';
|
||||
BEGIN
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
VAR
|
||||
RequirementOptionsTxt@1002 : TextConst 'ENU=Shipment,Receive,Pick,Put-Away';
|
||||
...
|
||||
PROCEDURE GetRequirementText@6(...) : Text[50];
|
||||
BEGIN
|
||||
```
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
+++
|
||||
title = "Use Text Constants"
|
||||
weight = 1360
|
||||
+++
|
||||
Pass user messages using Text Constants. It makes translation easy.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
ImportAttachmentQst@1021 : TextConst 'ENU="Import attachment "';
|
||||
...
|
||||
IF CONFIRM(ImportAttachmentQst + Caption +'?',TRUE) THEN BEGIN
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
ImportAttachmentQst@1021 : TextConst 'ENU="Import attachment %1?"';
|
||||
...
|
||||
IF CONFIRM(STRSUBSTNO(ImportAttachmentQst, Caption),TRUE) THEN BEGIN
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
...
|
||||
IF NOT
|
||||
CONFIRM(
|
||||
STRSUBSTNO(
|
||||
'Difference on Periodic entries: %1 on %2' +
|
||||
'Do you want to continue?',Balance,Date),
|
||||
TRUE)
|
||||
THEN
|
||||
ERROR('Program terminated by the user');
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
DiffOnPeriodEntiesQst@100 : TextConst 'ENU="Difference on Periodic entries: %1 on %2\\ Do you want to continue?"';
|
||||
ProgramTerminatedErr@200 : TextConst 'ENU="Program terminated by the user"';
|
||||
...
|
||||
IF NOT CONFIRM(STRSUBSTNO(DiffOnPeriodEntiesQst,Balance,Date),TRUE) THEN
|
||||
ERROR(ProgramTerminatedErr);
|
||||
```
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
+++
|
||||
title = "Using OptionCaptionML"
|
||||
weight = 1380
|
||||
+++
|
||||
The OptionCaptionML should be filled in for sourceexpression using option data types.
|
||||
|
||||
Bad code
|
||||
|
||||
{ 30 ;TextBox ;17850;0 ;150 ;423 ;Name=Selection;
|
||||
SourceExpr=Selection;
|
||||
DataSetFieldName=Selection }
|
||||
...
|
||||
Selection@1008 : 'Open,Closed,Open and Closed';
|
||||
...
|
||||
|
||||
Good code
|
||||
|
||||
{ 30 ;TextBox ;17850;0 ;150 ;423 ;Name=Selection;
|
||||
OptionCaptionML=ENU=Open,Closed,Open and Closed;
|
||||
SourceExpr=Selection;
|
||||
DataSetFieldName=Selection }
|
||||
...
|
||||
Selection@1008 : 'Open,Closed,Open and Closed';
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
+++
|
||||
title = "Readability"
|
||||
weight = 980
|
||||
+++
|
||||
## C/AL Coding Guidelines
|
||||
|
||||
## **Readability**
|
||||
|
||||
Generally, all readability rules are Microsoft style choices only. You can use them to keep consistency with the existing code.
|
||||
|
||||
Find the C/AL guidelines by expanding the menu in the left.
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
+++
|
||||
title = "Begin as an 'After Word'"
|
||||
weight = 230
|
||||
+++
|
||||
When BEGIN follows THEN, ELSE, DO, it should be on the same line, preceded by one space character.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF ICPartnerRefType = ICPartnerRefType::"Common Item No." THEN
|
||||
BEGIN
|
||||
...
|
||||
END;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```
|
||||
IF ICPartnerRefType = ICPartnerRefType::"Common Item No." THEN BEGIN
|
||||
...
|
||||
END;
|
||||
```
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
+++
|
||||
title = "Begin-End - Compound Only"
|
||||
weight = 240
|
||||
+++
|
||||
Only use BEGIN..END to enclose compound statements.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF FINDSET THEN BEGIN
|
||||
REPEAT
|
||||
...
|
||||
UNTIL NEXT = 0;
|
||||
END;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF FINDSET THEN
|
||||
REPEAT
|
||||
...
|
||||
UNTIL NEXT = 0;
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF IsAssemblyOutputLine THEN BEGIN
|
||||
TESTFIELD("Order Line No.",0);
|
||||
END;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF IsAssemblyOutputLine THEN
|
||||
TESTFIELD("Order Line No.",0);
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF FINDSET THEN
|
||||
REPEAT
|
||||
BEGIN
|
||||
...
|
||||
END;
|
||||
UNTIL NEXT = 0;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF FINDSET THEN
|
||||
REPEAT
|
||||
...
|
||||
UNTIL NEXT = 0;
|
||||
```
|
||||
|
||||
Exception
|
||||
|
||||
```al
|
||||
// Except for this case
|
||||
IF X THEN BEGIN
|
||||
IF Y THEN
|
||||
DO SOMETHING;
|
||||
END ELSE (not X)
|
||||
```
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
+++
|
||||
title = "Binary Operator to Start Line"
|
||||
weight = 250
|
||||
+++
|
||||
Do not start a line with a binary operator.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
"Quantity to Ship" :=
|
||||
Quantity
|
||||
- "Quantity Shipped"
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
"Quantity to Ship" :=
|
||||
Quantity -
|
||||
"Quantity Shipped"
|
||||
```
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
+++
|
||||
title = "Blank Lines"
|
||||
weight = 260
|
||||
+++
|
||||
Do not use blank lines at the beginning or end of any functions, after BEGIN, before END, or inside multiline expressions.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
PROCEDURE MATRIX_OnDrillDown@1133(MATRIX_ColumnOrdinal : Integer);
|
||||
BEGIN
|
||||
|
||||
SetupDrillDownCol(MATRIX_ColumnOrdinal);
|
||||
DrillDown(FALSE,ValueType);
|
||||
|
||||
END;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
PROCEDURE MATRIX_OnDrillDown@1133(MATRIX_ColumnOrdinal : Integer);
|
||||
BEGIN
|
||||
SetupDrillDownCol(MATRIX_ColumnOrdinal);
|
||||
DrillDown(FALSE,ValueType);
|
||||
END;
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF NameIsValid AND
|
||||
|
||||
Name2IsValid
|
||||
THEN
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF NameIsValid AND
|
||||
Name2IsValid
|
||||
THEN
|
||||
```
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
+++
|
||||
title = "CASE Action"
|
||||
weight = 310
|
||||
+++
|
||||
A CASE action should start on a line after the possibility.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
CASE Letter OF
|
||||
'A': Letter2 := '10';
|
||||
'B': Letter2 := '11';
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
CASE Letter OF
|
||||
'A':
|
||||
Letter2 := '10';
|
||||
'B':
|
||||
Letter2 := '11';
|
||||
```
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
+++
|
||||
title = "Colon usage in CASE"
|
||||
weight = 340
|
||||
+++
|
||||
The last possibility on a CASE statement must be immediately followed by a colon.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
CASE DimOption OF
|
||||
DimOption::"Global Dimension 1" :
|
||||
DimValue."Dimension Code" := GLSetup."Global Dimension 1 Code";
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
CASE DimOption OF
|
||||
DimOption::"Global Dimension 1":
|
||||
DimValue."Dimension Code" := GLSetup."Global Dimension 1 Code";
|
||||
```
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
+++
|
||||
title = "Comments inside Curly Brackets"
|
||||
weight = 350
|
||||
+++
|
||||
Never use curly bracket comments. During development, the "Block comment" functionality can be used instead. However, in production code, block comments are not recommended.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
PeriodTxt: {Period}
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
PeriodTxt: // Period
|
||||
```
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
PROCEDURE MATRIX_OnAfterGetRecord@10(MATRIX_ColumnOrdinal : Integer);
|
||||
BEGIN
|
||||
{
|
||||
IF ShowColumnName THEN
|
||||
MatrixHeader := MatrixRecords[MATRIX_ColumnOrdinal].Name
|
||||
ELSE
|
||||
MatrixHeader := MatrixRecords[MATRIX_ColumnOrdinal].Code;
|
||||
}
|
||||
MatrixRecord := MatrixRecords[MATRIX_ColumnOrdinal];
|
||||
AnalysisValue := CalcAmt(ValueType,TRUE);
|
||||
MATRIX_CellData[MATRIX_ColumnOrdinal] := AnalysisValue;
|
||||
END;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
PROCEDURE MATRIX_OnAfterGetRecord@10(MATRIX_ColumnOrdinal : Integer);
|
||||
BEGIN
|
||||
MatrixRecord := MatrixRecords[MATRIX_ColumnOrdinal];
|
||||
AnalysisValue := CalcAmt(ValueType,TRUE);
|
||||
MATRIX_CellData[MATRIX_ColumnOrdinal] := AnalysisValue;
|
||||
END;
|
||||
```
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
+++
|
||||
title = "Comment Spacing"
|
||||
weight = 360
|
||||
+++
|
||||
Always start comments with // followed by one space character.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
RowNo += 1000; //Move way below the budget
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
RowNo += 1000; // Move way below the budget
|
||||
```
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
+++
|
||||
title = "END ELSE Pair"
|
||||
weight = 540
|
||||
+++
|
||||
The END ELSE pair should always appear on the same line.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF OppEntry.FIND('-') THEN
|
||||
IF SalesCycleStage.FIND('-') THEN BEGIN
|
||||
...
|
||||
END
|
||||
ELSE
|
||||
...
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF OppEntry.FIND('-') THEN
|
||||
IF SalesCycleStage.FIND('-') THEN BEGIN
|
||||
...
|
||||
END ELSE
|
||||
...
|
||||
```
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
+++
|
||||
title = "Indentation"
|
||||
weight = 650
|
||||
+++
|
||||
In general, use an indentation of two space characters. Logical expressions in the IF, WHILE, and UNTIL parts are indented at least 3, 6, and 6 spaces respectively.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF GLSetup."Unrealized VAT" OR
|
||||
(GLSetup."Prepayment Unrealized VAT" AND NewCVLedgEntryBuf.Prepayment)
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF GLSetup."Unrealized VAT" OR
|
||||
(GLSetup."Prepayment Unrealized VAT" AND NewCVLedgEntryBuf.Prepayment)
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF GenJnlLine."Account No." <> ICPartner.Code THEN
|
||||
ICPartner.GET("Account No.");
|
||||
IF GenJnlLine.Amount \> 0 THEN BEGIN
|
||||
...
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF GenJnlLine."Account No." <> ICPartner.Code THEN
|
||||
ICPartner.GET("Account No.");
|
||||
IF GenJnlLine.Amount > 0 THEN BEGIN
|
||||
...
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
Dialog.OPEN(WindowTxt +
|
||||
'@1@@@@@@@@@@@@@@@@@@@@@@@');
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
Dialog.OPEN(
|
||||
WindowTxt +
|
||||
'@1@@@@@@@@@@@@@@@@@@@@@@@');
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
TempOldCustLedgEntry.DELETE;
|
||||
// Find the next old entry for application of the new entry
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
TempOldCustLedgEntry.DELETE;
|
||||
// Find the next old entry for application of the new entry
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF NOT ("Applies-to Doc. Type" IN
|
||||
["Applies-to Doc. Type"::Receipt,
|
||||
"Applies-to Doc. Type"::"Return Shipment"])
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF NOT ("Applies-to Doc. Type" IN
|
||||
["Applies-to Doc. Type"::Receipt,
|
||||
"Applies-to Doc. Type"::"Return Shipment"])
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
WHILE (RemAmt > 0) OR
|
||||
(RemAmtLCY > 0)
|
||||
DO
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
WHILE (RemAmt > 0) OR
|
||||
(RemAmtLCY > 0)
|
||||
DO
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
UNTIL (RemAmt > 0) AND
|
||||
(RemAmtLCY > 0);
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
UNTIL (RemAmt > 0) AND
|
||||
(RemAmtLCY > 0)
|
||||
```
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
+++
|
||||
title = "Keyword Pairs - Indentation"
|
||||
weight = 730
|
||||
+++
|
||||
The IF..THEN pair, WHILE..DO pair, and FOR..DO pair must appear on the same line or the same level of indentation.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF (x = y) AND
|
||||
(a = b) THEN
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF (x = y) AND
|
||||
(a = b)
|
||||
THEN
|
||||
```
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
+++
|
||||
title = "Line Start Keywords"
|
||||
weight = 740
|
||||
+++
|
||||
The END, IF, REPEAT, FOR, WHILE, ELSE and CASE statement should always start a line.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF IsContactName THEN ValidateContactName
|
||||
ELSE IF IsSalespersonCode THEN ValidateSalespersonCode
|
||||
ELSE IF IsSalesCycleCode THEN ValidatSalesCycleCode;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF IsContactName THEN
|
||||
ValidateContactName
|
||||
ELSE
|
||||
IF IsSalespersonCode THEN
|
||||
ValidateSalespersonCode
|
||||
ELSE
|
||||
IF IsSalesCycleCode THEN
|
||||
ValidatSalesCycleCode;
|
||||
```
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
+++
|
||||
title = "Lonely Repeat"
|
||||
weight = 760
|
||||
+++
|
||||
The REPEAT statement should always be alone on a line.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF ReservEntry.FINDSET THEN REPEAT
|
||||
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF ReservEntry.FINDSET THEN
|
||||
REPEAT
|
||||
```
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
+++
|
||||
title = "Named Invocations"
|
||||
weight = 830
|
||||
+++
|
||||
When calling an object statically use the name, not the number
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
PAGE.RUNMODAL(525,SalesShptLine)
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
PAGE.RUNMODAL(PAGE::"Posted Sales Shipment Lines",SalesShptLine)
|
||||
```
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
+++
|
||||
title = "Nested WITHs"
|
||||
weight = 850
|
||||
+++
|
||||
Do not nest WITHs that reference different types of objects.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
WITH PostedWhseShptLine DO BEGIN
|
||||
...
|
||||
WITH ItemLedgEntry DO
|
||||
InsertBufferRec(...,"Serial No.","Lot No.",...);
|
||||
...
|
||||
END;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
WITH PostedWhseShptLine DO BEGIN
|
||||
...
|
||||
InsertBufferRec(...,ItemLedgEntry."Serial No.",ItemLedgEntry."Lot No.",...);
|
||||
...
|
||||
END;
|
||||
```
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
+++
|
||||
title = "One Statement Per Line"
|
||||
weight = 910
|
||||
+++
|
||||
A line of code should not have more than one statement.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF OppEntry.FIND('-') THEN EXIT
|
||||
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF OppEntry.FIND('-') THEN
|
||||
EXIT
|
||||
|
||||
```
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
TotalCost += Cost; TotalAmt += Amt;
|
||||
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
TotalCost += Cost;
|
||||
TotalAmt += Amt;
|
||||
```
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
+++
|
||||
title = "Separate IF and ELSE"
|
||||
weight = 1050
|
||||
+++
|
||||
IF and ELSE statements should be on separate lines.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF Atom[i+1] = '>' THEN HasLogicalOperator := TRUE ELSE BEGIN
|
||||
...
|
||||
END;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF Atom[i+1] = '>' THEN
|
||||
HasLogicalOperator := TRUE
|
||||
ELSE BEGIN
|
||||
...
|
||||
END;
|
||||
```
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
+++
|
||||
title = "Spacing Binary Operators"
|
||||
weight = 1120
|
||||
+++
|
||||
There must be exactly one space character on each side of a binary operator such as = + - AND OR =. The parameter comma operator however, should have no spaces.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
"Line Discount %" := "Line Discount Amount"/"Line Value"*100
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
"Line Discount %" := "Line Discount Amount" / "Line Value" * 100;
|
||||
```
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
StartDate := CALCDATE('<+'+FORMAT(Days + i)+'D>', StartDate);
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
StartDate := CALCDATE('<+' + FORMAT(Days + i) + 'D>',StartDate);
|
||||
```
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
StartDate := 0D; // Initialize
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
StartDate := 0D; // Initialize
|
||||
```
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
+++
|
||||
title = "Spacing Brackets and ::"
|
||||
weight = 1130
|
||||
+++
|
||||
There must be no spaces characters before and after [] dimension brackets symbols or :: option symbols.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
A[i] [j] := Amt;
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
A[i][j] := Amt;
|
||||
```
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
"Currency Exchange Rate"."Fix Exchange Rate Amount" :: Currency:
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
"Currency Exchange Rate"."Fix Exchange Rate Amount"::Currency:
|
||||
```
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF FIND (Which) THEN
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF FIND(Which) THEN
|
||||
```
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
+++
|
||||
title = "Spacing Unary Operators"
|
||||
weight = 1140
|
||||
+++
|
||||
There must be no space between a unary operator and its argument (except for the NOT keyword).
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF NOT(Type = Type::Item) THEN
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF NOT (Type = Type::Item) THEN
|
||||
```
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
DiscAmt := - "Discount Amount";
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
DiscAmt := -"Discount Amount";
|
||||
```
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
+++
|
||||
title = "Temporary Variable Naming"
|
||||
weight = 1200
|
||||
+++
|
||||
The name of a temporary variable must be prefixed with the word Temp and not otherwise.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
JobWIPBuffer@1002 : TEMPORARY Record 1018;
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
TempJobWIPBuffer@1002 : TEMPORARY Record 1018;
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
TempJobWIPBuffer@1002 : Record 1018;
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
CopyOfJobWIPBuffer@1002 : Record 1018;
|
||||
```
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
+++
|
||||
title = "TextConst Suffixes"
|
||||
weight = 1210
|
||||
+++
|
||||
TextConst variable names should have a suffix (an approved three-letter suffix: Msg, Tok, Err, Qst, Lbl, Txt) describing usage.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
CannotDeleteLine@1005 : TextConst 'ENU=You cannot delete this line because one or more rating values exists.';
|
||||
...
|
||||
ERROR(CannotDeleteLine,TABLECAPTION);
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
CannotDeleteLineErr@1005 : TextConst 'ENU=You cannot delete this line because one or more rating values exists.';
|
||||
...
|
||||
ERROR(CannotDeleteLineErr,TABLECAPTION);
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
Text000@1011 : TextConst 'ENU="has been changed (initial a %1: %2= %3, %4= %5)"';
|
||||
...
|
||||
SalesLine.FIELDERROR(Type,STRSUBSTNO(Text000,...);
|
||||
...
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
TypeHasBeenChangedErr@1011 : TextConst 'ENU="has been changed (initial a %1: %2= %3, %4= %5)"';
|
||||
...
|
||||
SalesLine.FIELDERROR(Type,STRSUBSTNO(TypeHasBeenChangedErr,...);
|
||||
...
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
Text004@1004 : TextConst 'ENU=Indenting the Job Tasks \#1\#\#\#\#\#\#\#\#\#\#.';
|
||||
...
|
||||
Window@1007 : Dialog;
|
||||
...
|
||||
Window.OPEN(Text004);
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IndentingMsg@1004 : TextConst 'ENU=Indenting the Job Tasks \#1\#\#\#\#\#\#\#\#\#\#.';
|
||||
...
|
||||
Window@1007 : Dialog;
|
||||
...
|
||||
Window.OPEN(IndentingMsg);
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
Text002@1005 : TextConst 'ENU=You cannot delete a %1 that is used in one or more setup windows.\\ Do you want to open the G/L Account No. Where-Used List Window?';
|
||||
...
|
||||
IF CONFIRM(Text002,TRUE,GLAcc.TABLECAPTION) THEN
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
OpenWhereUsedWindowQst@1005 : TextConst 'ENU=You cannot delete a %1 that is used in one or more setup windows.\\ Do you want to open the G/L Account No. Where-Used List Window?';
|
||||
...
|
||||
IF CONFIRM(OpenWhereUsedWindowQst,TRUE,GLAcc.TABLECAPTION) THEN
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
Selection := STRMENU(Text003,2);
|
||||
...
|
||||
Text003@1002 : TextConst 'ENU=&Copy dimensions from BOM,&Retrieve dimensions from components';
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
Selection := STRMENU(CopyFromQst,2);
|
||||
...
|
||||
CopyFromQst@1002 : TextConst 'ENU=&Copy dimensions from BOM,&Retrieve dimensions from components';
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
DATASET
|
||||
{
|
||||
...
|
||||
{ 1 ;1 ;Column ;Chart_of_AccountsCaption;
|
||||
SourceExpr=Chart_of_AccountsCaption }
|
||||
...
|
||||
Chart_of_AccountsCaption@9647 : TextConst 'ENU=Chart of Accounts';
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
DATASET
|
||||
{
|
||||
...
|
||||
{ 1 ;1 ;Column ;Chart_of_AccountsCaption;
|
||||
SourceExpr=ChartOfAccountsLbl }
|
||||
...
|
||||
ChartOfAccountsLbl@9647 : TextConst 'ENU=Chart of Accounts';
|
||||
```
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
+++
|
||||
title = "Unary Operator Line End"
|
||||
weight = 1250
|
||||
+++
|
||||
Do not end a line with unary operator.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
"Quantity Handled (Base)" := -
|
||||
"Quantity Handled (Base)");
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
"Quantity Handled (Base)" :=
|
||||
- "Quantity Handled (Base)");
|
||||
```
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
+++
|
||||
title = "Unnecessary Compound Parenthesis"
|
||||
weight = 1260
|
||||
+++
|
||||
Use parenthesis only to enclose compound expressions inside compound expressions.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF ("Costing Method" = "Costing Method"::Standard) THEN
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF "Costing Method" = "Costing Method"::Standard THEN
|
||||
```
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
ProfitPct = -(Profit) / CostAmt * 100;
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
ProfitPct = -Profit / CostAmt * 100;
|
||||
```
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
+++
|
||||
title = "Unnecessary ELSE"
|
||||
weight = 1270
|
||||
+++
|
||||
ELSE should not be used when the last action in the THEN part is an EXIT, BREAK, SKIP, QUIT, ERROR.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF IsAdjmtBinCodeChanged THEN
|
||||
ERROR(AdjmtBinCodeChangeNotAllowedErr,...)
|
||||
ELSE
|
||||
ERROR(BinCodeChangeNotAllowedErr,...);
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF IsAdjmtBinCodeChanged THEN
|
||||
ERROR(AdjmtBinCodeChangeNotAllowedErr,...)
|
||||
ERROR(BinCodeChangeNotAllowedErr,...);
|
||||
```
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
+++
|
||||
title = "Unnecessary Function Parenthesis"
|
||||
weight = 1280
|
||||
+++
|
||||
Do not use parenthesis in a function call if the function does not have any parameters.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF ReservMgt.IsPositive() THEN
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF ReservMgt.IsPositive THEN
|
||||
```
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF ChangeStatusForm.RUNMODAL() <> ACTION::Yes THEN
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF ChangeStatusForm.RUNMODAL <> ACTION::Yes THEN
|
||||
```
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
+++
|
||||
title = "Unnecessary Separators"
|
||||
weight = 1290
|
||||
+++
|
||||
There should be no unnecessary separators.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF Customer.FINDFIRST THEN;;
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF Customer.FINDFIRST THEN;
|
||||
```
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
+++
|
||||
title = "Unnecessary TRUE/FALSE"
|
||||
weight = 1300
|
||||
+++
|
||||
Do not use TRUE or FALSE keywords unnecessarily if the expression is already an logical expression.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
IF IsPositive() = TRUE THEN
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF IsPositive THEN
|
||||
```
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
```
|
||||
IF Complete <> TRUE THEN
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
IF NOT Complete THEN
|
||||
```
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
+++
|
||||
title = "Variable Already Scoped"
|
||||
weight = 1400
|
||||
+++
|
||||
Do not use scope ''.'' qualifier unnecessarily when a variable is already implicitly or explicitly scoped. It keeps the code simpler.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
ReturnRcptHeader.SETRANGE(ReturnRcptHeader."Return Order No.","Document No.");
|
||||
```
|
||||
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
ReturnRcptHeader.SETRANGE("Return Order No.","Document No.");
|
||||
```
|
||||
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
WITH ChangeLogSetupTable DO BEGIN
|
||||
...
|
||||
IF ChangeLogSetupTable.DELETE THEN
|
||||
...
|
||||
END;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
WITH ChangeLogSetupTable DO BEGIN
|
||||
...
|
||||
IF DELETE THEN
|
||||
...
|
||||
END;
|
||||
```
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
+++
|
||||
title = "Variable Naming"
|
||||
weight = 1420
|
||||
+++
|
||||
Variables that refer to a C/AL object must contain the objects name, abbreviated where necessary.
|
||||
|
||||
A variable must begin with a capital letter.
|
||||
|
||||
Blanks, periods, and other characters (such as parentheses) that would make quotation marks around a variable necessary must be omitted.
|
||||
|
||||
If a variable is a compound of two or more words or abbreviations, each word or abbreviation should begin with a capital letter.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
...
|
||||
WIPBuffer@1002 : Record 1018
|
||||
...
|
||||
OBJECT Table Job WIP Buffer
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
...
|
||||
JobWIPBuffer@1002 : Record 1018
|
||||
...
|
||||
OBJECT Table Job WIP Buffer
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
...
|
||||
Postline@1004 : Codeunit 12;
|
||||
...
|
||||
OBJECT Codeunit Gen. Jnl.-Post Line
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
...
|
||||
GenJnlPostLine@1004 : Codeunit 12;
|
||||
...
|
||||
OBJECT Codeunit Gen. Jnl.-Post Line
|
||||
```
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
LOCAL PROCEDURE HandleCustDebitCredit@17(...;"Amount (LCY)"@1001 : Decimal;...);
|
||||
BEGIN
|
||||
IF ((... ("Amount (LCY)" \> 0)) ...) OR
|
||||
((... ("Amount (LCY)" < 0)) ...)
|
||||
THEN BEGIN
|
||||
...
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
LOCAL PROCEDURE HandleCustDebitCredit@17(...;AmountLCY@1001 : Decimal;...);
|
||||
BEGIN
|
||||
IF ((... (AmountLCY \> 0)) ...) OR
|
||||
((... (AmountLCY < 0)) ...)
|
||||
THEN BEGIN
|
||||
...
|
||||
```
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
+++
|
||||
title = "Variables Declarations Order"
|
||||
weight = 1430
|
||||
+++
|
||||
Variables declarations should be ordered by type. In general, object and complex variable types are listed first followed by simple variables. The order should be the same as the object list in the object designer for C/AL objects. Afterwards come the complex variables like RecordRef, .NET, FieldRef etc. At the end come all the simple data types in no particular order.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
StartingDateFilter@1002 : Text[30];
|
||||
Vend@1003 : Record 23;
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
Vend@1003 : Record 23;
|
||||
StartingDateFilter@1002 : Text[30];
|
||||
```
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
+++
|
||||
title = "UX"
|
||||
weight = 1390
|
||||
+++
|
||||
## C/AL Coding Guidelines
|
||||
|
||||
## **User eXperience**
|
||||
|
||||
Find the C/AL guidelines by expanding the menu in the left.
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
+++
|
||||
title = "Actions - Images"
|
||||
weight = 200
|
||||
+++
|
||||
All actions must have an image assigned to them.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
{ 7 ;1 ;Action ;
|
||||
CaptionML=ENU=Customer - &Balance;
|
||||
RunObject=Report 121 }
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
{ 7 ;1 ;Action ;
|
||||
CaptionML=ENU=Customer - &Balance;
|
||||
RunObject=Report 121 }
|
||||
Image=Report }
|
||||
```
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
+++
|
||||
title = "CONFIRM"
|
||||
weight = 380
|
||||
+++
|
||||
Always end CONFIRM with a question mark.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
ChangeAllOpenedEntriesQst@1000 : TextConst 'ENU=Do you want to change all open entries for every customer and vendor that are not blocked';
|
||||
...
|
||||
IF CONFIRM(ChangeAllOpenedEntriesQst,TRUE) THEN
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
ChangeAllOpenedEntriesQst@1000 : TextConst 'ENU=Do you want to change all open entries for every customer and vendor that are not blocked?';
|
||||
...
|
||||
IF CONFIRM(ChangeAllOpenedEntriesQst,TRUE) THEN
|
||||
```
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
+++
|
||||
title = "FIELDERROR"
|
||||
weight = 590
|
||||
+++
|
||||
Never use FIELDERROR with a period as it is automatically inserted.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
InvalidValue@1025 : TextConst 'ENU=is invalid.';
|
||||
...
|
||||
Cust.FIELDERROR("No.",InvalidValue);
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
InvalidValue@1025 : TextConst 'ENU=is invalid';
|
||||
...
|
||||
Cust.FIELDERROR("No.",InvalidValue);
|
||||
```
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
+++
|
||||
title = "MESSAGE and ERROR"
|
||||
weight = 790
|
||||
+++
|
||||
Always end MESSAGE or ERROR with a period.
|
||||
|
||||
Bad code
|
||||
|
||||
```al
|
||||
CustIsBlockedErr@1025 : TextConst 'ENU=You cannot %1 this type of document when Customer %2 is blocked with type %3';
|
||||
...
|
||||
ERROR(CustIsBlockedErr,...);
|
||||
```
|
||||
|
||||
Good code
|
||||
|
||||
```al
|
||||
CustIsBlockedErr@1025 : TextConst 'ENU=You cannot %1 this type of document when Customer %2 is blocked with type %3.';
|
||||
...
|
||||
ERROR(CustIsBlockedErr,...);
|
||||
```
|
||||
42
content/docs/NAVPatterns/4-get-involved/_index.md
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
+++
|
||||
chapter = true
|
||||
title = "(OLD) Get Involved"
|
||||
weight = 170
|
||||
+++
|
||||
|
||||
**Reminder, this is an ARCHIVE of the Patterns site, this information is not current.**
|
||||
---
|
||||
|
||||
**Spread the info**
|
||||
|
||||
NAV Design Patterns are excellent materials for training and knowledge transfer. In your company, you can help new developer get to speed with NAV by recommending them to read and then present to the team one of the most common patterns: [No. Series][anchor0], [Setup Table][anchor1] and definitely [Hooks ][anchor2]which will be a great investment in reducing your upgrade time. The more experienced developers can read directly the newer patterns, like [Surrogate Key][anchor3], [Easy Update][anchor4], [Totals on Subpages][anchor5], [Using Queries instead of nested loops][anchor6] etc.
|
||||
|
||||
**Become a NAV Design Pattern author**
|
||||
|
||||
You have a pattern that you have used successfully? You have ideas on new patterns? You've found some existing design patterns which are used in the product but nobody (except a few) knows how it works, but you find it worth it to explain it for the NAV C/AL developers out there?
|
||||
|
||||
Send your pattern idea to [Bogdana Botez][anchor7] as a private message on the community webpage. Once your first pattern is ready, we will review it as a team, and when signed off, you get author permissions on the Wiki site and from then on, you can continue publishing and editing existing patterns. However, only publish on the Wiki materials that we had signed-off (we don't have moderation capabilities yet, so we count on each author to only make meaningful/agreed changes).
|
||||
|
||||
You and your company also get credit by being mentioned on the pattern and also on our patterns authors page.
|
||||
|
||||
Once you have the idea, writing it down shouldn't take long. You will be helped by adopting [the template ][anchor8]that we've used.
|
||||
|
||||
**Remember the rules**
|
||||
|
||||
When handling design patterns, content quality is critical. We are trying our best to only publish content that is correct, relevant and has been reviewed by multiple developers. Therefore, we review and sign-off all patterns before publishing them (except for the videos made prior to 2015). All text content found on this Wiki and on the NAV Team Blog has been through one, usually multiple iterations of review. If you find something to correct, please comment on the pattern or contact [Bogdana Botez][anchor9], and we will review and update it.
|
||||
|
||||
We are working on creating a set of rules, which would help keeping the content clean and the project on the correct track. [Find the rules here][anchor10].
|
||||
|
||||
|
||||
|
||||
[anchor0]: /navpatterns/1-patterns/no-series/ "No. Series"
|
||||
[anchor1]: /navpatterns/1-patterns/singleton/singleton-table/setup-table/ "Setup Table"
|
||||
[anchor2]: /navpatterns/1-patterns/hooks/ "Hooks"
|
||||
[anchor3]: /navpatterns/1-patterns/implementation-of-surrogate-keys-using-autoincrement-pattern/
|
||||
[anchor4]: /navpatterns/1-patterns/easy-update-of-setup-or-supplementary-information/
|
||||
[anchor5]: /navpatterns/1-patterns/totals-and-discounts-on-subpages-sales-and-purchases/
|
||||
[anchor6]: /navpatterns/1-patterns/queries/use-queries-to-replace-nested-loops/
|
||||
[anchor7]: /members/bogdana-botez/default.aspx
|
||||
[anchor8]: /navpatterns/4-get-involved/template-for-writing-nav-design-patterns/
|
||||
[anchor9]: /members/bogdana-botez/default.aspx "Bogdana Botez"
|
||||
[anchor10]: /navpatterns/4-get-involved/code-of-conduct/ "Find the rules here"
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
+++
|
||||
title = "Code of Conduct"
|
||||
weight = 330
|
||||
+++
|
||||
Find below the rules to be used when disseminating or relating to the NAV Design Patterns.
|
||||
|
||||
1. Only use materials published in text on the NAV Design Patterns Wiki site. If you received from us, any unpublished materials, please observe that they are subject to change and have not been approved for external use.
|
||||
2. When referencing a NAV Design Pattern, you must remember to also reference its author and company where the author is employed. You will find the author and his/her company at the beginning of each pattern, under the title.
|
||||
3. When referencing a NAV Design Patterns project, you must make it clear that this is a community project, driven by Microsoft, with multiple developers involved from both Microsoft and the community.
|
||||
4. When using published text content of NAV Design Patterns, do not alter the text in any way that was published on the Wiki site, unless is has been reviewed by the patterns team and signed off by someone at Microsoft in writing.
|
||||
5. If you have other materials which have not received explicit signoff in writing from me, where I have specifically stated that they are valid design patterns ready for publishing, please do not name them "NAV Design Patterns" (or anything similar). You are free to use your own content, but do not associate it in any way with NAV Design Patterns unless it is signed off in writing.
|
||||
6. If you do choose to use your own content, you must make it clear that it is not a NAV Design Pattern.
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
+++
|
||||
title = "Patterns Authors"
|
||||
weight = 930
|
||||
+++
|
||||
This is the list of people that have been part of the NAV Design Patterns team. If you would like to join the project follow the instructions provided on [Be a NAV Pattern Author][anchor0] page.
|
||||
|
||||
Project administrator: [Bogdana Botez][anchor1].
|
||||
|
||||
Authors (in alphabetical order):
|
||||
|
||||
* Abshishek Ghosh, Microsoft (4 patterns)
|
||||
* Using Query Objects to Detect Duplicates
|
||||
* Blocked Entity
|
||||
* Single-Record (Setup) Table
|
||||
* Temporary Dataset Report
|
||||
|
||||
* Anders Larsen, Microsoft (1 pattern)
|
||||
|
||||
* Easy Update of Setup or Supplementary Information
|
||||
|
||||
* Andreas Moth, Microsoft (1 pattern)
|
||||
|
||||
* Anti-pattern: Reusable bugs
|
||||
|
||||
* Bogdan Sturzoiu, Microsoft (4 patterns)
|
||||
|
||||
* Feature Localization for Data Structures
|
||||
* Copy Document
|
||||
* Using C/AL Query Objects Instead of Nested Loops
|
||||
* Data-Driven Blocked Entity
|
||||
|
||||
* Bogdana Botez, Microsoft (18 patterns)
|
||||
|
||||
* Silent File Upload and Download
|
||||
* Standard Journal
|
||||
* No. Series
|
||||
* Data Model Proxy
|
||||
* Journal Error Processing
|
||||
* Journal Template-Batch-Line
|
||||
* Multilanguage Application Data
|
||||
* SELECT DISTINCT using Queries
|
||||
* Anti-patterns: reusable bugs
|
||||
* Sensitive Data Encapsulation
|
||||
* Data Encryption
|
||||
* Single Point of Access
|
||||
* Masked Text
|
||||
* SSL in NAV
|
||||
* Singleton
|
||||
* Singleton Codeunit
|
||||
* Anti-Patterns in NAV Upgrade
|
||||
* Cue table
|
||||
|
||||
* Carlos Raul Garcia, Microsoft (1 pattern)
|
||||
|
||||
* Anti-Patterns in NAV Upgrade
|
||||
|
||||
* Ciprian Iordache, Microsoft (2 patterns)
|
||||
|
||||
* Activity Log
|
||||
* Totals and Discounts on Subpages (Sales and Purchases)
|
||||
|
||||
* David Bastide, Microsoft (3 patterns)
|
||||
* Notification Lifecycle Management pattern
|
||||
* Data Migration Façade
|
||||
* Extending the Role Center Headlines
|
||||
|
||||
* Elly Nkya, Microsoft (2 patterns)
|
||||
|
||||
* Singleton Table
|
||||
* Anti-patterns: reusable bugs
|
||||
|
||||
* Eric Wauters (waldo), iFacto, PRS (6 patterns)
|
||||
|
||||
* Hooks
|
||||
* Posting Routine - Select Behaviour
|
||||
* Variant Facade
|
||||
* Argument Table
|
||||
* Discovery Event
|
||||
* Anti-patterns: reusable bugs
|
||||
|
||||
* Gary Winter, agiles (1 pattern)
|
||||
* Variant Façade
|
||||
|
||||
* Henrik Langbak, Kim Ginnerup, Bording Data A/S (2 patterns)
|
||||
|
||||
* Currently Active Record
|
||||
* Released Entity
|
||||
|
||||
* Jan Hoek, IDYN (2 patterns)
|
||||
|
||||
* Conditional Cascading Update
|
||||
* Setup Specificity Fallback
|
||||
|
||||
* Jesper Schulz, Microsoft (1 pattern)
|
||||
|
||||
* Error Message Processing, part I
|
||||
|
||||
* Martin Dam, Microsoft (1 pattern)
|
||||
|
||||
* Multi-File Download
|
||||
|
||||
* Mike Borg Cardona, Microsoft (1 pattern)
|
||||
|
||||
* Creating URLs to NAV Clients
|
||||
|
||||
* Mostafa Balat, Microsoft (3 patterns)
|
||||
|
||||
* .NET Exception Handling
|
||||
* Cached Web Service Calls
|
||||
* Try Method
|
||||
|
||||
* Nikola Kukrika, Microsoft (7 patterns)
|
||||
|
||||
* Totals and Discounts on Subpages (Sales and Purchases)
|
||||
* Create Data from Templates
|
||||
* Argument Table
|
||||
* Instructions in UI
|
||||
* Creating Custom Charts
|
||||
* Variant Façade
|
||||
* Anti-patterns: reusable bugs
|
||||
|
||||
* Nikolai L'Estrange, from TVision Technology Ltd. in the UK_
|
||||
_ (2 patterns)
|
||||
|
||||
* Observer
|
||||
* Cross Session Events
|
||||
|
||||
* Raed Selim, Microsoft (1 pattern)
|
||||
* Product Name
|
||||
|
||||
* Soumya Dutta (2 patterns)
|
||||
* In-context notifications
|
||||
* Data Migration Façade
|
||||
|
||||
* Søren Klemmensen, 360 Visibility, PRS (2 patterns)
|
||||
|
||||
* Master Data
|
||||
* Surrogate Key
|
||||
|
||||
* Tim Grant, Trendmicro (1 pattern)
|
||||
|
||||
* Read Once Setup Record
|
||||
* Report Selector (this pattern was started by 2 workgroups by multiple authors, but finalized and corrected by Tim).
|
||||
|
||||
* Xavier Garonnat, knk Ingénierie (1 pattern)
|
||||
|
||||
* Document
|
||||
|
||||
They have also invested their time and energy in this project:
|
||||
|
||||
* Eva Dupont, Microsoft - our publisher on MSDN NAV Team Blog.
|
||||
* Kurt Juvyns, Microsoft - coordinator of pattern videos
|
||||
|
||||
Pattern evangelists:
|
||||
|
||||
* Eric Wauters (waldo), iFacto, PRS
|
||||
* Gary Winter, agiles, PRS
|
||||
* Mark Brummel, Brummel Dynamics Services, PRS
|
||||
* Mike Doster, Mergetool
|
||||
* Søren Klemmensen, 360 Visibility, PRS
|
||||
|
||||
And last but not least, we have collaborated with Plataan who hired Eric Wauters from ifacto and Mark Brummel from Brummel Dynamics Services and PRS, to publish on video some of our patterns.
|
||||
|
||||
|
||||
|
||||
[anchor0]: /navpatterns/4-get-involved/
|
||||
[anchor1]: /members/bogdana-botez/default.aspx "NAV Design Patterns project administrator"
|
||||
|
After Width: | Height: | Size: 4.4 KiB |
|
|
@ -0,0 +1,59 @@
|
|||
+++
|
||||
title = "Template for writing Nav Design Patterns"
|
||||
weight = 1180
|
||||
+++
|
||||
This is a guideline, some parts are optional (if there's no content, remove the whole paragraph).
|
||||
|
||||
<_Your name here in italics, plus your company name_\>
|
||||
|
||||
## **<<Pattern Title\>\>**
|
||||
|
||||
Short, descriptive and easy to remember.
|
||||
|
||||
## **Pattern Logo**
|
||||
|
||||
Black & white, no text on it.
|
||||
|
||||
[![ ][image0]][anchor0]
|
||||
|
||||
**Context**: Sets the stage where the pattern takes place. 1-2 sentences.
|
||||
|
||||
**Problem**: What happens before this pattern is used? How can it go wrong? 1-5 lines.
|
||||
|
||||
**Forces:** (explain why the problem is difficult to solve; state the considerations that must be taken into account when choosing a solution to a problem)
|
||||
|
||||
* <<**Force 1: **short description (What is the impact of not using this pattern? Or using only partially?) \>\>
|
||||
* <<**Force 2: **short description \>\>
|
||||
* ...
|
||||
|
||||
**Solution:** 1-2 sentences. The full description will come below.
|
||||
|
||||
<<Solution descriptions\>\>
|
||||
|
||||
<<Diagrams. Please add numbers and captions to both figures and tables\>\>
|
||||
|
||||
**Usage**: <<short usage description\>\>
|
||||
|
||||
<<Examples\>\>
|
||||
|
||||
<<Where it's used in Dynamics NAV\>\>
|
||||
|
||||
**Benefits:**
|
||||
|
||||
* **<< Benefit 1: **solves Force 1, short description\>\>
|
||||
* **<< Benefit 2: **solves Force 2, short description\>\>
|
||||
* ...
|
||||
|
||||
**Consequences:**
|
||||
|
||||
* **<<Consequence 1: **are there any drawbacks? Is there anything the developer needs to be aware of when implementing this pattern? Any known limitations? **\>\>**
|
||||
* **...**
|
||||
|
||||
**List of references**
|
||||
|
||||
|
||||
|
||||
[anchor0]: PatternLogo.png
|
||||
|
||||
|
||||
[image0]: PatternLogo.png
|
||||
16
content/docs/NAVPatterns/_index.md
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
+++
|
||||
title = "NAV Patterns Archive"
|
||||
weight = 20
|
||||
+++
|
||||
|
||||
## About the archive
|
||||
|
||||
This section of the site is a careful reproduction of the content of the Original Microsoft Community NAV Design Patterns project, created with permission.
|
||||
|
||||
## Reading the archive
|
||||
|
||||
Bear in mind, many of the style and formatting guidelines in this section have been brought forward into:
|
||||
- The automatic formatting provided by the AL Extension
|
||||
- The Code Analyzers
|
||||
|
||||
Additionally, a variety of topics around the Windows Client and DotNet are outdated, and should only be used for either reference or if working in older environments.
|
||||
12
content/docs/NAVPatterns/patterns/_index.md
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
title: "Patterns"
|
||||
weight: 110
|
||||
tags: ["NAV", "C/AL"]
|
||||
categories: ["Archived Pattern"]
|
||||
description: >
|
||||
Patterns described to be used with Microsoft Dynamics NAV
|
||||
---
|
||||
|
||||
{{% alert title="Warning" color="warning" %}}
|
||||
Please note that these patterns may not be up-to-date with the patterns for AL and Business Central Development.
|
||||
{{% /alert %}}
|
||||
|
After Width: | Height: | Size: 37 KiB |
BIN
content/docs/NAVPatterns/patterns/activity-log/Activity-Log.jpg
Normal file
|
After Width: | Height: | Size: 16 KiB |
116
content/docs/NAVPatterns/patterns/activity-log/index.md
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
+++
|
||||
title = "Activity Logs"
|
||||
weight = 210
|
||||
+++
|
||||
_Originally by Ciprian Iordache at Microsoft Development Center Copenhagen_
|
||||
|
||||
## Activity Log
|
||||
|
||||
**Abstract**
|
||||
|
||||
The Activity Log pattern tracks execution of activities. This is a Dynamics NAV specific implementation of the [Audit Log][anchor0] pattern.
|
||||
|
||||
[![ ][image0]][anchor1]
|
||||
|
||||
**Problem**
|
||||
|
||||
In general, integrating with external systems can be very challenging, due to the complexity of the situation -- connectivity issues, asynchronous operations, user errors, etc. These challenges require sometimes re-trying several times, polling the external system, re-send/re-get data as all these activities can succeed but can very well fail.
|
||||
|
||||
Similar challenges exist in situations where a lengthy, complex task, composed of different steps is to be executed by various people in various timeframes. In case of errors (but sometimes also in case of success) there will be a need to track these activities to see what happened and the actual person which did a specific step.
|
||||
|
||||
In all these cases, we need to be able to troubleshoot.
|
||||
|
||||
A tracking/logging functionality could be implemented for each activity/step separately, but this would lead to code duplication and problems in maintaining the code in future.
|
||||
|
||||
In NAV there is already the Change Log functionality which can record all the data changes that have been done to specific tables, specific fields. However, this functionality is not available for activities. Also, there are few places where separate logging/tracking implementations were done but the current pattern proposes an unified, central way of data recording and enables the user to track all/most of the activities.
|
||||
|
||||
**Solution**
|
||||
|
||||
The Activity Log pattern tracks specific outcome of the activities, in order to be able to assess what went wrong/fine or who performed a specific activity.
|
||||
|
||||
Activity Log pattern
|
||||
|
||||
* records the activity and its outcome (error or success messages)
|
||||
* assembles all messages in one central view and presents them to the user filtered for the specific activity and ordered in reverse chronological order.
|
||||
|
||||
Figure below illustrates the how the Activity Log manifests in the UI. The figure shows a part of an activity log for a posted document that was sent to the document exchange service and illustrates both successful and failed activities.
|
||||
|
||||
[![ ][image1]][anchor2]
|
||||
|
||||
This functionality is implemented in the following way - Activity Log table (TAB710) contains a simple function that allows you to log the result of a task or activity:
|
||||
|
||||
ActivityLog.LogActivity(ContextRecordID,ActivityLog.Status::Failed,ContextDescription,ActivityDescription,ActivityMessage);
|
||||
|
||||
Similar to TAB700 for Error Messaging, the Activity Log table contains a RECORDID that is a link to the parent/context entity. That permits the Activity Log to be used in a generic way, for any kind of entities (tables) and it also permits filtering the data to a specific related entity only before being presenting to the user.
|
||||
|
||||
The following parameters should be provided to the function:
|
||||
|
||||
* RecordID: The record/context for which the activity is logged
|
||||
* Status: The task/activity outcome
|
||||
* Descriptions/Messages: fields that will clearly describe the state and outcome of the task
|
||||
|
||||
To show the log, add a page action, with the caption including the name "<prefix\> Log" and link it to the image named "Log":
|
||||
|
||||
```AL
|
||||
{ ;1 ;Action ;
|
||||
Name=ActivityLog;
|
||||
CaptionML=ENU='Activity Log';
|
||||
ToolTipML=ENU='View the status and any errors if the document was sent as an electronic document or OCR file through the document exchange service.';
|
||||
ApplicationArea=#Basic,#Suite;
|
||||
Image=Log;
|
||||
OnAction=
|
||||
VAR
|
||||
ActivityLog@1000 : Record 710;
|
||||
BEGIN
|
||||
ActivityLog.ShowEntries(RECORDID);
|
||||
END;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
**NAV usages**
|
||||
|
||||
In Dynamics NAV 2016, there is a new feature for sending documents in electronic format to a document exchange service. In this case, sending documents requires multiple steps as it is an asynchronous activity and as such, in order to keep track of what's happening and when the Activity Log functionality was used. That offers later the possibility to see who sent and when a document was sent, when it was dispatched, if any dispatch errors and how many tries have been made until the document was finally dispatched or rejected.
|
||||
|
||||
So as usages in NAV 2016, we have the document exchange and OCR features plus the related posted documents involved in the document exchange feature.
|
||||
|
||||
* COD1294.TXT
|
||||
* COD1410.TXT
|
||||
* PAG1270.TXT
|
||||
* PAG1275.TXT
|
||||
* PAG143.TXT
|
||||
* PAG144.TXT
|
||||
* PAG189.TXT
|
||||
* TAB112.TXT
|
||||
* TAB114.TXT
|
||||
* TAB130.TXT
|
||||
|
||||
**Ideas for improvement**
|
||||
|
||||
Replace the scattered similar functionality (as mentioned above, we have several places having close functionality or similar requirements) with this new pattern.
|
||||
|
||||
**Consequences**
|
||||
|
||||
* Use with caution, similar to the Change Log functionality, as if the pattern will be used extensively in all the activities/operations within NAV, the table might become large containing many records and might cause some performance issues when presenting the data to the client (filtering on the specific activity).
|
||||
* Do not log private or confidential information (passwords, amounts, salaries, sensitive data), unless you are ok with this data to be showed to all users (even to users which normally would not have access to this data), thus overriding the permission sets.
|
||||
* Log only essential information (quality over quantity). Can the logged data be used to analyze the problem, or is it just junk data?
|
||||
|
||||
**NAV Versions**
|
||||
|
||||
Supported from NAV 2016
|
||||
|
||||
**Related Topics**
|
||||
|
||||
Error Message Processing -- provides a similar view and uses similar concepts: has a generic implementation (uses as link the same RECORDID feature) and uses same filtering functionality when displaying the data to the user.
|
||||
|
||||
Audit Log -- as mentioned in the beginning, this pattern is a NAV specific implementation of the audit log pattern.
|
||||
|
||||
|
||||
|
||||
[anchor0]: http://martinfowler.com/eaaDev/AuditLog.html
|
||||
[anchor1]: Activity-Log.jpg
|
||||
[anchor2]: Activity-Log-NAV.jpg
|
||||
|
||||
|
||||
[image0]: Activity-Log.jpg
|
||||
[image1]: Activity-Log-NAV.jpg
|
||||
|
After Width: | Height: | Size: 32 KiB |
123
content/docs/NAVPatterns/patterns/argument-table/index.md
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
+++
|
||||
title = "Argument Table"
|
||||
weight = 220
|
||||
+++
|
||||
_Originally By Nikola Kukrika and waldo_
|
||||
|
||||
### Abstract
|
||||
|
||||
The Argument Table pattern is used to provide an extension point for adding new arguments without changing the signature. By grouping multiple arguments into a table the code becomes more readable (function signature and the usage of the function).
|
||||
|
||||
[![ ][image0]][anchor0]
|
||||
|
||||
### Problem
|
||||
|
||||
In CAL overloading function signature is not supported. It is also not possible to provide default values for the function arguments.
|
||||
|
||||
When an argument needs to be added to the function, the existing function needs to be extracted to a new method with an additional argument and the original function will call new method. This will cause an upgrade problem in the future, since the entire body of the method is replaced.
|
||||
|
||||
Second commonly occurring problem is option duplication. In order to pass options often they are duplicated in the signature.
|
||||
|
||||
The last problem that can be solved is high number of arguments. Functions with a high number of arguments are hard to understand. Having arguments grouped within the table with a meaningful name will improve readability and make code easier to understanding.
|
||||
|
||||
Few examples of the bad implementations are as illustrated here:
|
||||
|
||||
#### Bad example 1
|
||||
|
||||
|
||||
```AL
|
||||
PROCEDURE FillInVATReturnData@1200001(VAR DeclarationID@1200000 : Code [20];VAR LineID@1200001 : Code [20];VAR PeerID@1200002 : Code [20]; VAR DocumentNo@1200003: Code[20]; VAR NumberOfCopies@1200007: Integer; VAR Uploaded@1200004 : Boolean; VAR Correction@1200005 : Boolean; VAR HasValidationErr@1200006 : Boolean);
|
||||
```
|
||||
|
||||
**Call**
|
||||
|
||||
|
||||
```AL
|
||||
FillInVATReturnData(NoSeries, NextLineID, CustomerID, DocumentNo, SingleCopy, ???, ??, ...., ...)
|
||||
```
|
||||
|
||||
In this example the code is hard to read and understand. Adding an additional argument will require refactoring of the existing function. Each time a new argument is added a new function will be created.
|
||||
|
||||
#### Bad example 2
|
||||
|
||||
```AL
|
||||
LOCAL PROCEDURE GetTableSyncSetupW1@3(OldTableId@1002 : Integer; VAR UpgradeTableId@1001 : Integer; VAR TableUpgradeMode@1000 : 'Check, Copy, Move, Force') : Boolean;
|
||||
BEGIN
|
||||
CASE OldTableId OF
|
||||
DATABASE::"Sales Header":
|
||||
SetTableSyncSetup(0,TableUpgradeMode::Check,UpgradeTableId,TableUpgradeMode);
|
||||
DATABASE::"Posting Exch. Column Def":
|
||||
SetTableSyncSetup(104025,TableUpgradeMode::Copy,UpgradeTableId,TableUpgradeMode);
|
||||
DATABASE::"Payment Export Data":
|
||||
SetTableSyncSetup(0,TableUpgradeMode::Force,UpgradeTableId,TableUpgradeMode);
|
||||
ELSE
|
||||
EXIT(FALSE);
|
||||
END;
|
||||
EXIT(TRUE);
|
||||
END;
|
||||
```
|
||||
|
||||
In this example each time a new argument is added all function calls will have to be updated. Option is duplicated in the signature, which will cause issues if a new option is defined or the existing options are renamed.
|
||||
|
||||
### Solution
|
||||
|
||||
By grouping the arguments within the table it is possible to add additional argument and reuse it where it is needed without changing the signature.
|
||||
|
||||
Multiple parameters are grouped within the single object with a meaningful name so the code becomes more readable.
|
||||
|
||||
It is possible to assign default values and to have the code validation.
|
||||
|
||||
Argument table should preferably be a temporary table since the implementation is simpler.
|
||||
|
||||
The examples of usages addressing problems shown above are:
|
||||
|
||||
#### Good example 1
|
||||
|
||||
New table
|
||||
```AL
|
||||
TAB 50003 VAT Return Data
|
||||
PROCEDURE FillInVATReturnData@1200001(VAR VATReturnData@1200000 : Record 50003);
|
||||
|
||||
VATReturnData.INIT;
|
||||
VATReturnData.NumberOfCopies := GetDefaultNumberOfCopies;
|
||||
VATReturnData.Uploaded := FALSE;
|
||||
|
||||
FillInVATReturnData(VATReturnData);
|
||||
```
|
||||
|
||||
By introducing an argument table, code is much more readable since there is a single argument for a function. It is easy to see which arguments are passed in and which are modified in a function.
|
||||
|
||||
#### Good example 2
|
||||
|
||||
Good example
|
||||
```AL
|
||||
PROCEDURE GetTableSyncSetupW1@3(VAR TableSynchSetup@1000 : Record 2000000135);
|
||||
BEGIN
|
||||
SetTableSyncSetup(DATABASE::"Sales Header",0,TableSynchSetup.Mode::Check);
|
||||
SetTableSyncSetup(DATABASE::"Posting Exch. Column Def",104025,TableSynchSetup.Mode::Copy);
|
||||
SetTableSyncSetup(DATABASE::"Payment Export Data",0,TableSynchSetup.Mode::Force);
|
||||
END;
|
||||
```
|
||||
|
||||
Option definition is not encapsulated within the table. Arguments are grouped and we can add additional arguments without the need to change the signature.
|
||||
|
||||
### Downsides
|
||||
|
||||
You need to create one more table
|
||||
|
||||
Complex types can't be embedded as fields in tables (cannot have a record field type etc).
|
||||
|
||||
### NAV Usages
|
||||
|
||||
Upgrade Codeunits
|
||||
|
||||
### Related Patterns
|
||||
|
||||
Posting Routine, Select behavior: Setting fields on existing records in order not to change the signatures.
|
||||
|
||||
|
||||
|
||||
[anchor0]: 0218.Argument-Table-image.png
|
||||
|
||||
|
||||
[image0]: 0218.Argument-Table-image.png
|
||||
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 107 KiB |
104
content/docs/NAVPatterns/patterns/blocked-entity/_index.md
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
+++
|
||||
title = "Blocked Entity"
|
||||
weight = 270
|
||||
+++
|
||||
_Originally by Abhishek Ghosh at Microsoft Development Center Copenhagen_
|
||||
|
||||
## Abstract
|
||||
|
||||
The Blocked Entity is used when it is required to stop transactions for an entity (mostly master data), temporarily or permanently.
|
||||
|
||||
[![ ][image0]][anchor0]
|
||||
|
||||
## Description
|
||||
|
||||
To block entities through metadata, read this pattern. To do the same thing through data, read [Data Driven Blocked Entity pattern][anchor1].
|
||||
|
||||
The business entity holds a state that controls if a given transaction is allowed. The state is used by the logic controlling transactions. The change of state could either be temporary or permanent.
|
||||
|
||||
An example of a temporary halt is when a retail chain selling items has received lot of complaints about an item, and the company wants to stop all transactions, both purchase and sale, with that item until the dealer has clarified the issue with his supplier and possibly received a replacement for the defective stock. Another common example is during counting the physical inventory using cycle counting where the counting is done in one section of a warehouse at a time, so that the regular operations can continue in the other parts of the warehouse. In these situations, it is necessary to block all transactions, such as picks and put-aways, for a bin while warehouse counting is in progress for that bin.
|
||||
|
||||
In contrast, a permanent halt to transactions could be required when an item has become obsolete (or is about to become obsolete), and the company wants to stop further purchase or sale of the item. However, the company wants to maintain the transaction history of the item and, therefore, does not want to delete the item record.
|
||||
|
||||
A simple design implementation of such requirements in Microsoft Dynamics NAV is to add a Blocked field in the entity table (and on the associated page). The implementation takes this state into the logic and checks for the value of this field in related transactions. For most simple scenarios, it is sufficient to have two states on the Blocked field, specifying whether it is allowed to perform transactions for the entity or not.
|
||||
|
||||
In certain situations, however, there could be different levels of blocking. For example, the company could block all sales to a customer that has overdue payments, and the company does not want to allow transactions with this customer until the payments are received. In other situations, the customer may have raised objections about an invoice, and the company has decided not to generate new invoices for the customer until the issue has been resolved. However, the company does want to continue shipping goods to the customer so as not to impact the customer's operations. In these scenarios, it may be necessary to have multiple states on the Blocked field depending on the level of restriction that is needed.
|
||||
|
||||
## Usage
|
||||
|
||||
As mentioned in the previous section, there are two implementations depending on business requirements: The 2-state Boolean field for simple implementations and the multi-state option field for more complex requirements. The implementation flow is similar for both patterns, except how the validation is implemented. The following discusses the two scenarios one by one.
|
||||
|
||||
### Boolean Implementation
|
||||
|
||||
Add a Boolean field named Blocked in the table.
|
||||
|
||||
In the relevant logic, add a condition to check the status of the Blocked flag. The cheapest way is to use a TESTFIELD:
|
||||
|
||||
```AL
|
||||
<rec variable\>.TESTFIELD(Blocked,FALSE);
|
||||
```
|
||||
|
||||
Alternatively, you can throw a custom error message. However, you should only do that if the default error message thrown by TESTFIELD is not sufficient.
|
||||
|
||||
### Option-Field Implementation
|
||||
|
||||
Add an option field named Blocked in the table. The option values will reflect the different blocked states required by the company.
|
||||
|
||||
Add this field on the card page (or on the List page if the entity does not have a card). As with the Boolean implementation, the convention is to add this field in the right-hand column in the General FastTab of the card page.
|
||||
|
||||
Implement a function in the table that takes the transaction context as input and evaluates the Blocked field to decide whether the transaction should be allowed or not. Optionally, the function can be responsible for notifying the user and bubble up an error message straight away.
|
||||
|
||||
Note: the option field assumes that only one of the multiple options can be active at a time. In other words, the options should be mutually exclusive.
|
||||
|
||||
How not to use the option field in this case: if we want to block an item from sale and/or purchase, the 4 combined options would be **Block none** | **Block Sales** | **Block Purchases** | **Block Sales and Purchases**. This doesn't scale, because if now we need to block another transaction, the number of option would grow too fast. In this situations, it is better to use two Boolean fields: **Blocked Sale**: **true|false** and **Blocked Purchase: true|false**.
|
||||
|
||||
A good example of usage would be for varying the behavior depending on the chosen option, for example by displaying a different error message depending on the reason an Item is blocked. In this case we can have the item **Not Blocked** | **Blocked due to defect** | **Blocked waiting for approval**, etc.
|
||||
|
||||
## NAV Specific Example
|
||||
|
||||
### Boolean Implementation
|
||||
|
||||
[![ ][image1]][anchor2]
|
||||
|
||||
An example of the Boolean implementation on the Item card.
|
||||
|
||||
In codeunit 22 -- Item Jnl.-Post Line, the following lines of code have implemented a check based on the value of the Blocked field:
|
||||
|
||||
```AL
|
||||
IF NOT CalledFromAdjustment THEN
|
||||
Item.TESTFIELD(Blocked,FALSE);
|
||||
```
|
||||
### Option-Field Implementation
|
||||
|
||||
[![ ][image2]][anchor3]
|
||||
|
||||
An example of the option field implementation on the Customer card.
|
||||
|
||||
The CheckBlockedCustOnDocs and CheckBlockedCustOnJnls functions in the Customer table are responsible for validating the Blocked state with respect to the input document type. These functions are invoked in several areas, such as posting routines, where a status check on the Blocked field is required. This is a good practice where the Blocked implementation gets more complex, as this encourages reuse and ensures uniformity of implementation.
|
||||
|
||||
## NAV Usages
|
||||
|
||||
Entities where the Blocked Entity has been implemented include:
|
||||
|
||||
* Item
|
||||
* G/L Account
|
||||
* Customer
|
||||
* Vendor
|
||||
* Bin
|
||||
|
||||
## Related Topics
|
||||
|
||||
The [Released Entity][anchor4].
|
||||
|
||||
{{< youtube O2R-fTSup1o >}}
|
||||
|
||||
[anchor0]: 2260.BlockedEntityPattern.png
|
||||
[anchor1]: /navpatterns/1-patterns/blocked-entity/data-driven-blocked-entity/
|
||||
[anchor2]: 8637.BlockedEntityPattern_5F00_5F00_5F00_Boolean.png
|
||||
[anchor3]: 3056.BlockedEntityPattern_5F00_5F00_5F00_Option.png
|
||||
[anchor4]: /navpatterns/1-patterns/released-entity/
|
||||
|
||||
|
||||
[image0]: 2260.BlockedEntityPattern.png
|
||||
[image1]: 8637.BlockedEntityPattern_5F00_5F00_5F00_Boolean.png
|
||||
[image2]: 3056.BlockedEntityPattern_5F00_5F00_5F00_Option.png
|
||||
|
After Width: | Height: | Size: 15 KiB |
|
|
@ -0,0 +1,111 @@
|
|||
+++
|
||||
title = "Data Driven Blocked Entity"
|
||||
weight = 470
|
||||
+++
|
||||
_Written by Bogdan Andrei Sturzoiu, at Microsoft Development Center Copenhagen_
|
||||
|
||||
## Abstract
|
||||
|
||||
This pattern implements a generic mechanism for dynamically restricting and allowing usage of a record by the business process administrator.
|
||||
|
||||
## Problem
|
||||
|
||||
A NAV record can be used in a number of functionalities across the app. There are situations, however, when the administrator wants to restrict the consumption of such a record, as well as lift the restriction when it is no longer relevant.
|
||||
|
||||
For example, a new customer record should not be used for posting documents until it is approved by the relevant approver.
|
||||
|
||||
We could solve this by using the [Blocked Entity pattern][anchor1], but it requires database schema changes, which have an upgrade impact.
|
||||
|
||||
The blocked entity pattern involves:
|
||||
|
||||
1. Adding a "blocked" status field on the record (either a Boolean or in the more advanced cases, an option field refining the usage).
|
||||
2. Adding specific code for the record in every place where the restriction needs to be enforced.
|
||||
|
||||
In contrast, the Data-driven Blocked Entity pattern involves adding a new record (data change) to mark the restriction, instead of adding a new field (metadata change).
|
||||
|
||||
## Solution
|
||||
|
||||
This pattern describes a generic mechanism of adding and lifting restrictions for any type of record.
|
||||
|
||||
The restriction mechanism has the following elements:
|
||||
|
||||
1. Adding a restriction record for a specific reason (e.g. the record requires approval), which will act as a surrogate key (unique identifier) for the restricted record. This can be implemented through a workflow response, or directly, by calling the Restriction Management codeunit function.
|
||||
2. Lifting the restriction when it is no longer necessary. Again, this can be done using a workflow response or directly by calling the dedicated function.
|
||||
3. Consuming the restriction in the places of interest for a specific purpose. This is an application feature that requires a call to the Restriction Management codeunit to check for restrictions.
|
||||
|
||||
Currently, the restrictions are record-based and type-less. They act as simple tokens, and they have:
|
||||
|
||||
* A reason (e.g. the record requires approval)
|
||||
* A purpose (e.g. the record cannot be posted).
|
||||
|
||||
You must make sure to differentiate between the reason and the purpose. That is because the restriction can only be added once per record, but consumed in multiple places.
|
||||
|
||||
|
||||
|
||||
## Example
|
||||
|
||||
For example, we want to restrict posting Gen. Journal Lines if a customer has not been added in Account No. field.
|
||||
|
||||
For this, the following components are needed:
|
||||
|
||||
1. When a Gen. Journal Line is inserted, call RestrictRecordUsage in COD1550, either directly in the trigger or using an event subscriber.
|
||||
2. When you validate a Customer No. as Account no. and Customer as Account Type, lift the restrictions by calling AllowRecordUsage in COD1550\.
|
||||
3. The consumption of the restriction at posting is already implemented as an event in TAB81, OnCheckGenJournalLinePostRestrictions. No further action necessary.
|
||||
|
||||
## NAV Usage
|
||||
|
||||
All the approval workflows include a response that restricts usage of a record, and then, at the end of an approval loop, a response that allows the usage again by lifting the restriction. See responses "Add record restriction" and "Remove record restriction" implemented in COD1521\.[
|
||||
][anchor2]
|
||||
|
||||
The code behind the "Add record restriction" workflow response:
|
||||
|
||||
```AL
|
||||
RecRef.GETTABLE(Variant);
|
||||
Workflow.GET(WorkflowStepInstance."Workflow Code");
|
||||
RecordRestrictionMgt.RestrictRecordUsage(RecRef.RECORDID,STRSUBSTNO(RestrictUsageDetailsTxt,Workflow.Code,Workflow.Description));
|
||||
```
|
||||
|
||||
The code behind the "Remove record restriction" response:
|
||||
|
||||
```AL
|
||||
RecRef.GETTABLE(Variant);
|
||||
CASE RecRef.NUMBER OF
|
||||
DATABASE::"Approval Entry":
|
||||
BEGIN
|
||||
RecordRestrictionMgt.AllowRecordUsage(RecRef.RECORDID);
|
||||
RecRef.SETTABLE(ApprovalEntry);
|
||||
RecRef.GET(ApprovalEntry."Record ID to Approve");
|
||||
AllowRecordUsage(RecRef);
|
||||
END;
|
||||
DATABASE::"Gen. Journal Batch":
|
||||
BEGIN
|
||||
RecRef.SETTABLE(GenJournalBatch);
|
||||
RecordRestrictionMgt.AllowGenJournalBatchUsage(GenJournalBatch);
|
||||
END
|
||||
ELSE
|
||||
RecordRestrictionMgt.AllowRecordUsage(RecRef.RECORDID);
|
||||
END;
|
||||
```
|
||||
|
||||
Notice how lifting a restriction for a Gen. Journal Batch involves lifting all the restrictions for the individual journal lines in the batch (hence the special branching of the code).
|
||||
|
||||
|
||||
|
||||
## Consequences
|
||||
|
||||
Currently, there can only be one restriction per record. There are no restriction types.
|
||||
|
||||
In the future, a type field should be added to the restriction table, to allow adding restrictions for different purposes, and to refine their consumption. For example, a posting restriction might only be enforced for restrictions originating from approvals.
|
||||
|
||||
## NAV Versions
|
||||
|
||||
This pattern has been introduced in Dynamics NAV 2016\.
|
||||
|
||||
|
||||
|
||||
[anchor0]: attention.jpg
|
||||
[anchor1]: /navpatterns/1-patterns/blocked-entity/
|
||||
[anchor2]: https://microsoft.sharepoint.com/teams/DynamicsNAV/Wiki/Nav%20Wiki%20Documents/NAV%20App%20Patterns/NAV%20App%20Patterns%20for%20Review/Data-Driven%20Blocked%20Entity.docx#_msocom_2
|
||||
|
||||
|
||||
[image0]: attention.jpg
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
|
@ -0,0 +1,118 @@
|
|||
+++
|
||||
title = "Cached Web Server Calls"
|
||||
weight = 290
|
||||
+++
|
||||
_Originally by Mostafa Balat, Microsoft Development Center Copenhagen_
|
||||
|
||||
## Abstract
|
||||
|
||||
In a service-oriented deployment, web services are used to extend NAV's functionality and reach. Depending on how volatile this data is and the corresponding usage scheme, it is expected to be up-to-date within a pre-defined period of time (e.g. once a day).
|
||||
|
||||
## Description
|
||||
|
||||
When NAV is integrated with external services, then the user scenarios become dependent on the data and functions offered by such services. Eventually, there are different approaches through which the external data can be retrieved, stored and used.
|
||||
|
||||
* Dynamic: either exposed by the external service itself or by a separate catalog that NAV can query.
|
||||
* Advantage: data is always up-to-date
|
||||
* Disadvantage: it requires constant connection to the data source.
|
||||
* Static: hard-coded in the database for the user to benefit from.
|
||||
* Advantage: data is promptly available when needed.
|
||||
* Disadvantage: if data changes at some point, it will require a maintenance effort, which exposes the business process to a risk of failure.
|
||||
* Cached: offered through an external service and gets pulled according to a pre-defined refresh rate or manually.
|
||||
* Advantage: data is 'up-to-date' within the rules acceptable by the business process, without extra load on the network resources or the external service.
|
||||
* Disadvantage: if data changes while the auto-refresh did not happen yet, the user may not have access to the latest data; however, the user can manually force a refresh of the data, if asked to do so.
|
||||
|
||||
### When to Use It
|
||||
|
||||
Offer data in lookups that were cached from an external service.
|
||||
|
||||
### Diagram
|
||||
|
||||
[![ ][image0]][anchor0]
|
||||
|
||||
## Usage
|
||||
|
||||
Set up an NAV feature to consume the data from an external service. Refresh the data on a pre-defined refresh rate (e.g. once a day) or when enforced by a power user or an admin. Cache the data in a table and offer it in lookups, as applicable.
|
||||
|
||||
## NAV Specific Example
|
||||
|
||||
### Overview
|
||||
|
||||
**PAG1259 Bank Name - Data Conv. List** offers the required functionality to refresh and display the list of bank names needed to specify which file format to convert to. The page can be accessed from **PAG1260 Bank Data Conv. Service Setup** to display all available bank names. It is also used as a lookup on **PAG370 Bank Account Card**, where it offers a filtered view of the cached bank names based on the Country/Region Code field.
|
||||
|
||||
If **PAG1259 Bank Name - Data Conv. List** is being open and the cached data is 'old', it refreshes the cache. The cached data is stored in **TAB1259 Bank Data Conv. Bank**. Meanwhile, the user has the chance to refresh the data using
|
||||
|
||||
### Code Sample
|
||||
|
||||
```AL
|
||||
OnInit=BEGIN
|
||||
ShortTimeout := 5000;
|
||||
LongTimeout := 30000;
|
||||
END;
|
||||
|
||||
OnOpenPage=VAR
|
||||
BankDataConvBank@1002 : Record 1259;
|
||||
ImpBankListExtDataHndl@1000 : Codeunit 1289;
|
||||
CountryRegionCode@1004 : Text;
|
||||
HideErrors@1003 : Boolean;
|
||||
BEGIN
|
||||
CountryRegionCode := IdentifyCountryRegionCode(Rec,GETFILTER("Country/Region Code"));
|
||||
IF BankDataConvBank.ISEMPTY THEN BEGIN
|
||||
ImpBankListExtDataHndl.GetBankListFromConversionService(HideErrors,CountryRegionCode,ShortTimeout);
|
||||
EXIT;
|
||||
END;
|
||||
RefreshBankNamesOlderThanToday(CountryRegionCode,HideErrors,ShortTimeout);
|
||||
END;
|
||||
|
||||
OnAction=VAR
|
||||
ImpBankListExtDataHndl@1000 : Codeunit 1289;
|
||||
FilterNotUsed@1001 : Text;
|
||||
ShowErrors@1003 : Boolean;
|
||||
BEGIN
|
||||
ShowErrors := TRUE;
|
||||
ImpBankListExtDataHndl.GetBankListFromConversionService(ShowErrors,FilterNotUsed,LongTimeout);
|
||||
END;
|
||||
|
||||
LOCAL PROCEDURE IdentifyCountryRegionCode@1(VAR BankDataConvBank@1002 : Record 1259;Filter@1000 : Text) : Text;
|
||||
VAR
|
||||
CompanyInformation@1001 : Record 79;
|
||||
BlankFilter@1003 : Text;
|
||||
BEGIN
|
||||
BlankFilter := '''''';
|
||||
|
||||
IF Filter = BlankFilter THEN BEGIN
|
||||
CompanyInformation.GET;
|
||||
BankDataConvBank.SETFILTER("Country/Region Code",CompanyInformation."Country/Region Code");
|
||||
EXIT(BankDataConvBank.GETFILTER("Country/Region Code"));
|
||||
END;
|
||||
|
||||
EXIT(Filter);
|
||||
END;
|
||||
|
||||
LOCAL PROCEDURE RefreshBankNamesOlderThanToday@5(CountryRegionCode@1000 : Text;ShowErrors@1002 : Boolean;Timeout@1004 : Integer);
|
||||
VAR
|
||||
BankDataConvBank@1001 : Record 1259;
|
||||
ImpBankListExtDataHndl@1003 : Codeunit 1289;
|
||||
BEGIN
|
||||
IF CountryRegionCode <> '' THEN
|
||||
BankDataConvBank.SETFILTER("Country/Region Code",CountryRegionCode);
|
||||
BankDataConvBank.SETFILTER("Last Update Date",'<%1',TODAY);
|
||||
IF BankDataConvBank.FINDFIRST THEN
|
||||
ImpBankListExtDataHndl.GetBankListFromConversionService(ShowErrors,CountryRegionCode,Timeout);
|
||||
END;
|
||||
```
|
||||
|
||||
## NAV Usages
|
||||
|
||||
Bank name lookup on the Bank Account card for dynamically identifying the format to use to generate a bank-specific payment file.
|
||||
|
||||
## Ideas for Improvement
|
||||
|
||||
Expose the refresh rate through a setup table to make it easily configurable without changing the code.
|
||||
|
||||
|
||||
|
||||
[anchor0]: Cached_5F00_Web_5F00_Service_5F00_Calls_5F00_Diagram.png
|
||||
|
||||
|
||||
[image0]: Cached_5F00_Web_5F00_Service_5F00_Calls_5F00_Diagram.png
|
||||
|
After Width: | Height: | Size: 31 KiB |
|
|
@ -0,0 +1,38 @@
|
|||
+++
|
||||
title = "Conditional Cascading Update"
|
||||
weight = 370
|
||||
+++
|
||||
_Originally by Jan Hoek at IDYN_
|
||||
|
||||
## Abstract
|
||||
|
||||
The Conditional Cascading Update pattern is used to intelligently populate fields whose values depend on other field values. In this pattern description, the field triggering the update will be called "source field", and the depending field will be called "target field".
|
||||
|
||||
## Description
|
||||
|
||||
The value of one table field sometimes depends on the value of another field, typically following an application-defined transformation (note that we're talking about transformations of field values here. This has nothing to do with e.g. form transformation), such as conversion to uppercase, removal of certain characters etc.
|
||||
|
||||
If the target field is non-editable, said transformation is usually the only way for the target field to receive new values, so no irreproducible information can be lost. However, if the target field is editable, the user may have cared enough to override the default (transformed) value, in which case revalidating the source field should not blindly replace the target field's value.
|
||||
|
||||
## Usage
|
||||
|
||||
In the OnValidate trigger of the source field, test if the target field value is either blank, or equal to the transformed value of the source field's previous contents. If it is, populate the target field's value with the transformed source field value. If it is not, do nothing (effectively preserving the value set by the user).
|
||||
|
||||
## NAV Specific Example
|
||||
|
||||
In the base application, this pattern can be found in Search Name/Search Description fields, which are updated with the uppercase value from the corresponding Name/Description field when the latter is validated, only if the Search Name/Description in question is currently blank, or equal to the (uppercase equivalent) of the previous contents of the Name/Description field.
|
||||
|
||||
[![ ][image0]][anchor0]
|
||||
|
||||
In this particular case, the transformation between source and target fields is implicit and due to the different data types of the fields (text vs. code). Note how the field triggers of the Search Name field itself do not contain any logic linked to this pattern.
|
||||
|
||||
## Consequences
|
||||
|
||||
There is a case when this pattern should not be used. If the target field is non-editable, this pattern will not add any value, since there won't be any user-overridden values to protect.
|
||||
|
||||
|
||||
|
||||
[anchor0]: 3124.T18_5F00_Name_5F00_OnValidate.png
|
||||
|
||||
|
||||
[image0]: 3124.T18_5F00_Name_5F00_OnValidate.png
|
||||
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 153 KiB |
119
content/docs/NAVPatterns/patterns/copy-document/index.md
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
+++
|
||||
title = "Copy Document"
|
||||
weight = 390
|
||||
+++
|
||||
_By Bogdan Sturzoiu at Microsoft Development Center Copenhagen_
|
||||
|
||||
## Abstract
|
||||
|
||||
The goal of the Copy Document pattern is to create a replica of an existing open or closed document (posted or not posted), by moving the lines and, optionally, the header information from the source document to a destination document.
|
||||
|
||||
## Description
|
||||
|
||||
Documents are widely used by most of our customers. Many times, a significant portion of these documents are similar to each other, either by sharing the same customer, vendor, type, or line structure. Being able to re-use a document as a base for creating a new one is therefore an important means of saving time.
|
||||
|
||||
Other business scenarios require that a newly created document is applied to an existing document. For example, in returns management, a return order can be the reversal of an existing order and can therefore be copied from the original order. Other times, there is even a legal requirement to match the document to its source. For example, credit memos need to be applied to the originating Invoice.
|
||||
|
||||
For these reasons, NAV supports the copying of documents as a method to re-use or link documents.
|
||||
|
||||
The Copy Document functionality is used in the following situations:
|
||||
|
||||
* The user wants to create a new open sales document (Quote, Order, Blanket Order, Invoice, Return Order, Credit Memo) based on an existing posted or non-posted sales document (Quote, Blanket Order, Order, Invoice, Return Order, Credit Memo, Posted Shipment, Posted Invoice, Posted Return Receipt, Posted Credit Memo).
|
||||
* The user wants to create a new open purchase document (Quote, Order, Blanket Order, Invoice, Return Order, Credit Memo) based on an existing posted or non-posted purchase document (Quote, Blanket Order, Order, Invoice, Return Order, Credit Memo, Posted Shipment, Posted Invoice, Posted Return Receipt, Posted Credit Memo).
|
||||
* The user wants to create a new production order (Simulated, Planned, Firm Planned or Released) based on an existing production order (Simulated, Planned, Firm Planned, Released or Finished).
|
||||
* The user wants to create a new assembly order based on an existing assembly document (Quote, Blanket Order, Order and Posted Order).
|
||||
* The user wants to create a new service contract or quote based on an existing service contract or quote.
|
||||
* The user wants to create all relevant return-related documents. For example, from a sales return order, the user can recreate the involved supply chain documentation, by copying the information upwards to a purchase return order (if the items need to be returned to the vendor), purchase order (if the items need to be reordered), and sales order (if the items need to be re-sent to the customer).
|
||||
|
||||
**Note**
|
||||
|
||||
* Not all to and from combinations are allowed. For example, you can only copy to open document types, since the posted documents are not editable.
|
||||
* The destination document needs to have the header fully created. For example, a Sales Order will need to have the Sell-To Customer No. populated.
|
||||
|
||||
## Usage
|
||||
|
||||
The Dynamics NAV application developer can take into account using the Copy Document design pattern when they have requirements such as:
|
||||
|
||||
* To provide a quick and efficient way of moving content from a document to another.
|
||||
* To allow reusing the document history as a template for new documents.
|
||||
* To allow linking of documents that need to be applied to each other.
|
||||
|
||||
The Copy Document pattern involves the following entities:
|
||||
|
||||
1. Source document tables for document header and line. For example,Sales Header/Line.
|
||||
2. Destination document tables for document header and line.
|
||||
**Note:** The source document header/line and destination document header/line tables do not need to be the same. For example, you can copy a Sales Shipment Header/Lines into a Sales Header/Lines.
|
||||
|
||||
3. Copy Document engine: COD6620, Copy Document Mgt.
|
||||
4. Copy Document report for a specific document type. The report requires the following parameters:
|
||||
* Source Document Type
|
||||
* Source Document No.
|
||||
* Include Header (optional)
|
||||
* Recalculate Lines (optional)
|
||||
|
||||
Example: REP901, Copy Assembly Document
|
||||
|
||||
[![ ][image0]][anchor0]
|
||||
|
||||
## Usage Sequence
|
||||
|
||||
**Precondition**: The user creates a new destination document Header, filling up the required information.
|
||||
|
||||
**Step 1**: The user runs the Copy Document report (element no. 4), filling up the parameters:
|
||||
|
||||
* Source Document Type
|
||||
* Source Document No.
|
||||
* Include Header and/or Recalculate Lines (not all Copy Document reports have these).
|
||||
|
||||
**Step 2**: The report copies the information in the source tables (Header and Line) into the destination tables (Header and Line).
|
||||
|
||||
**Post processing**: The user performs additional editing of the destination document.
|
||||
|
||||
The sequence flow of the pattern is described in the following diagram.
|
||||
|
||||
[![ ][image1]][anchor1]
|
||||
|
||||
Example: Copy Sales Document for Credit Memos.
|
||||
|
||||
In the standard version of Microsoft Dynamics NAV, the Copy Document functionality is implemented in the Sales Credit Memo window as shown in the following section.
|
||||
|
||||
****
|
||||
|
||||
**Precondition**: The user enters data in PAGE44, Sales Credit Memo.
|
||||
|
||||
[![ ][image2]][anchor2]
|
||||
|
||||
**Step 1**: The user runs REP292, Copy Sales Document from the Sales Credit Memo window, populating the required parameters. The Include Header and Recalculate Lines fields are selected.
|
||||
|
||||
[![ ][image3]][anchor3]
|
||||
|
||||
**Step 2**: The Sales Credit Memo window is populated with information from the source sales document.
|
||||
|
||||
[![ ][image4]][anchor4]
|
||||
|
||||
**Post processing**: The user can now do additional editing of the sales credit memo.
|
||||
|
||||
# NAV Implementations
|
||||
|
||||
1. Copy Sales Document (REP292)
|
||||
2. Copy Purchase Document (REP492)
|
||||
3. Copy Service Document (REP5979)
|
||||
4. Copy Assembly Document (REP901)
|
||||
|
||||
{{< youtube aTiwroXwW0 >}}
|
||||
|
||||
|
||||
|
||||
[anchor0]: clip_image002.gif-750x0.png
|
||||
[anchor1]: clip_image004.gif-750x0.png
|
||||
[anchor2]: clip_5F00_image006.jpg
|
||||
[anchor3]: clip_5F00_image008.jpg
|
||||
[anchor4]: clip_5F00_image010.jpg
|
||||
[anchor5]: https://www.youtube.com/watch?v=aTiwroXwW_0&list=PLhZ3P-LY7CqmVszuvtJLujFyHpsVN0U_w&index=17
|
||||
|
||||
|
||||
[image0]: clip_image002.gif-750x0.png
|
||||
[image1]: clip_image004.gif-750x0.png
|
||||
[image2]: clip_5F00_image006.jpg
|
||||
[image3]: clip_5F00_image008.jpg
|
||||
[image4]: clip_5F00_image010.jpg
|
||||
|
After Width: | Height: | Size: 94 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 65 KiB |