AZ AL Dev Tools Update

I’ve recently again updated my “AZ AL Dev Tools” VS Code extension, but this time the list of all changes is a bit longer than usual, so I’ve decided to describe them here instead of using Twitter.

Bigger enhancements

Code Clean-up commands

I’ve added a new commands that can run other code transformation commands one after another to fix multiple problems in the code. There are 3 of them:

  • “AZ AL Dev Tools: Run Code Cleanup on the Active Editor”
  • “AZ AL Dev Tools: Run Code Cleanup on the Active Project”
  • “AZ AL Dev Tools: Run Code Cleanup on Uncommitted Files in the Active Project”

The third command allows to run clean-up on uncommitted files only which can be faster if you are working on a big project and want to update your files only before committing and pushing them to the git server.

List of commands run by the clean-up can be defined in the “alOutline.codeCleanupActions” setting. It is an array of strings, these values can be selected:

  • “RemoveWithStatements”
  • “AddApplicationAreas”
  • “AddToolTips”
  • “RefreshToolTips”
  • “AddTableFieldCaptions”
  • “LockRemovedFieldCaptions”
  • “AddPageFieldCaptions”
  • “AddObjectCaptions”
  • “FixKeywordsCase”
  • “FixIdentifiersCase”
  • “ConvertObjectIdsToNames”
  • “AddMissingParentheses”
  • “AddDataClassifications”
  • “RemoveUnusedVariables”
  • “SortPermissions”
  • “SortPermissionSetList”
  • “SortProcedures”
  • “SortProperties”
  • “SortReportColumns”
  • “SortTableFields”
  • “SortVariables”
  • “RemoveBeginEnd”
  • “FormatDocument” – formats document, can be used if format on save is disabled
  • “TrimTrailingWhitespace” – removes spaces at the end of each line

Some of these commands ask for additional parameters before they can be run. If you run them separately, they will always do that but for code clean-up you can disable it and define parameters in the VS Code settings. If you do it in the project settings, you can be sure that every developer will run it the same way. These settings can be used:

  • “alOutline.defaultAppArea”: default application area
  • “alOutline.defaultDataClassification”: default DataClassification value
  • “alOutline.defaultAddPageFieldCaptionsSettings” default settings for the AddPageFieldCaption command when run by the Code Cleanup, these properties can be set:
    • “setActionsCaptions”: set action captions
    • “setActionGroupsCaptions”: set action groups captions
    • “setGroupsCaptions”: set groups captions
    • “setPartsCaptions”: set parts captions
    • “setFieldsCaptions”: set fields captions
    • “setLabelsCaptions”: set labels captions
  • “alOutline.defaultRemoveUnusedVariablesSettings”: default settings for the RemoveUnusedVariables command when run by the Code Cleanup, these properties can be set:
    • “removeGlobalVariables”: remove global variables
    • “removeLocalVariables”: remove local variables
    • “removeLocalMethodParameters”: remove local methods parameters
  • “alOutline.reuseToolTipsFromDependencies”: reuse tooltips only defined in these dependencies, if empty, all dependencies will be used. Each entry should be defined as “dependency publisher” + “space” + “-” + “space” + “dependency name”. You can also use “*” to use all dependencies and make sure that code clean-up won’t ask for these values.

Reusing tooltips from other pages

Another enhancement is the ability to reuse existing tooltips defined for the same table field on other pages in page wizards, “add multiple fields” page code action, “`Add ToolTips to the Active Editor/Project” and new “Refresh ToolTips from Dependencies in the Active Editor/Project” commands. The difference between “Add ToolTips…” and “Refresh ToolTips…” commands is that the second one updates all tooltips on the page fields while the first one adds them only if they are missing.

Reusing tooltips can be disabled by changing “alOutline.doNotReuseToolTipsFromOtherPages” setting to true.

You can also limit searching for existing tooltips to only a few dependencies of your project by listing them in the “alOutline.reuseToolTipsFromDependencies” setting.

There is also new “Reuse tooltip from other pages” code action available on the first line of page/page extension field and ToolTip property, it allows to select tooltip value from a list of tooltips defined for this table field on other pages in the current project and it’s dependencies.

New image browsers

There are 3 new image browsers that you can open using these commands:

  • “AZ AL Dev Tools: Show CueGroup Action Images”
  • “AZ AL Dev Tools: Show CueGroup Fields Images” – “Copy as action” and “Copy as promoted action” context menu items are not available in this case
  • “AZ AL Dev Tools: Show Role Center Action Images”

Fix begin..end warning (AA0005)

I’ve added 2 new commands that can be used to remove begin..end statements around single commands. They don’t need warnings reported by CodeCop code analyzer, so you can keep code analyzers disabled:

  • “Remove Begin..End around Single Statements from the Active Editor”
  • “Remove Begin..End around Single Statements from the Active Project”

Lock removed fields captions

These new commands can be used to lock captions of “removed” table fields, so they no longer appear on the list of texts that have to be translated:

  • “Lock Removed Table Field Captions in the Active Editor”
  • “Lock Removed Table Field Captions in the Active Project”

Performance changes

Some time ago Frédéric Vercaemst reported that CodeActionsOnSave are very slow when you open large al project. It was very interesting investigation to find what is wrong. I’ve found that when you open a big project and enable code analyzers, they sometimes can use 100% processor power and won’t leave much for other processes, including my extension.

I’ve also found that with recent changes, when VS Code collects code actions, warning from code analyzers are no longer available on the list of diagnostics. It is good, because this function no longer is blocked by code analyzers still running in the background, so other extensions should return their code actions faster, but it also means that any code actions that needs these diagnostics no longer can return anything and you won’t see them under that yellow light bulb menu in code. It means that my functionality that was adding missing parentheses on document save or code fixes for AA0137 (unused variable) and AA0139 (possible text overflow) no longer works. I’ve decided to leave it like that by default as I already have separate commands for parentheses and unused variables that don’t need code analyzers diagnostics, but if you want, you can enable them back by changing “alOutline.enableCodeCopFixes” setting to true. Just remember that it will have negative impact on performance if you have large al project.

There is also another change that could improve performance. Because Microsoft code analyzers can be very busy with large al project and waiting for the response from Microsoft AL language server can take some time, saving documents can sometimes be slow as VS Code will wait for this language server to format your document. That’s why I’ve added a new “FormatDocument” option to the “alOutline.codeActionsOnSave” setting in my extension. If default saving document is slow, you can try to disable default VS Code auto format on save and use this new option. It calls the same formatting functionality from AL compiler that the standards Microsoft AL Extension, but because my extension runs separate process that is not busy with code analyzers, there is a chance that it will finish earlier.

Small fixes and enhancements

There are also other small bug fixes and enhancements, here is the list:

  • Issue #301 – PermissionSet wizard – PermissionSet name length cannot be longer than 20 characters
  • Issue #306 – Add MaxLength property to Caption when Permission Set is created
  • Issue #215 – Empty lines at end of new files – new setting “alOutline.noEmptyLinesAtTheEndOfWizardGeneratedFiles”
  • Issue #310 – SortProperties fails in case of extra semicolon
  • Issue #295 – Typo fix
  • Issue #275 – Symbols Browser – Go to definition (project file or server definition) – shared file access fix
  • Issue #307 – Allow the Syntax Visualizer sash to be moved further
  • Issue #308 – If possible, only show context menu items when relevant

Thank you

I won’t be able to do all these changes without people reporting them on my github. I would like to say “Thank You for all your ideas and bug reports” to all these people:

  • mjmatthiesen
  • fvet
  • GreatScott000
  • fvet
  • dannoe
  • rvanbekkum
  • jhoek
  • and others that reported issues earlier and are still waiting to be fixed or have been fixed in the previous releases

AL Interfaces – dependency injection

With the introduction of interfaces in the latest BC 2020 Wave 1, we are finally able to start using more object-oriented design patterns in our projects. As these patterns are not complicated and using them might increase quality and design speed of our solutions, is always good to know more about them. One of these patterns that is worth knowing is dependency injection.

Dependency inversion principle

Dependency inversion is a way of decoupling software modules. The idea behind it is simple – to make high-level modules reusable and unaffected by changes in low-level modules, they should not depend on each other. Instead of dependencies, we should introduce an abstraction that decouples our modules from each other. As a result of this approach, both low-level and high-level modules don’t reference each other directly, they only depend on the same abstraction. The important thing to understand here is that by “high-level module” we don’t mean “the highest level code”, there is still code above that calls it.

This “abstraction” is something that describes behaviour of our code, but does not contain any logic inside. As you can guess this is exactly what interfaces are supposed to do.

Dependency injection

As I wrote above, the dependency inversion principle removes direct dependency between low-level and high-level modules. It means that our high-level code works with an interface describing low-level functionality and doesn’t know about any implementation of that interface. To make it work, we have to create instances of the codeunits implementing these interfaces and pass them to the high-level module ourselves. This passing of the codeunit instance is called dependency injection because we are injecting an instance of interface implementation to the module that needs it to run its functionality.

I know that it all might sound a bit complicated so let’s explain it using some examples

Example – order approvals

Let’s imagine that we have a codeunit that approves a sales order and then sends an email notification to a salesperson. The sample code can look like this one:

codeunit 50001 OrderAppMgt
{

    procedure ApproveAndNotify(SalesOrderHeader: record "Sales Header")
    begin
        ApproveOrder(SalesOrderHeader);
        SendNotification(SalesOrderHeader);
    end;

    local procedure SendNotification(SalesOrderHeader: record "Sales Header")
    var
        TempBlob: Codeunit "Temp Blob";
        OutputStream: OutStream;
        InputStream: InStream;
        RecRef: RecordRef;
        Salesperson: record "Salesperson/Purchaser";
        txtSubject: label 'Order has been approved';
        txtBody: label 'Order has been approved';
        MailMgt: Codeunit "SMTP Mail";
        Recipients: List of [Text];
    begin
        Salesperson.Get(SalesOrderHeader."Salesperson Code");
        TempBlob.CreateOutStream(OutputStream);

        RecRef.GetTable(SalesOrderHeader);
        RecRef.SetRecFilter();
        Report.SaveAs(Report::"Standard Sales - Order Conf.", '',
            ReportFormat::Pdf, OutputStream, RecRef);

        TempBlob.CreateInStream(InputStream);

        Recipients.Add(Salesperson."E-Mail");
        MailMgt.AddRecipients(Recipients);
        MailMgt.AddSubject(txtSubject);
        MailMgt.AddBody(txtBody);
        MailMgt.AddAttachmentStream(InputStream,
            SalesOrderHeader."No." + '.pdf');
        MailMgt.Send();
    end;
    ....

As you can see, both creating pdf and sending email notifications are implemented in the same place. Let’s change it a bit and move sending email to a separate EmailMessageMgt codeunit. New version of the SendNotification function can look like this:

local procedure SendNotification(SalesOrderHeader: record "Sales Header")
var
    TempBlob: Codeunit "Temp Blob";
    OutputStream: OutStream;
    InputStream: InStream;
    RecRef: RecordRef;
    Salesperson: record "Salesperson/Purchaser";
    txtSubject: label 'Order has been approved';
    txtBody: label 'Order has been approved';
    MessageMgt: Codeunit EmailMessageMgt;
begin
    Salesperson.Get(SalesOrderHeader."Salesperson Code");
    TempBlob.CreateOutStream(OutputStream);

    RecRef.GetTable(SalesOrderHeader);
    RecRef.SetRecFilter();
    Report.SaveAs(Report::"Standard Sales - Order Conf.", '',
        ReportFormat::Pdf, OutputStream, RecRef);

    TempBlob.CreateInStream(InputStream);

    MessageMgt.SendMessage(Salesperson, txtSubject,
        txtBody, InputStream,
        SalesOrderHeader."No." + '.pdf');
end;

And our EmailMessageMgt codeunit can look like this one:

codeunit 50002 EmailMessageMgt
{

    procedure SendMessage(Salesperson: record "Salesperson/Purchaser";
        Subject: Text; Body: Text;
        var Source: InStream; FileName: Text)
    var
        MailMgt: Codeunit "SMTP Mail";
        Recipients: List of [Text];
    begin
        Recipients.Add(Salesperson."E-Mail");
        MailMgt.AddRecipients(Recipients);
        MailMgt.AddSubject(Subject);
        MailMgt.AddBody(Body);
        MailMgt.AddAttachmentStream(Source, FileName);
        MailMgt.Send();
    end;

}

We now have 2 “modules”, high-level one which approves and exports an order and low-level one which sends a message with exported document to the salesperson.

The problem that we have here is that SendNotification function from OrderAppMgt codeunit directly depends on EmailMessageMgt codeunit. As a result whole functionality can only send notifications using emails. If we want to add another way of sending these messages, we will have to change OrderAppMgt.

To solve this problem, we can move these 2 codeunits into 2 separate extensions and define a new IMessageMgt interface which will be used by SendNotification function and will be implemented by our EmailMessageMgt codeunit. This new interface can be implemented in the same extension where we keep our EmailMessageMgt codeunit or we can move it to another extension. It can look like this one:

interface IMessageMgt
{

    procedure SendMessage(Salesperson: record "Salesperson/Purchaser";
        Subject: Text; Body: Text;
        var Source: InStream; FileName: Text);

}

New EmailMessageMgt codeunit looks almost the same as before, the only difference is information that it implements our new interface:

codeunit 50002 EmailMessageMgt implements IMessageMgt
{

    procedure SendMessage(Salesperson: record "Salesperson/Purchaser";
        Subject: Text; Body: Text;
        var Source: InStream; FileName: Text)
    var
        MailMgt: Codeunit "SMTP Mail";
        Recipients: List of [Text];
    begin
        Recipients.Add(Salesperson."E-Mail");
        MailMgt.AddRecipients(Recipients);
        MailMgt.AddSubject(Subject);
        MailMgt.AddBody(Body);
        MailMgt.AddAttachmentStream(Source, FileName);
        MailMgt.Send();
    end;

}

And finally, our OrderAppMgt codeunit has to be modified a bit:

codeunit 50001 OrderAppMgt
{

    procedure ApproveAndNotify(SalesOrderHeader: record "Sales Header";
        MessageMgt: Interface IMessageMgt)
    begin
        ApproveOrder(SalesOrderHeader);
        SendNotification(SalesOrderHeader, MessageMgt);
    end;

local procedure SendNotification(SalesOrderHeader: record "Sales Header";
    MessageMgt: Interface IMessageMgt)
var
    TempBlob: Codeunit "Temp Blob";
    OutputStream: OutStream;
    InputStream: InStream;
    RecRef: RecordRef;
    Salesperson: record "Salesperson/Purchaser";
    txtSubject: label 'Order has been approved';
    txtBody: label 'Order has been approved';
begin
    Salesperson.Get(SalesOrderHeader."Salesperson Code");
    TempBlob.CreateOutStream(OutputStream);

    RecRef.GetTable(SalesOrderHeader);
    RecRef.SetRecFilter();
    Report.SaveAs(Report::"Standard Sales - Order Conf.", '',
        ReportFormat::Pdf, OutputStream, RecRef);

    TempBlob.CreateInStream(InputStream);

    MessageMgt.SendMessage(Salesperson, txtSubject,
        txtBody, InputStream,
        SalesOrderHeader."No." + '.pdf');
end;

As you can see, our new version of OrderAppMgt codeunit no longer depends on EmailMessageMgt codeunit. We can now create more extensions with different implementations of IMessageMgt, i.e. TeamsMessageMgt, SkypeMessageMgt and easily extend functionality without changing anything in OrderAppMgt. The only difference is that when we call ApproveAndNotify procedure, we have to pass one of the codeunits implementing IMessageMgt to it. If we know which messaging implementation we want to use, we can directly inject it into the function by calling it this way:

procedure RunApproval(var SalesHeader: Record "Sales Header")
var
    AppMgt: Codeunit OrderAppMgt;
    MessageMgt: Codeunit EmailMessageMgt;
begin
    AppMgt.ApproveAndNotify(SalesHeader, MessageMgt);
end;

But there might be a case when we want to allow users or consultants to choose the implementation in the solution settings. Fortunately, AL allows us to do it easily. We can define a new enum and link enum values with interface implementation. Then, we can use that enum value everywhere where interface is expected. When we do that, AL will automatically create correct codeunit that implements our interface.

In our case, we can have “Base Notification Management” extension, where we define IMessageMgt interface, EmailMessageMgt codeunit, NotificationType enum type linked with the interface and a settings table where the users can select which notification method they want to use. That’s how our enum and settings table can look like:

enum 50000 NotificationType implements IMessageMgt
{
    Extensible = true;
    DefaultImplementation = IMessageMgt = EmailMessageMgt;

    value(0; Email)
    {
        Caption = 'Email';
        Implementation = IMessageMgt = EmailMessageMgt;
    }

}

table 50001 "Notification Settings"
{
    Caption = 'Notification Settings';
    DataClassification = CustomerContent;

    fields
    {
        field(1; PrimaryKey; Code[10])
        {
            Caption = 'PrimaryKey';
            DataClassification = SystemMetadata;
        }
        field(2; "Notification Type"; Enum NotificationType)
        {
            Caption = 'Notification Type';
            DataClassification = CustomerContent;
        }
    }
    keys
    {
        key(PK; PrimaryKey)
        {
            Clustered = true;
        }
    }
}

Our code calling approval process can now look like here:

procedure RunApproval(var SalesHeader: Record "Sales Header")
var
    AppMgt: Codeunit OrderAppMgt;
    Settings: Record "Notification Settings";
begin
    Settings.Get();
    AppMgt.ApproveAndNotify(SalesHeader, Settings."Notification Type");
end;

At this moment we have approvals and notification modules (extensions) than can work with emails. If we want to allow users to send messages using Teams, we can create a new extension with TeamsMessageMgt codeunit and extend NotificationType enum by adding Teams value linked with TeamsMessageMgt to it:

enumextension 50000 NotificationTypeExt extends NotificationType
{

    value(50000; Teams)
    {
        Caption = 'Teams';
        Implementation = IMessageMgt = TeamsMessageMgt;
    }

}

When we install this new extension, a new Teams option will be added to the “Notification Type” field in the “Notification Settings” table. When users select it, all approval messages will be sent using Microsoft Teams.

Custom AL Code Analyzers

When we develop a solution in AL in Visual Studio Code, we have the ability to turn on static code analysis and use one or more analyzers delivered by Microsoft. In most cases it is enough, but sometimes we might want to add some new checks specific to our own development standards or type of the project.

If you open Visual Studio Code, go to settings and take a look at “al.codeAnalyzers” setting description, you will see this: “Sets the list of paths to code analyzers to use for performing code analysis”. It means that it not only accepts names of standard analyzers, but also paths to custom analyzer dlls. The only thing that we need to know is how to create these libraries. There is no documentation how to do it for AL, but it does not mean that there is no documentation at all, we just have to know where to look for it.

Roslyn

Roslyn is C# and Visual Basic .NET compiler that adds rich code analysis APIs to these languages. There is plenty of documentation how to use it and there are good news for us – AL compiler is based on Roslyn source code. Microsoft team has made amazing job porting it for us. Not all parts of it have been moved to AL, there are also some differences (i.e. C# has classes and interfaces and we have tables, pages, reports and other types of objects), but if something is available in AL compiler dlls, then there is a big chance that Roslyn documentation will be a good starting point.

I used that approach when I wanted to learn how to create AL analyzers and I’ve found some information how to do it for C# here:
https://docs.microsoft.com/en-us/visualstudio/extensibility/getting-started-with-roslyn-analyzers?view=vs-2019

Code analyzer

AL compiler can trigger a few events at different stages of source code compilation. Code analyzer is a .net class that can subscribe to these events, analyze metadata generated by the compiler and report warnings or errors.

To implement an analyzer, we have to create a new class that inherits from Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics.DiagnosticAnalyzer, add DiagnoticAnalyzer attribute to it and implement a property that returns list of supported diagnostics and 2 methods – one that registers analyzer and second one that analyzes data generated by the compiler.

First, empty analyzer

To create our first, empty analyzer, we have to use Visual Studio, create a new C# “Class Library (.NET Framework)” project and select “.Net Framework 4.7.2” as our target version of .net. We also have to add references to Systems.Collections.Immutable.dll and Microsoft.Dynamics.Nav.CodeAnalysis.dll libraries from bin folder of Microsoft AL Extension. It can be found in “%UserProfile%\.vscode\extensions\ms-dynamics-smb.al-<Version>\bin”.

When we have our new, empty project ready, we have to add a new code analyzer class. It can look like this one:

using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ALCodeAnalyzerSample
{
    [DiagnosticAnalyzer]
    public class SampleCodeAnalyzer : DiagnosticAnalyzer
    {
        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => throw new NotImplementedException();

        public override void Initialize(AnalysisContext context)
        {
        }
    }
}

Let’s add some logic

Once we have our new, empty analyzer, we can add a bit of logic to it. It would be good to create something simple, so let’s report an error if name of declared variable is shorter that 3 characters.

Diagnostic descriptors

Analyzer should return list of descriptions of supported diagnostics. We are going to implement only one check, so that list will contain one element. DiagnosticDescriptor will never change, so we can cache it in private static variable like in this example:

        private static DiagnosticDescriptor _variableNameDescriptor = new DiagnosticDescriptor(
            "VARNAME", "Variable Name", "'{0}' cannot be used as a variable name. Names should be longer than 2 characters", "Usage",
            DiagnosticSeverity.Error,
            isEnabledByDefault: true,
            description: "Checks variable names");

Now we can override SupportedDiagnostics property and return our DiagnosticDescriptor:

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
        {
            get
            {
                return ImmutableArray.Create(_variableNameDescriptor);
            }
        }

Initialization

Next step is to override Initialize function, register analysis callback function and specify that it should be called for VariableDeclaration syntax tree nodes.

public override void Initialize(AnalysisContext context)
        {
            context.RegisterSyntaxNodeAction(AnalyzeVariableSyntax,
                SyntaxKind.VariableDeclaration);
        }

Analyzing code

The last step is to declare callback function that we’ve just registered in the step above. It will be called for each VariableDeclaration node and it contains all processing logic.

protected static void AnalyzeVariableSyntax(SyntaxNodeAnalysisContext context)
        {
            VariableDeclarationSyntax syntax = context.Node as VariableDeclarationSyntax;
            if (syntax != null)
            {
                string name = syntax.GetNameStringValue();
                if (name.Length <= 2)
                    context.ReportDiagnostic(
                        Diagnostic.Create(
                            _variableNameDescriptor, context.Node.GetLocation(),
                            name));
            }
        }

Complete analyzer

That’s how complete class should look like:

using Microsoft.Dynamics.Nav.CodeAnalysis;
using Microsoft.Dynamics.Nav.CodeAnalysis.Diagnostics;
using Microsoft.Dynamics.Nav.CodeAnalysis.Syntax;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ALCodeAnalyzerSample
{
    [DiagnosticAnalyzer]
    public class SampleCodeAnalyzer : DiagnosticAnalyzer
    {
        private static DiagnosticDescriptor _variableNameDescriptor = new DiagnosticDescriptor(
            "VARNAME", "Variable Name", "'{0}' cannot be used as a variable name. Names should be longer than 2 characters", "Usage",
            DiagnosticSeverity.Error,
            isEnabledByDefault: true,
            description: "Checks variable names");

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
        {
            get
            {
                return ImmutableArray.Create(_variableNameDescriptor);
            }
        }

        public override void Initialize(AnalysisContext context)
        {
            context.RegisterSyntaxNodeAction(AnalyzeVariableSyntax,
                SyntaxKind.VariableDeclaration);
        }

        protected static void AnalyzeVariableSyntax(SyntaxNodeAnalysisContext context)
        {
            VariableDeclarationSyntax syntax = context.Node as VariableDeclarationSyntax;
            if (syntax != null)
            {
                string name = syntax.GetNameStringValue();
                if (name.Length <= 2)
                    context.ReportDiagnostic(
                        Diagnostic.Create(
                            _variableNameDescriptor, context.Node.GetLocation(),
                            name));
            }
        }
    }
}

Let’s test it

Once we compile our dll, we have to enable code analysis, register our code analyzer in Visual Studio Code and check if it works. I’ve copied my dll to d:\temp\al folder, so that’s how my VS Code settings look like:

    "al.enableCodeAnalysis": true,
    "al.codeAnalyzers": ["d:\\temp\\al\\ALCodeAnalyzerSample.dll"]

Now if declare variable with a name that is only 2 characters long, I should see an error like on this screenshot:

Other callback functions

Syntax node actions is not the only type of callback that we can use in a code analyzer. AnalysisContext that is passed to Initialize function has more registration methods that we can use. Here is list of them:

  • RegisterCodeBlockAction
  • RegisterCodeBlockStartAction
  • RegisterCompilationAction
  • RegisterCompilationStartAction
  • RegisterOperationAction
  • RegisterOperationAction
  • RegisterOperationBlockAction
  • RegisterOperationBlockStartAction
  • RegisterSemanticModelAction
  • RegisterSymbolAction
  • RegisterSymbolAction
  • RegisterSyntaxNodeAction
  • RegisterSyntaxNodeAction
  • RegisterSyntaxTreeAction

If you want to know more, you can check documentation of C# version of AnalysisContext here: https://docs.microsoft.com/en-us/dotnet/api/microsoft.codeanalysis.diagnostics.analysiscontext?view=roslyn-dotnet

How can I know what syntax note should I check?

There is no good answer to that question now. You can try to guess or you can create sample AL code that you want to analyze and use Microsoft.Dynamics.Nav.CodeAnalysis.Syntax.SyntaxTree.ParseObjectText() static method to build syntax tree and check what is inside.

For big Visual Studio and C# there is a Syntax Visualizer tool that can display syntax tree of current document. We don’t have anything like that for AL, so I have a plan to create something and include it in my AZ AL Dev Tools/AL Code Outline extension for Visual Studio Code.

Foldable regions in AL in VS Code

I am using foldable regions in each of my C# project as they allow me to split content of bigger classes into much smaller parts. I really like this functionality, so I started searching if there is any way of getting it for AL files in Visual Studio Code.

Default Visual Studio Code functionality uses indentation to detect foldable regions. If you indent your code properly, it should be enough to be able to collapse content of each procedure or loop, but it is not enough to group a few procedures into a logical region and collapse them all into a single line with just a description.

If you open C# of JavaScript file in VS Code, you will see, that it is possible to use #region and #endregion tags in C# and //#region and //#endregion tags in JavaScript. Second case seems much better for us as it uses standard comments and should not have any impact on AL compiler.

After a bit of time with google and VS Code source code I’ve found how this functionality has been implemented. The only required change to have it in AL is to add information about region start and region end to language configuration file. This file can be found here:

%USERPROFILE%\.vscode\extensions\<al-extension-folder>\al.configuration.json

I’ve decided to use JavaScript type of regions so the only thing that I had to do was to copy “folding” node from JavaScript language definition from Visual Studio Code source code and paste it into al.configuration.json file. You can find that original definition here:

https://github.com/Microsoft/vscode/blob/master/extensions/javascript/javascript-language-configuration.json

and that’s how folding node looks like (after removing ? character):

"folding": {
 "markers": {
 "start": "^\\s*//\\s*#region\\b",
 "end": "^\\s*//\\s*#endregion\\b"
 }
}

After adding it to al language file, restarting VS Code and opening test al project, I was able define new region and collapse it.

Regions

As you can see it looks a bit ugly, it is hard to see if we are looking at the comment or at the region definition.

AL syntax coloring is defined in this file:

%USERPROFILE%\.vscode\extensions\<al-extension-folder>\syntaxes\alsyntax.tmlanguage

This time I’ve decided to use C# approach to coloring region tags. After a bit of investigation and a few experiments, I’ve added these nodes to alsyntax.tmlanguage file, just above comments definition node:

<dict>
	<key>begin</key>
	<string>(^\s*)?(?=//\s*#region\b)</string>
	<key>beginCaptures</key>
	<dict>
		<key>1</key>
		<dict>
			<key>name</key>
			<string>keyword.preprocessor.region.al</string>
		</dict>
	</dict>
	<key>end</key>
	<string>(?!\G)</string>
	<key>patterns</key>
	<array>
		<dict>
			<key>begin</key>
			<string>//\s*#region</string>
			<key>beginCaptures</key>
			<dict>
				<key>0</key>
				<dict>
					<key>name</key>
					<string>keyword.preprocessor.region.al</string>
				</dict>
			</dict>
			<key>end</key>
			<string>\n</string>
			<key>name</key>
			<string>string.unquoted.preprocessor.message.al</string>
		</dict>
	</array>
</dict>

<dict>
	<key>match</key>
	<string>^\s*//\s*#endregion\b</string>
	<key>name</key>
	<string>keyword.preprocessor.region.al</string>
</dict>

And here is the result of this modification:

Regions2

It looks nice, but it will be overwritten by the next AL extension update, so I’ve added a new comment to one of AL github issues where I asked Microsoft to include this small change in the next version of AL extension. You can find this issue here:

https://github.com/Microsoft/AL/issues/1007

 

Generating documentation from C/AL source code

Dynamics Nav development is slowly moving into extension 2.0 direction and we won’t be able to quickly check how standard Nav functionality works from Visual Studio Code, so it would be nice to have some API documentation that could help us browse information about base Nav code that we want to extend with our extensions.

Microsoft promised us support for documentation comments in new development tools, so I will not be focusing on them in this post. I would like to write here how to generate documentation from classic C/AL development environment, as there are a few reasons why you might want to do it:

  • You might have a big add-on and want to focus on on-prem only but because it is big, it is hard to find where some functions are and different developers might implement the same functions multiple times just because they don’t know that somebody else already implemented them somewhere
  • You might want to develop your solution using extensions 2.0, but you would like to have nice list of API available in database that you want to extend
  • You might just want to try how DocFx works before Microsoft adds support for it to extensions 2.0
  • Dynamics Nav help is generated using DocFx, so maybe you just want to know what else this tool can do

DocFx

DocFx is a tool that can generate documentation website from source code and markup files. It runs on Linux, macOS and Windows and generates website that can be deployed to any web server without any additional configuration. You can find more information and download it from here: http://dotnet.github.io/docfx/

Documentation comments

If you are a c# developer, you are probably using documentation comments in your projects all the time.

When you type /// above function, property or class, Visual Studio automatically inserts xml template that describes element below and you just have to fill it in with your descriptions. DocFx can pick up these comments and include them in generated documentation.

It would be nice to use this kind of comments in Dynamics Nav, but unfortunately, they are not supported by classic development environment. Fortunately, we can add partial support for them to Dynamics Nav 2016/2017/2018 as these versions of Nav are using Visual Studio code editor that can be extended using visual studio extensibility. You can find extension doing it and also adding support for code snippets on my github here: https://github.com/anzwdev/ClassicAL-CodeEditorExtension

Obviously, we cannot write comments above procedure header, so you have to type /// in a first empty line of a procedure to automatically insert xml comment template:

Documentation2

If you do not want to build it from source code, you can download it directly from here: https://github.com/anzwdev/ClassicAL-CodeEditorExtension/releases

To start using it, you only have to copy AnZw.NavCodeEditor.Extensions.dll to Add-ins\CodeViewer\EditorComponents folder of your RTC and restart development environment.

Once it is running, there are 2 keyboard shortcuts available in code editor: Ctrl+Shift+E opens extension settings and Ctrl+Shift+T opens list of available snippets.

DocFX and .net

If you are a .net developer and you want to generate documentation for your c# project, it is very easy, you just have to do these steps:

1. Create new, empty DocFx project

docfx init -q

This command creates new, empty project called docfx_project on your disk

2. Copy source code of your project into docfx_project\src folder

3. Go to docfx_project and run DocFx there:

cd docfx_project
docfx

This command will generate metadata from your source code and then html documentation. Generated website can be found in docfx_project\_site folder, you can also find your API documentation in docfx_project\_site\api folder.

DocFx and C/AL

As you can see above, generating c# documentation is very easy. Unfortunately, it is not so easy for C/AL. DocFx does not understand C/AL source code, so it cannot generate metadata/ C/AL language has also different structure than c#, we are using codeunits, tables, pages and other Nav objects while c# has namespaces, classes, enums, inheritance and other object oriented structures. We need a tool that could export metadata from C/AL and we have to modify standard DocFx documentation template to understand C/AL structures.

I don’t want to make this post too long, so I am not going to write very detailed description how to implement these 2 tools, you can find sample implementations on my GitHub

Tool to generate metadata from C/AL source code and DocFx C/AL plugin source code can be found here: https://github.com/anzwdev/ClassicAL-DocFx-Plugin

DocFx C/AL documentation template can be found here (it already contains DocFx C/AL plugin dll from project listed above): https://github.com/anzwdev/ClassicAL-DocFx-Template

How to use it

1. Download metadata generation project from my ClassicAL-DocFx-Plugin repository

2. Download empty DocFx project from my ClassicAL-DocFx-Template repository

3. Export your Dynamics Nav objects to text file to docfx_project\src folder of project downloaded in step 2

4. Build solution downloaded from ClassicAL-DocFx-Plugin repository. You will have to update dll references, as it requires Microsoft.Dynamics.Nav.Model.dll file, which can be found inside Dynamics Nav RTC folder

5. Run ClassicDynamicsNavDocExporter.exe and specify Dynamics Nav source code text file, output path for object metadata (docfx_project\api) and name of your project (it will be used in generated documentation). Assuming that you docfx_project downloaded in step 2 is in c:\temp\, your project name is MyDynamicsNavProject and your objects are in AllObjects.txt file, you should run this command:

ClassicDynamicsNavDocExporter.exe c:\temp\docfx_project\src\AllObjects.txt c:\temp\docfx_project\api MyDynamicsNavProject

6. Go to your docfx_project folder and run DocFx to build documentation from the metadata:

cd c:\temp\docfx_project 
docfx

7. Your documentation will be generated in docfx_project\_site\api folder, go there and open index.html file to check if it looks fine. It might not work for you this way, because our template uses plenty of JavaScript. I was able to open index.html file directly in Firefox but it did not work this way in any other web browser for me. It works fine if you publish whole _site folder on IIS.