What are we going to look at?
I want to spend some time on some best practices around app development. Today, I want to look at different practices that make it easier for others to extend your functionality (even if the “others” are you in the future) and that make it easier to integrate into different solutions.
We used to always develop our functionality as well as we could, but typically did not care too much about the ability for others to customize or extend the functionality. It wasn’t necessary, since everyone had access to the full source code and could modify whatever they wanted.
These times are gone; we have now apps that are smaller or larger pieces of functionality that do a certain task a certain way. If you want to change that, you would have to rewrite the app and, typically, only the original developer/publisher can do that.
So, it is a lot more important to think about giving others the ability to customize your solution to make it fit their way. It is also important now to develop your apps in a way that you can integrate those into different solutions, for instance, different verticals, or you want to make your apps work with another app from someone else. You might even want to extend your own app in the future, but do not want to have to touch everything that is already working.
This does not only affect “AppSource Apps”, but also any ISV products for on premise or customer specific (in tenant) apps. Just think of it – you are creating an app that does a specific functionality for your client. If you know that that is working, it might make more sense to create a dependent app that you use to extend that functionality (and do not have to touch the core), instead of changing the existing app. It could turn into more issues deploying different apps, but this is a different story…
Table Structures
If you have data that you need to store for each sales document or each sales line or even for each master record, it is very easy to do. You copy the primary key of the table that you want to store something for and add your own primary key fields and then the extra data you need to store. But is that the best way of doing it?
What happens, when you decide that you also need to store this data for Service Documents? Or, what happens, if you later want to integrate your solution into a vertical that has completely different document tables? It won’t be possible without having to add new tables for everything.
Use of Enums and SystemId
If you had a primary key that has only two fields:
field(1; Document; Enum "Demo Document")
field(2; "Record Guid"; Guid)
And you create and extensible enum:
Enum 50100 "Demo Document"
{
Extensible = true;
value(36; "Sales Document")
{
Caption = 'Sales Document';
}
}
If you now want to store service documents, you just extend the enum with the next value. As you can see in the enum, I did not start at value 0. I used 36, since this is the table number for the sales header. This gives me the advantage that I can technically set the value this way
Document := Database::"Sales Header";
And now, to store the primary key of the sales header or the service header, you would do it like this:
"Record Guid" := SalesHeader.SystemId;
This works for every single record in the database. You could technically also use “RecordId” as the data type, but I like the system id better, since it also gives me “API readiness”. I already have the links to the records in my data structure the way that an API would expect them and do not have to do anything else.
Functions
Working with Data
Now, obviously, you also need to do something with your data. Assume that you would want to get the customer number from a sales header (or service header) and assign it to your new table. You obviously can do it like this:
case Document of
Document::"Sales Header":
begin
SalesHeader.GetBySystemId("Record Guid");
CustomerNo := SalesHeader."Sell-to Customer No.";
end;
else
OnGetCustomerNo(Rec, CustomerNo, Handled);
end;
You need the “else statement” to raise an event that someone can subscribe to provide you with the customer number for their documents. If you already raise an event, though, why don’t you raise it instead of the case statement and save yourself the case statement? Then your code would look like this:
OnGetCustomerNo(Rec, CustomerNo, Handled);
And then you create a codeunit that handles anything sales header related to your app and implement the event subscriber. And you copy your codeunit and switch it over to handle everything service header related later. And, if someone added a new document type, they will subscribe to the same event to get you the requested customer number out of their tables.
Filtering your data
If you need to filter your table from code, you can do it like this. Assume that you want to filter for a sales header from somewhere that you already have the sale header variable:
MyTable.SetRange(Document, MyTable.Document::"Sales Document");
MyTable.SetRange("Record Guid", SalesHeder.SystemId);
Obviously, you can generalize this as well. You can create a new function in your table:
procedure FilterFor(Rec: Variant)
var
RecRef: RecordRef;
begin
if Rec.IsRecord() then
RecRef.GetTable(Rec)
else
if Rec.IsRecordRef() then
RecRef := Rec
else
if Rec.IsRecordId() then
RecRef.Get(Rec)
else
Error('no idea what you are trying to filter on');
SetRange(Document, RecRef.Number());
SetRange("Record Id", RecRef.Field(RecRef.SystemIdNo()).Value();
end;
This function filters the “Document” enum field on the table number of the record that you passed into the function – remember why I said before to use the table number as the enum value?
And then it uses the “SystemIdNo” function of the RecordRef that provides you with the “internal field id” that is used for the SystemId and then you just get the value of that field.
And since the function was created using a variant as the parameter, you can pass any record in that you want – or RecordRef or even a RecordId. Now, you can filter this way:
MyTable.FilterFor(SalesHeader);
MyTable.FilterFor(ServiceHeader);
MyTable.FilterFor(SomeOtherHeader);
Why did I show those examples?
I want to raise some awareness that it pays off thinking about extensibility and integration scenarios during the initial development. Not only for someone else to extend it, but also for yourself, if you want to extend your functionality later.
Those were a couple very simple examples why it makes sense to generalize your approach. If you would “hard code” the available document tables or if you would even create different tables for different document sub tables, you would have to trace down every single occurrence in code and make sure that you add your new supported document table into the mix – and make sure you don’t make a mistake.
Thinking about it beforehand, it makes it a lot easier to extend the functionality. If you have everything coded for the sales header support, you just copy the codeunit that you have created to subscribe to the proper events and switch it over to use the service header instead of the sales header (and maybe make some other small changes). But, you do not have to touch any other code or spend hours finding all references of something in the code. It is a lot cleaner and makes it easier in the future.
I will continue throwing some of those thoughts and ideas into the mix to share with you what I am doing. I have not done everything from the beginning, but over time, things like that became a habit of mine so that we have clean code and easy extensibility in the future.
Disclaimer: Those are only my opinions illustrated. Those are not official best practice guidelines or rules. I do not provide any support or warranty for the code, it is shown and can be used as is.
No animals were harmed during the writing of this post, but one coffee maker was abused.
*This post is locked for comments