More prettifications
This commit is contained in:
parent
71e12a44a4
commit
7ff6ff73fa
11 changed files with 248 additions and 249 deletions
|
|
@ -2,17 +2,17 @@
|
||||||
title = "Argument Table"
|
title = "Argument Table"
|
||||||
weight = 220
|
weight = 220
|
||||||
+++
|
+++
|
||||||
# Argument table pattern
|
## Argument table pattern
|
||||||
|
|
||||||
_Originally By Nikola Kukrika and Waldo_
|
_Originally By Nikola Kukrika and waldo_
|
||||||
|
|
||||||
# Abstract
|
### 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).
|
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]
|
[![ ][image0]][anchor0]
|
||||||
|
|
||||||
# Problem
|
### Problem
|
||||||
|
|
||||||
In CAL overloading function signature is not supported. It is also not possible to provide default values for the function arguments.
|
In CAL overloading function signature is not supported. It is also not possible to provide default values for the function arguments.
|
||||||
|
|
||||||
|
|
@ -24,7 +24,7 @@ The last problem that can be solved is high number of arguments. Functions with
|
||||||
|
|
||||||
Few examples of the bad implementations are as illustrated here:
|
Few examples of the bad implementations are as illustrated here:
|
||||||
|
|
||||||
## Bad example 1
|
#### Bad example 1
|
||||||
|
|
||||||
|
|
||||||
```AL
|
```AL
|
||||||
|
|
@ -40,7 +40,7 @@ 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.
|
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
|
#### Bad example 2
|
||||||
|
|
||||||
```AL
|
```AL
|
||||||
LOCAL PROCEDURE GetTableSyncSetupW1@3(OldTableId@1002 : Integer; VAR UpgradeTableId@1001 : Integer; VAR TableUpgradeMode@1000 : 'Check, Copy, Move, Force') : Boolean;
|
LOCAL PROCEDURE GetTableSyncSetupW1@3(OldTableId@1002 : Integer; VAR UpgradeTableId@1001 : Integer; VAR TableUpgradeMode@1000 : 'Check, Copy, Move, Force') : Boolean;
|
||||||
|
|
@ -61,7 +61,7 @@ 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.
|
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
|
### 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.
|
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.
|
||||||
|
|
||||||
|
|
@ -73,7 +73,7 @@ Argument table should preferably be a temporary table since the implementation i
|
||||||
|
|
||||||
The examples of usages addressing problems shown above are:
|
The examples of usages addressing problems shown above are:
|
||||||
|
|
||||||
## Good example 1
|
#### Good example 1
|
||||||
|
|
||||||
New table
|
New table
|
||||||
```AL
|
```AL
|
||||||
|
|
@ -92,7 +92,7 @@ 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.
|
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 2
|
||||||
|
|
||||||
Good example
|
Good example
|
||||||
```AL
|
```AL
|
||||||
|
|
@ -106,17 +106,17 @@ 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.
|
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
|
### Downsides
|
||||||
|
|
||||||
You need to create one more table
|
You need to create one more table
|
||||||
|
|
||||||
Complex types can't be embedded as fields in tables (cannot have a record field type etc).
|
Complex types can't be embedded as fields in tables (cannot have a record field type etc).
|
||||||
|
|
||||||
# NAV Usages
|
### NAV Usages
|
||||||
|
|
||||||
Upgrade Codeunits
|
Upgrade Codeunits
|
||||||
|
|
||||||
# Related Patterns
|
### Related Patterns
|
||||||
|
|
||||||
Posting Routine, Select behavior: Setting fields on existing records in order not to change the signatures.
|
Posting Routine, Select behavior: Setting fields on existing records in order not to change the signatures.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,11 @@ weight = 470
|
||||||
+++
|
+++
|
||||||
_Written by Bogdan Andrei Sturzoiu, at Microsoft Development Center Copenhagen_
|
_Written by Bogdan Andrei Sturzoiu, at Microsoft Development Center Copenhagen_
|
||||||
|
|
||||||
**Abstract**
|
## Abstract
|
||||||
|
|
||||||
This pattern implements a generic mechanism for dynamically restricting and allowing usage of a record by the business process administrator.
|
This pattern implements a generic mechanism for dynamically restricting and allowing usage of a record by the business process administrator.
|
||||||
|
|
||||||
**Problem**
|
## 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.
|
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.
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ The blocked entity pattern involves:
|
||||||
|
|
||||||
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).
|
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**
|
## Solution
|
||||||
|
|
||||||
This pattern describes a generic mechanism of adding and lifting restrictions for any type of record.
|
This pattern describes a generic mechanism of adding and lifting restrictions for any type of record.
|
||||||
|
|
||||||
|
|
@ -40,9 +40,9 @@ Currently, the restrictions are record-based and type-less. They act as simple t
|
||||||
|
|
||||||
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.
|
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**
|
|
||||||
|
## Example
|
||||||
|
|
||||||
For example, we want to restrict posting Gen. Journal Lines if a customer has not been added in Account No. field.
|
For example, we want to restrict posting Gen. Journal Lines if a customer has not been added in Account No. field.
|
||||||
|
|
||||||
|
|
@ -52,7 +52,7 @@ For this, the following components are needed:
|
||||||
2. When you validate a Customer No. as Account no. and Customer as Account Type, lift the restrictions by calling AllowRecordUsage in COD1550\.
|
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.
|
3. The consumption of the restriction at posting is already implemented as an event in TAB81, OnCheckGenJournalLinePostRestrictions. No further action necessary.
|
||||||
|
|
||||||
**NAV Usage**
|
## 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\.[
|
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]
|
][anchor2]
|
||||||
|
|
@ -88,15 +88,15 @@ 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).
|
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**
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
Currently, there can only be one restriction per record. There are no restriction types.
|
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.
|
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**
|
## NAV Versions
|
||||||
|
|
||||||
This pattern has been introduced in Dynamics NAV 2016\.
|
This pattern has been introduced in Dynamics NAV 2016\.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,21 @@ weight = 370
|
||||||
+++
|
+++
|
||||||
_Originally by Jan Hoek at IDYN _
|
_Originally by Jan Hoek at IDYN _
|
||||||
|
|
||||||
## **Abstract**
|
## 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".
|
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**
|
## 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.
|
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.
|
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**
|
## 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).
|
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**
|
## 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.
|
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.
|
||||||
|
|
||||||
|
|
@ -26,7 +26,7 @@ In the base application, this pattern can be found in Search Name/Search Descrip
|
||||||
|
|
||||||
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.
|
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**
|
## 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.
|
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.
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
|
|
@ -50,6 +50,7 @@ The Copy Document pattern involves the following entities:
|
||||||
* Source Document No.
|
* Source Document No.
|
||||||
* Include Header (optional)
|
* Include Header (optional)
|
||||||
* Recalculate Lines (optional)
|
* Recalculate Lines (optional)
|
||||||
|
|
||||||
Example: REP901, Copy Assembly Document
|
Example: REP901, Copy Assembly Document
|
||||||
|
|
||||||
[![ ][image0]][anchor0]
|
[![ ][image0]][anchor0]
|
||||||
|
|
@ -103,16 +104,16 @@ In the standard version of Microsoft Dynamics NAV, the Copy Document functionali
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[anchor0]: /cfs-file/__key/communityserver-wikis-components-files/00-00-00-00-42/clip_5F00_image002.gif
|
[anchor0]: clip_image002.gif-750x0.png
|
||||||
[anchor1]: /cfs-file/__key/communityserver-wikis-components-files/00-00-00-00-42/clip_5F00_image004.gif
|
[anchor1]: clip_image004.gif-750x0.png
|
||||||
[anchor2]: clip_5F00_image006.jpg
|
[anchor2]: clip_5F00_image006.jpg
|
||||||
[anchor3]: clip_5F00_image008.jpg
|
[anchor3]: clip_5F00_image008.jpg
|
||||||
[anchor4]: clip_5F00_image010.jpg
|
[anchor4]: clip_5F00_image010.jpg
|
||||||
[anchor5]: https://www.youtube.com/watch?v=aTiwroXwW_0&list=PLhZ3P-LY7CqmVszuvtJLujFyHpsVN0U_w&index=17
|
[anchor5]: https://www.youtube.com/watch?v=aTiwroXwW_0&list=PLhZ3P-LY7CqmVszuvtJLujFyHpsVN0U_w&index=17
|
||||||
|
|
||||||
|
|
||||||
[image0]: /resized-image/__size/750x0/__key/communityserver-wikis-components-files/00-00-00-00-42/clip_5F00_image002.gif
|
[image0]: clip_image002.gif-750x0.png
|
||||||
[image1]: /resized-image/__size/750x0/__key/communityserver-wikis-components-files/00-00-00-00-42/clip_5F00_image004.gif
|
[image1]: clip_image004.gif-750x0.png
|
||||||
[image2]: clip_5F00_image006.jpg
|
[image2]: clip_5F00_image006.jpg
|
||||||
[image3]: clip_5F00_image008.jpg
|
[image3]: clip_5F00_image008.jpg
|
||||||
[image4]: clip_5F00_image010.jpg
|
[image4]: clip_5F00_image010.jpg
|
||||||
|
|
|
||||||
|
|
@ -2,22 +2,22 @@
|
||||||
title = "Cross Session Events"
|
title = "Cross Session Events"
|
||||||
weight = 430
|
weight = 430
|
||||||
+++
|
+++
|
||||||
#### _By Nikolai L'Estrange, from TVision Technology Ltd. in the UK__
|
|
||||||
_
|
|
||||||
|
|
||||||
### Abstract
|
_By Nikolai L'Estrange, from TVision Technology Ltd. in the UK_
|
||||||
|
|
||||||
|
## Abstract
|
||||||
|
|
||||||
Track things that happen in other NAV Sessions.
|
Track things that happen in other NAV Sessions.
|
||||||
|
|
||||||
[![ ][image0]][anchor0]
|
[![ ][image0]][anchor0]
|
||||||
|
|
||||||
### Problem
|
## Problem
|
||||||
|
|
||||||
In Microsoft Dynamics NAV you can fire a function whenever something changes within your session (and from NAV 2016 this is even easier with the new Event model), however there is not an easy way to know what is happening in other sessions. Sometimes you would like to know what has happened since your last read, without reading everything again, e.g. when you need to pass a large dataset to a Control Add-in.
|
In Microsoft Dynamics NAV you can fire a function whenever something changes within your session (and from NAV 2016 this is even easier with the new Event model), however there is not an easy way to know what is happening in other sessions. Sometimes you would like to know what has happened since your last read, without reading everything again, e.g. when you need to pass a large dataset to a Control Add-in.
|
||||||
|
|
||||||
A common way of handling this with Ledger Tables is to make note of the last record you read, and continuously poll to see if there are any new records. However this is restricted to strictly sequentially entered tables.
|
A common way of handling this with Ledger Tables is to make note of the last record you read, and continuously poll to see if there are any new records. However this is restricted to strictly sequentially entered tables.
|
||||||
|
|
||||||
### Solution
|
## Solution
|
||||||
|
|
||||||
There is a common pattern in many other languages called [Publish-Subscribe][anchor1] (or PubSub) that solves the same issue. We can implement the same pattern in NAV using a Table as a message queue platform and polling this table. We have named this pattern "Cross Session Events" in order to avoid confusion with the standard NAV Events which use the terms Publisher and Subscriber, and to try and describe more accurately when you would need this pattern.
|
There is a common pattern in many other languages called [Publish-Subscribe][anchor1] (or PubSub) that solves the same issue. We can implement the same pattern in NAV using a Table as a message queue platform and polling this table. We have named this pattern "Cross Session Events" in order to avoid confusion with the standard NAV Events which use the terms Publisher and Subscriber, and to try and describe more accurately when you would need this pattern.
|
||||||
|
|
||||||
|
|
@ -28,25 +28,26 @@ The pattern has four components:
|
||||||
* **Message Broker**: This distributes all messages sent in to all Subscribers that have expressed an interest (i.e. the message is within their filters).
|
* **Message Broker**: This distributes all messages sent in to all Subscribers that have expressed an interest (i.e. the message is within their filters).
|
||||||
* **Message Queue**: To hold the messages for each Subscriber. Generally once these messages are read, they are deleted.
|
* **Message Queue**: To hold the messages for each Subscriber. Generally once these messages are read, they are deleted.
|
||||||
|
|
||||||
### Example
|
## Example
|
||||||
|
|
||||||
An example of this would be when we have multiple users looking at the same set of data and we want their screens to update in "real time" whenever one of them makes a change, without doing a full refresh. We will use the [Observer pattern][anchor2] to capture the change (act as the Publisher) and then create a Table to hold Subscribers and Filters (Change Observer), a Table to be the Message Queue (Change Notification), and a Codeunit to be the Message Broker and help with the polling (ObserverMgt).
|
An example of this would be when we have multiple users looking at the same set of data and we want their screens to update in "real time" whenever one of them makes a change, without doing a full refresh. We will use the [Observer pattern][anchor2] to capture the change (act as the Publisher) and then create a Table to hold Subscribers and Filters (Change Observer), a Table to be the Message Queue (Change Notification), and a Codeunit to be the Message Broker and help with the polling (ObserverMgt).
|
||||||
|
|
||||||
Below are the table definitions:
|
Below are the table definitions:
|
||||||
|
```al
|
||||||
**Change Observer:
|
//Change Observer:
|
||||||
**"Table ID" Integer "Observable Table"
|
//"Table ID" Integer "Observable Table"
|
||||||
"Server ID" Integer
|
"Server ID" Integer
|
||||||
"Session ID" Integer
|
"Session ID" Integer
|
||||||
|
|
||||||
**Change Notification:
|
//Change Notification:
|
||||||
**"Table ID" Integer "Observable Table"
|
//"Table ID" Integer "Observable Table"
|
||||||
"Server ID" Integer
|
"Server ID" Integer
|
||||||
"Session ID" Integer
|
"Session ID" Integer
|
||||||
"Entry No." Integer AutoIncrement
|
"Entry No." Integer AutoIncrement
|
||||||
"Type of Change" Option Insert,Modify,Delete,Rename
|
"Type of Change" Option Insert,Modify,Delete,Rename
|
||||||
"Record ID" RecordID
|
"Record ID" RecordID
|
||||||
... (other fields to indicate what has changed)
|
... (other fields to indicate what has changed)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
The Change Observer table identifies the Subscriber using Server ID and Session ID, and then in this example there is only one filter, which is the Table ID we want to listen to any changes. In this case all three fields are in the Primary Key.
|
The Change Observer table identifies the Subscriber using Server ID and Session ID, and then in this example there is only one filter, which is the Table ID we want to listen to any changes. In this case all three fields are in the Primary Key.
|
||||||
|
|
@ -56,83 +57,93 @@ The Change Notification table then has the same three fields plus an Entry No. a
|
||||||
_**Note:** _Other examples of the pattern could have very different fields to identify the Subscriber, Filters and then whatever fields needed for content of the Message.
|
_**Note:** _Other examples of the pattern could have very different fields to identify the Subscriber, Filters and then whatever fields needed for content of the Message.
|
||||||
|
|
||||||
Our Message Broker Codeunit will also serve as a central place to create Subscribers (Listen and StopListening functions) and a place to Poll for Messages. Note that the Poll function deletes the Messages as it reads them.
|
Our Message Broker Codeunit will also serve as a central place to create Subscribers (Listen and StopListening functions) and a place to Poll for Messages. Note that the Poll function deletes the Messages as it reads them.
|
||||||
|
```al
|
||||||
|
//Listen(TableID : Integer)**
|
||||||
|
WITH Observer DO BEGIN
|
||||||
|
"Table ID" := TableID;
|
||||||
|
"Server ID" := SERVICEINSTANCEID;
|
||||||
|
"Session ID" := SESSIONID;
|
||||||
|
INSERT(TRUE);
|
||||||
|
COMMIT;
|
||||||
|
END;
|
||||||
|
|
||||||
**Listen(TableID : Integer)**
|
//StopListening(TableID : Integer)
|
||||||
WITH Observer DO BEGIN
|
WITH Observer DO BEGIN
|
||||||
"Table ID" := TableID;
|
RESET;
|
||||||
"Server ID" := SERVICEINSTANCEID;
|
SETRANGE("Server ID",SERVICEINSTANCEID);
|
||||||
"Session ID" := SESSIONID;
|
SETRANGE("Session ID",SESSIONID);
|
||||||
INSERT(TRUE);
|
SETRANGE("Table ID",TableID);
|
||||||
COMMIT;
|
DELETEALL(TRUE);
|
||||||
|
COMMIT;
|
||||||
END;
|
END;
|
||||||
**StopListening(TableID : Integer)
|
|
||||||
**WITH Observer DO BEGIN
|
//NotifyAll(ChangeNotification : Record "Change Notification")**
|
||||||
RESET;
|
|
||||||
SETRANGE("Server ID",SERVICEINSTANCEID);
|
|
||||||
SETRANGE("Session ID",SESSIONID);
|
|
||||||
SETRANGE("Table ID",TableID);
|
|
||||||
DELETEALL(TRUE);
|
|
||||||
COMMIT;
|
|
||||||
END;
|
|
||||||
**NotifyAll(ChangeNotification : Record "Change Notification")**
|
|
||||||
WITH Observer DO BEGIN
|
WITH Observer DO BEGIN
|
||||||
RESET;
|
RESET;
|
||||||
SETRANGE("Table ID",ChangeNotification."Table ID");
|
SETRANGE("Table ID",ChangeNotification."Table ID");
|
||||||
IF FINDSET THEN REPEAT
|
IF FINDSET THEN REPEAT
|
||||||
Notify(Observer,ChangeNotification);
|
Notify(Observer,ChangeNotification);
|
||||||
UNTIL NEXT = 0;
|
UNTIL NEXT = 0;
|
||||||
END;
|
END;
|
||||||
**Notify(Observer : Record "Change Observer";ChangeNotification : Record "Change Notification")**
|
|
||||||
|
//Notify(Observer : Record "Change Observer";ChangeNotification : Record "Change Notification")**
|
||||||
WITH ChangeNotification DO BEGIN
|
WITH ChangeNotification DO BEGIN
|
||||||
"Server ID" := Observer."Server ID";
|
"Server ID" := Observer."Server ID";
|
||||||
"Session ID" := Observer."Session ID";
|
"Session ID" := Observer."Session ID";
|
||||||
"Entry No." := 0;
|
"Entry No." := 0;
|
||||||
INSERT;
|
INSERT;
|
||||||
END;
|
END;
|
||||||
**Poll(TableID : Integer;VAR TempChangeNotification : TEMPORARY Record "Change Notification")**
|
|
||||||
|
//Poll(TableID : Integer;VAR TempChangeNotification : TEMPORARY Record "Change Notification")**
|
||||||
WITH ChangeNotification DO BEGIN
|
WITH ChangeNotification DO BEGIN
|
||||||
TempChangeNotification.RESET;
|
TempChangeNotification.RESET;
|
||||||
TempChangeNotification.DELETEALL;
|
TempChangeNotification.DELETEALL;
|
||||||
RESET;
|
RESET;
|
||||||
SETRANGE("Table ID",TableID);
|
SETRANGE("Table ID",TableID);
|
||||||
SETRANGE("Server ID",SERVICEINSTANCEID);
|
SETRANGE("Server ID",SERVICEINSTANCEID);
|
||||||
SETRANGE("Session ID",SESSIONID);
|
SETRANGE("Session ID",SESSIONID);
|
||||||
IF FINDSET THEN REPEAT
|
IF FINDSET THEN REPEAT
|
||||||
TempChangeNotification := ChangeNotification;
|
TempChangeNotification := ChangeNotification;
|
||||||
TempChangeNotification.INSERT;
|
TempChangeNotification.INSERT;
|
||||||
MARK(TRUE);
|
MARK(TRUE);
|
||||||
UNTIL NEXT = 0;
|
UNTIL NEXT = 0;
|
||||||
MARKEDONLY(TRUE);
|
MARKEDONLY(TRUE);
|
||||||
DELETEALL;
|
DELETEALL;
|
||||||
END;
|
END;
|
||||||
|
```
|
||||||
|
|
||||||
**__**The final part of this example is an object that calls the functions above. In this example we will use a Page with a PingPong Timer Control to do the polling in (almost) real time. These are the functions on the page:
|
**__**The final part of this example is an object that calls the functions above. In this example we will use a Page with a PingPong Timer Control to do the polling in (almost) real time. These are the functions on the page:
|
||||||
|
|
||||||
**OnQueryClosePage(CloseAction : Action None) : Boolean**
|
```al
|
||||||
|
//OnQueryClosePage(CloseAction : Action None) : Boolean**
|
||||||
ObserverMgt.StopListening(DATABASE::"NAV Whiteboard Booking");
|
ObserverMgt.StopListening(DATABASE::"NAV Whiteboard Booking");
|
||||||
**Timer::AddInReady()**
|
|
||||||
|
//Timer::AddInReady()**
|
||||||
IF ObserverMgt.Listen(DATABASE::"NAV Whiteboard Booking") THEN
|
IF ObserverMgt.Listen(DATABASE::"NAV Whiteboard Booking") THEN
|
||||||
CurrPage.Timer.Ping(1000);
|
CurrPage.Timer.Ping(1000);
|
||||||
**Timer::Pong()**
|
|
||||||
|
//Timer::Pong()**
|
||||||
CallUpdate;
|
CallUpdate;
|
||||||
CurrPage.Timer.Ping(1000);
|
CurrPage.Timer.Ping(1000);
|
||||||
**LOCAL CallUpdate()**
|
|
||||||
|
//LOCAL CallUpdate()**
|
||||||
ObserverMgt.Poll(DATABASE::"NAV Whiteboard Booking",TempChangeNotification);
|
ObserverMgt.Poll(DATABASE::"NAV Whiteboard Booking",TempChangeNotification);
|
||||||
WITH TempChangeNotification DO BEGIN
|
WITH TempChangeNotification DO BEGIN
|
||||||
IF FINDSET THEN REPEAT
|
IF FINDSET THEN REPEAT
|
||||||
IF "Type of Change" = "Type of Change"::Delete THEN BEGIN
|
IF "Type of Change" = "Type of Change"::Delete THEN BEGIN
|
||||||
...
|
...
|
||||||
END ELSE IF RecRef.GET("Record ID") THEN BEGIN
|
END ELSE IF RecRef.GET("Record ID") THEN BEGIN
|
||||||
...
|
...
|
||||||
END;
|
END;
|
||||||
UNTIL NEXT = 0;
|
UNTIL NEXT = 0;
|
||||||
END;
|
END;
|
||||||
|
```
|
||||||
|
|
||||||
### Consequences
|
## Consequences
|
||||||
|
|
||||||
The PingPong control is only available on the Windows Client, so if you want to use another client you will need to use another solution to Poll for Messages. Therefore this pattern is not always going to be "real time".
|
The PingPong control is only available on the Windows Client, so if you want to use another client you will need to use another solution to Poll for Messages. Therefore this pattern is not always going to be "real time".
|
||||||
|
|
||||||
### Related Topics
|
## Related Topics
|
||||||
|
|
||||||
This pattern was originally described in the following blog:
|
This pattern was originally described in the following blog:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,12 @@ A side effect is reduced and simplified code, increased performance and a more s
|
||||||
|
|
||||||
There is no way in NAV to get a set of records from the database, which all have the newest starting date, that is less than or equal to today's date. Having an ending date on the record will help, but it introduces some other problems. In Dynamics NAV this is normally done by reading too many records, either at the SQL Server level or in the middle tier and throw away the ones you do not need. That is a waste of resources:
|
There is no way in NAV to get a set of records from the database, which all have the newest starting date, that is less than or equal to today's date. Having an ending date on the record will help, but it introduces some other problems. In Dynamics NAV this is normally done by reading too many records, either at the SQL Server level or in the middle tier and throw away the ones you do not need. That is a waste of resources:
|
||||||
|
|
||||||
< --\[if supportLists\]--\>\* < --\[endif\]--\>The SQL Server is reading too many records
|
* The SQL Server is reading too many records
|
||||||
|
|
||||||
< --\[if supportLists\]--\>\* < --\[endif\]--\>There would be too much data sent over the network.
|
* There would be too much data sent over the network.
|
||||||
(If the SQL Server and the NAV Service tier are on different machines.)
|
(If the SQL Server and the NAV Service tier are on different machines.)
|
||||||
|
|
||||||
< --\[if supportLists\]--\>\* < --\[endif\]--\>The NAV Service Tier receives and throws away data.
|
* The NAV Service Tier receives and throws away data.
|
||||||
|
|
||||||
### Ending Date Problem
|
### Ending Date Problem
|
||||||
|
|
||||||
|
|
@ -38,99 +38,85 @@ Use the pattern whenever you read a set of data containing a Starting Date and y
|
||||||
|
|
||||||
In the following example, we have a fictive table containing: Code, Starting Date and Price. The Primary Key consist of Code, Starting Date. The Database is the Demo Database, and the Company is Cronus.
|
In the following example, we have a fictive table containing: Code, Starting Date and Price. The Primary Key consist of Code, Starting Date. The Database is the Demo Database, and the Company is Cronus.
|
||||||
|
|
||||||
< --\[if supportLineBreakNewLine\]--\>[![ ][image0]][anchor0]
|
[![ ][image0]][anchor0]
|
||||||
< --\[endif\]--\>
|
|
||||||
|
|
||||||
### 1\. Create the view
|
|
||||||
|
### 1. Create the view
|
||||||
|
|
||||||
You will need to create the view before you define the Table Object.
|
You will need to create the view before you define the Table Object.
|
||||||
You will need to create a view for every company in the database.
|
You will need to create a view for every company in the database.
|
||||||
|
```sql
|
||||||
CREATE VIEW \[dbo\].\[CRONUS$PriceView\]
|
CREATE VIEW [dbo].[CRONUS$PriceView]
|
||||||
AS
|
AS
|
||||||
SELECT \[Code\], \[Starting Date\], \[Price\]
|
SELECT [Code], [Starting Date], [Price]
|
||||||
FROM dbo.\[CRONUS$Price\] AS A
|
FROM dbo.[CRONUS$Price] AS A
|
||||||
WHERE \[Starting Date\] =
|
WHERE [Starting Date] =
|
||||||
(SELECT MAX(\[Starting Date\])
|
(SELECT MAX([Starting Date])
|
||||||
FROM dbo.\[CRONUS$Price\] AS B
|
FROM dbo.[CRONUS$Price] AS B
|
||||||
WHERE B.\[Code\] = A.\[Code\] AND
|
WHERE B.[Code] = A.[Code] AND
|
||||||
B.\[Starting Date\] <= GETDATE())
|
B.[Starting Date] <= GETDATE())
|
||||||
|
```
|
||||||
|
|
||||||
Test the view to ensure that you get the correct result. It is much easier to test now than later.
|
Test the view to ensure that you get the correct result. It is much easier to test now than later.
|
||||||
|
|
||||||
### 2\. Create the Table object
|
### 2. Create the Table object
|
||||||
|
|
||||||
Remember to set the link table property before you save it.
|
Remember to set the link table property before you save it.
|
||||||
|
|
||||||
### 3\. Implement the code
|
### 3. Implement the code
|
||||||
|
|
||||||
IF PriceView.FINDSET THEN // You have them
|
IF PriceView.FINDSET THEN // You have them
|
||||||
|
|
||||||
### 4\. Create a deployment codeunit
|
### 4. Create a deployment codeunit
|
||||||
|
|
||||||
Create a SQL Deployment codeunit to manage your views.
|
Create a SQL Deployment codeunit to manage your views.
|
||||||
The codeunit needs to Create or Alter the views for all companies.
|
The codeunit needs to Create or Alter the views for all companies.
|
||||||
To see an example of how to talk to SQL Server using .NET see Waldo's blog here:
|
To see an example of how to talk to SQL Server using .NET see waldo's blog here:
|
||||||
[http://dynamicsuser.net/blogs/waldo/archive/2011/07/19/net-interop-calling-stored-procedures-on-sql-server-example-1.aspx][anchor1]
|
[http://dynamicsuser.net/blogs/waldo/archive/2011/07/19/net-interop-calling-stored-procedures-on-sql-server-example-1.aspx][anchor1]
|
||||||
< --\[if supportLineBreakNewLine\]--\>
|
|
||||||
< --\[endif\]--\>
|
|
||||||
|
|
||||||
### 5\. Deployment
|
### 5. Deployment
|
||||||
|
|
||||||
You need to deploy in three steps:
|
You need to deploy in three steps:
|
||||||
|
|
||||||
< --\[if supportLists\]--\>1\. < --\[endif\]--\>Delete the table objects referencing the views
|
1. Delete the table objects referencing the views
|
||||||
|
2. Deploy and run the deployment codeunit
|
||||||
< --\[if supportLists\]--\>2\. < --\[endif\]--\>Deploy and run the deployment codeunit
|
3. Deploy the new table objects that reference the views
|
||||||
|
|
||||||
< --\[if supportLists\]--\>3\. < --\[endif\]--\>Deploy the new table objects that reference the views
|
|
||||||
|
|
||||||
### General precaution
|
### General precaution
|
||||||
|
|
||||||
If you later want to change the view, you need to follow these rules:
|
If you later want to change the view, you need to follow these rules:
|
||||||
|
|
||||||
< --\[if supportLists\]--\>\* < --\[endif\]--\>If you add columns, you need to add them to the view first and then add them to the Table Object.
|
\* If you add columns, you need to add them to the view first and then add them to the Table Object.
|
||||||
|
\* If you want to remove columns from the view, you need to delete the Table Object, then change the view and last recreate the Table Object without the new columns.
|
||||||
< --\[if supportLists\]--\>\* < --\[endif\]--\>If you want to remove columns from the view, you need to delete the Table Object, then change the view and last recreate the Table Object without the new columns.
|
|
||||||
|
|
||||||
### Code example that accomplish the same but without using the pattern
|
### Code example that accomplish the same but without using the pattern
|
||||||
|
|
||||||
This following example will give you the same result but the performance will deteriorate as time goes by and you get more and more old data.
|
This following example will give you the same result but the performance will deteriorate as time goes by and you get more and more old data.
|
||||||
|
```al
|
||||||
Price.SETCURRENTKEY(Code,"Starting Date");
|
Price.SETCURRENTKEY(Code,"Starting Date");
|
||||||
|
|
||||||
Price.SETFILTER("Starting Date",'..%1', TODAY
|
Price.SETFILTER("Starting Date",'..%1', TODAY
|
||||||
|
|
||||||
IF Price.FINDSET THEN BEGIN
|
IF Price.FINDSET THEN BEGIN
|
||||||
REPEAT
|
REPEAT
|
||||||
|
Price.SETRANGE(Code, Price.Code);
|
||||||
Price.SETRANGE(Code, Price.Code);
|
Price.FINDLAST;
|
||||||
|
Price.SETRANGE(Code);
|
||||||
Price.FINDLAST;
|
PriceTemp := Price;
|
||||||
|
PriceTemp.INSERT;
|
||||||
Price.SETRANGE(Code);
|
UNTIL Price.NEXT = 0;
|
||||||
|
|
||||||
PriceTemp := Price;
|
|
||||||
|
|
||||||
PriceTemp.INSERT;
|
|
||||||
|
|
||||||
UNTIL Price.NEXT = 0;
|
|
||||||
|
|
||||||
END;
|
END;
|
||||||
// PriceTemp will contain the Prices
|
// PriceTemp will contain the Prices
|
||||||
|
```
|
||||||
|
|
||||||
### Comparison
|
### Comparison
|
||||||
|
|
||||||
The above NAV example is for a very simple date controlled solution and is provided to give an idea of what the pattern changes seen from a NAV development point of view. But consider the following:
|
The above NAV example is for a very simple date controlled solution and is provided to give an idea of what the pattern changes seen from a NAV development point of view. But consider the following:
|
||||||
|
|
||||||
< --\[if supportLists\]--\>\* < --\[endif\]--\>The table has a more complex key.
|
* The table has a more complex key.
|
||||||
This will require setting and clearing more filters
|
This will require setting and clearing more filters
|
||||||
|
* You need to read from more than one table.
|
||||||
< --\[if supportLists\]--\>\* < --\[endif\]--\>You need to read from more than one table.
|
|
||||||
Say you need to apply discount from a separate table.
|
Say you need to apply discount from a separate table.
|
||||||
This may give several lines in PriceTemp.
|
This may give several lines in PriceTemp.
|
||||||
|
* If the Code field is controlled by a Type field.
|
||||||
< --\[if supportLists\]--\>\* < --\[endif\]--\>If the Code field is controlled by a Type field.
|
|
||||||
The Code field reference keys in different tables
|
The Code field reference keys in different tables
|
||||||
|
|
||||||
All three examples above can be implemented directly in the view. By using the pattern, it will still only require a single line of NAV code.
|
All three examples above can be implemented directly in the view. By using the pattern, it will still only require a single line of NAV code.
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,15 @@ _By David Bastide and Soumya Dutta at Microsoft Development Center Copenhagen_
|
||||||
|
|
||||||
[![ ][image0]][anchor0]
|
[![ ][image0]][anchor0]
|
||||||
|
|
||||||
## **Context:**
|
## Context:
|
||||||
|
|
||||||
This pattern is describing how you can migrate data using the Data Migration Façade.
|
This pattern is describing how you can migrate data using the Data Migration Façade.
|
||||||
|
|
||||||
## **Problem:**
|
## Problem:
|
||||||
|
|
||||||
Writing migration code from an external source, such as a product from a competitor, can be time consuming, as you need to tackle the problems of what to migrate, in which order, exception handling... and can result in code that is fragile due to deep dependencies on the NAV data model (high coupling). Any change to the NAV data model can easily break this code. ****
|
Writing migration code from an external source, such as a product from a competitor, can be time consuming, as you need to tackle the problems of what to migrate, in which order, exception handling... and can result in code that is fragile due to deep dependencies on the NAV data model (high coupling). Any change to the NAV data model can easily break this code. ****
|
||||||
|
|
||||||
## **Solution:**
|
## Solution:
|
||||||
|
|
||||||
The idea of the Data Migration Façade is to provide an API that creates and updates master data and related entities (including transactions) without referencing NAV tables.
|
The idea of the Data Migration Façade is to provide an API that creates and updates master data and related entities (including transactions) without referencing NAV tables.
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ The façade framework has the following components:
|
||||||
|
|
||||||
* * **Data Migration Overview** (page 1799)
|
* * **Data Migration Overview** (page 1799)
|
||||||
|
|
||||||
## **Usage:**
|
## Usage:
|
||||||
|
|
||||||
There are the following use cases:
|
There are the following use cases:
|
||||||
|
|
||||||
|
|
@ -55,7 +55,7 @@ This deletes existing status lines for migrating Items for 'My Migration Type',
|
||||||
**"Data Migration Façade".StartMigration('My Migration Type',false) **
|
**"Data Migration Façade".StartMigration('My Migration Type',false) **
|
||||||
This starts the migration. False means this is not a retry. A re-try is when you migrate one or more records from the **Show Errors** page, which is described later in this document. Retry = true is only used by the **Show Errors** page and should not be used from extensions.
|
This starts the migration. False means this is not a retry. A re-try is when you migrate one or more records from the **Show Errors** page, which is described later in this document. Retry = true is only used by the **Show Errors** page and should not be used from extensions.
|
||||||
|
|
||||||
### **Usage without staging tables:**
|
### Usage without staging tables:
|
||||||
|
|
||||||
The overall workflow is:
|
The overall workflow is:
|
||||||
|
|
||||||
|
|
@ -75,58 +75,58 @@ The overall workflow is:
|
||||||
_Figure 1: sequence diagram of the data migration without staging tables _
|
_Figure 1: sequence diagram of the data migration without staging tables _
|
||||||
|
|
||||||
The following example shows how to migrate items without staging tables:
|
The following example shows how to migrate items without staging tables:
|
||||||
|
```al
|
||||||
trigger OnRun();
|
trigger OnRun();
|
||||||
var
|
var
|
||||||
ItemDataMigrationFacade: Codeunit "Item Data Migration Facade";
|
ItemDataMigrationFacade: Codeunit "Item Data Migration Facade";
|
||||||
ItemNumber: Integer;
|
ItemNumber: Integer;
|
||||||
ItemJson: Text;
|
ItemJson: Text;
|
||||||
begin
|
begin
|
||||||
// loop on items retrieved through a web service for example
|
// loop on items retrieved through a web service for example
|
||||||
for ItemNumber := 1 to ExternalWebService.GetItemCount do begin
|
for ItemNumber := 1 to ExternalWebService.GetItemCount do begin
|
||||||
ExternalWebService.GetItem(ItemNumber,ItemJson);
|
ExternalWebService.GetItem(ItemNumber,ItemJson);
|
||||||
// create item using the facade
|
// create item using the facade
|
||||||
if not ItemDataMigrationFacade.CreateItemIfNeeded(ItemJson.ItemNumber,ItemJson.ItemName1,
|
if not ItemDataMigrationFacade.CreateItemIfNeeded(ItemJson.ItemNumber,ItemJson.ItemName1,
|
||||||
ItemJson.ItemName2,ConvertItemType(ItemJson.ItemType)) then
|
ItemJson.ItemName2,ConvertItemType(ItemJson.ItemType)) then
|
||||||
exit; // item already exists
|
exit; // item already exists
|
||||||
// set some fields using the facade
|
// set some fields using the facade
|
||||||
ItemDataMigrationFacade.SetVendorItemNo(ItemJson.VendItemNumber);
|
ItemDataMigrationFacade.SetVendorItemNo(ItemJson.VendItemNumber);
|
||||||
ItemDataMigrationFacade.SetUnitVolume(ItemJson.Volume);
|
ItemDataMigrationFacade.SetUnitVolume(ItemJson.Volume);
|
||||||
ItemDataMigrationFacade.SetAlternativeItemNo(ItemJson.AltItemNumber);
|
ItemDataMigrationFacade.SetAlternativeItemNo(ItemJson.AltItemNumber);
|
||||||
if ItemJson.PrimaryVendor <\> '' then
|
if ItemJson.PrimaryVendor <\> '' then
|
||||||
ItemDataMigrationFacade.SetVendorNo(ItemJson.PrimaryVendor);
|
ItemDataMigrationFacade.SetVendorNo(ItemJson.PrimaryVendor);
|
||||||
// migrate dependencies
|
// migrate dependencies
|
||||||
MigrateItemUnitOfMeasure(ItemDataMigrationFacade,ItemJson);
|
MigrateItemUnitOfMeasure(ItemDataMigrationFacade,ItemJson);
|
||||||
// modify the item (+run trigger) to save the changes made by setters
|
// modify the item (+run trigger) to save the changes made by setters
|
||||||
ItemDataMigrationFacade.ModifyItem(true);
|
ItemDataMigrationFacade.ModifyItem(true);
|
||||||
// update the status in the migration dashboard
|
// update the status in the migration dashboard
|
||||||
DataMigrationStatusFacade.IncrementMigratedRecordCount('My Migration Type',
|
DataMigrationStatusFacade.IncrementMigratedRecordCount('My Migration Type',
|
||||||
Database::Item,1);
|
Database::Item,1);
|
||||||
end;
|
end;
|
||||||
|
end;
|
||||||
|
procedure MigrateItemUnitOfMeasure(ItemDataMigrationFacade : Codeunit "Item Data Migration Facade";
|
||||||
|
ItemJson : Text);
|
||||||
|
var
|
||||||
|
MyUnitCodeStagingTable: Record "My Unit Code Staging Table";
|
||||||
|
DataMigrationStatusFacade: Codeunit "Data Migration Status Facade";
|
||||||
|
DescriptionToSet: Text\[10\];
|
||||||
|
UnitCodeJson: Text;
|
||||||
|
begin
|
||||||
|
if ItemJson.UnitCode = '' then
|
||||||
|
// log an error using the Data migration façade
|
||||||
|
DataMigrationStatusFacade.RegisterErrorNoStagingTablesCase(
|
||||||
|
'My Migration Type',Database::Item,'Unit of measure is empty.');
|
||||||
|
if ExternalWebService.GetUnitCode(ItemJson.UnitCode,UnitCodeJson) then
|
||||||
|
DescriptionToSet := UnitCodeJson.Description;
|
||||||
|
ItemDataMigrationFacade.CreateUnitOfMeasureIfNeeded(ItemJson.UnitCode, DescriptionToSet);
|
||||||
|
// set the unit of measure on the item
|
||||||
|
ItemDataMigrationFacade.SetBaseUnitOfMeasure(ItemJson.UnitCode);
|
||||||
end;
|
end;
|
||||||
procedure MigrateItemUnitOfMeasure(ItemDataMigrationFacade : Codeunit "Item Data Migration Facade";
|
```
|
||||||
ItemJson : Text);
|
|
||||||
var
|
|
||||||
MyUnitCodeStagingTable: Record "My Unit Code Staging Table";
|
|
||||||
DataMigrationStatusFacade: Codeunit "Data Migration Status Facade";
|
|
||||||
DescriptionToSet: Text\[10\];
|
|
||||||
UnitCodeJson: Text;
|
|
||||||
begin
|
|
||||||
if ItemJson.UnitCode = '' then
|
|
||||||
// log an error using the Data migration façade
|
|
||||||
DataMigrationStatusFacade.RegisterErrorNoStagingTablesCase(
|
|
||||||
'My Migration Type',Database::Item,'Unit of measure is empty.');
|
|
||||||
if ExternalWebService.GetUnitCode(ItemJson.UnitCode,UnitCodeJson) then
|
|
||||||
DescriptionToSet := UnitCodeJson.Description;
|
|
||||||
ItemDataMigrationFacade.CreateUnitOfMeasureIfNeeded(ItemJson.UnitCode, DescriptionToSet);
|
|
||||||
// set the unit of measure on the item
|
|
||||||
ItemDataMigrationFacade.SetBaseUnitOfMeasure(ItemJson.UnitCode);
|
|
||||||
end;
|
|
||||||
|
|
||||||
|
|
||||||
_Figure 2: Example of Item and Item Unit of Measure migration without staging tables _
|
_Figure 2: Example of Item and Item Unit of Measure migration without staging tables _
|
||||||
|
|
||||||
### **Usage with staging tables:**
|
### Usage with staging tables:
|
||||||
|
|
||||||
The overall workflow is:
|
The overall workflow is:
|
||||||
|
|
||||||
|
|
@ -146,77 +146,78 @@ The overall workflow is:
|
||||||
|
|
||||||
[![ ][image2]][anchor2]
|
[![ ][image2]][anchor2]
|
||||||
|
|
||||||
_Figure 3: Simplified sequence diagram of the data migration with staging tables _
|
_Figure 3: Simplified sequence diagram of the data migration with staging tables_
|
||||||
|
|
||||||
Below is a simplified example showing how to create an item:
|
Below is a simplified example showing how to create an item:
|
||||||
|
```al
|
||||||
\[EventSubscriber(ObjectType::Codeunit, Codeunit::"Item Data Migration Facade", 'OnMigrateItem', '', true, true)\]
|
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Item Data Migration Facade", 'OnMigrateItem', '', true, true)]
|
||||||
procedure OnMigrateItem(VAR Sender : Codeunit "Item Data Migration Facade";RecordIdToMigrate : RecordId);
|
procedure OnMigrateItem(VAR Sender : Codeunit "Item Data Migration Facade";RecordIdToMigrate : RecordId);
|
||||||
var
|
var
|
||||||
MyItemStagingTable : Record "My Item Staging Table";
|
MyItemStagingTable : Record "My Item Staging Table";
|
||||||
begin
|
begin
|
||||||
// handle the event if it targets this extension's staging table
|
// handle the event if it targets this extension's staging table
|
||||||
if RecordIdToMigrate.TableNo <\> Database::"My Item Staging Table" then
|
if RecordIdToMigrate.TableNo <\> Database::"My Item Staging Table" then
|
||||||
exit;
|
exit;
|
||||||
MyItemStagingTable.Get(RecordIdToMigrate);
|
MyItemStagingTable.Get(RecordIdToMigrate);
|
||||||
// create item using the facade
|
// create item using the facade
|
||||||
if not Sender.CreateItemIfNeeded(MyItemStagingTable.ItemNumber,MyItemStagingTable.ItemName1,
|
if not Sender.CreateItemIfNeeded(MyItemStagingTable.ItemNumber,MyItemStagingTable.ItemName1,
|
||||||
MyItemStagingTable.ItemName2,ConvertItemType(MyItemStagingTable.ItemType)) then
|
MyItemStagingTable.ItemName2,ConvertItemType(MyItemStagingTable.ItemType)) then
|
||||||
exit; // item already exists
|
exit; // item already exists
|
||||||
// set some fields using the facade
|
// set some fields using the facade
|
||||||
Sender.SetVendorItemNo(MyItemStagingTable.VendItemNumber);
|
Sender.SetVendorItemNo(MyItemStagingTable.VendItemNumber);
|
||||||
Sender.SetUnitVolume(MyItemStagingTable.Volume);
|
Sender.SetUnitVolume(MyItemStagingTable.Volume);
|
||||||
Sender.SetAlternativeItemNo(MyItemStagingTable.AltItemNumber);
|
Sender.SetAlternativeItemNo(MyItemStagingTable.AltItemNumber);
|
||||||
if MyItemStagingTable.PrimaryVendor <\> '' then
|
if MyItemStagingTable.PrimaryVendor <\> '' then
|
||||||
Sender.SetVendorNo(MyItemStagingTable.PrimaryVendor);
|
Sender.SetVendorNo(MyItemStagingTable.PrimaryVendor);
|
||||||
// modify the item (+run trigger) to save the changes made by setters
|
// modify the item (+run trigger) to save the changes made by setters
|
||||||
Sender.ModifyItem(true);
|
Sender.ModifyItem(true);
|
||||||
end;
|
end;
|
||||||
|
```
|
||||||
|
|
||||||
_Figure 4: Example of event subscriber for Item migration _
|
_Figure 4: Example of event subscriber for Item migration_
|
||||||
|
|
||||||
__
|
|
||||||
|
|
||||||
Below is another example showing how to use additional events to set fields that reference other tables, here the unit of measure:
|
Below is another example showing how to use additional events to set fields that reference other tables, here the unit of measure:
|
||||||
|
|
||||||
\[EventSubscriber(ObjectType::Codeunit, Codeunit::"Item Data Migration Facade", 'OnMigrateItemUnitOfMeasure', '', true, true)\]
|
```al
|
||||||
|
[EventSubscriber(ObjectType::Codeunit, Codeunit::"Item Data Migration Facade", 'OnMigrateItemUnitOfMeasure', '', true, true)]
|
||||||
procedure OnMigrateItemUnitOfMeasure(VAR Sender : Codeunit "Item Data Migration Facade";RecordIdToMigrate : RecordId);
|
procedure OnMigrateItemUnitOfMeasure(VAR Sender : Codeunit "Item Data Migration Facade";RecordIdToMigrate : RecordId);
|
||||||
var
|
var
|
||||||
MyItemStagingTable : Record "My Item Staging Table";
|
MyItemStagingTable : Record "My Item Staging Table";
|
||||||
MyUnitCodeStagingTable : Record "My Unit Code Staging Table";
|
MyUnitCodeStagingTable : Record "My Unit Code Staging Table";
|
||||||
DescriptionToSet: Text\[10\];
|
DescriptionToSet: Text[10];
|
||||||
begin
|
begin
|
||||||
// handle the event if it targets this extension's staging table
|
// handle the event if it targets this extension's staging table
|
||||||
if RecordIdToMigrate.TableNo <\> Database::"My Item Staging Table" then
|
if RecordIdToMigrate.TableNo <\> Database::"My Item Staging Table" then
|
||||||
exit;
|
exit;
|
||||||
MyItemStagingTable.Get(RecordIdToMigrate);
|
MyItemStagingTable.Get(RecordIdToMigrate);
|
||||||
if MyItemStagingTable.UnitCode = '' then
|
if MyItemStagingTable.UnitCode = '' then
|
||||||
error('Unit of measure is empty.');
|
error('Unit of measure is empty.');
|
||||||
MyUnitCodeStagingTable.SetRange(UnitCode,MyItemStagingTable.UnitCode);
|
MyUnitCodeStagingTable.SetRange(UnitCode,MyItemStagingTable.UnitCode);
|
||||||
if MyUnitCodeStagingTable.FindFirst then
|
if MyUnitCodeStagingTable.FindFirst then
|
||||||
DescriptionToSet := MyUnitCodeStagingTable.Description;
|
DescriptionToSet := MyUnitCodeStagingTable.Description;
|
||||||
// create the unit of measure through the facade
|
// create the unit of measure through the facade
|
||||||
Sender.CreateUnitOfMeasureIfNeeded(MyItemStagingTable.UnitCode, DescriptionToSet);
|
Sender.CreateUnitOfMeasureIfNeeded(MyItemStagingTable.UnitCode, DescriptionToSet);
|
||||||
// set the unit of measure on the item
|
// set the unit of measure on the item
|
||||||
Sender.SetBaseUnitOfMeasure(MyItemStagingTable.UnitCode);
|
Sender.SetBaseUnitOfMeasure(MyItemStagingTable.UnitCode);
|
||||||
// modify the item to save the changes made by setter
|
// modify the item to save the changes made by setter
|
||||||
Sender.ModifyItem(false);
|
Sender.ModifyItem(false);
|
||||||
end;
|
end;
|
||||||
|
```
|
||||||
|
|
||||||
_Figure 5: Example of event subscriber for Item Unit of Measure migration _
|
_Figure 5: Example of event subscriber for Item Unit of Measure migration_
|
||||||
|
|
||||||
### **Combining both approaches:**
|
### Combining both approaches:
|
||||||
|
|
||||||
If you want to migrate additional entities, the **Data Migration **framework lets you initialize the migration with entities other than master data. In this case, the **Data Migration Overview** page will show additional lines. Item, vendor, customer, an general ledger accounts are migrated with an event driven approach, and the additional entities are migrated by calling an extension codeunit **OnRun** method.
|
If you want to migrate additional entities, the **Data Migration **framework lets you initialize the migration with entities other than master data. In this case, the **Data Migration Overview** page will show additional lines. Item, vendor, customer, an general ledger accounts are migrated with an event driven approach, and the additional entities are migrated by calling an extension codeunit **OnRun** method.
|
||||||
|
|
||||||
## **Error handling with staging tables:**
|
## Error handling with staging tables:
|
||||||
|
|
||||||
The migration starts by calling **RUN** on the façade codeunit. Errors thrown during the call are captured by **GETLASTERRORTEXT** and displayed when you choose the **Show Errors** action on the **Data Migration Overview** page.
|
The migration starts by calling **RUN** on the façade codeunit. Errors thrown during the call are captured by **GETLASTERRORTEXT** and displayed when you choose the **Show Errors** action on the **Data Migration Overview** page.
|
||||||
|
|
||||||
[![ ][image3]][anchor3]
|
[![ ][image3]][anchor3]
|
||||||
|
|
||||||
_Figure 6: List of errors shown when clicking **Show Errors** on the **Data Migration Overview** page _
|
_Figure 6: List of errors shown when clicking **Show Errors** on the **Data Migration Overview** page_
|
||||||
|
|
||||||
__
|
__
|
||||||
|
|
||||||
|
|
@ -224,7 +225,7 @@ The **Edit Record** action opens a view of the staging table, where you can edit
|
||||||
|
|
||||||
[![ ][image4]][anchor4]
|
[![ ][image4]][anchor4]
|
||||||
|
|
||||||
_Figure 7: Edit a staging table record _
|
_Figure 7: Edit a staging table record_
|
||||||
|
|
||||||
__
|
__
|
||||||
|
|
||||||
|
|
@ -238,14 +239,14 @@ When migrating data without staging tables, errors can be registered manually by
|
||||||
|
|
||||||
Errors will be displayed in the error list, but you cannot open and edit records because there is no staging table. The Edit action will not be available.
|
Errors will be displayed in the error list, but you cannot open and edit records because there is no staging table. The Edit action will not be available.
|
||||||
|
|
||||||
## **Limitations:**
|
## Limitations:
|
||||||
|
|
||||||
* Data migration will fail if there are customers, vendors, items in the database and if these entities are selected for migration. For example, if you choose to migrate items and your company already contains items, you will get an error. This should not be an issue if you migrate your data from another tool to NAV, in which case you will most likely start on a fresh empty company. However, if you just want to import additional items to a company with existing items, then it is not supported by the framework. however, you can still use the different functions provided by the different codeunits (such as **Item Data Migration Facade**) to create the entities without strong coupling on the NAV data model.
|
* Data migration will fail if there are customers, vendors, items in the database and if these entities are selected for migration. For example, if you choose to migrate items and your company already contains items, you will get an error. This should not be an issue if you migrate your data from another tool to NAV, in which case you will most likely start on a fresh empty company. However, if you just want to import additional items to a company with existing items, then it is not supported by the framework. however, you can still use the different functions provided by the different codeunits (such as **Item Data Migration Facade**) to create the entities without strong coupling on the NAV data model.
|
||||||
* **G/L entries** are deleted automatically.
|
* **G/L entries** are deleted automatically.
|
||||||
|
|
||||||
* There is no automated rollback in case of failure: data that is successfully migrated will be commited, and data that is not successfully migrated with be shown in the errors list. The retry feature (in case of staging tables) then makes it possible for you to retry individual entities or ignore them.
|
* There is no automated rollback in case of failure: data that is successfully migrated will be commited, and data that is not successfully migrated with be shown in the errors list. The retry feature (in case of staging tables) then makes it possible for you to retry individual entities or ignore them.
|
||||||
|
|
||||||
## **Usages in NAV:**
|
## Usages in NAV:
|
||||||
|
|
||||||
The Data Migration Façade is available starting from version 2018\.
|
The Data Migration Façade is available starting from version 2018\.
|
||||||
|
|
||||||
|
|
@ -264,7 +265,7 @@ The following Entity data migration façade codeunits are available:
|
||||||
* COD6113 (**Item Data Migration Facade**)
|
* COD6113 (**Item Data Migration Facade**)
|
||||||
* COD6114 (**Ex. Rate Data Migration Facade**)
|
* COD6114 (**Ex. Rate Data Migration Facade**)
|
||||||
|
|
||||||
## **References:**
|
## References:
|
||||||
|
|
||||||
Façade pattern on Wikipedia: https://en.wikipedia.org/wiki/Facade\_pattern
|
Façade pattern on Wikipedia: https://en.wikipedia.org/wiki/Facade\_pattern
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ weight = 1440
|
||||||
+++
|
+++
|
||||||
# Variant Façade
|
# Variant Façade
|
||||||
|
|
||||||
By Nikola Kukrika, Waldo and Gary Winter
|
By Nikola Kukrika, waldo and Gary Winter
|
||||||
|
|
||||||
# Abstract
|
# Abstract
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ A design pattern is a repeatable template of how to solve a common development c
|
||||||
|
|
||||||
In 2013, Microsoft NAV Dev Team and prominent members of the community collaborated on a Community and Microsoft collection of commonly needed / used Design Patterns.
|
In 2013, Microsoft NAV Dev Team and prominent members of the community collaborated on a Community and Microsoft collection of commonly needed / used Design Patterns.
|
||||||
|
|
||||||
For some history on this, Waldo's posts [Code is Poetry](https://www.waldo.be/2013/06/14/code-is-poetry/) and [Design is Philosophy](https://www.waldo.be/2013/08/28/design-is-philosophy-2/) are a great read.
|
For some history on this, waldo's posts [Code is Poetry](https://www.waldo.be/2013/06/14/code-is-poetry/) and [Design is Philosophy](https://www.waldo.be/2013/08/28/design-is-philosophy-2/) are a great read.
|
||||||
|
|
||||||
### Contributing
|
### Contributing
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue