Data Loading...

EXTENDING MICROSOFT DYNAMICS 365 FOR FINANCE AND OPERATIONS COOCKBOOK ( PDFDrive.com ) Flipbook PDF

EXTENDING MICROSOFT DYNAMICS 365 FOR FINANCE AND OPERATIONS COOCKBOOK ( PDFDrive.com )


187 Views
82 Downloads
FLIP PDF 6.03MB

DOWNLOAD FLIP

REPORT DMCA

This book provides a collection of “recipes” to instruct you on how to create—and extend—a real-world solution using Operations. All key aspects of the new release are covered, and insights into the development language, structure, and tools are discussed in detail. New concepts and patterns that are pivotal to elegant solution designs are introduced and explained, and readers will learn how to extend various aspects of the system to enhance both the usability and capabilities of Operations. Together, this gives the reader important context regarding the new concepts and the confidence to reuse in their own solution designs. This “cookbook” provides the ingredients and methods needed to maximize the efficiency of your business management using the latest in ERP software—Dynamics 365 for Operations.

Things you will learn:

• Create enumerated and extended encoding="utf-8"?>

SalesConfirmHeaderTmp.ConReports

[ 197 ]

Leveraging Extensibility

ConSalesPoolName

/// [> /// [>The calling record /// [> /// [> /// [> [PreHandlerFor(tableStr(SalesTable), tableMethodStr(SalesTable, insert))] public static void SalesTable_Pre_insert( XppPrePostArgs args) { }

4. What we do at this point depends on the requirements; the common methods in args are as follows: Method

Use

boolean existsArg(str)

Does the handled method have the parameter

AnyType getArThig(str)

This returns the method's parameter value using the parameter's name. It is returned as an AnyType object. For example, if we are handling SalesTine.insert(Boolean _skipMethod = false), we can get the value of _skipMethod using args.getArg('_skipMethod'). There is no validation; we will use existsArg to check first. This is not an intrinsic function, and we can't cause a compilation error to highlight any runtime errors.

AnyType getArgNum(int)

This gets the handled method's parameter by using its position from the left.

AnyType getThis()

This gets the instance of the object, in our case, the current SalesTable record. There is no compiler validation on the type, so we must check this manually. Again, we can't use intrinsic functions to force a compilation error.

[ 209 ]

Leveraging Extensibility

AnyType getReturnValue

This is only useful on post-event handlers and gets the value the method has returned. We would usually use this in conjunction with the setReturnValue method.

setReturnValue(AnyType) This is only useful on post-event handlers and lets us

override the value the method returns.

setArg(str, AnyType)

This allows us to set the method's parameter named in this method.

setArgNum(int, AnyType) This allows us to set the method's parameter by its position

from the left.

5. Once done, save and close the designer.

How it works... Pre-event and post-event handlers work in the same way as the other event handlers, but these carry a risk of regression. The events we handle through this technique are not delegates, and the developer did not specifically write the method to be handled in this way: otherwise they would have written a delegate. The reason we are restricted to public methods is because these are considered as a public API, and will not be changed or deprecated without a proper procedure. Protected methods can be changed without warning, and we could, therefore, find that our code no longer works. Private methods are private for a reason: you can't guarantee the internal class state, or how these will be called. This also reinforces that we need to be careful when assigning public, protected, or private to a method. Always make your methods as private as possible, as this not only ensures that your code can't be called erroneously, but it also makes it easier for other developers to use your code correctly.

Extending standard forms without customization footprint Adjusting the layout of a form, as an extension, has been made very easy for us. This is covered in the first part of the recipe. We will also cover a new technique to work with form code.

[ 210 ]

Leveraging Extensibility

Getting ready We just need a Dynamics 365 for Operations project open.

How to do it... To write a form extension for the sales order form, SalesTable, follow these steps: 1. Locate the desired form in the Application Explorer, right click on it and choose Create extension. This will add a new form extension to our project. 2. Locate the new form extension in our project, and rename it so it will remain globally unique, for example, SalesTable.Con. 3. Open the form extension in the designer. 4. We can now drag any field or field group, including extension fields that are available to the current package, to the design. 5. We can also choose to change properties of the controls on the form's design. The rule here is that, if it lets you change, it will work. Remember that whilst doing this, all changes should be at the lowest level possible as this ensures consistency and minimizes maintenance effort. This covers the design, but not the code. We can right-click on most methods and use Copy event-handler method to create a pre-event or post-event handler. This may suffice in some cases, but we can go further. In the November update, we can now create extension classes that act as an extension to the form's code. The following example explains one of the many benefits that may not immediately be obvious. To create an extension class for a form's code, follow these steps: 1. Create a new class that must end in _Extension; so, for a SalesTable extension, use ConSalesTable_Extension. For this to work, we only need to have the ExtensionOf decoration and ensure that the name ends in _Extension.

[ 211 ]

Leveraging Extensibility

2. The class declaration should read as follows: [ExtensionOf(formStr(SalesTable))] final class ConSalesTable_Extension

3. In this example, we will determine the caller record by handling the initialized event and use the stored record that is scoped to the form when we close the form. The code for this is as follows: [ExtensionOf(formStr(SalesTable))] final class ConSalesTable_Extension { Private CustTable conCustTable; [FormEventHandler(formstr(SalesTable), FormEventType::Initialized)] public void initializedFormHandler(xFormRun formRun, FormEventArgs e) { Args args = formRun.args(); switch(args.>calling control /// form events [FormControlEventHandler(formControlStr(SalesCreateOrder, SalesTable_CustAccount), FormControlEventType::Lookup)] public static void SalesTable_CustAccount_OnLookup( FormControl sender, FormControlEventArgs e) { FormStringControl custAccountCtrl = sender; CustTable selectedCustomer; FormRun formRun; Form custTableLookupForm; custTableLookupForm = new Form(formStr(CustTableLookup)); FormControlCancelableSuperEventArgs eventArgs = e; Args formArgs = new Args(); formArgs = new Args(); formArgs.name(formStr(CustTableLookup)); formArgs.caller(sender.formRun()); selectedCustomer = CustTable::find(custAccountCtrl.text()); if (selectedCustomer.RecId != 0) { formArgs.lookupRecord(selectedCustomer); } formRun = FormAutoLookupFactory::buildLookupFromCustomForm( custAccountCtrl, custTableLookupForm, AbsoluteFieldBinding::construct( fieldStr(CustTable, AccountNum),

[ 216 ]

Leveraging Extensibility tableStr(CustTable)), formArgs); custAccountCtrl.performFormLookup(formRun); eventArgs.CancelSuperCall(); }

If you wish to create your own lookup, it is also fine. Just create a new form using Lookup - basic pattern. Let's use an example where we are writing a custom product lookup: 1. We need to bind the select control (the ID column, such as the Item Id field in our example). 2. Add the > /// The FormStringControl control that the /// lookup will be attached to. /// /// /// The item that is used to filter the lookup. /// public static void lookupItemId( FormStringControl _lookupCtrl, ItemId _itemId) { SysTableLookup sysTableLookup Query query = new Query(); QueryBuildRange queryBuildRange; QueryBuild>ItemId caller control /// args so we can cancel super [FormControlEventHandler(formControlStr(SalesTable, SalesLine_ItemId), FormControlEventType::Lookup)] public static void SalesLine_ItemId_OnLookup( FormControl sender, FormControlEventArgs e) { FormStringControl itemIdCtrl;

[ 219 ]

Leveraging Extensibility FormControlCancelableSuperEventArgs eventArgs; eventArgs = e; itemIdCtrl = sender; ConGeneralhandlers::lookupItemId( itemIdCtrl, itemIdCtrl.text()); eventArgs.CancelSuperCall(); }

4. These techniques can be used as desired to adjust or replace the lookups in Dynamics 365 for Operations without any customization to the standard code.

How it works... The first part is to use a custom form. The binding to the lookup event is done using a specific event handler for this purpose. These events have one very important feature, apart from allowing us to bind to a form control event; in that, they pass the arguments object as a FormControlEventArgs object. We can then cast this as a FormControlCancelableSuperEventArgs object. When we call the CancelSuperCall() method, it tells the calling control not to call its super. In the case of a lookup event, we would otherwise end up with two lookups: our new lookup and then the standard lookup. Apart from gathering information, the other key part is that we construct the FormRun object using a factory, which creates FormRun so that it behaves as a lookup and not just a form. The second part was to create our own lookup, which was largely repeating what we have already learnt. Just follow the pattern. However, the pattern does need a little help, so it understands how to behave itself. This is why we have to override the init and run methods. Even so, this is a lot less effort than in prior releases. The third part was to create a lookup programmatically. This is often preferable for simple lookups, and we could also add child /> Allow Form adapters for the ConWHSVehicleManament package ConWHSVehicleManagementFormAdapter ConWHSVehicleManagement 895571399

[ 321 ]

Unit Testing

Do not edit any other part of this file. The option to set the form adaptor source model directly in Visual Studio may be added to later versions, so this step might not be required in the future. 11. Open Visual Studio and the ConWHSVehicleManagement solution, then rightclick on the ConWHSVehicleManagementFormAdaptor project, and choose Properties. 12. Change the Generate Form Adapters property to True. 13. Right-click on the ConWHSVehicleManagementForm project and choose Properties and change the Generate Form Adapters property to True. 14. Select Build models... from the Dynamics 365 menu and build both models. 15. This should generate and add a class for each form, suffixed with FormAdaptor. If they don't appear, you can locate them in the Application Explorer and add them manually.

How it works... We will create a new package in order to separate the classes from the main package. This should always be done, as we can end up with a lot of automatically generated code that would be a distraction. The key part of this process was the XML tag we entered in the form adaptor model's Descriptor file--this is what told the main package that the form adapter code should be generated in our form adaptor model. The next part was to turn on Generate Form Adaptors in the properties form for both projects. The build will then generate form adapters into our form adaptor project. We will use this later when we create a unit test for the user interface.

Creating a Unit Test project The test project would ideally be a new project (and model) inside the package we are testing. Each package should have one test project, and, if writing user interface tests, we should have one form adapters also.

[ 322 ]

Unit Testing

Getting ready Open the project that we intend to write tests for.

How to do it... To create the unit test project, follow these steps: 1. Select Dynamics 365 for Operations | Create model from the top menu and complete the Create model form as follows: Field

Value

Model name

ConWHSVehicleManagementTest

Model publisher

Contoso IT

Layer

VAR

Model description

Test cases for the ConWHSVehicleManagement package

Model display name ConWHSVehicleManagementTest The suffix is important and should always be Test.

2. Click on Next. 3. Select Create new package and click on Next. 4. Select ConWHSVehicleManagement, ConWHSVehicleManagementFormAdaptor, ApplicationFoundation, and TestEssentials from the Packages [Models] list and click on Next. Model packages may be required, depending on the code we need to write.

5. Uncheck the default for creating a project, but leave the default option of making the new model the default for new projects. Click on OK. 6. Right-click on the solution node in the Solution Explorer and choose Add | New project....

[ 323 ]

Unit Testing

7. In the New Project window, ensure that the Dynamics 365 for Operations template is selected in the left pane, and Operations Project is selected in the right. Enter ConWHSVehicleManagementTest as Name and click on OK.

How it works... This was basically a simpler version of the steps we took to create the form adaptor project. This is done after the form adaptor, as we need to reference it; this project will contain test cases for manually crafted unit tests and those that will test the user interface.

Creating a Unit Test case for code In this recipe, we will create a test case for the ConWHSVehicleGroupChange class, where we will test each part of this class. This includes when it should fail and when it should succeed. The process will involve programmatically creating some test > /// The company on which the workflow is running. /// /// /// The table ID of the table which is associated /// with the workflow. /// /// /// The record ID of the table which is associated /// with the workflow. /// /// /// The days since the vehicle was acquired. /// public Days parmDaysSinceAcquired( CompanyId _companyId, tableId _tableId, recId _recId) { Days days; if(_tableId == tableNum(ConWHSVehicleTable)) { ConWHSVehicleTable vehicle; select crosscompany AcquiredDate from vehicle where vehicle.DataAreaId == _companyId && vehicle.RecId == _recId; days = vehicle.AcquiredDate - systemDateGet(); } return days; } }

[ 367 ]

Workflow Development

We added the parm method as an example of how to add display methods that can be used in the workflow expression builder and as a placeholder in the workflow text presented to the user. 11. There will be two new Action Menu Items created, prefixed with the approval name, ConWHSVehWF. These are suffixed with CancelMenuItem, and SubmitMenuItem. For each of these, set the Label and Help Text properties with a suitable label, for example: Menu item

Label

Help Text

CancelMenuItem Cancel Cancel the vehicle workflow SubmitMenuItem Submit Submit vehicle to workflow Create the labels using names and not numbers, as we will reuse these labels in other elements.

12. We need to handle the state change, so first we need to create a Base Enum name, ConWHSVehApprStatus, with the following elements: Element

Label

Description

Draft

@SYS75939 Draft--the workflow has not yet been submitted to workflow

Waiting

Waiting

The workflow has been submitted, but has not yet been allocated an approver

Inspection Inspection The vehicle is being inspected InReview In review

The workflow has been allocated one or more approvers

Approved Approved The workflow has been approved Rejected

Rejected

The workflow was rejected by the approvers

Revise

Revised

A change was requested by an approver

These labels should be created using named label identifiers, as we did for the menu items as we will reuse them on other elements.

[ 368 ]

Workflow Development

13. Use the @SYS101302 label (Approval Status) in the Base Enum's Label property. Add the new Base Enum to the ConWHSVehicleTable table as Status, and add it to the Overview field group. Make the field read-only. 14. Open the ConWHSVehWF workflow type and complete the Label and Help Text fields. These are to assist the workflow designer in selecting the correct workflow type when creating a new workflow. 15. Next, create a class called ConWHSVehicleStatusHandler, and write the following piece of code: class ConWHSVehicleStatusHandler { /// /// Sets the vehicle's approval /// status /// /// /// The vehicle record id /// /// /// The new status /// public static void SetStatus(RefRecId _vehicleRecId, ConWHSVehApprStatus _status) { ConWHSVehicleTable vehicle; ttsbegin; select forupdate vehicle where vehicle.RecId == _vehicleRecId; if (vehicle.RecId != 0 && vehicle.Status != _status) { vehicle.Status = _status; vehicle.update(); } ttscommit; } }

Create common select statements, such as the preceding one, as a static Find method, for example, FindByRecId. When writing any select statement, check that the table has a suitable index. 16. Save and close the class.

[ 369 ]

Workflow Development

17. Create a new class named ConWHSVehWFBase and add the following method: public boolean ValidateContext(WorkflowContext _context) { If (_context.parmTableId() != tableNum(ConWHSVehicleTable)) { //Workflow must be based on the vehicle table throw error("@ConWHS:ConWHS72"); } ConWHSVehicleTable vehicle; select RecId from vehicle where vehicle.RecId == _context.parmRecId(); if (vehicle.RecId == 0) { //Vehicle cannot be found for the workflow instance throw error("@ConWHS:ConWHS73"); } return true; }

We throw an error in case of a validation error, as we need the workflow to stop with an error should it fail; the workflow cannot continue with an invalid context. 18. Next, add a method so that the workflow type's handler class knows whether or not the supported elements actually ran. Since that final result will be approved or rejected, we can't use the same field to state that the workflow is completed. In fact, if the workflow type completed, but nothing was done, the document should reset back to Draft (not submitted). Write the method as follows: public boolean CanCompleteWF(WorkflowContext _context) { ConWHSVehicleTable vehicle; select RecId from vehicle where vehicle.RecId == _context.parmRecId(); if (vehicle.RecId != 0) { // Code to check if the workflow can be completed, // i.e. nothing in progress return true; } return true; }

[ 370 ]

Workflow Development

19. Save and close this class, and open the ConWHSVehWFEventHandler class and alter the class declaration so that it extends ConWHSVehWFBase. 20. Add the following methods in order to handle the workflow type's events: public void started(WorkflowEventArgs _workflowEventArgs) { WorkflowContext context; context = _workflowEventArgs.parmWorkflowContext(); if(this.ValidateContext(context)) { ConWHSVehicleStatusHandler::SetStatus( context.parmRecId(), ConWHSVehApprStatus::Waiting); } } public void canceled(WorkflowEventArgs _workflowEventArgs) { WorkflowContext context; context = _workflowEventArgs.parmWorkflowContext(); if(this.ValidateContext(context)) { ConWHSVehicleStatusHandler::SetStatus( context.parmRecId(), ConWHSVehApprStatus::Draft); } } public void completed(WorkflowEventArgs _workflowEventArgs) { WorkflowContext context; context = _workflowEventArgs.parmWorkflowContext(); if(this.ValidateContext(context)) { If (!this.CanCompleteWF(context)) { ConWHSVehicleStatusHandler::SetStatus( context.parmRecId(), ConWHSVehApprStatus::Draft); } } }

[ 371 ]

Workflow Development

The status changes here are that we move from Draft to Waiting when the workflow engine starts, and back to Draft if canceled. Should the workflow complete, but fail the CanCompleteWF check, reset it back to Draft. 21. Finally, open the ConWHSVehWFSubmitManager class and complete the main method, as shown here: public static void main(Args _args) { RefRecId recId; CompanyId companyId; RefTableId tableId; WorkflowComment comment; WorkflowSubmitDialog dialog; WorkflowVersionTable version; recId = _args.record().RecId; tableId = _args.record().TableId; companyId = _args.record().DataAreaId; // The method has not been called correctly. if (tableId != tablenum(ConWHSVehicleTable) || recId == 0) { throw error(strfmt("@SYS19306", funcname())); } version = _args.caller().getActiveWorkflowConfiguration(); dialog = WorkflowSubmitDialog::construct(version); dialog.run(); if (dialog.parmIsClosedOK()) { comment = dialog.parmWorkflowComment(); Workflow::activateFromWorkflowConfigurationId( version.ConfigurationId, recId, comment, NoYes::No); } // Set the workflow status to Submitted. ConWHSVehicleStatusHandler::SetStatus( _args.record().RecId, ConWHSVehApprStatus::Waiting);

[ 372 ]

Workflow Development if(FormDataUtil::isFormDataSource(_args.record())) { FormDataUtil::getFormDataSource( _args.record()).research(true); } _args.caller().updateWorkflowControls(); }

22. Open the ConWHSVehSubmitMenuItem menu item and change the Object property to ConWHSVehWFSubmitManager. 23. Close all code editors and designers and build the project. The compiler will highlight code we forgot to handle by showing the TODO comments as warnings.

How it works... The workflow type required a few elements before we created the actual workflow type. The document is defined by a query, which has a main table. This could be a query of sales orders and sales order lines, where the sales order is the main table, and lets the workflow designer use fields from the query to define messages to the user, and also control how the workflow behaves. The workflow has special application element types for workflow, which point to classes that implement specific interfaces. The workflow type is a higher level than the workflow elements. Workflow elements are the tasks assigned to the user, and they handle states such as Review, Reject, Approve, and so on. The workflow type is at a higher level, and controls whether the workflow is started, cancelled, or completed. It may seem odd that we don't map the workflow event types directly to the Base Enum elements. The workflow engine doesn't read this field; it knows within itself the status of the workflow. The status field is to allow us to easily read the status or act on a particular workflow event. For this reason, we don't actually need to handle all of the events that the workflow provides. The ConWHSVehWFEventHandler class was tied to the workflow type, and is used to persist the workflow's state in the target document record--the vehicle record, in our case. The parm method on the workflow document class, ConWHSVehWFDocument, adds a calculated member that can be used by the workflow designer to either make decisions in the workflow design, or displayed in messages to the users.

[ 373 ]

Workflow Development

The parm methods have to be written with the same input parameters as shown in the example method, and we are free to write any code we like, and return data of any base type that can be converted to a string, such as strings, dates, and Base Enum. We cannot, therefore, return types such as records, objects, or containers. Consider how the method will perform, as this will be run whenever it needs to be evaluated by the workflow engine.

See also... Check out the following links for help setting up workflows and for further reading: Workflow system architecture (https://docs.microsoft.com/en-us/dynamics365/operations/organizationadministration/workflow-system-architecture) Creating a workflow (https://docs.microsoft.com/en-us/dynamics365/operations/organizationadministration/create-workflow ) Overview of the workflow system (https://docs.microsoft.com/en-us/dynamics365/operations/organizationadministration/overview-workflow-system and https://ax.help.dynamics.c om/en/wiki/overview-of-the-workflow-system/) Workflow elements (https://docs.microsoft.com/en-us/dynamics365/operations/organizationadministration/workflow-elements)

Creating a workflow approval A workflow approval is an element that allows approval tasks to be routed, which can be approved or rejected. The design can then use this outcome in order to trigger tasks, or simply inform the user. The workflow approval status is persisted as a field on the document record (that is, the vehicle record in our case), in the same way that the workflow type does.

[ 374 ]

Workflow Development

As a result of this, there are often two fields on the workflow's main table, one for workflow document state, and another for workflow element state. In some cases, such as human resource workflows, the Base Enum is combined into one field. This can seem confusing, but when the workflow status field is properly defined, it simplifies the process. We cannot create extensions for workflow elements, so we cannot use workflow types created by other parties without customization (overlayering).

Getting ready We just need to have created a workflow type, or have a suitable workflow type to add the approval to.

How to do it... To create a workflow approval, follow these steps: 1. Add a new item to the project by selecting Business Process and Workflow from the left-hand list, and then Workflow Approval from the right. Enter ConWHSVehApprWF as the Name and click on Add. 2. Complete the Workflow Approval dialog as shown here:

3. Click on Next. 4. You will be presented with all of the elements the wizard will create for us, reminding us again why the limit is 20 characters and also why the naming is important. Click on Finish. 5. Open the new ConWHSVehApprWF workflow approval, expand the Outcomes node, and note that the system has associated a workflow event handler class

[ 375 ]

Workflow Development

with Approve, Reject, and RequestChange. To complete this element, complete the Label and HelpText properties on the root ConWHSVehApprWF node element. The workflow designer will need this to identify the correct workflow. 6. There will be five new Action Menu Items created, prefixed with the with approval name, ConWHSVehApprWF. These are suffixed with Approve, DelegateMenuItem, Reject, RequestChange, and ResubmitMenuItem. For each of these, set the Label and Help Text properties with a suitable label, for example: Menu item

Label

Help text

Approve

Approve Approve the new vehicle request

DelegateMenuItem Delegate Delegate this approval to a colleague Reject

Reject

Reject the new vehicle request

RequestChange

Revise

Send the request back for revision

ResubmitMenuItem Resubmit Resubmit the new vehicle request Create the labels using names and not numbers, as we will reuse these labels in other areas.

As well as menu items, it also created an event handler class, which is named based on the workflow approval, suffixed with EventHandler. This class will implement seven interfaces, which enforce that a method is implemented, one per event type. 7. Open the work event handler class, ConWHSVehApprWFEventHandler, and alter the class declaration so that it extends ConWHSVehWFBase. 8. This class implements the WorkflowElementDeniedEventHandler interface, even though we chose not to in the creation dialog; remove this from the list. 9. Then, locate the denied method and delete it. 10. We now need to write some code for each method that was generated for us with a TODO. The sample code to write for each method is as follows: public void started(WorkflowElementEventArgs _workflowElementEventArgs) { WorkflowContext context; context =

[ 376 ]

Workflow Development _workflowElementEventArgs.parmWorkflowContext(); if(this.ValidateContext(context)) { ConWHSVehicleStatusHandler::SetStatus( context.parmRecId(), ConWHSVehApprStatus::InReview); } }

11. Follow this pattern for each method using the following table to determine which status to set: Element

Method

Waiting

started

InReview created Approved completed Rejected

returned

Revise

changeRequested

Draft

cancelled 12. For the created method, the input parameter is a different type; simply change the method as follows: public void created(WorkflowWorkItemsEventArgs _workflowWorkItemsEventArgs) { WorkflowContext context; WorkflowElementEventArgs workflowArgs; workflowArgs = _workflowWorkItemsEventArgs.parmWorkflowElementEventArgs(); context = workflowArgs.parmWorkflowContext(); if(this.ValidateContext(context)) { ConWHSVehicleStatusHandler::SetStatus( context.parmRecId(), ConWHSVehApprStatus::InReview); } }

[ 377 ]

Workflow Development

13. In the previous recipe, we wrote a method to determine if the workflow did anything that was used to reset the workflow should nothing have been done when the workflow type completed. Open the ConWHSVehWFBase class and alter the method as follows: public boolean CanCompleteWF(WorkflowContext _context) { ConWHSVehicleTable vehicle; select RecId from vehicle where vehicle.RecId == _context.parmRecId(); boolean canComplete; if (vehicle.RecId != 0) { switch (vehicle.Status ) { case ConWHSVehApprStatus::Approved: case ConWHSVehApprStatus::Rejected: canComplete = true; default: canComplete = false; } } return canComplete; }

14. The final piece of code to write is the resubmission code. A template was created for us, so open the ConWHSVehAppWFResubmitActionMgr class. 15. In the main method, remove the TODO comment and write the following code snippet: public static void main(Args _args) { // The method has not been called correctly. if (_args.record().TableId != tablenum(ConWHSVehicleTable) || _args.record().RecId == 0) { throw error(strfmt("@SYS19306", funcname())); } //Resubmit the same workflow, Workflow handles // resubmit action WorkflowWorkItemActionManager::main(_args); // Set the workflow status to Submitted. ConWHSVehicleStatusHandler::SetStatus( _args.record().RecId, ConWHSVehApprStatus::Waiting);

[ 378 ]

Workflow Development _args.caller().updateWorkflowControls(); }

16. Open the ConWHSVehApprWF workflow approval, select the Deny outcome, and change the Enabled property to No. 17. Finally, open the workflow type and then right-click on Supported Elements node. Select New Workflow Element Reference and set the properties as follows: Field

EDT / Enum

Description

Element Name ConWHSVehApprWF This is the element's name Name

ApprovalVehicle

This is a short version of the name, prefixed with the type

Type

Approval

This is the workflow element's type

18. Save and close all code editors and designers and build the project. Don't forget to synchronize, as we have added a new field.

How it works... The workflow approval is set up with outcomes, which are referenced to an event handler class that implements an interface for each outcome it handles. Each outcome is tied, internally, to that interface. When the outcome occurs, it will construct the referenced event handler class using the interface as the type. It then calls the appropriate method. This pattern of instantiating a class using the interface as the type is common pattern, and we have used this ourselves in Chapter 10, Extensibility Through Metadata and Data DateEffectiveness. There are some events (Started and Cancelled, for example) that are set on the work approval's main property sheet. All this was created for us when we created the workflow approval element. The class that the code generated for us implements all required interfaces with TODO statements where we need to write code. The code is usually simple, and, in our case, we are just updating the vehicle's status field. The generated code will always implement all interfaces that the workflow element can support, so it is common to remove methods and interfaces from the event handler class.

[ 379 ]

Workflow Development

Creating a manual workflow task A manual task is a task that is assigned to a user in order to perform an action. The action can be any task, such as Inspect vehicle, and the user will then state that the task was complete. This workflow will be used to instruct the vehicle to be inspected, and record whether it was inspected in a new field on the vehicle table.

Getting ready This follows from the Creating a Workflow Type recipe, as we need a workflow document class.

How to do it... To create the manual workflow task, follow these steps: 1. We need a new Base Enum for the inspection status, as this will be used both to see whether a vehicle has been inspected and also to control the state of the workflow task; name it ConWHSVehInspStatus and create the elements as shown in the following table: Element

Label

Description

NotInspected Not inspected This vehicle has not yet been inspected Waiting

Waiting

This workflow has been submitted, but has not yet been allocated an approver

InProgress

InProgress

This workflow has been allocated to one or more workers to perform the task

Completed

Completed

This workflow has been completed

[ 380 ]

Workflow Development

2. Create a new Date EDT for ConWHSVehInspDate, setting the properties as follows: Field

EDT/Enum

Description

Extends

TransDate

This EDT should be used for all dates.

Label

Date inspected

Create a named label for this, such as @ConWHS:DateInspected.

Help Text The date the inspection was carried out

This is left generic and not tied to its eventual implementation in order to make the EDT reusable. The help text does not reference the vehicle for this reason.

3. Add the following fields to the vehicle table and set the Allow Edit and Allow Edit On Create to No: Field

EDT / Enum

InspStatus

ConWHSVehInspState This is the status Base Enum created in the previous step

InspComment WorkflowComment InspDate

Description

This will hold the last note when the task is completed

ConWHSVehInspDate This is the date on which the workflow task was completed

4. Create a field group named Inspection and set the Label property to a label for Inspection. Add the fields to this group and then add the field group to a suitable place in the ConWHSVehicleTable form. 5. Next, let's add a status handler class; create a new class name, ConWHSVehicleInspStatusHandler. Create a method to handle the status change, and set the InspComment and InspDate fields from the method's parameters. The code is written as follows: /// /// /// /// /// /// /// ///

Handle the inspection date change

The record id of the vehicle

The new status

[ 381 ]

Workflow Development /// /// /// Comment is set when the status /// is complete /// /// /// InspDate is set when the /// status is complete /// public static void SetStatus(RefRecId _vehicleRecId, ConWHSVehInspStatus _status, WorkflowComment _comment = '', ConWHSVehInspDate _inspDate = dateNull()) { ConWHSVehicleTable vehicle; ttsbegin; select forupdate vehicle where vehicle.RecId == _vehicleRecId; if(vehicle.RecId != 0) { vehicle.InspStatus = _status; // if the inspection is complete set // the comment and inspection date fields // otherwise clear them, as the workflow // may have been cancelled. switch (_status) { case ConWHSVehInspStatus::Complete: vehicle.InspComment = _comment; vehicle.InspDate = _inspDate; break; default: vehicle.InspComment = ''; vehicle.InspDate = dateNull(); } vehicle.update(); } ttscommit; }

6. Against the project, add a new item and choose Workflow Task from the Business process and Workflow list. Use the ConWHSVehWFInspect name and click on Add.

[ 382 ]

Workflow Development

7. Configure the Workflow Task dialog as shown in the following screenshot:

8. Click on Next. 9. On the next page, choose Complete in the Type drop-down list, and enter Complete in the field before clicking on Add. You can add further outcomes, which will follow the same pattern when implemented.

10. Click on Next and then Finish. 11. For each action menu item created by the wizard, complete the Label and Help Text properties. You may recognize that the code generated by this process is very similar to the Workflow approval. We will follow that pattern again by handling the required methods in the ConWHSVehWFInspectEventHandler class. 12. Since we don't handle all of the possible outcomes, we should only implement the required interfaces. Also, in order to have access to the ValidateContext method, we should extend ConWHSVehWFBase. The class declaration should read as shown here: public final class ConWHSVehWFInspectEventHandler extends ConWHSVehWFBase implements WorkflowElementCanceledEventHandler, WorkflowElementCompletedEventHandler, WorkflowElementStartedEventHandler, WorkflowWorkItemsCreatedEventHandler

13. Also, remove the methods linked to the interface that we removed. Change the started method as shown here. It maintains the vehicle status and inspection status fields:

[ 383 ]

Workflow Development public void started( WorkflowElementEventArgs _workflowElementEventArgs) { WorkflowContext context; context = _workflowElementEventArgs.parmWorkflowContext(); if(this.ValidateContext(context)) { ConWHSVehicleInspStatusHandler::SetStatus( context.parmRecId(), ConWHSVehInspStatus::Waiting); ConWHSVehicleStatusHandler::SetStatus( context.parmRecId(), ConWHSVehApprStatus::Inspection); } }

14. The canceled method should reset both status fields back to their initial states: public void canceled( WorkflowElementEventArgs _workflowElementEventArgs) { WorkflowContext context; context = _workflowElementEventArgs.parmWorkflowContext(); if(this.ValidateContext(context)) { ConWHSVehicleInspStatusHandler::SetStatus( context.parmRecId(), ConWHSVehInspStatus::NotInspected); ConWHSVehicleStatusHandler::SetStatus( context.parmRecId(), ConWHSVehApprStatus::Draft); } }

15. The completed method needs to get the current system date, and also fetch the last comment from the workflow. This is done in the following code: public void completed(WorkflowElementEventArgs _workflowElementEventArgs) { WorkflowContext context; context = _workflowElementEventArgs.parmWorkflowContext();

[ 384 ]

Workflow Development WorkflowCorrelationId correlationId; correlationId = context.parmWorkflowCorrelationId(); WorkflowTrackingTable trackingTable; trackingTable = Workflow::findLastWorkflowTrackingRecord( correlationId); WorkflowTrackingCommentTable commentTable; commentTable = trackingTable.commentTable(); WorkflowComment comment = commentTable.Comment; Timezone timezone = DateTimeUtil::getUserPreferredTimeZone(); if(this.ValidateContext(context)) { ConWHSVehicleInspStatusHandler::SetStatus( context.parmRecId(), ConWHSVehInspStatus::Complete, comment, DateTimeUtil::getSystemDate(timezone)); } }

16. Finally, write the created method. This is when the task is assigned to one or more users. The code should be written as follows: public void created( WorkflowWorkItemsEventArgs _workflowWorkItemsEventArgs) { WorkflowContext context; WorkflowElementEventArgs workflowArgs; workflowArgs = _workflowWorkItemsEventArgs. parmWorkflowElementEventArgs(); context = workflowArgs.parmWorkflowContext(); if(this.ValidateContext(context)) { ConWHSVehicleInspStatusHandler::SetStatus( context.parmRecId(), ConWHSVehInspStatus::InProgress); ConWHSVehicleStatusHandler::SetStatus( context.parmRecId(), ConWHSVehApprStatus::Inspection); } }

[ 385 ]

Workflow Development

17. We should also update the CanComplete method on the ConWHSVehWFBase class, but what we do here is dependent on what we want to control. We are in danger of hardcoding a business rule, which is ironically what workflows are designed to avoid. As a result of this, we just want to ensure that the document (vehicle record) is always left in a consistent state when the workflow type completes. The following piece of code will only return false if either the approval or task is in progress: public boolean CanCompleteWF(WorkflowContext _context) { ConWHSVehicleTable vehicle; select RecId from vehicle where vehicle.RecId == _context.parmRecId(); boolean canComplete = true; if (vehicle.RecId != 0) { switch (vehicle.Status ) { case ConWHSVehApprStatus::Revise: case ConWHSVehApprStatus::Waiting: case ConWHSVehApprStatus::InReview: case ConWHSVehApprStatus::Inspection: canComplete = false; default: canComplete = true; } switch (vehicle.InspStatus) { case ConWHSVehInspStatus::InProgress: case ConWHSVehInspStatus::Waiting: canComplete = false; } } return canComplete; }

18. Next, complete the ConWHSVehApprWFResubmitActionMgr class as follows: public static void main(Args _args) { // The method has not been called correctly. if (_args.record().TableId != tablenum(ConWHSVehicleTable) || _args.record().RecId == 0) { throw error(strfmt("@SYS19306", funcname()));

[ 386 ]

Workflow Development } //Resubmit the same workflow, Workflow handles resubmit action WorkflowWorkItemActionManager::main(_args); // Set the workflow status to Submitted. ConWHSVehicleInspStatusHandler::SetStatus( _args.record().RecId, ConWHSVehInspStatus::Waiting); _args.caller().updateWorkflowControls(); }

19. Finally, open the workflow type and then right-click on the Supported Elements node. Select New Workflow Element Reference and set the properties as follows: Field

EDT / Enum

Description

Element Name ConWHSVehWFInspect This is the element's name Name

TaskInspect

This is a short version of the name, prefixed with the type

Type

Task

This is the workflow element's type

20. Copy and paste the task name into the Element Name and Name properties. 21. Save and close all designers and code editors and build the project, followed by synchronizing the database with the project.

How it works... The concept is the same as for the workflow approval in the previous recipe. The Workflow task element is a definition that the designer will use when creating a workflow. The code we wrote simply handles the events as we need to. The complicated part to understand is the status handling. It seems natural to have a status field for each workflow element (the type, approval, and task), and with this paradigm, we would be left thinking why there isn't a standard Base Enum we could simply use. The status of the document, and the statuses the document can be defined by us--what makes sense to the business, and not what makes sense in code. For the inspection task, we want to know if a vehicle is waiting inspection, is in progress, or if it is complete.

[ 387 ]

Workflow Development

Hooking up a workflow to the user interface This is the final step in designing our workflow, and involves setting a property on the form referenced by the document menu item, ConWHSVehicleTable, and adding the option to design workflows to the menu.

Getting ready The minimum we need to have completed for this is create a workflow type.

How to do it... In order to be able to design and process workflows, follow these steps: 1. Expand User interface, Menu items, and then Display from the Application Explorer. Located WorkflowConfgurationBasic, right-click on it and choose Duplicate in project. 2. Locate the new menu item in the Solution explorer, and rename it to ConWHSWorkflowConfiguration. Depending on the version of the development tools, you will need to undo the change to WorkflowConfigurationBasicCopy and then add the renamed element to source control. 3. Open the new menu item in the designer and set the Enum Parameter property to ConWHS, and then create labels for the Label and Help Text properties. The label should be the module name, followed by the word workflows, Vehicle management system workflows, for example. 4. Add this menu item to the Setup submenu of our menu. This should usually be the second option in the list. 5. Open the ConWHSVehicleTable form in the designer.

[ 388 ]

Workflow Development

6. Select the Design node of the form design and locate the following properties: Property

Value

Workflow Data Source ConWHSVehicleTable Workflow Enabled

Yes

Workflow Type

ConWHSVehWF

7. Right-click on the Methods node of the form and select Override | canSubmitToWorkflow. Alter the code so that it reads as follows: public boolean canSubmitToWorkflow() { If (ConWHSVehicleTable.Status == ConWHSVehApprStatus::Draft) { return true; } return false; }

We may need this to be more elaborate in some cases, but the minimum is that we can't allow a workflow to be submitted that is already in progress. 8. Save and close all designers and build the project.

How it works... The workflow configuration is a generic form that builds based on the ModuleAxapta Base Enum. We linked ConWHS to the workflow category, which was then linked to the workflow type. This will, therefore, allow the workflow designer to create and modify workflows for this module. The form changes were simply to link the workflow type to the form, and which data source is the document data source. This is then used to query if there are any active workflows for that type, and will show the option to submit the vehicle for approval if there is an active vehicle workflow design.

[ 389 ]

Workflow Development

Creating a sample workflow design Let's test the elements we have created. The following workflow design is only intended to test the workflow we have created, and omits many of the features that we would normally use. We will also use the same user for submission and approval, and you will see that appear to be waiting for the workflow engine as we test. This seems a problem at first glance, but in real-life scenarios, this is fine. In practice, the tasks and approvals are performed by different users and are not done as a series of tasks. They will receive a notification, and they can then perform that action and pass the ball back to the workflow engine.

Getting ready Before we start, ensure that the project is built and synchronized with the database.

How to do it... To create the workflow design, follow these steps: 1. Open the following URL, and from there, open Vehicle management | Setup | Vehicle management workflows: https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=usmf

You can navigate directly to the configuration form using the following URL: https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp =usmf&mi=ConWHSWorkflowConfiguration 2. Click on NEW, and you should see the workflow type in the list, and is listed using the Label and Help Text properties that we set.

[ 390 ]

Workflow Development

3. Select the workflow type link, as shown in the following screenshot:

4. You will then be asked to log in, which you should do as the same user you used to log in to Operations. You will then be presented with a new window, with the two workflow elements that we wrote and some flow control options in the lefthand pane. 5. Drag the New vehicle approval workflow and Inspect vehicle elements onto the design surface, as shown in the following screenshot:

6. As your mouse hovers over the Start element, you will see small handles appear-drag one of these handles so that it connects the Start element to the Inspect vehicle 1 element. Connect all elements, as shown in the following screenshot:

[ 391 ]

Workflow Development

7. Select the Inspect vehicle 1 element, and click on Basic Settings from the action pane. Configure it as shown in the following screenshot:

[ 392 ]

Workflow Development

The text within % symbols was added using the Insert placeholder button; click on this and you will see that our parm method was added and the parm prefix was automatically removed. 8. Click on Assignment from the left, and choose User from the Assign users to this workflow element list. Select the User tab, and manually assign this to Admin. This is the default administration user. Since we are simply testing our code works, we will assign all tasks and approvals to our user. 9. Click on Close. 10. Double-click on the New vehicle approval workflow 1 element and click on Basic Settings from the action pane. Change Name to New vehicle approval workflow and click on Close. We would normally configure the notifications; for example, we would usually notify Workflow Originator if the approval was rejected.

11. Then, select Step 1, press Basic Settings, and configure as shown here:

[ 393 ]

Workflow Development

12. Click on Assignment and assign this to our user as we did before. Press Close. There is a bread crumb at the top of the workflow designer's design surface, which shows Workflow | New vehicle approval. Click on the word Workflow from the bread crumb. 13. Finally, we must give submission instructions to the person who submitted the vehicle approval. Right-click on an empty area of the workflow designer's design surface and click on Properties. Enter suitable instructions in the Submission instructions field and click on Close. 14. Click on Save and close, and then the OK on the Save workflow dialog. Select Active the new version on the Activate workflow dialog and click on OK. Let's now test if the workflow works: 1. Open the vehicle form, and you should see the Workflow button as shown in the following screenshot:

2. Click on the button and select Submit, which is the label we assigned to the ConWHSVehWFSubmitMenuItem menu item. Enter a comment and click on Submit. 3. The options should change to Cancel and View history. You can choose View history to see the progress of the workflow engine. If the tasks aren't assigned within a minute, check that the Microsoft Dynamics 365 for Operations - Batch Management Service Windows service is running. Also check that there are batch jobs for Workflow message processing, Workflow due data processing, and Workflow line-item notifications. If not, open Workflow infrastructure configuration from System administration | Workflow, and click on OK.

[ 394 ]

Workflow Development

4. After approximately two minutes, having pressed the refresh icon, you should see the Complete, Delegate, and Cancel options. These are the options for the inspection task. Select Complete. Enter an inspection comment and click on Complete. View the vehicle details and wait for about a minute. Click on the refresh button. You should see information similar to the following:

5. After a further minute, refresh the form, and the options will change to the approval options. Choose Approve from the list.

How it works... The workflow design has many options available to us, and is used to tell the workflow engine how the approvals and tasks should be processed. When we develop workflows, we do so as generically as possible in order to leave the business logic to the workflow designer. This means that we reduce the need for changes to the code as the business evolves. The test was to help demonstrate what happens to the status fields as the workflow is processed. This should help us in our own workflow development in understanding the link between events and status changes.

[ 395 ]

15

State Machines In this chapter, we will cover the following recipes: Creating a state machine Creating a state machine handler class Using menu items to control a state machine Hooking up the state machine to a workflow

Introduction State machines are a new concept in D365O, and a very welcome feature. Previously, the control of status fields was handcrafted in code, which could be often hard to read as there was no obvious pattern to follow; having said that, we will always look at a similar standard example to our case and use that idea. This is not plagiarism, it is good practice. It is a good general rule to seek examples in standard code, as there is a much higher chance that another developer will understand the code we've written. State machines allow us to define in metadata how the status transitions from an initial state to its final state. These rules are then enforced by code that the state machine will generate. There is a restriction though. There must be one initial state and one final state. When we are at the final state, there is no going back. If we take the sales order status, we have two final states: Invoiced and Cancelled. There is another reason why we wouldn't use a state machine on this type of status. The sales order status is a reflection of actual order state; it is system controlled. State machines are designed to enforced status change logic when the state is asserted by a user.

State Machines

Creating a state machine This first recipe is to create a state machine for vehicle inspection. In Chapter 14, Workflow Development, we created a workflow task and an inspection status field. In this recipe, we will use a state machine to handle the inspection status change logic.

Getting ready We need to have a table with a status field with an initial and final status, such as the InspStatus field we added to the ConWHSVehicleTable table in Chapter 14, Workflow Development.

How to do it... To create a state machine, follow these steps: 1. Open ConWHSVehicleTable in the designer. Right-click on the State Machines node and choose New State machine. 2. Rename the new state machine InspStateMachine and complete the properties as shown in the following table, creating labels for the Description and Label properties: Property

Value

Description Use this to control the inspection status Label

Inspection status

Data Field

InspStatus

3. Right-click on the new state machine definition and select New State. 4. Complete the properties of this state using the following table: Property

Value

Enum Value NotInspected - change this to Waiting, and then back again to default the Label property. Description The vehicle has not yet been inspected Label

Not inspected

[ 397 ]

State Machines

State Kind

Initial

We will create a state for each element in the ConWHSVehInspStatus Base Enum, so it is a good idea to create description labels in advance and just paste them in. Use named labels for this, not numeric. I use a suffix of HT, which is short for Help Text, for labels that are used for both help text and descriptions of elements. 5. Create the remaining states using the following table as a guide: Enum Value Name

State Kind

Waiting

Waiting

Intermediate The vehicle is awaiting inspection

InProgress

InProgress Intermediate The vehicle inspection is in progress

Complete

Complete

Final

Description

The vehicle inspection is complete

6. The result should look like the following screenshot:

7. We will now need to tell the state machine the transition rules. We will define the rules as follows: NotInspected can only transition to Waiting Waiting can only transition to InProgress InProgress can transition to both Waiting and Complete Complete is the final state and cannot transition backwards

[ 398 ]

State Machines

8. Again, create labels in advance. The following table explains the type of wording we should use: Label ID

Label

VehTransWaiting

Add to waiting list

VehTransWaitingHT

Add the vehicle to the list of vehicles awaiting inspection

VehTransInProgress

Start inspection

VehTransInProgressHT

Start the vehicle inspection process

VehTransBackWaiting

Revert to waiting

VehTransBackWaitingHT Place the vehicle back onto the waiting list VehTransComplete

Complete inspection

VehTransCompleteHT

Complete, and finalize the vehicle inspection

9. To do this, right-click on the NotInspected state and select New State transition. This time, the Label and Description properties define the action, not the state. Set the properties to define the transition to the Waiting state as follows: Property

Value

Description

@ConWHS:VehTransWaitingHT

Label

@ConWHS:VehTransWaiting

Name

TransitionToWaiting

Transition To State Waiting 10. Add a new transition state to the WaitingState state using the following table: Property

Value

Description

@ConWHS:VehTransInProgressHT

Label

@ConWHS:VehTransInProgress

Name

TransitionToInProgress

Transition To State InProgress

[ 399 ]

State Machines

11. Next, add two transition states to the InProgress state. The first is to revert back to waiting: Property

Value

Description

@ConWHS:VehTransBackWaitingHT

Label

@ConWHS:VehTransBackWaiting

Name

TransitionToWaiting

Transition To State Waiting 12. The second state to add to the InProgress state completes the state machine, and should be configured as follows: Property

Value

Description

@ConWHS:VehTransCompleteHT

Label

@ConWHS:VehTransComplete

Name

TransitionToComplete

Transition To State Complete 13. Save your changes, the result should look like the following screenshot:

[ 400 ]

State Machines

14. The final step is to right-click on the InspStateMachine state machine, and click on Generate. This generates the code that will be used to control the inspection status progression. If you get the error Given key does not exist in the dictionary, it is because the name of the state did not match the Enum Value property. This may be changed in future releases so that it can be named differently. 15. The generated classes may not be added to your project; to do so, locate the classes that start with ConWHSVehicleTableInspStateMachine and drag them on to the Classes node of your project. Do not modify these classes; these are shown in the following screenshot:

How it works... What this process actually does is generate four classes. The main class is named ConWHSVehicleTableInspStateMachine, which is a concatenation of the table's name and the state machine's name. The other three classes are all prefixed with this class, and allow typed date to be passed to the delegates that were written into this class. The fact we have a state machine does not prevent the user from manually changing the status field's value. It also does not stop us from manually changing the status in code. So the restriction on the final status being final is only true when using the state machine. There are two ways in which we can use the state machine: Attach to workflow events Use with menu items added to a form We will explore these in the following recipes.

[ 401 ]

State Machines

Creating a state machine handler class The state machine provides control over the transition rules, but, sometimes, we want to ensure that other validation rules are obeyed in order to validate whether the transition can be done. This is done by subscribing to the Transition delegate of the ConWHSVehicleTableInspStateMachine class that was generated by the state machine. The code in this recipe refactors the ConWHSVehicleInspStatusHandler class that we created in Chapter 14, Workflow Development. The code written in this recipe will tie it programmatically to the state machine. Should you wish to attach the statement to the workflow directly (which is a great idea), the status will be set by the state machine. Therefore, the event handlers must not set the status. Furthermore, should the validation written in this recipe fail, we must ensure that the workflow's internal status matches the state machine's status. This could be by canceling the workflow by throwing an error.

Getting ready We created a class named ConWHSVehicleInspStatusHandler; we will extend this class so that we can use it with the state machine.

How to do it... To create a handler class to add further validation to the state machine, follow these steps: 1. Open the ConWHSVehicleInspStatusHandler class and add the following piece of code: public ConWHSVehInspStatus fromStatus; public ConWHSVehInspStatus toStatus; public ConWHSVehicleTable vehicle; public boolean Validate() { switch (toStatus) { case ConWHSVehInspStatus::Complete: if (vehicle.InspComment == '') { DictField field = new DictField( tableNum(ConWHSVehicleTable),

[ 402 ]

State Machines fieldNum(ConWHSVehicleTable, InspComment)); //The field %1 must be filled in" return checkFailed (strFmt( "@SYS110217", field.label())); } break; } return true; } public void run() { if(toStatus == fromStatus) { return; } if(this.Validate()) { switch (toStatus) { case ConWHSVehInspStatus::Complete: Timezone tz = DateTimeUtil:: getClientMachineTimeZone(); ConWHSVehInspDate inspDate; inspDate = DateTimeUtil::getSystemDate(tz); vehicle.InspDate = inspDate; break; } } else { vehicle.InspStatus = fromStatus; } }

There is nothing new about the preceding code, except that we don't (and must not) call update on the record. It is just a validation class that will stop the transition if the comment is blank. 2. The code to tie it to the transition delegate is as follows: [SubscribesTo(classStr(ConWHSVehicleTableInspStateMachine), delegateStr(

[ 403 ]

State Machines ConWHSVehicleTableInspStateMachine, Transition))] public static void HandleTransition( ConWHSVehicleTableInspStateMachineTransitionEventArgs _eventArgs) { ConWHSVehicleInspStatusHandler handler; handler = new ConWHSVehicleInspStatusHandler(); handler.vehicle = _eventArgs.DataEntity(); handler.fromStatus = _eventArgs.ExitState(); handler.toStatus = _eventArgs.EnterState(); handler.Run(); }

How it works... When the state machine generated the classes, it added a delegate that is called whenever the state changes. This delegate is called before the changes are committed. The table is passed by reference, which means that we can revert the status back without calling update. If we did call update, we could cause concurrency issues within the standard code.

There's more... When working with a handler class, also be careful with transaction state. We could update data in a table, for instance, a manually crafted status history table. We can nicely handle any potential exception with a try...catch statement within our handler class, but we can't control what happens when control returns back to the state machine. For example, if we update a history table, but the code fails later on, we could end up with a non-durable transaction if the code handles the exception and continues to commit the transaction.

Using menu items to control a state machine In this section, we will actually add the state machine to the form, so we can use it. Using menu items for this is a nice concise way to control the state machine, and follows the UI patterns found in other areas, such as the projects module.

[ 404 ]

State Machines

Getting ready The prerequisite for this recipe is that we have a table with a state machine that has been generated.

How to do it... To create the state machine menu items, follow these steps: 1. Add a new action menu item to the project named ConWHSVehInspStatusWaiting. Complete the property sheet as follows, in the order stated in the following table: Property

Value

State Machine Data Source

ConWHSVehicleTable

State Machine Transition To Waiting Label

The label you use in the Waiting state's Label property

Help Text

The label you use in the Waiting state's Description property

Needs Record

Yes

2. Create the menu items for the remaining states (InProgress and Complete) following the same pattern. 3. Open the ConWHSVehicleTable form in the designer. 4. Under the form's Design node, expand the ActionPaneHome control, and then ActionPaneActionButtonGroup. Right-click on this control and choose New | Menu Button. 5. Rename the new control InspStatusMenuButton. Set the Text and Help Text properties to the same as we used on the state machine, for example, @ConWHS:InspectionStatus and @ConWHS:InspectionStatusHT respectively. 6. Then, drag the three menu items onto this menu button. Set the Data Source property to ConWHSVehicleTable--the table that the state machine operates on. 7. If you can't add them directly, drag them first onto the ActionPaneActionButtonGroup button group, and then drag them from there to the correct place.

[ 405 ]

State Machines

8. Save and close all code editors and design windows and build the project.

How it works... When we created the menu items, the system defaulted many properties for us. If the table only has one state machine, all we had to do was set the label properties. You may notice that it changes the menu item's properties so that it referenced the state machine class that was generated by the table's state machine. When we test the buttons, you can see that if we choose a transition that is not valid, we get this error:

We can't change this message, as it is controlled by a protected method, and we shouldn't edit the generated classes, as the code changes will be lost should the state machine be regenerated. This is a little odd, as the generated code does gather the user friendly labels we added.

Hooking up the state machine to a workflow In this recipe, we will hook up our state machine to the ConWHSVehWFInsp workflow task.

Getting ready We need to have a workflow task and have completed the recipes in this chapter.

How to do it... To hook up the state machine to a workflow task, follow these steps: 1. Open the ConWHSVehWFInsp workflow task. 2. Set the Canceled State Machine property to InspStateMachine.

[ 406 ]

State Machines

Yes, this is spelled Canceled, but it is more than compensated by the clever way it has determined the list of valid state machines.

3. Set the Canceled State Machine Target State property to Waiting; we won't be allowed to use NotInspected. Due to this being the state machine's initial state, it will let you set this value, but the state machine will reject the change. 4. Set the Started State Machine property to InspStateMachine, and the Started State Machine Target State property to Waiting. You will also need to allow the Waiting state to transition directly to Complete. Don't forget to click on Generate to regenerate the state machine class. 5. Select the Completed outcome, set the State Machine property to InspStateMachine, and the set State Machine Target State property to Complete. 6. Since we now have two ways to set the status, we will (as a short term fix) disable the status update code that set the status in the SetStatus method of the ConWHSVehInspStatusHandler class. Open this class and remove the line that set the InspStatus field. We will update this code properly in the There's more... section.

7. Upon testing this, we will find that the task completed sets the comment correctly, but the status doesn't change to completed. The reason is because the workflow events fire last, so the state machine validation rejected the update because the comment was not yet set. We need to update our validation logic so that it only runs when triggered from the form. Alter the Validate method of the ConWHSVehInspStatusHandler class so that it reads as follows: public boolean Validate() { switch (toStatus) { case ConWHSVehInspStatus::Complete: if (vehicle.InspComment == ''

[ 407 ]

State Machines && FormDataUtil::isFormDataSource(vehicle)) { //The field %1 must be filled in" DictField field = new DictField( tableNum(ConWHSVehicleTable), fieldNum(ConWHSVehicleTable, InspComment)); return checkFailed(strFmt( "@SYS110217", field.label())); } break; } return true; }

The highlighted code checks if the record buffer is a form data source; if the code was called from within workflow, the table will not be linked to a form's data source. 8. Build and test the workflow; all should work correctly.

How it works... The change we have made was to simply tie the events on the workflow task to a state of the state machine. This means that the event handler methods we normally write should not update the status, but they can perform actions that should happen when the event happens. The state machine is called by the workflow engine, just before the events are called. This is why we had to remove the validation on the comment--the state is changed before the completed event was called, which means that the comment was empty. There isn't much we can do in this case but to allow the workflow to continue. We could use the workflow designer to check for this event and resubmit the task to the user.

There's more... This seems to greatly simplify the workflow development. We don't need all of the event handler methods, since most of them only update the record's status. There is some thought required, if we consider the following scenario.

[ 408 ]

State Machines

The workflow is cancelled by the user, which means that the status will go back to Waiting. We chose Waiting, for when the workflow task was cancelled, because the state machine will throw an error if we try to set it to the initial state. The problem is that we can't change the status to the same status; we will still get an error. The error is not just a message to the user; it will place the workflow in a failed state, which will require an administrator to cancel, resubmit, or resume it. The problem is that we should not have a scenario where a user action can cause a failure that requires an administrator to rescue them; we need to handle this eventuality elegantly within our code. The first thing we could do is add an internal state to the status Base Enum and to the state machine, for example, "Internal processing". We would not create a menu item for this as it is only for internal use. On the state machine, we would allow any transition from this state; it can transition freely from and to this state. This is the state we use for the Cancelled event. This means that the workflow can set the status to Waiting after the workflow was canceled. The next part of the changes we would make would be to remove all calls to ConWHSVehiInspStatusHandler::SetStatus(...). We would write a new method called SetComment, which is called from the Completed event on the workflow task event handler class.

[ 409 ]

Index A agent queues 335 aggregate data entities about 174 creating 176 aggregate dimensions about 166 creating 166, 167, 169 aggregate measures creating 169, 170, 172, 173, 174 Analytics in Operations reference 169 Application Extension factory used, for creating handler class 117, 118, 119, 121, 122, 124, 126, 127, 130 Application Integration Framework (AIF) 256 Azure 10

B balance, in testing software reference 320 Base Enums 33 batch framework used, for executing code 157 binary updates applying 356, 357, 358 build operations about 56 managing 342, 344, 345, 346 build options configuring 26 build server about 337 servicing 358, 359, 360 setting up 337, 339, 340, 341 build

releasing, to User Acceptance Testing (UAT) 347, 349, 350 Business Intelligence (BI) 165

C client access license (CAL) type 186 Cloud solution provider 11 Cloud-powered support 11 clustered index 65 code executing, batch framework used 157 Unit Test case, creating for 324, 326, 327, 329, 330 create dialog creating, for Details Transaction forms 146, 148, 149, 151

D data access metadata, using for 290, 291, 293, 294, 295, 296, 297, 298, 299, 301 data contract used, for making changes to dialog 159 data entity creating 225, 226, 227, 228, 229, 230, 232, 233 references 236, 242 special methods 233 Data Import/Export Framework (DIXF) about 224 data, importing through 239, 240, 241, 242 data-event handler methods creating 198, 199, 200, 201 data date-effective, making 310, 311, 312, 313, 314 reading, through OData 243, 244, 245, 247, 249, 250

updating, through OData 243, 244, 245, 247, 249, 250 writing, through OData 243, 244, 245, 247, 249, 250 DataEventArgs specialized classes 203 Deployable package 24 Details Master (Main table) forms creating 94, 96, 98 Details Transaction (order entry) forms creating 99, 101, 102, 103, 105 Details Transaction forms create dialog, creating for 146, 148, 149, 151 developer topology deployment, with continuous build and test automation reference 342 development and continuous delivery FAQ reference 342 document layout customizing, without over-layer 204, 206, 208 duties creating 187 Dynamics 365 for Operations JSON service consuming 272, 273, 274, 275, 276, 278, 279, 281 Dynamics 365 for Operations SOAP service consuming 262, 263, 265, 267, 269, 270 Dynamics 365 for Operations external service, consuming within 284, 285, 286, 287

E entry points 184 enumerated types creating 33, 34, 35, 36, 37 Enums using, for comparison 37, 38 using, for status 37, 38 event handler methods creating 208, 210 Extended Data Types (EDTs) AccountMST 42 AccountNum 42 AmountCur 43 creating 39, 40, 41

Description 42 Name 42 Num 42 SysGroup 42 extensibility, in Base Enums 39 Extensible Data Security (XDS) 191 external service consuming, within Dynamics 365 for Operations 284, 285, 286, 287, 288

F Form Adaptor project creating 320, 322 form event handler used, for replacing lookup 215, 217, 219, 220 form parts creating 106, 108 FormRun class 87 forms process, calling from 157 testing 93

G general form guidelines reference 90 Guided Learning for Power BI reference 181

H handler class creating, Application Extension factory used 117, 118, 119, 121, 122, 124, 126, 127, 130

I indexes 65 installation, of deployable package reference 358 interface about 131 adding, to SysOperation framework 160, 161, 162, 164 benefits 132 features 131 reference 133

[ 411 ]

using, for extensibility 301, 302, 304, 305, 306, 308, 309, 310 Internet Information Services (IIS) 288 Intrinsic Functions [AX 2012] reference 66

K key performance indicators (KPIs) about 174 creating 176 using 177, 179, 181

L label file creating 28, 29 latest update, of Dynamics 365 for Operations reference 352 Lifecycle Services (LCS) about 10 reference 10, 15 lookup replacing, form event handler used 215, 217, 219, 220

M main data tables creating 58, 59, 60, 61, 62, 63, 64 manual workflow task creating 380, 381, 382, 383, 384, 386, 387 menu items about 90 creating 90, 91 used, for controlling state machine 404, 405, 406 menu structure about 78 creating 78, 79, 80, 81 metadata fixes about 352 applying 352, 353, 354, 355 metadata hotfix reference 356 metadata interface, using for extensibility 301, 302, 304, 305, 306, 308, 309, 310

using, for data access 290, 291, 293, 294, 295, 296, 297, 298, 299, 301 methods copying 57 pasting 57 Microsoft / Dynamics-AX-Integration reference 254 Microsoft Connect 11 Microsoft Dynamics 365 9 Microsoft Dynamics 365 for Operations for Developers and IT Pros reference 10 Microsoft Dynamics 365 for Operations about 9, 224 application updates 10 options 26 platform updates 9 reference 10 Microsoft Dynamics AX 2012 9 Model about 24 creating 20, 21, 23

N naming conventions 25 Newtonsoft JSON Samples reference 283 Non-Clustered Column Store Indexes (NCCI) 66 number sequence hooking up 133, 135, 137, 139, 141, 143, 145 setting up 144

O OData data, reading through 243, 244, 245, 247, 249, 250 data, updating through 243, 244, 245, 247, 249, 250 data, writing through 243, 244, 245, 247, 249, 250 references 236, 254 Odometer field using, on vehicle form 315 On Delete property 71 Optimistic concurrency (OCC) 57

[ 412 ]

order header tables creating 66, 67, 68, 69, 70, 71 order line tables creating 72, 74, 75, 76

security roles creating 189, 190 selectForUpdate 58 service endpoints reference 272 service creating 256, 257, 258, 260, 261 set up forms creating 91, 93 setup tables creating 43, 44, 45, 46, 47, 48, 49, 50, 51 standard data entities extending 237, 238, 239 standard forms extending, without customization footprint 210, 211, 213, 214 standard tables extending, without customization footprint 196, 197, 198 state machine handler class creating 402, 403, 404 state machine controlling, menu items used 404, 406 creating 397, 398, 399, 400, 401 hooking up, to workflow 406, 407, 408 Surrogate key reference 76 SysOperation framework interface, adding to 160, 161, 162, 164 SysOperation process Controller class 152 creating 152, 153, 155, 156 Data contract class 152 Processing class 152

P package about 24 creating 20, 21, 23 parameter form creating 82, 83, 85, 87 parameter table creating 52, 53, 54, 55 permissions 184 policies creating 191, 193 Power BI app reference 182 Power BI Free reference 181 Power BI Pro reference 181 Power BI reports reference 182 prefixes 25 privileges creating 184, 186 process calling, from form 157 project-specific parameters setting up 27 project configuring 26

Q

T

query functions about 221 creating 221, 222

S sample workflow design creating 390, 391, 392, 393, 394, 395 Sandbox - Standard Acceptance Test environment production, servicing 363 servicing 361, 362

table properties reference 58 Table Relation Properties [AX 2012] reference 58 task recording test case, creating from 330, 332, 333 Team Foundation Server (TFS) 11, 19 Team Services Build Agent Queue creating 335, 336

[ 413 ]

Technical Design Document (TDD) 24 test case creating, from task recording 330, 332, 333 Test Driven Development (TDD) 319 test steps disabling, on build definitions 342 tiles creating, for workspace 108, 109, 111 typical index 65

Visual Studio, connecting to 16, 18, 19 Visual Studio Team Services project creating 11, 12, 13 linking, to LCS project 14 Visual Studio connecting, to Visual Studio Team Services 16, 18, 19 VSTS site reference, for creating 12

U

W

Unit Test case creating, for code 324, 326, 327, 329, 330 Unit Test project creating 322, 323 unit testing 319 update policies reference 351 User Acceptance Testing (UAT) build, releasing to 348, 349, 350 user interface workflow, hooking up to 388, 389

WebserviceX.net reference 284 workflow approval creating 374, 375, 376, 378, 379 workflow type creating 365, 366, 368, 369, 370, 371, 372, 373 workflow hooking up, to user interface 388, 389 state machine, hooking up to 406, 407, 408 workspace creating 112, 115 tiles, creating for 108, 109, 111

V Visual Studio Team Services (VSTS) about 10 configuring, for LCS project 14 reference 15

X X++ variables and data types reference 66