Introducing C/Breeze

April 9th, 2014

Introduction

C/SIDE development is about modifying objects - I don’t think anybody could possibly disagree. The most common way to do so is through C/SIDE itself, adding elements such as table fields or page controls, setting properties and writing C/AL code in the appropriate places - nothing new there.

Doing things differently

If you wanted to do things differently, perhaps because you got tired of some of the more repetitious and labour intensive aspects of NAV development, you could write a tool to do some of the work for you. Eventually, however, the fruit of your labour would need to end up in the NAV database, and since the binary object file format (*.fob) is proprietary and closed, your only viable option would be the object text format. As a consequence, your tool or script needs to be able to write (and possibly also read) files in that format.

Similar challenges: XML

This challenge is far from unique in the IT world. It’s easy to think of another text-based format with strictly defined syntactical rules that we regularly need to query and modify: XML. In theory, you could treat XML files as just text files, read the entire contents of the file into memory as a huge text string and work from there. In practice, however, we typically use an abstraction layer that allows us to efficiently find, insert, change and remove elements and attributes, before we (optionally) write the file back to disk. The abstraction layer helps us to easily navigate the file’s contents, and focus on the actual changes that we want to make, without having to worry too much about the file format’s syntax details. If you wanted to create a new XML file, you program against the abstraction layer, and when you’re done, you tell the abstraction layer to write the in-memory tree of nodes that you built to a disk file. What if we could do the same thing for NAV applications?


// Imagine being able to start a new application like this:
var myApplication = new Application();

//Then add a table object to it by doing this:
var myTable = myApplication.Tables.Add(50000, "My Table Name");

// The table needs some fields, of course:
var myCodeField = myTable.Fields.AddCodeTableField(1, "Code", 10);
var myDescriptionField = myTable.Fields.AddTextTableField(2, "Description", 30);

// Let's make sure the primary key column cannot be left blank:
myCodeField.Properties.NotBlank = true;

// And write the resulting application to disk
application.Write(some_sort_of_text_writer);

Generating the application like this is very repeatable: requirement changes can be incorporated into the script, and an updated version of the application is a proverbial click of a button away.

Or what if you wanted to know which pages are based on a certain table?


var myApplication = new Application();
myApplication.Load(@"objects.txt");

foreach(var page in myApplication.Pages.Where(p => p.Properties.SourceTable == 50000)
{
Console.WriteLine("Page {0} {1}", page.ID, page.Name);
}

The possibilities are virtually endless, and everything can be done in your favourite .NET language! :)

Lexers and parsers

There are a few things I should mention at this point. The most important thing is probably that I don’t have a background in Computer Science, and I have never written a lexer or parser before. In fact, I didn’t even know what a lexer was until a few months ago. I’m pretty sure some of you will laugh at my naive approach to parsing. That’s OK - we both know you’re right. Luckily, the different components of C/Breeze are nicely decoupled. With some expert help, we could properly rewrite the parser without breaking any of the other components.

Great minds think alike ;)

Another point, which is also the reason for me writing this right now, is that I’m aware that Microsoft is working on something similar - they hinted at it during the 2013 NAV TechDays in Antwerp. My guess would be that their solution is more targeted towards their own goals, but an overlap in functionality is probably unavoidable. I’m OK with that. The idea of C/Breeze, as I mentioned, has been on my mind for a very long time, and I’m happy to finally be able to show an actual beta release. For me, this is primarily about ideas. If Microsoft comes up with a proper object model for querying and manipulating object text files, I will gladly move out of their way, and welcome in their solution (if its any good, of course). C/Breeze, to me, is about scratching my own itch, and if somebody else is willing to scratch that itch for me, I guess that’s even better! ;)

Learning points: generated code

What makes the idea of Microsoft’s object model (should there ever be such a thing) taking my brainchild’s place even more bearable is the incredible amount of learning and experience that C/Breeze has brought me. More than one previous incarnation of C/Breeze has failed to reach maturity because at one point or another, I noticed the code was inconsistent and lost interest. The current version of the UncommonSense.CBreeze.Core library is entirely generated - the structure of the object model is defined in (oh, irony!) a NAV database. This approach has taught me a thing or two about generated software. Most importantly, the resulting code has a pleasant (to me) consistency.

Learning points: representing default property values

Another example of a learning point (at least for me) was how to deal with default property values in objects. In the resp. designers in the NAV IDE, default property values show up as values surrounded with angled brackets. When you export an object, these values do no end up in the resulting export file. That’s why, in an object model like C/Breeze.Core, these values require a special representation. I choose to represent these values as Nullable<T>s. It allows you to type-safely assign non-null values directly to the property, easily test for the presence of a value (Nullable<T>.HasValue), and the generic nature of Nullable implies that this logic works the same for any property type.

Interested to see what I have come up with so far? Check out www.uncommonsense.nl/cbreeze.

Function Return Value Types

March 31st, 2014

Programmers skilled in other languages may have some difficulty understanding why C/SIDE doesn’t allow all of its data types to be used as function return value types. I guess it is a limitation that you and I have learnt to work around, or even use to our advantage.

Take option values, for example. While they are perfectly valid as variable types, or even parameter types, function return values cannot be options - the option Option ;) is simply not listed:

Option not listed as possible return value

Not surprisingly, typing the return value type doesn’t help either:

Option is not an option

That’s why I was rather stunned to find the base application contains functions that actually use Option as their return value type - e.g. table 5601:

Table 5601

Not fair. How about if I cheated a little, by creating a codeunit containing a function with Integer as its return value type, like so:

My codeunit with integer return value

Then, using a special nifty little tool called errr… Notepad, I could change the return value type from this…

Notepad before edit

…to this:

Notepad after edit

The object imports and compiles without any issues:

My own codeunit with Option return value

Apparently the run-time doesn’t mind Option as a return value type; it’s the C/AL editor that’s a bit more picky.

Knee deep in the upgrade to NAV 2013 R2 of one of our add-ons, I found the following, rather confusing situation. As far as I can tell, my databases are clean CRONUS databases, and it’s the build system at Microsoft that has somehow messed up the NAVNL2013R2 version lists?

navnl

navw1

In general, version lists don’t go “back in time”, and non-localized W1 objects in localized databases should have their original W1 version list?!

Translating C/SIDE applications is not something I do regularly, which is probably why, every time I do, I always forget certain important rules for dealing with translation files. “Be extremely careful with hard-coded strings” is probably the most important one. Let’s see why that is using a very simple example. Consider this table:

Table Designer

I have dutifully given the object itself, and the contained fields, ENU captions. Exporting a translation file renders the following (unsurprising) result:

Initial Translation.png

Now let’s assume that the developer introduces a line of code and an - otherwise harmless - comment in one of the triggers

Code and Comment

No big surprises when we export the translation file again. The hard-coded strings show up as sequentially numbered “-X” entries in the translation file.

Translation with Hardcoded Text

The developer, obviously keen on application maintainability ;), adds another comment:

More Comments

However, if, unaware of that new comment, we now take the most recently exported translation file, and import it, the effect will be far from desirable:

Effect of Translation Import

Since the “-X” entries are numbered sequentially, the value of the first one in a certain context (trigger, function etc.) is assigned to the first hard-coded string in the object, the second entry’s value to the second hard-coded string etc. Change the number or the sequence of the hard-coded strings in the object, and you’re in for some unpleasant surprises.

Unlike the run-time, the compiler won’t mind the strange parameter in the FIND statement: the data type is still the same, after all…

You may argue that I have intentionally chosen a mixture of hard-coded application strings (i.c. “‘+’”) and comments to show the rather devastating impact, but simply shifting explanatory comments to a new and unintended context could cause major confusion for future maintainers of the codebase.

Since hard-coded strings, by their very definition, don’t need to be translated, I think the best solution would be to remove such strings from the translation file, immediately after exporting. After that, the file can be safely translated and reimported.

[edit]Due to a little hick-up in Wordpress’ comments feature, Luc van Vugt was unable to add the following response: “Fully agree. And this isn’t hypothetical as it was a major bug in early summer releases of the SEPA feature for the Netherlands (see:http://dynamicsuser.net/blogs/vanvugt/archive/2013/07/02/microsoft-dynamics-nav-2013-sepa-credit-transfer-and-direct-debit-updates-netherlands.aspx)”[/edit]

Try this: on a field page control whose underlying data type is Text (just to keep things fair - title casing doesn’t make much sense for a field of type Code or some numeric type), set the Title property to Yes.

Title Property set to Yes

Now export your page object as text. On my machine, the Title property value doesn’t appear in the export file, which means that reimporting this page object from my text file effectively clears the Title property. Hmmm…

Date Components

August 15th, 2013

You think you know exactly how the date functions DATE2DMY and DATE2DWY in C/SIDE work, don’t you? So did I, but take a look at the following example. Things are more complex than I thought, and C/SIDE is cleverer than I anticipated.

For the purpose of this demonstration, I wrote a function that shows the date components for a given date, like so:

image

Calling it for the 10th of August yields the following, predictable result. Note that Date2DWY’s day component is the day of the week (Monday = 1, Tuesday = 2 etc.).

image

Makes sense, right? Now look at this – the 31st of December 2013:

image

DATE2DMY’s results are predictable as before, but DATE2DWY may surprise you: according to C/SIDE’s calendar, 31-12-13 is the Tuesday of the first week of 2014. Depending on your definition of the first week of the year, this is correct, but it means that DATE2DWY(311213D, 3) actually returns 2014!

The other day, while talking about .NET Interop with an experienced and well-respected NAV developer, I noticed how he seemed to be confused by the difference between .NET assemblies and .NET namespaces. The concepts themselves may be quite straightforward to even novice .NET developers, but given that neither has a real counterpart in C/SIDE, I thought I might try and explain, perhaps helping a few others that are struggling with this.

Assemblies are the most “physical” of the two concepts. Most assemblies that you will deal with as a NAV developer take the form of a .exe or .dll file on disk. Assemblies are the building blocks of any .NET application, containing the types and resources that the .NET run-time needs to execute your logic.

When selecting a subtype for your DotNet variable in NAV, the first thing you select is the assembly in which the desired type resides:

Selecting an assembly

Namespaces - some of you will recognise this from working with XML - are merely a way to make sure your .NET type and my .NET type won’t collide, if they happen to have the same name. As such, they form a logical grouping of your application’s components. For example, we could both have an Order class, but since yours is in namespace “YourCompanyName” and mine is in “MyCompanyName”, it is clear to the compiler which Order class we are referring to, even if we use both classes in the same project.

Namespaces can be nested within other namespaces; the unambiguous name for referring to a particular type, more commonly called its fully qualified name, is formed by taking the type’s name, prefixing it with its namespace name, prefixing that with it’s parent namespace name, etc., until we reach the root namespace. These fully qualified names are exactly what C/SIDE shows us, once we have selected an assembly and are ready to select a type within that assembly:

Selecting a type

Here’s a situation that, on a bad day ;), still occasionally confuses me when using .NET interop in NAV: the StreamReader class lives in the System.IO namespace (and rightfully so), but being such an essential, low-level class, it is implemented in the .NET core library (called mscorlib.dll). However, in the list of assemblies displayed by NAV, there is an entry called System.IO.dll, that, every now and then, I mistakingly select before realising the StreamReader class lives elsewhere…

Streamreader class on msdn

PluralCaptionML

July 30th, 2013

In several situations, I have wondered if perhaps we need an additional multi-language property at table level that contains the localized plural name of the entity type in question, e.g. “ENU=Customers;NLD=Klanten”. The most common of these situations is probably the OnDelete trigger of a parent record (i.e. the “1″ side in a 1:n relationship), when it is supposed to prevent deletion if any child records (the “n” side) exist:

ChildRecord.SETRANGE(ChildField1, ParentField1);
ChildRecord.SETRANGE(ChildField2, ParentField2);
IF NOT ChildRecord.ISEMPTY THEN
  ERROR(Text000);

In this snippet, textconstant Text000 would probably explain to the user that the parent record cannot be deleted, because one or more child records remain (note that this is done only in contexts where a cascading delete is undesirable or impossible). In phrasing the text constant, we would like to refer to the *plural* form of the child entity type’s name - something like “Cannot delete this Bla because one or more Bla Ledger Entries exist.”. Sadly, all the definition of the Bla Ledger Entry table can provide us with is the *singular* localized caption “ENU=Bla Ledger Entry;NLD=Bla-post”.

Getting from the singular to the plural form is, at least for the English language, quite straightforward in most cases - simply postfix the word with an “s” (Customer/Customers). However, as the example above already illustrates, that is not always the case: the plural form of “entry” is “entries”, not “[entrys]”. And I’m sure that there are languages where the rules for building plural nouns are far more complex than they are in English. I guess that makes predefining the plural localized caption in a separate table property the safest approach.

I would love your feedback before I post my suggestion on Connect. If you can provide me with other situations in C/AL code where it would help to have the plural caption available, that would be great, too!

[edit]My usual work-around is phrasing the text constant a bit differently - something like “Cannot delete {CaptionML of parent record} because one or more records exist in the {CaptionML of child record} table.” Not ideal, though - most users don’t think in terms of records and tables…[/edit]

The NAV2013 command line interface is a great way to control the Development Environment from a script or batch file. The result file navcommandresult.txt and log file combined provide all the feedback you may need about the success of failure of your most recent command.

Sadly, the license expiry warning is still displayed as a message box. If your license is about to expire, the modal dialog box below pops up, effectively blocking further execution - not what you want if you’re running a script (typically unattendedly). Hopefully, Microsoft will reconsider this choice and put the license warning in the log file when running in command line mode.

License warning

DateFormula fields are not the most commonly used field type in NAV, and ValuesAllowed is definitely not the most commonly set field property. Maybe that’s why this escaped the MDCC testers’ attentention?

In the properties of a DateFormula field, I enter the following value in the ValuesAllowed property. A subsequent compile & save doesn’t produce any error messages.

valuesallowed before saving

However, when I reopen the object, this is what I see:

valuesallowed after saving

The original value is gone, but the property does’t have its default value (“<>”). Searching the knowledge base for entries containing “ValuesAllowed” does not produce any results.

Am I doing something wrong here? Making the dateformulas locale-agnostic by surrounding them with angled brackets did not fix this. Could this be a bug?

The deeper I dive into the NAV 2013 translation files, the quirkier things get. When I export page 760, I get the following translation entries:

[...]
N760-V-5-E12-P2818-L30:BusinessChart::DataPointClicked
N760-V-5-E12-P26176-L999:DataPointClicked
N760-V-5-E12-P26177-L999:BusinessChart
N760-V-5-E12-V1000-P2818-L30:point
N760-V-5-E13-P2818-L30:BusinessChart::DataPointDoubleClicked
N760-V-5-E13-P26176-L999:DataPointDoubleClicked
N760-V-5-E13-P26177-L999:BusinessChart
N760-V-5-E13-V1000-P2818-L30:point
N760-V-5-E14-P2818-L30:BusinessChart::AddInReady
N760-V-5-E14-P26176-L999:AddInReady
N760-V-5-E14-P26177-L999:BusinessChart
[...]

Unsure what this means. Is it a variable with ID -5? It seems Tools|Translate|Import doesn’t like the extra dash, either.

image

<edit>

And now that I’ve finally managed to remove all invalid entries from my translation file, I get the same result as Luc did:

stopped working

</edit>

Length of Variable Names

May 17th, 2013

For as long as I can remember, variable and function names in C/SIDE have been 30 characters or less. Most of us - especially those with a firm belief in descriptive names - are painfully aware of this limitation. But the days of cryptic, abbreviated names may be behind us.

In an attempt to quickly and easily add a Dutch language layer to the C/SIDE application I’m working on, I exported a complete translation file from a Dutch CRONUS database. When I tried to import the file into my own database, I was confronted with an error message claiming that one of the entries in the file was too long.

Import Translation

Technically speaking, the system is right - the translation entry in question cannot exceed 30 characters (which is what the "L30" means). But, a little searching on MSDN reveals that variable names in NAV 2013 can now have up to 128 characters. No more excuses for unlegible and/or undescriptive names!

Is anybody aware if Microsoft has released a patch that will cause the IDE to export the correct lengths in translation files? I can’t believe I’m the first person to run into this issue, but there appears to be nothing in the hotfixes list on PartnerSource about this subject.

<edit>And it gets worse! One could certainly argue about the desirability of text constants names such as “PADSTR_____G_L_Account__Indentation___2___G_L_Account__NameCaptionLbl”, but if the IDE considers them acceptable, shouldn’t the translation import/export funcionality do the same?…

image

</edit>

IWCWMLC#: Unreachable Code

April 5th, 2013

Another post in the I Wish C/AL Was More Like C# series. I realize that some people may accuse me of trying to blame C/SIDE for the consequences of my own, rather chaotic thinking process - if my brain worked in a more structured fashion, none of this compiler cleverness would be required. Then again, given the right project scale and complexity, I think any developer could make the kind of errors that I tend to make, and appreciate a compiler that helps to correct them.

Today’s problem may seem unrealistic at first, especially because the code below is simplified quite extremely, for the sake of the example. Trust me, on a codebase maintained by lots of different developers, some more skilled than others, I have seen this situation happen more than once.

Assume you have the following code in C/SIDE:

image

Somebody (you? Knipogende emoticon) has placed an unconditional exit statement above the other code, which means that the //… even more code … (or your MESSAGE statement, for that matter) will never be executed. The same logic in C#…

image

… would result in a much more helpful compiler warning (also indicated by the green squiggly line below Console), that, if desired, could even be promoted to a compiler error.

image

Capture

As kriki wrote in his comment on my previous post, C/SIDE and its programming language, C/AL, were probably never quite intended and designed for the scale and complexity of the customisations and extensions that the NAV community has built over time (and still does). Also, I think the field of computer science has since moved further away from development in the sense of “writing code”, and more towards software engineering. In other words, treating the computer as a helpful partner in the development process, rather than a wild animal that needs to be tamed by feeding it the right instructions…

Last time, we spoke about how C# prevents errors by forcing every code path of a function to have a return value (i.e., if the function itself doesn’t return a void). The check takes place at compile-time; your project won’t even build if one or more functions don’t comply with C#’s strict rules. Sadly, not all checks can be carried out before the code is compiled. Sometimes, a check depends on values (e.g. user input, command line arguments) that are simply not known to the compiler, only to the executing run-time. If the .NET run-time detects a problem with such a check, it will throw an exception. As undesirable as these exceptions may be (especially if they are found late in the development process, or even after shipping), I think the general consensus is that they are still better than code that fails without any notification of the error at all. Let’s take a look at a (extremely simplified) example.

The snippet below…

Right in cside

…produces this message - no surprises there:

Right result in cside

However, if I accidentally pass in too few parameters (or too many placeholders, depending on your perspective), like so…

Wrong in cside

…C/SIDE will not raise any run-time errors, and simply substitute a blank value (somehow, I believe that behaviour has changed over time - I expected it to leave the %2 in instead?! Must be getting old.). In other words, it fails silently, and it might take ages for someone to notice this bug:

Wrong result in cside

Now, if we were to do the same thing in C#…

Right in csharp

…this would be our first result:

Right result in csharp

Let’s break the code by removing the second parameter, but leaving the second placeholder intact:

Wrong in csharp

The .NET run-time throws an exception because it cannot find anything to put in placeholder {1}.

Exception in csharp

The first manual or automated test to cover this code in C# would trigger the exception, providing the developer with an opportunity to repair the issue before any users (or managers ;-)) are aware of it.

Today, the first post in a (potentially long ;-)) series of posts filed under “I Wish C/AL Was More Like C#”.

I like how C# prevents me from making the kind of mistakes that my - slightly chaotic - mind frequently makes: the kind of mistake that’s a lot more difficult to find at run-time than it is at “code-time”. Here’s an example:

Trim Prefix

The TrimPrefix function can be used to trim off a string from another one, if the latter starts with the former. A simple function that I wrote almost without thinking, while my brain was working on the proverbial bigger picture ;-). My first tests succeeded nicely, but after a while I started to notice blank values in some records. Of course, I had failed to EXIT(Text) in case the prefix is not found, i.e. Text starts with something other than Prefix.

C# is a lot more strict about these things, and would have told me straight away that some of the code paths in my function do not result in a proper return value. This function…

TrimPrefix in C  Wrong

…would lead the compiler to complain about this…

Error Message in C

…which in turn would tell me to make this correction:

TrimPrefix in C  Right

I wish this kind of check could be added to C/AL, but I realise doing so would break a lot of existing applications. Perhaps a compiler warning would be nice? If you tell the compiler that your function has a return value, you’d better make sure it does on all code paths!

Maybe you just want to test a NAV web service. Maybe you want to periodically carry out an unattended task (Windows Task Scheduler!) in NAV, but you can’t use the NAV Application Server (NAS), e.g. because the task uses .NET Interop, which can only be executed on the NAV Service Tier (NST). Or maybe you are using a version of NAV where the NAS doesn’t even exist anymore. ;) Whatever the scenario might be, in the past, when confronted with a challenge like this, unaware of any viable alternatives, I used to fire up the old Visual Studio IDE and code my web service calls in C#.

What I like about this approach is that Visual Studio transparently creates the web service proxy for you; what I strongly dislike about it is the fact the C# is a compiled language, meaning that ad-hoc changes will be more time consuming to make, and, even more importantly, we need to deal with two artefacts: the compiled assembly and the source code. Not all customers have a good place to store the latter safely. Wouldn’t it be great if we could do the same thing from, say, a scripting language, so that we we don’t need to deal with a separate set of source code files?

Here’s how to consume a NAV web service from PowerShell. Let’s start with a (very traditional) web service codeunit.

Codeunit

Next, we’ll publish the codeunit as a web service in Web Services page.

Webservices

In a web browser, my services now look like this. Simply copy the URL for the desired service - we are going to need it in the next step.

Browser

Open a Windows PowerShell console, or start the Windows Powershell Integrated Scripting Environment (ISE). All we need to do now is to let PowerShell create the proxy for our web service.

Createproxy

That’s all there is to it - we can now start calling the web service methods.

Calling the webservice

Surprised to find that the method’s name is not HelloWorld(), but CallHelloWorld()? So was I. It appears to have something to do with a collision of names between the service (which receives its name for the Web Services page in NAV) and the method (whose name matches the function name in the codeunit in which it is implemented). After I renamed the web service in NAV (see below), things looked as expected (even further below).

Renamed webservice

 

Without call prefix

And it sounded so promising… Using the FileSystemWatcher .NET class from a NAV 2013 page object to detect changes in a file system folder. I had it all figured out: using the right .NET assembly, responding to the FileSystemWatcher’s events and making sure the object runs on the RTC (as opposed to on the Service Tier).

image

In the OnOpenPage trigger, I instantiated the FileSystemWatcher, told it to monitor my desktop folder (Environment is another .NET Interop variable, also running on the client), and started the event raising mechanism.

image

The code above, which worked just fine in a Visual Studio console application, does not produce any results in NAV, i.e. does not trigger any of the events below.

image

Any ideas, anyone?

I have developed and documented a .NET assembly for use from within Dynamics NAV, and I was hoping to be able to assist callers of my API by providing them with context-sensitive on-line help. Does anybody know if NAV 2009R2 or 2013 can display context-sensitive (= F1) help in the Symbol Menu for members of custom .NET types? If so, what are the requirements for the file format, file name and file location of the help file?

P.S.: Running the client with ShowHelpID=1 does not give any indication that this may be supported at all.

[Question also posted on mibuso.com and stackoverflow.com]

On Concentrating INSERTS

January 17th, 2013

Part of the same C/SIDE application as the one described in the previous post was a codeunit that inserts records into two tables based on records in an entire tree of other, mutually 1:n-related tables. Think of the two tables as a summary for the data in the other tables.

In the current implementation, the codeunit contains a function that accepts the “root” of the tree as a parameter, and calls the same function for each of it’s children. That same function in turn calls another function for each of the child’s children, etc. Inserting the summary records was done from each of the functions. In pseudo-code, it looked something like this:

VAR
  SummaryRecord

function HandleRootLevel(Root)
{
	SummaryRecord.Field1 := "Foo";
	SummaryRecord.Field2 := "Baz";
	SummaryRecord.INSERT;

	foreach Child of Root
	{
		HandleChildLevel(Child);
	}
}

function HandleChildLevel(Child)
{
	SummaryRecord.Field1 := "Bar";
	SummaryRecord.Field2 := "Qux";
	SummaryRecord.INSERT;

	foreach Child of Child
	{
		HandleGrandChildLevel(Child);
	}
}

function HandleGrandchildLevel(Grandchild)
{
	SummaryRecord.Field1 := "Quux";
	SummaryRecord.Field2 := "Quuux";
	SummaryRecord.INSERT;
}

In situations like this, I tend to follow a different pattern. Let me show it first, then explain why I do.

function HandleRootLevel(Root)
{
	InsertSummaryRecord("Foo", "Baz");

	foreach Child of Root
	{
		HandleChildLevel(Child);
	}
}

function HandleChildLevel(Child)
{
	InsertSummaryRecord("Bar", "Qux");

	foreach Child of Child
	{
		HandleGrandChildLevel(Child);
	}
}

function HandleGrandchildLevel(Grandchild)
{
	InsertSummaryRecord("Quux", "Quuux");
}

local function InsertSummaryRecord(Field1Value, Field2Value)
{
	VAR
	  SummaryRecord

	SummaryRecord.Field1 := Field1Value;
	SummaryRecord.Field2 := Field2Value;
	SummaryRecord.INSERT;
}

Not much difference, you may say. Just a function that handles the inserts. Here’s why I think the second snippet is easier to understand and maintain.

  • First of all, there’s the DRY principle. That one speaks for itself, I guess. The first snippet contains some duplicate code.
  • A call to InsertSummaryRecord tells readers what the code will do, without bothering them with the how, leaving the function a black box until the reader decides she’s interested in what’s inside the function. As a result, I find the second snippet cleaner and more expressive.
  • The SummaryRecord variable in InsertSummaryRecord is local, which means it automatically gets initialised every time the function is called, thus making sure that previous INSERTS cannot have any unwanted side effects.
  • Whenever a new field is introduced in the SummaryRecord table, just add it as a parameter to InsertSummaryRecord and the compiler will point out (at design-time) what other parts of the code require modification in order to populate the new field.
  • Finally, imagine you need to debug the code (particularly, in pre-2013 NAV). Setting a breakpoint on all the codelines that insert SummaryRecords certainly gets easier if there’s just one. :)

I’m curious to hear what you think! Thanks for reading & responding! :)