community.dynamics.com · About the Author Simon Buxton has worked with Dynamics 365 for Operations...

Post on 22-May-2020

12 views 0 download

Transcript of community.dynamics.com · About the Author Simon Buxton has worked with Dynamics 365 for Operations...

TitlePageExtendingMicrosoftDynamics365forOperationsCookbookExtendthepotentialofyourDynamics365forOperationsimplementationSimonBuxton

BIRMINGHAM-MUMBAI

Copyright

Credits

Author

SimonBuxton

CopyEditor

ZainabBootwala

Reviewers

SimonKlingler

MartinWinkler

ProjectCoordinator

VaidehiSawant

CommissioningEditor

AaronLazar

Proofreader

SafisEditing

AcquisitionEditor Indexer

DenimPinto TejalDaruwaleSoni

ContentDevelopmentEditor

SiddhiChavan

ProductionCoordinator

DeepikaNaik

TechnicalEditor

DhirajChandanshive

AbouttheAuthorSimonBuxtonhasworkedwithDynamics365forOperationssinceitsearliestincarnations,startingoutasaconsultantanddeveloperinearly1999whenDynamics365forOperationswasknownasDamgaardAxapta1.5.HequicklybecameateamleaderatColumbusITPartnersandcarriedoutoneofthefirstAxaptaimplementationsintheUKbeforejoininganewreseller,SenseEnterpriseSolutions,asitstechnicaldirector.SenseEnterpriseSolutionsenjoyedaglobalreachthroughtheAxPactalliance,whereSimonwasplacedasAxPact'sTechnicalLead.

SimonplayedamajorroleingrowingthecompanyintoasuccessfulMicrosoftpartnerandwastheTechnicalLeadonanumberofhighlychallengingtechnicalprojectsaroundtheworld,rangingfromtheUK,toBahrain,totheUSA.Theseprojectsincludedevelopingsolutionsforthird-partylogistics,multichannelretail,andeventuallydevelopingananimalfeedvertical,aswellasintegratingDynamics365forOperationsintoproductioncontrolsystems,governmentgateways,ande-commercesolutions,amongothers.Now,workingwithBinaryConsultants,hewaspartofateamthatimplementedthefirstDynamics365forOperationsimplementationwithsupportfromMicrosoftaspartoftheCommunityTechnicalPreviewprogram(CTP).Theknowledgegainedaspartofthisprocessledtothecreationofthisbook.

SimonhasalsoworkedonMasteringMicrosoftDynamicsAX2012R3ProgrammingandMicrosoftDynamicsAX2012R2AdministrationCookbook.

IwouldliketothankmycolleaguesatBinaryConsultantsfortheircontinuedsupportthroughoutthewritingprocess.Iwouldalsoliketothankallthosewhohelpedreviewthisbook,MartinWinklerandSimonKlinglerinparticular,whoputinalotofeffortintoreviewingeachrecipe.AlotoftheinsightwrittenintothisbookwasonlypossiblebybeingpartoftheDynamics365forOperationscommunitytechnicalpreviewprocess,andworkingwithMicrosoft'sR&Dteamisatrueprivilege.Finally,Iamtold,Ihavetothankmypartner,Evi,forherpatienceandsupportasIdisappearedforhourseachnightandweekendworkingon"yetanotherbook?".Ofcourse,nobiocouldbecompletewithoutmentioningmychildren,TylerandIsabella,whoseembemusedastowhyIwouldvoluntarilydohomework.

AbouttheReviewersSimonKlinglerhasbeenworkingwithMicrosoftDynamics365forOperationsanditspredecessorproducts(MicrosoftDynamicsAX,Axapta)since2001.Hegainedexperienceasadeveloper,consultant,presalesconsultant,productmanager,andsolutionarchitect.HehassuccessfullyimplementedERPsolutionsinnationalandinternationalprojectsfrom3to1000+users.Hewasaproductmanagerforseveraladd-onsforMicrosoftDynamicsERPsolutions.

Hestartedworkingonthelatestreleaseinitsveryearlydaysandregularlyexchangedexperiences,feedbacks,andlessonslearnedwiththeauthorofthisbook.Currently,heisworkingonseveralcloudandlocalbusinessdataimplementationprojectsasasolutionarchitect.

In2013,Simonco-foundedSemantax,acompanyprovidingproductsandexpertconsultingservicesbasedonMicrosoftDynamicsAX.InDecember2014,heco-foundedtheSolutionsFactory(www.sf-ax.com),acompanydeliveringfull-scaleMicrosoftDynamicsERPimplementations,instableandlong-lastingpartnershipswithitscustomers.Heisespeciallyproudoftheteamthatwasformedinthecourseofthelastyears:therightpeoplewiththerightattitude,agreatmixofexperiencedadvisersanddevelopers,aswellasyoungpotentials.Theteammembersmotivateeachotherandunfailinglyimpresstheclients.

MartinWinklerhasover13yearsofexperiencewithMicrosoftDynamics365forOperationsanditspreviousreleases.

AfterreceivinghisMaster'sdegreeinMathematicalComputerSciencesatViennaUniversityofTechnology,hegainedexperienceasanITConsultantatCapgeminiandlater,asaBIConsultantatanAustrianconsultingcompanyspecializinginCFO-targetedservices.

In2003,MartinjoinedSolutionsFactory,anewly-foundedAustriancompanythatspecializedinMicrosoftDynamics365forOperationsservices.StartingasadeveloperandheadofIT,helaterbecametheheadofdevelopmentofupto10developers.From2007onwards,headditionallybuiltupateamofperformanceexperts.In2008,thecompany(thenFWI)becamethelargestMicrosoftDynamics365forOperationspartnerinAustria.Whilestayingindevelopment,hemostlyworkedasTechnicalLeadConsultantforseveralcorporatecustomerswithglobalimplementationsandwithupto1000users.HealsocarriedoutnumerousprojectsintheareasofperformanceandMicrosoftBIforthesecustomers.

Togetherwithtwolong-termcolleagues,hefoundedhisowncompanyin2013,Semantax,providingproductsandexpertconsultingservicesbasedonMicrosoftDynamics365forOperations.In2014,theexpertsofSemantaxteamedupwithtwofurtherlong-termcolleagueswithvastknow-howinindustryprocessesandcorporateERPprojectstorelaunchtheSolutionsFactory.

SolutionsFactory(www.sf-ax.com)isdedicatedtoshapingcustomerprocessesandtomappingthemtoDynamics365forOperationsinanefficientandoptimalway.ItfollowsitsvisionofcontributingtothehighcompetitivecapabilityofEuropeanmanufacturingcompanies.Duringthereviewofthisbook,the

teamwassupportingfivelargecustomerswiththeirMicrosoftDynamics365forOperationsimplementations.

Overthelastdecade,MartinhasmettheauthorofthebookatseveraltechnicalconferencesonMicrosoftDynamics365forOperations.Hewasimpressednotonlywiththeauthor'sin-depthtechnicalknowledge,butalsowithhisunderstandingofthebusinessandprocesssidesoftheERPbranch.Heenjoyedreviewingthepreviousbooksbythesameauthorandwashonoredtosupporttheauthorwiththisbookaswell.Hehopesthereaderswillenjoythereadasmuchashedid.

www.PacktPub.comForsupportfilesanddownloadsrelatedtoyourbook,pleasevisitwww.PacktPub.com.

DidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwithusatservice@packtpub.comformoredetails.

Atwww.PacktPub.com,youcanalsoreadacollectionoffreetechnicalarticles,signupforarangeoffreenewslettersandreceiveexclusivediscountsandoffersonPacktbooksandeBooks.

https://www.packtpub.com/mapt

Getthemostin-demandsoftwareskillswithMapt.MaptgivesyoufullaccesstoallPacktbooksandvideocourses,aswellasindustry-leadingtoolstohelpyouplanyourpersonaldevelopmentandadvanceyourcareer.

Whysubscribe?

FullysearchableacrosseverybookpublishedbyPacktCopyandpaste,print,andbookmarkcontentOndemandandaccessibleviaawebbrowser

CustomerFeedback

ThanksforpurchasingthisPacktbook.AtPackt,qualityisattheheartofoureditorialprocess.Tohelpusimprove,pleaseleaveusanhonestreviewonthisbook'sAmazonpageathttps://www.amazon.com/dp/1786467135.

Ifyou'dliketojoinourteamofregularreviewers,youcane-mailusatcustomerreviews@packtpub.com.WeawardourregularreviewerswithfreeeBooksandvideosinexchangefortheirvaluablefeedback.Helpusberelentlessinimprovingourproducts!

TableofContents

PrefaceWhatthisbookcovers

Whatyouneedforthisbook

Whothisbookisfor

SectionsGettingready

Howtodoit…

Howitworks…

There'smore…

Seealso

Conventions

Readerfeedback

CustomersupportDownloadingtheexamplecode

Errata

Piracy

Questions

1. StartingaNewProjectIntroduction

CreatingtheVisualStudioTeamServicesprojectGettingready

Howtodoit...

Howitworks...

Seealso...

ConnectingVisualStudiotoVisualStudioTeamServicesGettingready

Howtodoit...

Howitworks...

There'smore...

Seealso

CreatinganewModelandPackagesGettingready

Howtodoit...

Howitworks...

There'smore...Prefixesandnamingconventions

ConfiguringprojectandbuildoptionsGettingready

Howtodoit...Dynamics365forOperations'options

Theproject-specificparameters

There'smore...

CreatingaLabelfileGettingready

Howtodoit...

Howitworks...

There'smore...

2. DataStructuresIntroduction

CreatingenumeratedtypesGettingready

Howtodoit...

Howitworks...

There'smore...UsingEnumsforcomparisonandstatus

ExtensibilityinBaseEnums

CreatingextendeddatatypesGettingready

Howtodoit...

Howitworks...

There'smore...

CreatingsetuptablesGettingready

Howtodoit...

Howitworks...

CreatingaparametertableHowtodoit...

There'smore...Copyingandpastingmethodstosavetime

OptimisticconcurrencyandselectForUpdate

Seealso

CreatingmaindatatablesGettingready

Howtodoit...

Howitworks...

There'smore...Moreonindexes

Seealso

CreatingorderheadertablesGettingready

Howtodoit...

Howitworks...

There'smore...

CreatingorderlinetablesHowtodoit...

Howitworks...

Seealso

3. CreatingtheUserInterfaceIntroduction

CreatingthemenustructureGettingready

Howtodoit...

Howitworks...

CreatingaparameterformHowtodoit...

Howitworks...

There'smore...

Seealso

CreatingmenuitemsGettingready

Howtodoit...

Howitworks...

CreatingsetupformsHowtodoit...

Howitworks...

There'smore...

Creatingdetailsmaster(maintable)formsHowtodoit...

Howitworks...

Creatingadetailstransaction(orderentry)formHowtodoit...

Howitworks...

CreatingformpartsGettingready

Howtodoit...

Howitworks...

CreatetileswithcountersfortheworkspaceGettingready

Howtodoit...

Howitworks...

There'smore...

CreatingaworkspaceHowtodoit...

Howitworks...

There'smore...

4. ApplicationExtensibility,FormCode-Behind,andFrameworksIntroduction

CreatingahandlerclassusingtheApplicationExtensionfactoryGettingready

Howtodoit...

Howitworks...

There'smore...

Seealso...

HookingupanumbersequenceGettingready

Howtodoit...

Howitworks...Numbersequencesetup

Hookingupthenumbersequence

There'smore...

CreatingacreatedialogfordetailstransactionformsGettingready

Howtodoit...

Howitworks...

CreatingaSysOperationprocessHowtodoit...

Howitworks...

There'smore...Executingcodeusingthebatchframework

Callingaprocessfromaform

Usingthedatacontracttomakechangestothedialog

AddinganinterfacetotheSysOperationframeworkGettingready

Howtodoit...

Howitworks...

5. BusinessIntelligenceIntroduction

CreatingaggregatedimensionsGettingready

Howtodoit...

Howitworks...

Seealso

CreatingaggregatemeasuresGettingready

Howtodoit...

Howitworks...

CreatingaggregatedataentitiesGettingready

Howtodoit...

Howitworks...

CreatingandusingkeyperformanceindicatorsGettingready

Howtodoit...

Howitworks...

There'smore...

6. SecurityIntroduction

CreatingprivilegesGettingready

Howtodoit...

Howitworks...

There'smore...Impactonlicensing

Seealso

CreatingdutiesHowtodoit...

Howitworks...

There’smore…

CreatingsecurityrolesHowtodoit...

Howitworks...

Seealso...

CreatingpoliciesHowtodoit...

Howitworks...

There'smore...

Seealso...

7. LeveragingExtensibilityIntroduction

ExtendingstandardtableswithoutcustomizationfootprintGettingready

Howtodoit...

Howitworks...

There'smore...

Creatingdata-eventhandlermethodsGettingready

Howtodoit...

Howitworks...

There'smore...

Howtocustomizeadocumentlayoutwithoutanover-layerHowtodoit...

Howitworks...

Theremore...

CreatingeventhandlermethodsGettingready

Howtodoit...

Howitworks...

ExtendingstandardformswithoutcustomizationfootprintGettingready

Howtodoit...

Howitworks...

There'smore...

UsingaformeventhandlertoreplacealookupGettingready

Howtodoit...

Howitworks...

CreatingyourownqueryfunctionsHowtodoit...

Howitworks...

8. DataManagement,OData,andOfficeIntroduction

CreatingadataentityGettingready

Howtodoit...

Howitworks...

There'smore...

Seealso

ExtendingstandarddataentitiesGettingready

Howtodoit...

Howitworks...

There'smore...

ImportingdatathroughDataImport/ExportFrameworkGettingready

Howtodoit...

Howitworks...

Seealso

Reading,writing,andupdatingdatathroughODataGettingready

Howtodoit...

Howitworks...

Seealso

9. ConsumingandExposingServicesIntroduction

CreatingaserviceGettingready

Howtodoit...

Howitworks...

ConsumingaDynamics365forOperationsSOAPserviceGettingready

Howtodoit...

Howitworks...

Seealso

ConsumingaDynamics365forOperationsJSONserviceGettingready

Howtodoit...

Howitworks...

There'smore...

Seealso...

ConsuminganexternalservicewithinDynamics365forOperationsGettingready

Howtodoit...

Howitworks...

There'smore...

10. ExtensibilityThroughMetadataandDataDate-EffectivenessIntroduction

UsingmetadatafordataaccessGettingready...

Howtodoit...

Howitworks...

UsingInterfacesforextensibilitythroughmetadataGettingready...

Howtodoit...

Howitworks...

Makingdatadate-effectiveGettingready...

Howtodoit...

Howitworks...

There'smore...

11. UnitTestingIntroduction

CreatingaFormAdaptorprojectGettingready

Howtodoit...

Howitworks...

CreatingaUnitTestprojectGettingready

Howtodoit...

Howitworks...

CreatingaUnitTestcaseforcodeGettingready

Howtodoit...

Howitworks...

CreatingatestcasefromataskrecordingGettingready

Howtodoit...

Howitworks...

12. AutomatedBuildManagementIntroduction

CreatingaTeamServicesBuildAgentQueueGettingready

Howtodoit...

Howitworks...

SettingupabuildserverGettingready

Howtodoit...

Howitworks...

There'smore...

Seealso

ManagingbuildoperationsGettingready

Howtodoit...

Howitworks...

ReleasingabuildtoUserAcceptanceTestingGettingready

Howtodoit...

Howitworks...

13. ServicingYourEnvironmentIntroduction

ApplyingmetadatafixesGettingready

Howtodoit...

Howitworks...

There'smore...

Seealso

ApplyingbinaryupdatesGettingready

Howtodoit...

Howitworks...

Seealso

ServicingthebuildserverGettingready

Howtodoit...

Howitworks...

ServicingtheSandbox-StandardAcceptanceTestenvironmentGettingready

Howtodoit...

Howitworks...

There'smore...Servicingproduction

Seealso

14. WorkflowDevelopmentIntroduction

CreatingaworkflowtypeGettingready

Howtodoit...

Howitworks...

Seealso...

CreatingaworkflowapprovalGettingready

Howtodoit...

Howitworks...

CreatingamanualworkflowtaskGettingready

Howtodoit...

Howitworks...

HookingupaworkflowtotheuserinterfaceGettingready

Howtodoit...

Howitworks...

CreatingasampleworkflowdesignGettingready

Howtodoit...

Howitworks...

15. StateMachinesIntroduction

CreatingastatemachineGettingready

Howtodoit...

Howitworks...

CreatingastatemachinehandlerclassGettingready

Howtodoit...

Howitworks...

There'smore...

UsingmenuitemstocontrolastatemachineGettingready

Howtodoit...

Howitworks...

HookingupthestatemachinetoaworkflowGettingready

Howtodoit...

Howitworks...

There'smore...

ExtendingMicrosoftDynamics365forOperationsCookbookCopyright©2017PacktPublishingAllrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.

Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyoftheinformationpresented.However,theinformationcontainedinthisbookissoldwithoutwarranty,eitherexpressorimplied.Neithertheauthor,norPacktPublishing,anditsdealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecauseddirectlyorindirectlybythisbook.

PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesandproductsmentionedinthisbookbytheappropriateuseofcapitals.However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.

Firstpublished:May2017

Productionreference:1120517

PublishedbyPacktPublishingLtd.LiveryPlace35LiveryStreetBirminghamB32PB,UK.

ISBN978-1-78646-713-3

www.packtpub.com

PrefaceMicrosoftDynamics365forOperationsisanERPsolutionforcomplexsingle-site,multi-site,andmulti-languageglobalenterprises.Thisflexibleandagilesolutionprovidesadvancedfunctionalityformanufacturing,retail,publicsector,andservicesectorindustries.Notonlydoesthissolutionprovideastrongsetofbuilt-infunctionality,italsoprovidesanindustry-leadingintegrateddevelopmentenvironment,allowinganorganizationtoprovideevenhigherlevelsoffit.ThisbookshouldbeinthetoolbeltofanysoftwareengineerwhoworkswithorisabouttoembarkonacareerwithDynamics365forOperations.

ThisprovidessoftwareengineersandthoseinvolvedindevelopingsolutionswithinDynamics365forOperationswithatoolkitofpracticalrecipesforcommondevelopmenttasks,withimportantbackgroundinformationtoprovidedeepinsighttoallowtherecipestobeadaptedandextendedforyourownuse.Evenforexperiencedsoftwareengineers,thisbookwillprovideagoodsourceofreferenceforefficientsoftwaredevelopment.

ForthosemovingfromMicrosoftDynamicsAX2012,wecovercriticalchangesinhowsoftwareisadapted,howtousethenewextensibilityfeaturesofMicrosoftDynamics365forOperations,andtipsonhowtousetheminapracticalway.Wealsocoverthefundamentalchangesinthephysicalstructureoftheapplicationmetadata,theapplicationdevelopmentlifecycle,andhowwefitinwiththenewcloud-firstdevelopmentparadigmwithLifecycleservicesandVisualStudioTeamServices.IntegrationwillbeaconcerntoAXdevelopers,andwecoverthisindetailwithworkingexamplesofcodethatcanbeadaptedtoyourownneeds.

Inordertofacilitatethis,thebookfollowsthedevelopmentofasolutionasameanstoexplainthedesignanddevelopmentoftables,classes,forms,BI,menustructures,workflow,andsecurity.WebeginatthestartofthedevelopmentprocessbysettingupaVisualStudioTeamServicesproject,integratingLifecycleservices,andexplainingnewconceptssuchasPackages,Models,Projects,andwhathappenedtolayers.Thebookprogresseswithchaptersfocusedoncreatingthesolutioninapracticalorder,butitiswritteninsuchawaythateachrecipecanbeusedinisolationasapatterntofollow.

Thesamplesolutionwasdesignedanddevelopedasthebookwaswrittenandisavailablefordownload.ThereisasampleOperationsproject,ODataC#integrationtestproject,andaC#projectforusingwebservicessuppliedbyMicrosoftDynamics365forOperations.

Withthiscomprehensivecollectionofrecipes,youwillbearmedwiththeknowledgeandinsightyouneedtodevelopwell-designedsolutionsthatwillhelpyourorganizationtogetthemostvaluefromthiscomprehensivesolutionforboththecurrentandtheupcomingreleasesofMicrosoftDynamics365forOperations.

WhatthisbookcoversChapter1,StartingaNewProject,coverssettingupanewVisualStudioTeamServiceproject,integratingwithLifecycleServices,andcreatingaMicrosoftDynamics365forOperationspackageandmodel.

Chapter2,DataStructures,containscommonrecipesforcreatingdatastructureelementssuchastables,enumerateddatatypes,andextendeddatatypes.Therecipesarewrittentopatterns,guidingyouthroughthestepsyouwouldtakewhencreatingthetypesoftableusedinMicrosoftDynamics365forOperationsapplicationdevelopment.

Chapter3,CreatingtheUserInterface,explainshowtocreatetheuserinterfaceelementssuchasmenus,forms,formparts,tiles,andworkspaces.ThischapterincludesrecipesforeachofthemaintypesofuserinterfacesusedwhencreatingorextendingDynamics365forOperationswithpracticalguidanceandtipsonhowtodothisefficiently.

Chapter4,ApplicationExtensibility,FormCode-Behind,andFrameworks,helpsusstepintowritingthebusinesslogicbehindouruserinterfaceandunderstandhowtowritecodedesignedtobeextensible,allowingotherpartiestoextendourcodewiththeover-layeringthatcanversion-lockcustomers.WealsocovertheSysOperationframework,usingwhichprocessesaredeveloped,andwe'llseehowtoaddauserinterfacetothem.

Chapter5,BusinessIntelligence,coversthecreationofabusinessintelligenceprojectthatcanbeusedtocreatepowerfuldashboardsinMicrosoftPowerBI.Therecipesinthischaptercoverthecreationofaggregatedimensions,measures,dataentities,andKPIsinareal-worldcontext.Thisisdoneusingthesamplevehiclemanagementapplicationthatiscreatedthroughthecourseofthisbook.

Chapter6,Security,explainsthesecuritymodeldesigninMicrosoftDynamics365forOperationsandprovidesrecipesforthecreationoftheelementsusedinsecurity.Therecipesaugmentthestandarddocumentationinordertoprovidereal-worldexamplesonhowtocreateandmodelDynamics365forOperationssecurity.

Chapter7,LeveragingExtensibility,showshowextensibilitycanbesaidtobeoneofthebiggestchangesinDynamics365forOperations.Thischapterpaysspecialattentiontothekeyaspectsofhowtouseextendthestandardapplicationwithoutbecomingversionlockedinacustomizedsolution.

Chapter8,DataEntityExtensibility,OData,andOffice,coversthemanywaysinwhichweintegratewiththeworldoutsideofDynamics365forOperations.Thiscovershowtocreateandextenddataentities,workwithMicrosoftOffice,anduseODatatoread,write,andupdatedatainDynamic365forOperationsfromaC#project.

Chapter9,ConsumingandExposingServices,providesrecipesforcreatingaservicefromwithinDynamics365forOperations,consumingexternalservices,andalsoonconsumingDynamics365forOperationsservicesinC#usingSOAPandJSON.Allthisiscoveredusingpracticalexamplesthatshouldeasilytranslateintoyourownspecificrequirements.

Chapter10,ExtensibilitythroughMetadataandDataDate-Effectiveness,pushesextensibilityevenfurtherbyshowinghowwecanusemetadatastoredindatatoputmorepowerinthehandsofsystemadministrators.Wealsocoverhowtomakeourtablesdateeffective.

Chapter11,UnitTesting,providesrecipestoshowhowtocreateunittestsandhowtheyareusedwiththeapplicationlifecycle.Thischaptercoversaninsightintotest-drivendevelopment,automatedunittestingonthebuildserver,andhowtocreateandusethetaskrecordertocreatetestcases.

Chapter12,AutomatedBuildManagement,helpsusmovemoreintoapplicationlifecyclemanagementwherethischapterprovidesrecipesforsettingupandusingabuildserver.

Chapter13,ServicingYourEnvironment,providespracticalrecipesthatareintendedtoaugmentthestandarddocumentationprovidedbyMicrosoftinordertoprovidereal-worldexamplesonhowweserviceourDynamics365forOperationsenvironments.

Chapter14,Workflow,coversthedevelopmentofworkflowapprovalsandtasksinDynamics365forOperations.Therecipesaregivencontextbycontinuingtoworkwiththesampleapplicationthatiscreatedthroughthecourseofthisbook,effectivelyexplainingstatemanagement,whichiseasilymisunderstood.

Chapter15,StateMachines,coversstatemachines,whichisanothernewfeatureinDynamics365forOperations.Thischaptercoversallkeyareasofthisnewfeature,explainingwhenandhowtousethisfeatureappropriately.

WhatyouneedforthisbookInordertogainaccesstoMicrosoftDynamics365forOperations,youneedtobeeitheraMicrosoftpartnerorcustomer.Tosignupforagainaccessasapartner,youcanrefertoLifecycleServicesforDynamics365forOperationspartnersathttps://docs.microsoft.com/en-us/dynamics365/operations/dev-itpro/lifecycle-services/getting-started-lcs.

Tosignupforasubscriptionasacustomer,refertoLifecycleServicesforDynamics365forOperationspartnersathttps://docs.microsoft.com/en-us/dynamics365/operations/dev-itpro/lifecycle-services/getting-started-lcs.

YouwillneedtodownloadordeployaDynamics365forOperationsdevelopmentVMinAzure.ToruntheVMlocally,youwillneedatleast100GBfreespaceavailableandaminimumof12GBfreememory,ideally24GB.Itwillrunonaslittleas8GBofassignedmemory,buttheperformancewillsufferasaresult.

Theofficialsystemrequirementsareasfollows:

Systemrequirements(https://docs.microsoft.com/en-us/dynamics365/operations/dev-itpro/get-started/system-requirements)Developmentsystemrequirements(https://docs.microsoft.com/en-us/dynamics365/operations/dev-itpro/dev-tools/development-system-requirements)

Whothisbookisfor

IfyouareasoftwaredevelopernewtoDynamics365forOperationsprogrammingoranexperiencedsoftwareengineermigratingfromitspredecessor,DynamicsAX,thisbookisanidealtutorialtohelpyouavoidthecommonpitfallsandmakethemostofthisadvancedtechnology.Thisbookisalsousefulifyouareasolutionarchitectortechnicalconsultant,asitprovidesadeeperinsightintothetechnologybehindthesolution.

SectionsInthisbook,youwillfindseveralheadingsthatappearfrequently(Gettingready,Howtodoit,Howitworks,There'smore,andSeealso).

Togiveclearinstructionsonhowtocompletearecipe,weusethesesectionsasfollows:

GettingreadyThissectiontellsyouwhattoexpectintherecipe,anddescribeshowtosetupanysoftwareoranypreliminarysettingsrequiredfortherecipe.

Howtodoit…Thissectioncontainsthestepsrequiredtofollowtherecipe.

Howitworks…Thissectionusuallyconsistsofadetailedexplanationofwhathappenedintheprevioussection.

There'smore…Thissectionconsistsofadditionalinformationabouttherecipeinordertomakethereadermoreknowledgeableabouttherecipe.

SeealsoThissectionprovideshelpfullinkstootherusefulinformationfortherecipe.

ConventionsInthisbook,youwillfindanumberoftextstylesthatdistinguishbetweendifferentkindsofinformation.Herearesomeexamplesofthesestylesandanexplanationoftheirmeaning.

Codewordsintext,databasetablenames,foldernames,filenames,fileextensions,pathnames,dummyURLs,userinput,andTwitterhandlesareshownasfollows:"ThisshouldhavecreatedafoldercalledBaseEnums."

Ablockofcodeissetasfollows:

publicvoidmodifiedField(FieldId_fieldId)

{

super(_fieldId);

switch(_fieldId)

{

casefieldNum(ConWHSVehicleServiceLine,ItemId):

this.initFromInventTable(

InventTable::find(this.ItemId));

break;

}

}

Newtermsandimportantwordsareshowninbold.Wordsthatyouseeonthescreen,forexample,inmenusordialogboxes,appearinthetextlikethis:"IntheAddNewItemdialog,selectDataModelfromtheleft-handlistandQueryfromtheright."

Warningsorimportantnotesappearinaboxlikethis.

Tipsandtricksappearlikethis.

ReaderfeedbackFeedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthisbook-whatyoulikedordisliked.Readerfeedbackisimportantforusasithelpsusdeveloptitlesthatyouwillreallygetthemostoutof.

Tosendusgeneralfeedback,simplye-mailfeedback@packtpub.com,andmentionthebook'stitleinthesubjectofyourmessage.

Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingorcontributingtoabook,seeourauthorguideatwww.packtpub.com/authors.

CustomersupportNowthatyouaretheproudownerofaPacktbook,wehaveanumberofthingstohelpyoutogetthemostfromyourpurchase.

DownloadingtheexamplecodeYoucandownloadtheexamplecodefilesforthisbookfromyouraccountathttp://www.packtpub.com.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.

Youcandownloadthecodefilesbyfollowingthesesteps:

1. Loginorregistertoourwebsiteusingyoure-mailaddressandpassword.2. HoverthemousepointerontheSUPPORTtabatthetop.3. ClickonCodeDownloads&Errata.4. EnterthenameofthebookintheSearchbox.5. Selectthebookforwhichyou'relookingtodownloadthecodefiles.6. Choosefromthedrop-downmenuwhereyoupurchasedthisbookfrom.7. ClickonCodeDownload.

YoucanalsodownloadthecodefilesbyclickingontheCodeFilesbuttononthebook'swebpageatthePacktPublishingwebsite.Thispagecanbeaccessedbyenteringthebook'snameintheSearchbox.PleasenotethatyouneedtobeloggedintoyourPacktaccount.

Oncethefileisdownloaded,pleasemakesurethatyouunziporextractthefolderusingthelatestversionof:

WinRAR/7-ZipforWindowsZipeg/iZip/UnRarXforMac7-Zip/PeaZipforLinux

ThecodebundleforthebookisalsohostedonGitHubathttps://github.com/PacktPublishing/Extending-Microsoft-Dynamics-365-for-Operations-Cookbook.Wealsohaveothercodebundlesfromourrichcatalogofbooksandvideosavailableathttps://github.com/PacktPublishing/.Checkthemout!

Errata

Althoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdohappen.Ifyoufindamistakeinoneofourbooks-maybeamistakeinthetextorthecode-wewouldbegratefulifyoucouldreportthistous.Bydoingso,youcansaveotherreadersfromfrustrationandhelpusimprovesubsequentversionsofthisbook.Ifyoufindanyerrata,pleasereportthembyvisitinghttp://www.packtpub.com/submit-errata,selectingyourbook,clickingontheErrataSubmissionFormlink,andenteringthedetailsofyourerrata.Onceyourerrataareverified,yoursubmissionwillbeacceptedandtheerratawillbeuploadedtoourwebsiteoraddedtoanylistofexistingerrataundertheErratasectionofthattitle.

Toviewthepreviouslysubmittederrata,gotohttps://www.packtpub.com/books/content/supportandenterthenameofthebookinthesearchfield.TherequiredinformationwillappearundertheErratasection.

PiracyPiracyofcopyrightedmaterialontheInternetisanongoingproblemacrossallmedia.AtPackt,wetaketheprotectionofourcopyrightandlicensesveryseriously.IfyoucomeacrossanyillegalcopiesofourworksinanyformontheInternet,pleaseprovideuswiththelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy.

Pleasecontactusatcopyright@packtpub.comwithalinktothesuspectedpiratedmaterial.

Weappreciateyourhelpinprotectingourauthorsandourabilitytobringyouvaluablecontent.

QuestionsIfyouhaveaproblemwithanyaspectofthisbook,youcancontactusatquestions@packtpub.com,andwewilldoourbesttoaddresstheproblem.

StartingaNewProjectInthischapter,wewillcoverthefollowingrecipes:

CreatingtheVisualStudioTeamServicesprojectConnectingVisualStudiotoyourVisualStudioTeamServicesCreatinganewModelandPackageConfiguringprojectandbuildoptionsCreatingaLabelfile

IntroductionMicrosoftDynamicsAX2012underwentanamechangeinwhatwouldhavebeenversion7.TheofficialnameisnowMicrosoftDynamics365forOperations.Itisn'tjustthattheversionnumberhasbeendropped,butitappearstohavebeenadoptedintotheMicrosoftDynamics365productsuite.TheproductisnotacomponentofMicrosoftDynamics365,whichisjustawaytogroupMicrosoft'svariousbusinesssolutions.Wecan't,therefore,shortenthenametoDynamics365,wewillrefertheproductbyeitheritsfullnameortheabbreviationOperations.

NewfeatureswillbeintroducedtoOperationsasbothacontinualandcumulativeprocess.Therearetwomaintypesofupdate,PlatformandApplication.Platformupdatesaresimilartothebinaryupdatesinpriorreleases,butalsocontainAOTelementsthatarenowlockedandcannolongerbechanged.Platformupdatescancontainchangestothelanguage,andnewfeatureshavebeenbroughtinwitheachbi-yearlyrelease.WhenrunningintheCloud,MicrosoftwillperiodicallyreleaseupdatestothePlatformforyou.Thisisneeded,sincetheitusesAzureSQLServerandtheymayneedtoservicetheplatforminordertomaintaincompatibilitytothedatabaseserver.

ApplicationupdatesarechangestotheotherpackagesthatmakeupthesourcecodeofOperations,andcanbeconsideredsimilartothemetadataupdatesinpreviousreleasesofOperations.

ThisbookwasstartedontheMay2016,orUpdate1releaseandhasbeenupdatedwitheachrelease.TheversiononpublicationisUpdate5,releasedinMarch2017.

AlldevelopmentworkiscarriedoutinconjunctionwithVisualStudioTeamServicesorVSTS.Itusedtobeoptional,buttheimplementationprocessthatismanagedthroughLifecycleServices(LCS)requiresthatwehaveaVSTSprojectlinkedtoittofunctiontoitsfullest.Thisisnotjustforsourcecontrolandworkmanagement,butitisalsousedwhenperformingcodeupgrades.

PleaseseethefollowinglinksforfurtherreadingonMicrosoftDynamics365forOperations:

FormoreinformationonLCS,pleaseseethelink,https://lcs.dynamics.com/Logon/IndexAnoverviewofMicrosoftDynamics365forOperationsforDevelopersandITProsisavailableathttps://docs.microsoft.com/en-us/dynamics365/operations/dev-itpro/ToobtainanevaluationcopyofMicrosoftDynamics365forOperations,pleaseseethelink,https://docs.microsoft.com/en-us/dynamics365/operations/dev-itpro/dev-tools/get-evaluation-copy

AlldevelopmentworkinOperationsiseitherperformedonadevelopmentvirtualmachinehostedinAzure,oralocalvirtualmachine.Eachdeveloperwillusetheirownvirtualmachine.AzurehostedvirtualmachinesaredeployedthroughLCSunderyourownAzuresubscription,andcanbeusedtodevelopment,learning,anddemonstration.Once,asacustomer,acloudhostedsubscriptionhasbeenbought,youareprovided3environmentsaspartofthatsubscriptionunderMicrosoft'ssubscription.TheseareBuild,Sandbox,andProduction.ThebuildseverisaOneBoxserver(allinonevirtualmachine)thatisalsolabelledDevelopment,butitshouldalwaysbeusedasabuildserverandnotfordevelopment.Thesandboxserverisafullenvironment,withmultipleserversusingaseparateAzureSQLServer.Theproductionenvironmentistheenvironmentthatyou(asacustomer)willgolivewith.All

codemustfirstbedeployedtothesandboxbeforeitisappliedtolive,whichthisisenforcedbyLCS-nomore'quickfixes'directlyintolive,andnoaccesstoSQLserverfortheproductionenvironment.

TheonpremiseversionofOperationsmayallowustobypasssomeoftheserules,butweshouldn'ttry-thesepracticesofforcingcodethroughafulltestingcycleareveryimportant.Waitingacoupleofdaysforaneededfeaturemaybeinconvenient,butregressionisperceivedverynegativelybyusers.Duringtheimplementationweaskalotoftheusers,theyarealreadybusywiththeirjobsandarebeingaskedtoalsohelpwithtestingnewsoftware,souserbuy-intotheprojectisacriticalfactor,andregressionisthemostefficientwayoferodingtheinitialexcitementofdeliveringnewsoftware.

Forlocaldevelopmentvirtualmachinesthatareoftenthecheapestoption,wewilldownloadthevirtualmachinefromMicrosoftConnect.ThisisawebsiteusedformanyprogramsatMicrosoft,andaccessisprovidedtopartnersandcustomers.

CreatingtheVisualStudioTeamServicesprojectThetermsVisualStudioTeamServicesandTeamFoundationServer(TFS)areoftenusedinterchangeably.InVisualStudio,theuserinterfacestatesthatweareconnectingtoaTeamFoundationServer.However,weareactuallyconnectingtoVSTS,whichisanonlineservice.VSTSisrequiredforOperationsdevelopment,andthatiswhatwewilluse.

Theprojectisnormallycreatedundertheend-user'sVSTSsite,unlesstheworkisbeingwrittenasanISVsolution(orapersonaldevelopmentorlearningproject).Thereasonforusingtheclient'sVSTSsystemisthatLCSisassociatedwiththeVSTSsite,andsupportcallscreatedthroughCloud-poweredsupportaregeneratedwithintheassociatedVSTS.CloudpoweredsupportisanonlinesupportsolutionwithinLCSthatisexposedtotheOperationsclient,allowinguserstologsupportissueswiththeirinternalsupportteam.

Foruptofiveusers,VSTSisfree,andthecustomercancreatemanyaccountswithlimitedaccesswithoutcharge.Theseaccountsascalledstakeholderaccounts,andallowstheuseraccesstoworkitems,whichalsoallowstheuserstheabilitytologsupportcallsfromwithinOperations.ForthosewithanMSDNsubscription,thefiveuserlimitisnotcounted.

ThisprocessisnormallyperformedaspartoftheLCSprojectcreation.Ifthiswereanimplementationprojecttype,theprojectiscreatedwhenthecustomersignsupforOperations.ThecustomerwouldtheninvitetheirCloudsolutionprovider(Partner)totheproject.Ifthiswereaninternaldevelopmentproject,suchasanewverticalsolutionbyanISV,aMigrate,createsolutions,andlearnDynamics365forOperationsprojecttypewouldbeused.

Ineithercase,wewillhaveanLCSproject,whichwillusuallyhaveanAzureVMdeployedthatactsasabuildserver.

Forsimplicity,andtokeepthefocusonsoftwaredevelopment,aprojectoftypeMigrate,createsolutions,andlearnDynamics365forOperationswascreatedforthepurposeoftheexampleofthebook.

GettingreadyBeforewegetstarted,wewillneedanLCSprojectandaVSTSsite.TheVSTSsitecanbecreatedthroughthefollowinglink:

https://www.visualstudio.com/en-us/products/visual-studio-team-services-vs.aspx

Oncewehavethesitecreated,wecanthencreateourproject.

Howtodoit...Tocreatetheproject,followthesesteps:

1. NavigatetoyourVSTSsite,forexample,https://<yourdomain>.visualstudio.com/.2. UnderRecentprojects&teams,clickonNew.3. Completetheformasshownasfollows:

Field Value

Projectname

Uniquename,carefultonametheprojectsforeasyrecognition,andhowtheyareordered.ThisismoreimportantforISVswhomayhavemanyprojects.

Description Shortdescriptionoftheproject

Processtemplate Agile

Versioncontrol TeamFoundationVersionControl

4. PressCreateproject.

5. Oncecomplete,youcanthennavigatetoyourprojectandworkwithVSTSinordertoplanyourproject.

6. ToauthenticatewithLCS,wewillneedtogenerateapersonalaccesstoken;tosetthisup,clickonthecontrolpanel(cog)icon,asshowninthefollowingscreenshot:

7. Thistakesyoutothecontrolpanel,again,onthetop-rightclickonyournameandchooseSecurity,asshowninthefollowingscreenshot:

8. Thepersonalaccesstokensoptionisselectedbydefault;ontheright-handpane,clickonAdd.9. OnCreateapersonalaccesstokenform,enterashortdescription,forexample,theLCSproject

name.SettheExpiresinfieldbasedonhowlongyouwouldlikeittolastfor.10. LeavingtheAccountsandAuthorizedscopesfieldsasdefault;pressCreatetoken.11. Finally,copytheresultantaccesscodeintoasafeplace;wewillneeditwhenwelinkVSTStoLCS.

Ifwedon't,wewillhavetocreateanewaccesstokenasyoucan'tseeitafterthewebpageisclosed.

Next,wewillneedtolinktheprojecttoourLCSproject.IfanLCSprojectisnotcurrentlylinkedtoaVSTSproject,wegetthefollowingmessageonthelefthandside,asshowninthefollowingscreenshot:

ToconfigureVSTSfortheLCSproject,followthesesteps:

1. ToauthenticatewithLCS,wewillneedtogenerateapersonalaccesstoken,sofromwithinVSTS.2. ClickontheSetupVisualStudioTeamServicesbuttonintheActioncenterdialogbox.3. OntheEntertheVisualStudioTeamServicesitepage,entertheURLofourVSTSsiteintothe

VisualStudioTeamServicessiteURLfield;forexample,https://<mysite>.visualstudio.com/.4. EnterthepersonalaccesstokengeneratedearlierintothePersonalaccesstokenfield.5. PressContinue.6. OntheSelecttheVisualStudioTeamServiceprojectpage,selecttheprojectfromtheVisualStudio

TeamServicelist.7. YouarethenshowntheWorkitemtypemappinglist.ThisallowsyoutoselecthowtoLCSWorkitem

Type/LCSWorkitemSubTypeelementstoVSTSWorkitemTypeelements.LeavethisasthedefaultandpressContinue.

8. OnthefinalReviewandsavepage,pressSave.9. Thistakesusbacktothemainprojectpageandtheactioncenterwillaskyoutoauthorizethe

project;clickonAuthorize.

10. Youwillbewarnedaboutbeingredirectedtoanexternalsite;clickonYes.11. Youmaybeaskedtologon;ifso,doitwiththeaccountyouuseforVSTS,whichmightbeyour

Microsoftaccount.12. ThiswillopentheAuthorizeapplicationpagefromwithinVSTS,andyouwillbetoldthatyouare

allowingMicrosoftDynamicsLifecycleServicestoaccesstheVSTSandthespecificpermissionsitwillreceive.PressAccept.

Howitworks...

OperationsusesVSTSforitssourcecontrol,work,andbuildmanagement.Theonlystepsherethatwetechnicallymustperformarestep1throughstep5,butwithoutperformingtheprevioussteps,welosetheabilitytointegrateLCS.Ifourprojectwasforacustomerimplementation,weshouldconsideritmandatorytointegrateVSTSwithLCS.

Seealso...

FormoreinformationonVSTSandLCS,pleasecheckoutthefollowinglinks:

AXDevALMusageguideandresources(https://blogs.msdn.microsoft.com/axdevalm/)LCSforMicrosoftDynamics365forOperationscustomers(https://ax.help.dynamics.com/en/wiki/how-lifecycle-services-for-microsoft-dynamics-ax-works-lcs/)Developertopologydeploymentwithcontinuousbuildandtestautomation(https://ax.help.dynamics.com/en/wiki/developer-topology-deployment-with-continuous-build-and-test-automation/)

Thenextlinkisusefulbackgroundknowledge,butalotofthisisoneforyouwhenusinganimplementationLCSproject:

SetuptechnicalsupportforMicrosoftDynamics365forOperations(https://ax.help.dynamics.com/en/wiki/ax-support-experience/)

ThislinkisforwhenwehaveacustomerimplementationprojectanddemonstratesomeofthesynergyofleveragingVSTSandLCSwithOperations.

ConnectingVisualStudiotoVisualStudioTeamServices

EachdeveloperhastheirowndevelopmentVM,hostedeitherinAzureorlocally.Thisisbydesignandispartoftheapplicationlifecycleprocess.Eachdeveloperwouldgetthelatestcodefromsourcecontrol,andthencheckintheirchangesaccordingtheirorganization'sdevelopmentmethodology.Aspartofthischeckintheycanlinkthecheck-ins.Thisallowsabuildtothenbecreated,andwegainaleveloftraceabilitysinceeachworkitem(userstory,feature,bug,andsoon.)islinkedtothecheck-insinthatbuild.Thisalsoallowstestprojectstobeautomaticallyexecutedwhenthebuildisgenerated.

Gettingready

Oncethevirtualmachinehasstarted,ensurethatithasInternetaccess,andthatyouhaveusedtheadminuserprovisioningtooltoassociateyourO365accountwiththeadministratoraccountofOperations.

Beforeyoustartthis,especiallywhenworkinginateam,wemustrenametheVMtomakeituniqueacrossourteam;seetheThere'smore...sectionfordetailsonthis.

Howtodoit...ToconnectVisualStudiotoVSTS,followthesesteps:

1. Createafolderforyourprojects,andunderneathasubfolderwithyourinitials,orothersthatmakethefolderunique,withinyourteam;inmyexample,IchoseC:ProjectsTFS.

2. StartVisualStudio.3. Youwillbepresentedwiththelicensingpage.Usethepagetologintotheaccountusedtocreatethe

projectwithinVSTS.WhichcouldbeeitheryourMicrosoftaccount,orWork(O365)account.4. Onthetoptoolbar,selectTeamandthenManageconnections.5. TheTeamExplorerwillopen,underdefaultlayout,ontheright-handside.Onthispane,select

ManageConnections|ConnecttoTeamProject:

6. ThiswillopentheConnecttoTeamFoundationServerdialog,intheSelectaTeamFoundationServerdrop-downlistandselectyourVSTSsite.

7. Selectyourprojectinthelowerportionofthedialog,asshowninthefollowingscreenshot:

8. AfterpressingConnect,VisualStudioisconnectedtoyourproject.9. Wehaveonefinalstepbeforewecontinue;wehavetoconfigureourworkspacesoVisualStudio

knowswhichfoldersareundersourcecontrolandhowtheymaptoVSTS.OnTeamExplorer,clickonConfigureworkspaceundertheProjectsection.ThiswillshowtheConfigureWorkspacesectionatthetopoftheTeamExplorer.

10. DonotpressMap&Get.11. PressAdvanced....12. TheEditWorkspacedialogwilllooksimilartothefollowingscreenshot:

ForOperationsdevelopment,wewillneedtomapaprojectsfolderandtheOperationslocalpackagesfolder(theapplicationsourcecode,ormetadataasitisoftenreferredto)totwodifferentfoldersinVSTS.TheProjectsfolderistheonewecreatedearlier,whichwasC:ProjectsSBinmycase.TheOperationslocalpackagesfolderisC:AOSServicePackagesLocalDirectory.

Ifyoulookaroundthelocalpackagesfolder,youcanseehowtherelationshipbetweenpackageandModelisactuallystored.

13. Inmycase,theprojectisB05712_AX7_DevelopmentCookbook,soIwillconfigurethedialogasshowninthefollowingscreenshot:

14. PressOK.15. Youwillthenbetoldthattheworkspacehasbeenmodified,andifyouwanttogetthelatestcode.

Eitheroptionhasnoeffectifwearethefirstdeveloper,butitisagoodhabittoalwayspressYes.

Howitworks...

SourcecontrolinOperationshascomealongwayinthisrelease,mainlybecauseourdevelopmenttoolisnowVisualStudioandthatthesourcefilesarenowactualfilesinthefilesystem.OperationsnolongerneedsspecialcodetointegratewithaTeamFoundationServer.

Thereasonwehavetwofoldersisthatourprojectsdon'tactuallycontainthefileswecreatewhenwritingcode.Whenwecreateafile,suchasanewclass,itiscreatedwithinthelocalpackagesfolderandreferencedwithintheproject.Thisalsomeansthatwecan'tjustzipupaprojectande-mailittoaco-worker.ThisisdonebyconnectingtothesameVSTSprojectorusingaprojectexportfeature.

There'smore...

Whenworkingwithmultipledevelopers,oneoftenoverlookedtaskistorenamethevirtualmachine.Thishasgotteneasierwitheachupdate,andthestepswetakeatthecurrentreleaseareasfollows:

1. UseComputermanagementtorenamethemachine.UsesomethinglikeprojectIDandyourinitialsforthis;forexample,B05712SB.

2. Restartthevirtualmachine.3. UsetheSQLServerReportingServicesconfigurationutilitysothatitreferencesthecorrectserver

name.

Seealso

RenamingaVM(https://ax.help.dynamics.com/en/wiki/visual-studio-online-vso-machine-renaming/)ConfiguringyourVSTSmappingafteracodeupgrade(https://ax.help.dynamics.com/en/wiki/configuring-your-vso-solution/)

Inthislink,itsuggestsusingthedefaultvisualstudiofolder;however,forlocaldevelopmentVMs,thiswillalwaysbeAdministrator.Forthisreason,usingafoldersuchasC:ProjectsTFSisbetterintermsofeaseofuse.

CreatinganewModelandPackagesWhencreatinganewproject,itisusuallyanewPackageandanewModel.Thiskeepsthingssimple,andthereisusuallynobenefitinseparatingthem.YoumaywishtocreateatestprojectinadifferentModelinthesamesolution,butyoumaynotwishtodeploythetestprojectstolive.

Therearetwotypesofprojects:anextensionprojectandanover-layerproject.Over-layeringmeansmodifyingthesourcecodeofOperations,andrequiresacodeupgradeforeachapplicationhotfix.Extensionprojectsworkondeltachangestothestandardobject,orusingdelegatestoaffectcodeexecution.Extensionprojectsshouldn'tneedacodeupgradewhenapplicationhotfixesareapplied.Avoidanceofover-layeringcannotbeoverstated,inthetimethisbookwasbeingwrittenPlatformandFoundationhavebeenlocked,meaningthattheover-layeringmustberemoved.Theabilitytowritegoodcodethroughextensionhasbeenimprovedwitheachrelease,andwithcleverdesigntheneedtoover-layerhasbeensignificantlyreduced.

Wewilluseextensionprojectsexclusively,inordertotoavoidconflictswithfutureupgrades.Theymakeitpossibletoservicetheenvironmentwithouthavingtodeployanewbuildofthecustomsolution.ThisisveryexcitingforISVsolutions,butalsoveryimportantforVARandend-usercustomers.

SeetheThere'smore...sectionforinformationontherelationshipbetweenpackages,modelsandprojects.

GettingreadyStartupVisualStudioandensurethatwearecorrectlyconnectedtoVSTS.Asofthecurrentrelease,youmuststartvisualstudioasanadministrator.

Howtodoit...Tocreatetheproject,followthesesteps:

1. UndertheDynamics365menu,chooseModelManagement|Createmodel....2. TheModelnameisnamedaswewouldinAX2012,andshouldbenamedlikeanewtype,suchas

<prefix><area/module><ShortName>.3. Completethefirststepsasfollows:

Field Value

Modelname

Inourcase,ourcompanyiscalledContoso,soourprefixwillbeCon,theareaofchangeisanexistingmodule(WHS)anditisforgeneralextensions(extendingstandardobjectswithoutcustomization).

ItisthereforenamedConWHSGeneralExtensions.Youshoulduseyourownprefixandprefixes,andthenameforexplainedfurtherintheThere'smore...section.

Modelpublisher Yourorganization'sname.

Layer

Asfollows:

ISV/verticalsolution/add-on:ISV

VAR/Partnersolution/add-on:VAR

Customersolution/add-on:CUSorUSR

Thecustomerlayersweretraditionallyusedtosegregateacustomer'sglobalsolutionlayerfromtherequirementsofeachcountryimplementation.

Thelayertechnologyisprocessedverydifferentlyfortheextensionprojects,andhaslostsomesignificanceinthisrelease.

Version

Leaveas1.0.0.0

Youcanupdatethistomaintainaversionofthemodel.Thisdoesnotauto-incrementandisusuallyleftasdefault.

Modeldescription Afulldescriptionofthemodelforotherdeveloperstoread.

Modeldisplayname

Leaveasdefault,whichshouldbetheModelname.

4. PressNext.5. IntheSelectpackagepage,chooseCreatenewpackage.

IfyouchooseSelectexistingpackage,itwillmeanthatyourmodelwillbeplacedunderthepackageandisintendedtoover-layertheelementsininthatpackage.Youcannotover-layerelementsinextensionprojects,butunlessweabsolutelymustover-layer,alwayschooseCreatenewpackage.

6. PressNext.7. Wearenowofferedalistofpackagesthatwecanreference,thesearelistedaspackage'snameand

themodelsthatthepackagecontains,checkApplicationSuiteandpressNext.

MostoftheelementsinOperationsareinApplicationSuite;so,unlessourpackagedoesn'tneedanystandard,typethiswillalwaysbeselected.Theotherswewouldselectbasedontheelementsweknowwewilluse.Wecanaddmorepackagereferenceslater,butifweknowwhichelementswewilluse,itsavessometime..

8. Thetwocheckboxes,CreatenewprojectandMakethismydefaultmodelfornewprojects,shouldbothbechecked.

9. PressFinish.10. ThisopenstheNewProjectdialog.TheprojectnameisusuallythesameasthepackageandModel

name;then,enterthepackagenameintheNamefield.11. TheLocationfieldmustbechanged;itwillcreatetheprojectinthedefaultprojectfolder,butwe

linkedC:Projects<initials/username>tosourcecontrol.Theprojectmustbecreatedunderthisfolder.So,inmycase,LocationmustbeC:ProjectsSB.

12. TheSolutionnamefieldshouldbeleftastheprojectname.13. EnsurethatbothCreatedirectoryforsolutionandAddtosourcecontrolarechecked.14. PressOK.

Howitworks...Toseewhatwejustdid,wecansimplylookattheresults.UseWindowsexplorertonavigatetothelocalpackagesfolder,whichisusually,C:AOSServicePackagesLocalDirectory.There,youwillseethefollowingstructure,fortheexamplepackage,ConWHSGeneralExtensions:

Folder Description

ConWHSGeneralExtensions Thisisapackagefolder

+

ConWHSGeneralExtensions Thisisamodelfolderwithasubfolderpertype

+Descriptor Thiscontainsadescriptorfileforeachmodel

+XppMetadata

ThissystemmanagedfolderfortheXppmetadataforallmodelsinthepackage.Thisholdscompilermetadatainformationabouteachelement,nottheactuallysourcecode.Thisincludesthemethodsinaclass,thetypeofmethod,theparameters,andsoon.

Wewouldnevernormallychangeanythinghere,butthereareexceptions:

Iftwodeveloperscreateadifferentpackageatthesametime,theycanbothgetthesamemodelID,inwhichcase,badthingsstarttohappen.Thesolutionistocheckoutthemodel'sdescriptorxmlfileintheSourceControlExplorerandmanuallychangetheIDtothenextnumber.Youmaydecidethatastandardpackageshouldbedeleted,suchasthetutorialorthesamplefleetmanagementsolution.Youcandothisbysimplydeletingthepackagefolder.Shouldyouwanttoremoveastandardmodel,youcandeletethemodelfolder,butyoumustalsodeletetherelevantmodeldescriptorfilefromthepackage'sDescriptorfolder.Obviouscareneedstobetaken,asyoucan'tgetitback!

Thefirstpointaboutcanbesolvedbynominatingapersontocreatepackagesandmodels.

IfyoulookintheSourceControlExplorerinVisualStudio,youwillonlyseethattheProjectsfolderhasbeenadded.Thisiscorrect.TheMetadatafolderwillonlyappearswhenwecreatenewelements.

There'smore...Whenasolutionisdesigned,itwillbedonebybreakingthesolutionintopackagesoffunctionality.Thisisanormaldesignparadigmthathasnowbeenimplemented(and,toanextent,enforced)withinOperations.Thismeansthatoursolutiondesignwillnowdefinethevariouspackages,andhowtheydependoneachother.InthecaseofOperations,thepackageisadeployableunitthatbecomesadistinctDLL.

Wecanmakeahotfixtoapackageand,technically,deployitseparatelytootherpackagesinthesolution.Althoughthisispossible,wewouldnormallycreateareleaseofpackagesasaDeployablepackage.ADeployablepackageisacollectionofoneormorepackagesthatcontainsboththebuiltpackagecodeofoneormorepackages,andtheroutinerequiredtoinstallthem.Thisprocessissimplifiedusingabuildserverthatperformsthebuildprocessforus,executesanytests,andcreatesDeployablepackagesthatwecanthenapplytoourtestenvironment.

ThereisafurtherlevelwithinOperations,whichisaModel.AModelisasubsetofelements,suchasclasses,withinapackageandcanbeusedtomovecodefromonedevelopmentsystemtoanother,forexample.AModelcanonlybelongtoonepackage,andaPackagecancontainoneormoreModels.EachpackagebecomesaDLL,thathastohavereferencesaddedinorderto'see'elementsinorderpackages.Becauseofthisweshouldusealimitednumberofpackages.Asaguidewetendtohaveonepackageforthemainstream,andoneforreportingandbusinessintelligence.Tosimplifymanagementofdevelopmenttasks,wetendtohaveaprojectperspecification/TechnicalDesignDocument(TDD),allwithinthemainorreportingpackages,simplifyingmulti-developerprojects.JustlikeworkingoncomplexC#projects,wecanperformcodemerges,branching,andshelvingwithinVSTS.

Layershasbeenacorepartofpriorreleasesfromitsfirstrelease,butisnolongerthatsignificant.AsapartnerwestillusetheVARlayer,andrecommendthesameguidelinesasbeforetocustomersasbefore,butsinceweavoidover-layeringthisfeaturewillnotbecoveredinthisbook.

ThedependenciesaredefinedagainstthePackage,nottheModel.Whenwecreateaproject,theprojectisassociatedwithaModel.Itistypical,anddesirable,tokeepthisstructuresimpleandonlyhaveoneModel(orlimitedtoafewModels)foreachpackageandtogivebothentitiesthesamename.

ThefollowingdiagramshowsatypicalPackage,Model,andProjectstructure:

TheApplicationSuitepackageisastandardpackagethatwenormallyalwaysreference,asitcontainsthemajorityofthetypesthatweusuallyneed.Thearrowsindicatethereferencedirection,showingthatitisnotpossiblefortheVehiclemanagementpackagetoseetheelementscreatedintheVehiclemanagementreportingpackage.

PrefixesandnamingconventionsOperationsdoesnotusenamespaces.Neitherpackagesnormodelsequatetoanamespace.Amodelsimplyimpliesascope,butalltypesmustbegloballyunique;evenindifferentmodelsandpackages.Anamespacewouldallowaclasstohavethesamenameasanotherclassinadifferentnamespace.

Therefore,everyelementmustbegloballyuniquebytype;thisincludesmodels,packages,andeveryelementintheapplicationmetadata.So,wewillstillneedprefixes.Evenifwecreateanextensionofanelement,suchasaform,wemustchangethenamesothatitisguaranteedtobegloballyunique.

Forexample,ifwewantedtocreateanextensionoftheWHSLoadTabletable,itwillcalltheWHSLoadTable.extensionobjectbydefault.Asourcustomermightwantanadd-onthatalsoaddsfieldstothistable,weneedawanttoensurethattheelementisunique.

Thebestwaytodothiswouldbetouseourprefix,whichisConinourcase.Tomakeitobviousofwheretheelementisused,weusethepackagenameasthesuffix,forexample,WHSLoadTable.ConWHS.Thereisnoofficialbestpracticeavailableforthis,butthepointtorememberisthatallelementsofatypemustbegloballyunique-andextensionsarenoexception.

ConfiguringprojectandbuildoptionsBeforewecangetstuckintowritingsomecode,weneedtosetupsomeparameters.Manysettingsdescribedherearegoodforallprojects,butsomeyoumaywishtochange,dependingonthescenario.

GettingreadyThisfollowsthepreviousrecipe,butcanapplyequallytoanyOperationsproject.JustloadupVisualStudioandtheprojectyouwishtoconfigure.

Howtodoit...

Thiswillbesplitintotwoparts:genericoptionsforallprojects,Operationsoptions;andprojectspecificparameters.

Beforewedoeither,weshouldalwayshavetheApplicationExploreropen,whichishiddenbydefault.ThisistheApplicationObjectTree(AOT)ofpriorversions.ThisisopenedfromtheViewmenu.

Dynamics365forOperations'options

Toconfigurethegenericoptionsforallprojects,followthesesteps:

1. SelectOptionsfromtheDynamics365menu.

TheformisactuallytheVisualStudiooptionsdialog,butittakesyoutothesectionspecifictoOperations.

2. Thedefaultoptionintheleft-handtreeviewisDebugging;theoptionshereareusuallyfine.TheLoadsymbolsonlyforitemsinthissolutionoptionaffectsdebuggingandshouldbeleftcheckedforperformanceofthedebugger.Wewoulduncheckthisifwewanttotracecodethatcallscodeoutsideofthecurrentpackage.

3. SelecttheProjectsoptionontheleftandcheckOrganizeprojectsbyelementtype.Whenaddingnewelementstotheproject,itwillautomaticallycreateasubfolderintheprojectfortheelementtype.Thismakesorganizationmucheasiertomaintain.

4. Theothertwooptionsshouldbeleftblank.AlthoughtheSynchronizedatabaseonbuildfornewlycreatedprojectoptioncanbeuseful,thisdatabasesynchronizationtakestime,anditisusuallypreferabletodothisasrequired.Thatis,beforeyourunit.

5. TheBestpracticesnodeletsyouchoosewhichbestpracticechecksyouwishtobeexecutedonbuild.Thisdetailisbeyondthescopeofthisbookasthechecksrequiredarecasespecific.

Theproject-specificparameters

Theseareusuallyfineasdefaultfordevelopment,butweoftenwanttotestourcodeusingthesampledatathatcomeswiththevirtualmachine.

Tosetupthecommonparameters,followthesesteps:

1. Right-clickontheprojectinSolutionExplorerandchooseProperties.2. Tosavetimewhilstdebugging,youselectwhichobjectyouwishtostartwith.Tostartwiththeform

SalesTable,settheStartupObjectTypeoptiontoForm,andStartupObjecttoSalesTable.

3. SetCompanytoUSMForanyothercompanyyouwishtostartwithwhendebugging.4. LeavePartitionasinitial.Thesearenowonlysupportedforcertainunittestscenarios.5. Ifyouwishtoalwayssynchronizethedatabaseonbuild,youcansetSynchronizeDatabaseonBuild.

Myadviceistodothismanually,asrequiredafterthebuildcompletes.

There'smore...YoumaynoticethatwecanchangetheModelintheproject'sparameters.Ouradviceis,don'tdoit.Tochangetheproject'sModelalsomeansthatthefoldershavetobemovedinthelocalpackagesfolder.

CreatingaLabelfileMostprojectshavesomekindofuserinterface,andthereforeweneedtodisplaytexttotheuserotherthanthefieldnames.Thebestpracticemethodtodothisistousealabelfile.ThelabelfilecontainsalanguagespecificdictionaryoflabelIDsandthetranslation.

StandardelementstendtohavethelegacylabelIDsofan@symbol,followedbyathree-digitlabelIDandanumber.Thisformatworkedwellforthepast15years,buttheprefixwaspotentiallylimiting,especiallytoaidISVs.Labelsarenolongerrestrictedtothreedigits,whichhelpsMicrosoftattainoneofitsgoalsofmakingISVadd-onseasiertowrite,maintainandadopt.

Thechoiceofhowmanyandwhichpackagesneedalabelfiledependsonthesolutiondesign.

Itisn'taterribleideaforanendusercustomersolutiontohaveonepackagejustforlabelsthatarere-usedbyeachpackage.Thisdoesmeanthatweruntheriskofusingalabeloutofcontext.YoumaychoosetouseastandardlabelforNameonpersonalrecord,onlyforthelabeltobechangedbytheoriginaldevelopertosomethingspecifictotheoriginalcontext,forexample,Productname.

WetendtocreatealabelfileforeachpackageasthisensuresthatthepackagecancorrectandchangelabelswithoutworryingaboutregressioninotherModels.

GettingreadyTogetstarted,openVisualStudio,andtheprojectinquestion,inmycaseIwillcontinuewiththeConWHSGeneralExtensionsproject.

Howtodoit...

Tocreatethelabelfile,followthesesteps:

1. Right-clickontheprojectandselectAdd|Newitem...orusethekeyboardshortcut,Ctrl+Shift+A.2. ChooseLabelsandResourcesfromtheOperationsArtifactslist.3. Fromthelistontheleft,selectLabelFile.4. IntheNamefield,enterashort,butuniquelabelname,inmycaseConWHS.Iwantittobeasshortas

possible,butbecompletelysureitwillbegloballyunique,regardlessofanyfutureadd-onwemaychoosetoinstall.

5. PressAdd.6. IntheLabelfilewizard,leaveLabelfileIDasdefault.

Itseemsthatwecanspecifyadifferentnameinthisstep,butthepreviousonlycalledthiswizardwithasuggestedfilename.TheLabelfileIDfieldcontainsboththeIDandthefilenamethatwillbecreated.

7. PressNext.8. Inthelanguageselection,movethelanguagesfromtheleft-handlistintotheright-handlistusingthe

buttons.Onlyleavelanguagesselectedthatyouwillmaintain.Thisinvolvescreatingalabelineachlanguagefile.

Thisalsoappliestolanguagewithsubtledifferences,suchasEnglishandSpanish.Eventhoughthelabelwilloftenbethesame,westillneedtomanuallymaintaineachlanguagefile.CurrentlythisalsomeanswehavetobecarefultoensurewegivethemthecorrectID.

9. PressNext.10. CheckthattheSummarypageiscorrect,andpressFinish.

Howitworks...Thecreationisstraightforward.Theprocesscreatesatextfileonthediskthatcontainsatab-separatedlistoflabelIDsandtranslations.

Whenalabelisselectedagainstacontrol,itwillbegivenalabelfileIDthatensuresitisunique.Inourexample,thelabelfileIDwasConWHS.Aswecreatealabel,itwillbegiventheIDsinthesequence@ConWHS:ConWHS1,@ConWHS:ConWHS2,andsoon.

Inourexample,thelabelIDsgiventothecontrolwillbe@ConWHS:ConWHS1.Thisseemsneedlesslylong.Since,wecanactuallyspecifythelabelIDmanually,wecanchoosetoenterashorterIDperlabel,generatingalabelsuchas@ConWHS:L001,orenteramemorablenameasanID,wheretheIDwouldthenbecome@ConWHS:ItemNotExists.

There'smore...Currently,themaintenanceoflabelscanbetimeconsuming,inthat,wedonothaveatranslationlistperlabelIDaswedidinDynamicsAX2012,butaseparatefileperlanguage.Thisistargetedforimprovement,andmaychangebyrelease.Theconcept,however,willremainthesame.

Whenwritinglabelsforvariantsofthesameorsimilarlanguages,wecancopyandpastethelabelsbetweenfiles.Expandthelabelfiletothelowestnodetothefilewitha.txtextension,andrightclickonittoselectOpenWith....ChooseNotepadfromthelist.

Eachlabelwillhavetwolines,oneforthelabel,andtheotherforthecomment,asshowninthefollowingextractfromanen-gblabelfile:

ConWHS1=Vehicletype

;ConWHS

ConWHS2=Thetypeofvehicle

;ConWHS

VehTransComplete=Completeinspection

;ConWHS

VehTransCompleteHT=Complete,andfinalisethevehicleinspection

;ConWHS

Youcanthantranslatetothedesiredlanguageandpasteitintothetargetlabelfile,againbyopeningthetxtfileinNotepad.YoumustdothisfromwithinVisualStudio,otherwisethefilemaynotbecheckedoutfromsourcecontrol.Becareful,asitwillnotvalidatethattherearen'tduplicatesorfileformattingerrors.

Ifyouintendtowriteadd-ons,youshouldalwaysmaintaintheen-uslanguagefile.YouwillgetcompilationwarningsthatthelabelIDdoesnotexist,ifyoudonot.Ifyouaretoreleasethesoftwaretoaregionwithavariantofalanguage(en-au,en-gb,en-ie,en-us,andsoon),pleaseusethecorrecttranslation,asnotonlywillitmakeyouradd-onmoreprofessionalandglobal,butsometermshavecompletelydifferentmeanings.Forexample,stockmeansinventoryinen-gb,butmeansfinancialshareholdingsinen-us.

DataStructuresInthischapter,wewillcoverthefollowingrecipes:

CreatingenumeratedtypesCreatingextendeddatatypesCreatingsetuptablesCreatingaparametertableCreatingmaindatatablesCreatingorderheadertablesCreatingorderlinetables

IntroductionInthischapter,wewillcoverthetasksrequiredtowritethedatadictionaryelementscommonlyusedwithinOperationsdevelopment.

DatastructuresinOperationsarenotjusttablesandviews,butalsoincludetheabilitytodefineadatadictionary.ThisisnowcalledtheDataModelinOperations.Thedatamodelhasn'tchangedmuchinstructuresinceAX2012,butismuchmorerefinedwithmanymorefeaturestoaiddevelopmentandminimizethefootprintwhenextendingstandardtablesandtypes.

Thischapterdoesnotcovercreatingextensionsofstandardtypes,butitdoescoverhowtocreatetypesthatallowyourstructurestobeextensible.ExtensibilityiscoveredinExtendingstandardtableswithoutcustomizationfootprintsectionofChapter7,LeveragingExtensibility.

MostdevelopmentinOperationsisbasedonpatterns,evenifwearenotawareofit.Forexample,theVendorandCustomerlistsareverysimilar,andarecalledMaintables.PurchaseordersandSalesordersarealsoverysimilar,whichareWorksheettables;inthiscase,orderheaderandlines.

TablesinOperationshaveapropertythatdefinesthetypeoftable,andeachtypeisassociatedwithaparticularstyleofformfortheuserinterface.Wecouldthereforeconsiderthesetypesaspatterns.Ifwethinkofthemaspatterns,itisfareasiertoapplytherecipestoourowndevelopment.

Thefollowinglistisofthecommontabletypes,listedwiththeusualformdesignandtheirtypicalusage:

TablegroupFormdesignpattern

Usage

Miscellaneous Thisisessentially'undefined'andshouldn'tbeused,otherthanfortemporarytables.

Parameter Tableofcontents Thisisusedforsingle-recordparameterforms.

Group

Simplelist

SimplelistandDetails-ListGrid

Thisisusedforthebackingtablefordrop-downlists.TheSimplelistdesignpatternissuitableforwhenonlyafewfieldsarerequired;otherwise,theSimplelistandDetailspatternsshouldbeusedsothefieldsarepresentedinausefulwaytotheuser.

Main DetailsMaster Thisisusedformaintables,suchascustomers,suppliers,anditems.

TransactionHeader

TransactionLine

SimpleListandDetailswithStandardtabs

Thisisusedfordatasets,suchastheinvoicejournalform,thatcontainpostedtransactionaldatawithaheaderandlines.

Transaction

SimpleListandDetailsw/Standardtabs

Thisisusedfortables,suchastheInventorytransactionform,thatcontainpostedtransactionaldata,butatasinglelevel.

Worksheetheader

Worksheetline

DetailsTransaction

Thisisusedfordataentrydatasets,suchasthepurchaseorder,wherethedatasetismadeupofheaderandlinetables.Thepatternhastwoviews,alistviewofheaderrecords,andadetailviewwherethefocusisthelines.

Worksheet Detailsmaster Thisisasingle-leveldataentrydataset,whichisrarelyusedinOperations.

Inthischapter,wewillcreatetypesandtablesforthecommontypesoftable,butwewillcompletethepatternswhenwecovertheuserinterfaceinChapter3,CreatingtheUserInterface.

CreatingenumeratedtypesEnumeratedtypesarecalledBaseEnumsinOperations,whicharesimilartoenumeratedtypesinC#.TheyarecommonlyreferredtoasEnums.Itisanintegerreferencedasasymbol,whichcanbeusedincodetocontrollogic.Itcanalsobeusedasafieldonatablethatprovidesafixeddrop-downlisttotheuser.Whenusedasafield,ithastheabilitytoprovideuser-friendlylabelsforeachsymbol.

BaseEnumsareusuallyonlyusedasadrop-downlistifweneedtounderstand,incode,whateachvaluemeans.Theyshouldcontainasmallnumberofoptions,andwhenusedasafield,thelistcannotbesearchedorextendedbytheuser.

AllEnumshavetobedefinedinthedatamodelbeforeweusethemandcan'tbedefinedwithinaclassormethod.

BaseEnumsaregiventheabilitytobeextensibleinthisrelease;themechanicsofthisiscoveredinmoredetailintheThere'smore...section.

GettingreadyThefollowingtaskscontinuethevehiclemanagementproject,ConWHSVehicleManagement,whichwascreatedinChapter1,StartingaNewProject,although,thisisnotaprerequisite.WejustneedanOperationsprojectthatwascreatedasanextensionproject.

Howtodoit...InordertocreateanewEnum,followthesesteps:

1. Eitherright-clickontheproject(oranysubfolderintheproject)andchooseAdd|Newitem....

Alwaystrytousekeyboardshortcuts,whicharedisplayednexttotheoption.Inthiscase,tryCtrl+Shift+A.

2. ThiswillopentheAddNewItemwindow.Ontheleft-handlist,selectDataTypes,whichisundertheOperationsArtifactsnode.

3. Thiswillfiltertheavailableoptionsontheright-handlist.Fromthislist,chooseBaseEnum.4. Enteraname,prefixedappropriately.Inthisexample,wearecreatinganEnumtostorethetypeof

vehicle;so,wewillenterConWHSVehicleTypeasNameandpressAdd.

ThisshouldhavecreatedafoldercalledBaseEnums,oraddedthenewEnumtothefolderifitalreadyexisted.ThisisregardlessofthefolderweselectedwhentheEnumwascreated.Ifthisdidnothappen,theOrganizeprojectsbyelementtypesettinginDynamics365|Optionswasnotchecked.

5. WewillnowhaveanewtabopenwithouremptyEnum.Beforewecontinue,wemustcreatelabelsfortheLabelandHelpproperties.Double-clickonthelabelfilesowecanaddanewlabel.

Thiswascreatedinthepreviouschapter;ifyoudon'thavealabelfile,onemustbecreatedbeforewecontinue.

6. Thiswilladdanemptylineforus.Onthisemptyline,clickonLabelIDandenterthenextnumber,forexample,1,andpressTab.ThiswillchangethenumbersothatitisprefixedwiththelabelfileID;inourcase,ConWHS1.

7. EnterVehicletypeintheLabelcolumnandConWHSinthecomment:thisistoprovidecontexttootherdevelopers,incaseitisreused.

8. PressNewandrepeatforthenextlabelandenterthetextThetypeofthevehiclefortheLabelproperty.

Repeattheseforeachlabelfilelanguagewecreated;ifwesupportUSandBritishEnglishandNorwegianBokmål,wewillneedtocreatealabelwiththesameIDforConWHS.en-us.label.txt,ConWHS.en-gb.label.txt,andConWHS.no-no.label.txt.

9. ReselectthetabfortheEnum.Ifwedon'thaveapropertysheetopen,whichisusuallyonthelowerrightofthescreen,right-clickontheEnuminsidethetabandselectProperties.

10. Inthepropertysheet,fillintheLabelpropertyasVehicletype.11. Pressthelookupbuttonforthisproperty,selectourlabel,andpressPastelabel.

12. RepeatthisprocessfortheHelpproperty,whichshouldbeThetypeofvehicle.

Thesepropertiesdonothavetobepopulated,butitwillcauseabestpracticedeviationwarningwhentheprojectisbuiltandthesepropertiesareleftempty.

13. TheUseEnumValuepropertyshouldalwaysbeYes,asitallowsustosetthenumericvalueofeachsymbolwecreated.

14. TheIsExtensiblepropertyshouldusuallybeFalse.Thisaltersthewaythenumericvalueisassigned,andisdescribedlater.

15. Next,wewilladdtheelementstotheEnum.Theremustalwaysbeanelementwith0,asthisisthedefaultonnewrecords.Alsoconsiderthathavingadefaultisdesirable.Ifyouwishtoforcetheusertoselectanoption,makethefirstelement"Undefined".Since0isclassedasemptybythesystem,makingthefieldmandatorywillforcetheusertoselectanoption.Theoptionswewilladdshouldbecreatedasfollows:

Name(Symbol) EnumValue Label

Bike 0 Motorbike

Car 1 Car

Truck 2 Truck

TheLabelcolumnintheprecedingtableshowstheliteraltouse,theactualvaluewillbethelabelId,suchas@ConWHS:Motorbikeor@ConWHS:ConWHS2ifyouareusingnumbersforlabelIDs.

16. Toaddeachsymbol,orelementasitisreferredtointheeditor,right-clickontheEnumandchooseNewElement,asshowninthefollowingscreenshot:

17. Completethepropertysheetasperthetable,andrepeatforeachrequiredelement.18. YoucanreordertheelementsusingtheAlt+upkeyandAlt+downkey,butthiswillonlyaffecthow

theyaredisplayedintheeditor.YoumustalsochangeEnumValue.19. PresstheSaveorSaveallbuttonsinthetoolbar.

Howitworks...Enumsarestoredasintegersinthedatabase,andwecanassignanintegervaluetoafieldbasedonanEnum.

WhenthevaluesoftheEnumareshowntotheuser,OperationswilllookthisupusingtheEnum'sdefinitionfromthefieldintheuser'slanguage.Ifalabelisnotdefinedfortheuser'slanguage,thelabelID,andnotthesymbol,isshowntotheuser.Ifalabelwasnotdefined,thesymbolisshown.

Forstandard,non-extensibleEnums,thefollowinglinesofcodeareeffectivelythesame:

InventTable.ABCRevenue=1;

InventTable.ABCRevenue=ABC::B;

Ofcourse,wewouldneverwritethefirstoption-weuseenumeratedtypesspecificallytoavoidhavingtorememberwhateachnumbermeans.IftheEnumwascreatedasextensible,thevaluesassociatedwiththesymbolarenotassignedatdesigntime.Theyareinstallationspecific.ExtensibleEnumsareimplementedasaclass,andwethereforehaveaslightperformancehitaswitharenolongerdealingwithnumbers.

WeuseEnumsformanypurposes,rangingfromtableinheritance(differentfieldsarerelevanttoatypeofvehicle)toprovidinganoptionlisttoauser,andevencontrollingwhichtypeofclassiscreatedtohandlelogicspecifictothevehicletype.

There'smore...

BaseEnumsarethereforeagreatwaytoprovidealinkbetweenlogicandtheuserinterface.However,therearelimitations;theoptionsaredefinedincodeandtheusercannotaddtothelist.Theproblemwithmakingtheoptionsuser-definableisthatwecan'tusethevaluesincode,unlesswewanttoaddtheoptionstoaparameterform,whichisnotveryextendable!

WhatwecandoisusethecodepatternexemplifiedintheItemmodelgroupform.Here,wehaveauser-definablelist,withanEnumthatdeterminestheInventorymodelthattheitemswilluse.

UsingEnumsforcomparisonandstatusItisverycommontouseanEnumtodefinethestatusofarecord,andwewillcoverstatemachinesinChapter15,StateMachines.WhendefininganEnumforstatus,wewillordertheelementssothatwecancomparethemincode.

InthecaseoftheSalesStatusEnum,theelementsareasfollows:

Symbol Value

None 0

Backorder 1

Delivered 2

Invoiced 3

Canceled 4

Thisway,wecanselectallsalesorders,whicharenotyetinvoiced,usingthefollowingcodelines:

selectSalesId,SalesName

fromsalesTable

wheresalesTable.SalesStatus<SalesStatus::Invoiced

Sometimes,wewillneedtoreversetheorder.InthecaseoftheInvenTranstable,therearetwostatusfields:StatusIssueandStatusReceipt;oneisforissuetransactions,suchassalesordersorpositiveinventoryadjustments,andtheotherforreceipts.Whenthetransactionisanissue,StatusReceiptwillholdavalueof0(None),andviceversa.

Thismethodofusingcomparisonsonextensibleenumscannotbeused.Theywillprobablybenumberedinorderoftheelementsthatarelistedintheenum'sdefinition,butwecannotassumethat.Itispossibleforthemtodiffer.

ThefirstthreeelementsofStatusIssueareasfollows:

Symbol Value

None 0

Sold 1

Deducted 2

ThefirstthreeelementsofStatusReceiptareasfollows:

Symbol Value

None 0

Purchased 1

Received 2

Thereasonthestatusappearstobereversedistoallowustoformaquerysuchasthefollowingpieceofcode:

selectsum(Qty),ItemId

frominventTrans

whereinventTrans.StatusIssue<=StatusIssue::Sold

&&inventTrans.StatusReceipt<=StatusReceipt::Purchased

ThiswillsumtheQtyfieldforalltheInventTransrecordsthatwerefinanciallyissuedorreceived.

ExtensibilityinBaseEnumsOneconfusionthatoccursinOperationsisthetermsextendandextension.Extendisthetermusedwhereatype,suchasaclass,tableorEDT,extendsaparentofthesametype.Extensionorextensibleisusedwherewecanalteranelementinanotherpackagewithoutmodifyingtheoriginal.Whenthisisdoneandusedcorrectly,theoriginalauthorcanpublishrevisionstothepackagewithoutforcingtheircustomerstomergeandupdatetheircode,allowingend-usercustomersmoreflexibilityandenablingthemtoefficientlystayuptodatewithupdates.

Wewillcovertheextensibilityofeachtypethroughoutthisbook,withaparticularfocusinChapter7,LeveragingExtensibility.ThereisabigdifferencewithEnums;inthat,theauthoroftheEnumdecideswhetherornotthisisallowed.Forothertypesthatareextensible,theobjectsareallextensible.

AcommonviewistojustmakeallEnumsextensible,butthishasadrawback,specificallyintermsofanoverheadwhenthetypeisloaded.StandardEnumsarecompiledtoCLRenumerations,inthesamewaythatC#enumerationsare,andareexceptionallyquick.IfwemakeanEnumextensible,theyarecompiledtoaclasswithstaticmethodsforeachelementintheenumeration.Theactuallynumericvaluesareinstallation-specific,andwecan'tusethemforstatusfieldswherewewanttocompare;forexample,SalesTable.SalesStatus<SalesStatus::Invoicedmakesnosenseinthiscase.

Intermsofrefreshingdatabasesbetweenenvironmentsthishasbeenconsidered.TheactualvalueisstoredinatablewithintheOperationsdatabase,whichiscreatedwhenthedatabaseissynchronized.Itwillnotchangeafterthatpoint,andthismeansthatthevaluesaremovedwiththedatabase.

CreatingextendeddatatypesExtendedDataTypesarecommonlyreferredtoasEDTs.Theyextendbasetypes,suchasStringsandIntegersbyaddingpropertiesthataffecttheappearance,behavior,data(size),andtablereference/relationships.ThismeansthatwecanhavetypeslikeCustomeraccountthathavealabel,size,tablerelationinformation,andotherpropertiesthatprovideconsistencyandgreaterunderstandingwithinthedatamodel.

AnotherexampleofanEDTisName.ShouldwechangetheStringSizepropertyofthisfield,allfieldsbasedonthisEDTwillbeadjusted;andifwereducethesize,itwilltruncatethevaluestothenewsize.

AllfieldsshouldbebasedonanEDToranEnum,buttheyarenotjustusedtoenforceconsistencyintheDatamodel,butareusedastypeswhenwritingcode.

TheEDTinthisexamplewillbeaprimarykeyfieldforatablethatwewilluselaterinthechapter.

GettingreadyWejustneedtohaveaOperationsprojectopeninVisualstudio.Tolookatstandardexamples,ensurethatApplicationExplorerisopenbyselectingView|ApplicationExplorer.

Howtodoit...WewillcreateanEDTforthevehiclenumber.Avehicletableisofasimilarpatterntocustomersandvendors,andwewillextendtheAccountNumEDTforthistype.

TocreatetheEDT,followthesesteps:

1. CreatinganEDTstartsinthesamewayasallnewOperationsartifacts:bypressingCtrl+Shift+Aorright-clickingonafolderinthesolutionexplorerandchoosingAdd|NewItem.

2. SelectDataTypesintheleft-handlist,andthenselectEDTString.3. IntheNamefield,enterConWHSVehicleIdandpressAdd.4. Next,wewillneedtocompletethepropertysheet;themainpropertiesarecoveredinthefollowing

table:

Property Value Description

Label VehicleID Thisisthelabelthatwillbepresentedtotheuserontheuserinterfacewhenaddedasafieldtoatable.

HelpText Thevehiclenumber

ThisisthehelptextshowntotheuserwhenthisfieldisselectedorthemousehoversoverthecontrolsbasedonthisEDT.

Extends AccountNum Thisiscompletedformostfields,asweareusuallyfollowingapattern,suchasID,Name,andgroupingfields.ThisisexplainedintheThere'smore...section.

SizeThiswillberead-only,aswehavebasedthisEDTonanotherEDT.ThisisundertheAppearancesection,butitdoescontrolthesizeoftheassociatedfieldsinthedatabase.

ReferenceTable

Fortypesusedasaprimarykeyfieldonatable,thispropertyshouldbepopulated.

AlongwiththeTablereferences,itcanbeusedtocreatetheforeignkeyrelationonchildtables.

Asalways,remembertocreatelabelsfortheLabelandHelpTextpropertiesforeachofyoursupportedlanguages.

5. IfthisEDTistobeusedasaprimarykeyfield,wewillneedtopopulatetheTableReferencesnode.

Wewillcompletethislaterinthechapter,butyoucanseeagoodexamplebylooking

atthestandardAssetIdEDT.NavigateApplicationExplorertoAOT|DataTypes|ExtendedDataTypes,right-clickonAssetId,andselectOpendesigner.

6. PressSaveorSaveallinthetoolbartosavethechanges.

Howitworks...

ThereisabackandforthelementtoEDTcreationwhenwearecreatingaprimarykeyfield.Wecan'tcreatethefieldwithouttheEDT,yetwecan'tcompletetheEDTwiththefieldbeingonthetable.

EDTsaretypes.Therefore,theymustbegloballyuniqueamongstallothertypes,suchastables,views,dataentities,enums,classes,andotherEDTs.TheEDT'spropertiesaren'tjustdefaults,buttheycontrolbehaviortoo.ShouldweaddanunboundcontroltoaformbasedonanEDT,theEDTcanusetheTableReferencepropertytoprovideadrop-downlist,andthecontentswillbetakenfromafieldgrouponthetable.

There'smore...EDTscanalsoextendotherEDTs;although,thesechildEDTscanonlyaffectappearanceproperties.Thisisusefulwhenwewanttoenforcephysicalstorageattributesofarangeoftypes,buthaveadifferentlabeldependingoncontext.IfwechangethesizeofabaseEDT,allEDTsthatextenditwillbeaffectedand,consequently,allofthefieldsthatarebasedonthem.

WeoftenextendspecificEDTswhencreatinganEDTforcertaintypesoffields.

ThetypicalEDTsweusedforthisareasshowninthefollowingtable:

EDT Type Size Reason

SysGroup String 10

Thisisusedfortheprimarykeyfieldsforgrouptables.Grouptablesarethoseusedforbackingtablesfordrop-downlists.Theymayprovidefurtherdefinitiontoarecord,orjustbeusedforreporting.Examplesincludethefollowing:

ItemgroupCustomergroupItemmodelgroup

Num String 20

Thisisusedforprimarykeysonworksheettables,suchasthesalesordertable(SalesTable).Thesefieldsareusuallynumberedbasedonanumbersequence,whichmusthaveastringsizeof20characters.

Examplesincludethefollowing:

SalesordernumberPurchaseordernumber

AccountNum String 20

Thisisusedforprimarykeyfieldsformaintables,suchastheCustomertable.Thesetablesarealso,usually,basedonanumbersequence.

Examplesincludethefollowing:

CustomeraccountVendoraccount

Name String 60AllnamefieldsshouldbebasedonthisEDT,suchasvehiclename,customername,andsoon.ThisEDTcanbeusedasis,unlesswewishtospecifyalabelandhelptext.

Thisisusedasthedescriptionfieldongrouptables.ThisEDTisusuallyusedas

Description String 30 is,andisn'tusuallyextended.

AmountMST Real AllmonetaryvalueEDTsthatstoretheamountinlocalcurrencyshouldbebasedonthisEDT.MSTstandsforMonetaryStandard.

AmountCur Real AllmonetaryvalueEDTsthatstoretheamountinthetransaction'scurrencyshouldbebasedonthisEDT.

Qty Real AllfieldsthatstoreaquantityshouldbebasedonthisEDT.

Therearemanymore,andratherthanlistingthemallhere,agoodpracticeistolocateapatternusedinstandardOperationsandfollowthesamepattern.

Creatingsetuptables

Inthissection,wewillcreateagrouptable.Agrouptableisusedasaforeignkeyonmaintables,suchasthecustomergrouponthecustomertableandthevendorgrouponthevendortable;thecustomerandvendortablesareexamplesofmaintables.Grouptableshaveatleasttwofields,anIDandaDescriptionfield,butcancontainmoreasrequired.

Inthiscase,toaidflow,wewillcreatethegrouptablefirst.

GettingreadyWejustneedaOperationsprojectopeninVisualStudio.

Howtodoit...WewillcreateaVehiclegrouptable.Wedon'thavemuchchoiceonthenameinthisasithastostartwithourprefix,andendwithGroup;therefore,itwillbeConWHSVehicleGroup.Tocreatethistable,followthesesteps:

1. UsingtherecipeforcreatingEDTs,createaVehiclegroupEDTusingthefollowingparameters:

Property Value

Name ConWHSVehicleGroupId

Label Vehiclegroup

HelpText Thevehiclegroupid

Extends SysGroup

2. SavetheEDT,butdon'tclosethedesigner.3. Fromwithintheproject,choosetocreateanewitem.4. ChooseDataModelfromtheleft-handlistandselectTablefromtheright.5. EnterConWHSVehicleGroupintheNamefieldandpressAdd.6. Thisopensthetabledesignerinanewtab.Fromtheproject,dragtheConWHSVehicleGroupIdEDTontop

oftheFieldsnodeinourtable,asshowninthefollowingscreenshot:

7. ThiscreatesthefieldwiththesamenameastheEDT.Asthisisourtable,weshouldremovetheprefixandnameitVehicleGroupId.

8. WecannowcompleteourEDT,opentheConWHSVehicleGroupIdEDT(orselectthetabifitisstillopen),andenterConWHSVehicleGroupintheReferenceTableproperty.

9. Right-clickontheTableReferencesnodeandselectNew|TableReference.10. Inthepropertysheet,selecttheRelatedFieldpropertyandthenselectVehicleGroupIdfromthe

drop-downlist.

Ifthedropdownisblank,itislikelythattheReferenceTablepropertyonthetablewasmistyped.

11. SavetheEDTandcloseitsdesigner.ThisshouldmaketheactivetabtheConWHSVehicleGrouptabledesigner;ifnot,reselectit.

12. FromApplicationExplorer,whichisopenedfromtheViewmenu,expandDataTypesandthenexpandExtendedDataTypes.

13. LocatetheDescriptionfield,anddragitontotheFieldsnodeofourtable.

ThiscanbedonebyselectingthefirstEDTinthelistandtypingdescriptionuntilyoucanseeitinview.

14. Wewillnowneedtoaddanindex;eventhoughthistablewillonlyhaveafewrecords,wewillneedtoensurethattheIDfieldisunique.Right-clickontheIndexesnodeandchooseNewIndex.

15. Withthenewindexhighlighted,pressF2andrenameittoGroupIdx.ChangetheAlternateKeypropertytoYes.AlluniqueindexesthatwillbetheprimarykeymusthavethissettoYes.

Atthetimeofwriting,thispropertystillneedstobesettoyesinordertosupportcertaindataimportandexportscenariosandmayberemoved.Youwillgetabestpracticedeviationwarningshouldtheprimarykeynothavethispropertyset.

16. DragtheVehicleGroupIdfieldontopofthenewindex.

Thedefaultsforindexestocreateauniqueindex,sotheyarecorrectinthiscase.Indexeswillbediscussedlaterinthischapter.

17. Openthefield'spropertiesandsettheMandatorypropertytoYes,AllowEdittoNo,andleaveAllowEditOnCreateasYes.

SincewewillleaveAllowEditOnCreateasYes,wecanentertheID,butnotchangeitaftertherecordissaved;thishelpsenforcereferentialintegrity.TheMandatory,AllowEdit,andAllowEditOnCreatefieldpropertiesonlyaffectdatamanipulatedthroughaform.Theserestrictionsaren'tenforcedwhenupdatingdatathroughcode.

18. Wecannowcompletethetableproperties,selectthetablenodeinthetabledesign(thetablename),andcompletethepropertysheetasfollows:

Property Value Comment

Label Vehiclegroups Thisisthepluralnamethatappearstotheuser.

TitleField1 VehicleGroupId Thisappearsinthetitleofforms.

TitleField2 Description

Cachelookup Found

None:nocachingisfetchedfromtheDBeverytime.

NotInTTS:Fetchedoncepertransaction

Found:Cachedoncefound,notlookedupagain

EntireTable:Entiretableisloadedintomemory

Thecacheisonlyinvalidatedwhenrecordsareupdatedorflushed.

ClusteredIndex GroupIdx

Thisindexiscreatedasaclusteredindex.Clusteredindexesareusefulastheyincludetheentirerecordintheindex,avoidingabookmarklookup.Thismakesthemefficient,butthekeyshouldalwaysincrement,suchasasalesorderid,otherwiseitwillneedtoreorganizetheindexwhenrecordsareinserted.Thistableissmall,soitwon'tcauseaperformanceissue.ItwillalsosortthetableinVehicleGrouporder.

PrimaryIndex GroupIdx Thisdefinestheprimaryindexandisusedwhencreatingforeignkeyjoinstothistable.

TableGroup Group SetuptablesshouldalwaysbeGrouptables.

CreatedBy

CreatedDateTime

ModifiedBy

ModifiedDateTime

YesThiscreatesandmaintainstheCreatedbytrackingfieldsandisusefulifwewanttoknowwhocreatedandchangedtherecord,andwhen.

DeveloperDocumentation

TheConWHSVehicleGrouptablecontainsdefinitionsofvehiclegroups.

Thisisrequiredforbestpracticeandshouldcontainanythingthatotherdevelopersshouldunderstandaboutthetable.

FormRef ConWHSVehicleGroup

Thisshouldbeleftblankuntilwehavecreatedtheform,andresultingmenuitem.Thisallowstheuserinterfacetoknowwhichformtoopenwhenviewingdetailsviaaforeignkey.

Thepatternstatesthatthisshouldbethesamenameasthetable.

RememberthatLabel,HelpText,andDeveloperDocumentationrequirealabelineachofyoursupportedlanguages.

19. Wewillnowneedtocreateafieldgroup;aswewilluseaSimplelistdesignpatternforthistypeoftable,wewillonlyneedone.Right-clickontheFieldgroupsnodeandselectNewGroup.

Fieldgroupsareusedmainlyintheuserinterface.Wecanaddthegrouptotheuserinterfaceandthecontrolswillmatchthefieldsinthefieldgroup.Allvisiblefieldsmustbeinafieldgroup.

20. SelectNewgroupandrenamethegrouptoOverviewbypressingF2.21. CreatealabelforOverview.

Asweusesomelabelsinmanycontexts,typeinOverviewforthelabelID.ThelabelIDthatweuseforthefieldgroup'sLabelpropertywillbe@ConWHS:Overview.Forcommonlabels,thismakesthemeasiertorememberandreadwithouthavingtoopenthelabeleditor.

22. Inthepropertysheet,enterOverviewastheLabelpropertyandclickonthelookupbuttontolocatethelabelwecreatedinthepreviousstep.

Wecanreuseexistinglabels,andthisisfineifwearesurethatthecontextiscorrect.IfwechosealabelcalledOverview,wewillruntheriskthatMicrosoftmayrenametoFinancialoverview,forexample.Also,onlyreuse@SYSlabels--youmayfindthatyouhaveusedalabelinasampleapplicationthatisn'tshippedtotheproductionenvironment.

23. DragthetwofieldsontothegroupandorderthemsothatVehicleIdisfirstinthelist.

24. InorderforanyautomaticlookupstothistabletoshowbothIDandDescriptionfields,addbothfieldstotheAutoLookupfieldgroup.

25. WecanskiptotheMethodsnode,wherebestpracticedictatesweneedtoprovidetheFindandExistmethods.

MethodsinOperationshavechangedthenamingconvention,inthattheyshouldnowstartwithacapitalletter.Youwillfindmanystandardmethodsstillstartwithalowercaseletter,astheyhavenotbeenrefactored.

26. Right-clickontheMethodsnodeandchoseNewMethod.

Thisopensthecodeeditor.Elementsthatcontaincode,suchastablesandforms,canbeopenedinbothdesignerandcodeeditors.

27. Thecodeeditorhaschangedinthisreleasesothatallmethodsareshowninthesameeditor.Wearegivenamethodstub,asshownhere:

///<summary>

///

///</summary>

privatevoidMethod1()

{

}

WecanseethatthefirstlineintheeditorispublicclassConWHSVehicleGroupextendsCommon.Allmethodsarecreatedinsidethetwooutbraces.

28. RemovetheXMLdocumentationcommentsectionandcreatetheFindmethodasfollows:

publicstaticConWHSVehicleGroupFind(

ConWHSVehicleGroupId_groupId,

boolean_forUpdate=false)

{

ConWHSVehicleGroupvehGroup;

vehGroup.selectForUpdate(_forUpdate);

selectfirstonly*fromvehGroup

wherevehGroup.VehicleGroupId==_groupId;

returnvehGroup;

}

29. Createablanklineabovethemethoddeclarationandtypethreeslashes(///),whichcausesOperationstocreatetheXMLdocumentationbasedonthemethoddeclaration.Fillinthisdocumentationasfollows:

///<summary>

///Returnsarecordin<c>ConWHSVehicleGroup</c>

///basedonthe_groupIdparameter

///</summary>

///<paramname="_groupId">

///TheVehiclegroupIDtofind

///</param>

///<paramname="_forUpdate">

///Trueiftherecordshouldbeselectedforupdate

///</param>

///<returns>

///The<c>ConWHSVehicleGroup</c>record

///</returns>

Shouldthesuppliedvehiclegroupnotbefound,itreturnsanemptybuffer(wherethesystemRecIdfieldiszero).The_forUpdateparameterisexplainedintheThere'smore...section.

30. Now,tocreatetheExistmethod,gototheendofourFindmethodandcreateanewlineafterthemethod'sendbraceandjustbeforethefinalbraceforthetable,andtypeasfollows:

///<summary>

///Checksifarecordexistsin<c>ConWHSVehicleGroup</c>

///</summary>

///<paramname="_groupId">

///TheVehiclegroupIDtofind

///</param>

///<returns>

///Trueiffound

///</returns>

publicstaticbooleanExist(

ConWHSVehicleGroupId_groupId)

{

ConWHSVehicleGroupvehGroup;

if(_groupId!='')

{

selectfirstonlyRecIdfromvehGroup

wherevehGroup.VehicleGroupId==_groupId;

}

return(vehGroup.RecId!=0);

}

Thesemethodsareverysimilar,anditisbettertowritetheFindmethodandcopyitinordertocreatetheExistmethod.

31. Wewillhavetwotabsopen,thecodeeditorandthetabledesigner.Sincethecodeeditorwillbeopen,closethisfirst,savingthechanges.Thencloseandsavethetabledesigner.

32. Finally,wewillneedtosynchronizethedatabasesothatthetableiscreatedwithintheSQLServer.Beforewedothis,wewillneedtobuildthemodelbyfollowingthesesteps:1. Fromthemenu,selectOperationsandthenBuildmodels....2. CheckConWHSVehicleManagementfromthelistandpressBuild.

33. Tosynchronizethedatabasewiththetabledefinitions,right-clickontheprojectintheSolutionExplorerandchooseSynchronizeConWHSVehicleManagementwithDatabase.

34. ThiswillstarttheprocessandwillbevisibleintheOutputwindow.Wewillonlyneedtodothiswhenweneedtoeitherrununittestsortestswithinourenvironment.

Howitworks...CreatingatablecreatesadefinitionthatOperationswillusetocreatethephysicaltableintheSQLServer.Thetablesarealsotypesthatcontainalotofmetadataatapplicationlevel.

Whencreatingthefields,wedidn'tspecifythelabel,size,ortype.ThiscomesfromtheEDT.Wecanchangethelabel,giveitspecificcontext,butthesizeandtypecannotbechanged.

TherelationswecreatedareusedatapplicationlevelandnotwithinSQL.Theyareusedtogeneratedrop-downlistsandhandleorphanrecords.Withintheclient,youcannavigatetothemaintable.ItdeterminesthetableviatherelationandusestheFormRefpropertyonthemaintabletoworkoutwhichformtouse.

TheFindandExistmethodsareabestpracticerule,andshouldalwaysbewrittenandused.Forexample,althoughSelect*fromPurchLinewherePurchLine.InventTransId==_idmayappeartobecorrectasInventTransIdisauniquekey,itwouldbewrongasthereisnowafieldonPurchLinetoflagwhetheritismarkedasdeleted.UsingPurchLine::findInventTransIdwouldonlyfindarecordifitwasnotmarkedasdeleted.

Therearealsomanymethodsthatwecanoverridetoprovidespecialhandling.Whenoverridingamethod,itcreatesamethodthatsimplycallsthesuper()method.Thesuper()methodcallsthebaseclass'(Common)method,whichforupdate,insert,anddeleteaspecialmethodthatstartswithdo.Thedomethodscannotbeoverridden,butcanbecalleddirectly.ThedomethodisamethodonabaseclasscalledxRecordthatperformsthedatabaseoperation.

Themethodsforvalidation,suchasvalidateField,validateWriteandvalidateDelete,areonlycalledfromeventsonaformdatasource;thisiscoveredinthenextchapter.

Creatingaparametertable

Aparametertableonlycontainsonerecordpercompany.Thetablecontainsalistoffields,whichcanbedefaultsorcompany-specificoptionsusedincodetodeterminewhatshouldhappen.Theparametertableisusuallycreatedfirst,andthevariousfieldsthatactasparametersareaddedaswecreatethesolution.

ThisfollowsdirectlyfromtheCreatingsetuptablesrecipe.

Howtodoit...Tocreatetheparametertablefollowthesesteps:

1. CreateanewtablecalledConWHSVehiclePararameters;again,theprefixandsuffixisbasedonpractice.Usually,thenamewillonlybe<Prefix>+<Module>+Parameters.

2. Setthetable'sparametersasfollows:

Property Value

Label Vehiclemanagementparameters

Cachelookup Found

TableGroup Parameter

CreatedBy

CreatedDateTime

ModifiedBy

ModifiedDateTime

Yes

DeveloperDocumentation

TheConWHSVehicleParameterstablestorestheparametersfortheVehiclemanagementsolution

3. DragtheConWHSVehicleGroupIdEDTontotheFieldsnodeandrenameittoDefaultVehicleGroupId.4. DragtheParametersKeyEDTfromtheApplicationExplorertoourFieldsnode.5. RenameittoKeyandchangetheVisiblepropertytoNo.

Thisisonlyusedasaconstrainttolimitthetabletoonlyhavingonerecord.Allvisiblefieldsneedtobeinafieldgroup.

6. CreateafieldgroupcalledDefaultsandsettheLabelproperty.Usethelookuptolocateasuitablelabel.

Thelabel@SYS334126hasadescriptionindicatingthatwecanusethisforadefaultsfieldgroup.Youwillseeotherexamplesthatareclearlyunsuitablebyreadingthe

Descriptionfield.

7. DragtheDefaultVehicleGroupIdfieldtothenewDefaultsfieldgroup.

WewillusethisontheparameterformsothatithastheheadingasDefaults.Thisiswhywedon'tneedtochangethefield'slabeltospecifycontext.

8. Right-clickontheRelationsnodeandselectNew|ForeignKeyRelation.9. Completetheparametersasfollows;ifnotspecified,leaveasdefault:

Parameter Value Comment

Name ConWHSVehicleGroupThenameoftherelationisusuallythenameofthetable,unlesswehavetwoforeignkeystothesametable.

RelatedTable

ConWHSVehicleGroup Thetabletowhichourforeignkeyrelates.

Cardinality ZeroOne

Therewillbeeitheroneornoparameterrecordrelatingtothevehiclegrouprecord.

Aone-to-manyrelationshipwoulduseZeroMoreorOneMore.

RelatedTableCardinality

ZeroOneThevalueisnotmandatory,sowecanthereforerelatetozerovehiclegrouprecords,orone.

RelationshipType

Association

Theparameterrecordisassociatedwithavehiclerecord.Compositionwouldbeusedinheader/linesdatasets,wheredeletingtheheadershoulddeletethelinesrecords.

OnDelete Restricted

Thiswillpreventavehiclegrouprecordfrombeingdeleted,ifitisspecifiedonthistable.

SeetheThere'smoresectionformoreinformationondeleteactions.

Role

Thisistheroleoftherelation,anditmustbeuniquewithinthistable.

IfUseUniqueRoleNamesisYes,wewillonlyneedtospecifythisifwehavetwoforeignkeyrelationstothesametable.

10. Right-clickonRelationandchooseNew|Normal.11. IntheFieldproperty,specifytheforeignkey(thefieldinthistable),DefaultVehicleGroupId.12. IntheRelatedFieldproperty,specifythekeyintheparenttable:VehicleGroupId.13. CreateanewindexcalledKeyIdxandaddtheKeyfieldtoit.Itisuniquebydefault,soitactsasa

constraintindex.14. WecannowcreatetheFindandExistmethod.Thereisadifferenceforparametertables,inthatthe

Findmethodcreatesarecordinaparticularway.CreatetheFindmethodasshowninthefollowingpieceofcode:

publicstaticConWHSVehicleParametersFind(

boolean_forupdate=false)

{

ConWHSVehicleParametersparameter;

parameter.selectForUpdate(_forupdate);

selectfirstonlyparameter

indexKey

whereparameter.Key==0;

if(!parameter&&!parameter.isTmp())

{

//Createtheparameter

Company::createParameter(parameter);

}

returnparameter;

}

15. Wewilluseaslightlydifferentselectstatementwherewecanwritetheselectstatementinline,whichmeansthatwedon'thavetodeclarethetypeasavariable;writetheExistmethodasfollows:

publicstaticbooleanExist()

{

return(selectfirstonlyRecId

fromConWHSVehicleParameters).RecId!=0;

}

16. Wewanttoensurethattherecordcannotbedeleted.So,wewilloverridetheDeletemethod.PressReturnatthestartoftheFindmethodtocreateablanklineatthetop.Right-clickonthisblanklineandchooseInsertOverrideMethod|Delete.Changethemethodsothatitreadsasfollows:

publicvoiddelete()

{

throwerror("@SYS23721");

}

Thismethodiscalledwheneverwetryanddeletearecord,eitherthroughtheUIorincode.Thecalltosuper()thatweremoved,callsdoDelete(),whichinturntellsthekerneltodeletetherecord.

17. WesettheTableCachepropertytoEntireTable.Wheneverthistableisupdated,wewillneedtoflushthecachesothatthesystemusestheupdatedvalues.Overridetheupdatemethodasfollows:

publicvoidupdate()

{

super();

flushConWHSVehicleParameters;

}

WewanttotellOperationstowritetherecord,andthenflushthecache.Thisiswhysuper()isthefirstline.

There'smore...

ThebuildoperationwillvalidateandcompilethepackageintoaDLL.Thismustbedonebeforewesynchronizethedatabase.Thiscanfail,andatthisstage,itisnormallyduetomissingreferences.WithintheApplicationExplorer,eachelementshowsthepackagetowhichitbelongs.Wemustensurethatourmodelreferencesforalltypesthatweusewithinourproject.Ifwedon't,wewillgetbuilderrorslike

this:

Toaddtherequiredreferences,wecanfollowthesesteps:

1. LocatethetypewiththeerrorinApplicationExplorer.2. Notethepackageitisin,whichisinsquarebrackets.3. NavigatetoDynamics365|ModelManagement|Updatemodelparameters....4. SelecttheConWHSVehicleManagementmodel.5. ClickonNext.6. Checkiftherequiredpackageischecked,andthenpressNext.

WewouldnormallyreferencetheApplicationPlatform,ApplicationFoundation,andApplicationSuitepackagesanyway,aswewouldoftenuseelementsfromthesepackages.

7. PressFinish.8. NavigatetoDynamics365|ModelManagementandselectRefreshmodels.9. Trythebuildoperationagain;youmayneedtorepeatthisasoneerrorcanmaskanother.

ConWHSVehicleGroupgroup;<br/>group=ConWHSVehicleGroup::Find(<val>);

ConWHSVehicleGroup::Find(<val>).Description;

Thiswillcompilewithouterror,butitwilltrytofindvalueintheInventItemGrouptable.

OptimisticconcurrencyandselectForUpdateTherecordinOperationscontainsatleastfoursystemfields:

Field Description

DataAreaId ThisisthecompanyIDiftheSaveDataPerCompanyissettoYes.Thisfilters,atapplicationlevel,thedatainformsandselectstatementstobedatajustinthecurrentcompany.

RecId Thisisasystem-maintainedrecordID.

Partition Thisisusedtofilterdataatkernellevel;itislargelydeprecated,exceptfortestingscenarios.

RecVersion Thisisusedtodetermineiftherecordhaschangedsinceitwasread.

Optimisticconcurrency(OCC)isenabledbydefaultonnewtables.Weselectarecord"forupdate"byaddingaforUpdateclausetoaselectorwhileselectstatement,orbyusingtheselectForUpdate(true)methodthatexistsonalltables.

Whenweselectarecordforupdate,aphysicallockisnotplacedontherecordanditisthereforepossiblefortwodifferentprocessestoselectthesamerecordforupdate.

Astherecordorrecordsareread,theyarereadfromthedatabaseintoaninternalrecordbuffer.Whentherecordiswrittenback,itwillcheckthatthevalueoftheRecVersionfieldinthephysicaltableisthesameaswhentherecordwasfetchedintotheinternalbuffer.

IfRecVersionisdifferent,anexceptionwillbethrown.Ifthisisthrownwhilsteditingdata,theuserisgivenamessagethattherecordhaschangedandaskedtorefreshthedata.Iftheerroristhrownwithincode,wewillgetanupdateconflictexceptionthatcanbecaught.Shouldtheupdatesucceed,theRecVersionfieldwillbechangedtoadifferentnumber.

IfweareusingOCC,wecanmakethecalltoselectForUpdate()evenaftertherecordhasbeenfetchedfromthedatabase.

SeealsoThefollowingdocumentation(TableRelationProperties[AX2012])isforAX2012,butthedetailsarestillcorrectforOperations:

TableRelationProperties[AX2012](https://msdn.microsoft.com/en-us/library/hh803130.aspx)Tableproperties(https://ax.help.dynamics.com/en/wiki/table-properties/)

CreatingmaindatatablesInthissection,wewillcreateamaintable,similartothecustomertable.Thestepsaresimilartothevehiclegroup,andwewillabbreviatesomeofthestepswehavealreadydone.Thepatterndescribedinthisrecipecanbeappliedtoanymaintableusingyourowndatatypes.

Thetableinthisexamplewillbetostorevehicledetails.Thetabledesignwillbeasfollows:

Field Type Size EDT(:indicatesextends)

VehicleId String 20 ConWHSVehicleId:Num

VehicleGroupId String 10 ConWHSVehicleGroupId

RegNum String 10 ConWHSVehRegNum

AcquiredDate Date ConWHSAcquiredDate:TransDate

GettingreadyInordertofollowthesesteps,theelementscreatedearlierinthischaptermustbecreated.

Howtodoit...Tocreatethevehicletable,followthesesteps:

1. CreatetheConWHSVehicleIdstringEDTwiththefollowingproperties:

Property Value

Name ConWHSVehicleId

Extends Num

Label VehicleId

HelpText Thevehicleid

2. CreatetheConWHSVehRegNumstringEDTwiththefollowingproperties:

Property Value

Name ConWHSVehRegNum

Size 10

LabelRegistration

Addacommentthatthisisavehicleregistrationnumber

HelpText Thevehicleregistrationnumber

Changecase UpperCase

3. CreatetheConWHSAcquiredDatedateEDT,rememberingthatwewillneedtocreatetheEDTasanEDTdate.Usethefollowingproperties:

Property Value

Name ConWHSAcquiredDate

Extends TransDate

Label Dateacquired

HelpText Thedatethatthevehiclewasacquired.

AlthoughwecreatedthisEDTasadate,thisismainlyforthewayitappears.Itiscreatedinthedatabaseasadatetime,andcompilestoaCLRdatetimetype.

4. Createanewtable,andnameitConWHSVehicleTable.Theconventionisthatitstartswithourprefix,followedbytheentitynameasasingularnoun,andsuffixedwithTable.

5. DragtheEDTstothetableinthisorder:ConWHSVehicleIdDescriptionConWHSVehicleGroupIdConWHSVehicleTypeConWHSVehRegNumConWHSAcquiredDate

Thereasonfortheorder,isspecificallyfortheID,description,andgroupfields.Theseareusuallyplacedasthefirst3fields,andtheIDfieldisusuallyfirst.

6. RemovetheConWHSprefixfromthefieldsastheyareonourtable.

7. OntheVehRegNumfield,changetheAliasForpropertytoVehicleId.

TheAliasForpropertyallowstheusertoenteraregistrationnumberintheVehicleIdfieldinforeigntables,causingOperationstolookupavehicleandreplacetheentrywithVehicleId.Thisconceptiscommononmostmaintables.

8. SavethetableandopentheConWHSVehicleIdEDT.CompletetheReferenceTablepropertyontheTableReferencesnode.

9. ClosethedesignertablefortheEDTandnavigatebacktothetabledesigner.10. ChangetheVehicleIdfields'propertiesasanIDfieldasfollows:

Property Value

AllowEdit No

AllowEditOnCreate Yes

Mandatory Yes

Theprecedingpropertiesonlyaffectthewaythefieldbehavesonaform.

11. Amaintable'sGroupIdfieldusuallyhasanimpactonlogic,andisusuallymandatory.Evenifitdoesnot,weshouldstillmaketheVehicleGroupIdfieldmandatory.

Carefulconsiderationmustbetakenwhendecidingonwhetherthefieldismandatoryorwhenitcanbeedited.Insomecases,thedecisiononwhetheritcanbechangedisbasedondatainotherfieldsortables.ThiscanbeaccomplishedinthevalidateFieldeventmethods.

12. DonotmaketheVehicleTypefieldmandatory.

Enumsstartat0,andincrementbyoneeachtime.Operationsvalidatesthisusingtheintegervalue,whichwouldmakethefirstoptioninvalid.AsEnumsalwaysdefaulttothefirstoption,theonlywaytoforceaselectionfromthelistwouldbetomakethefirstelementcalledNotSet,forexample,withablanklabel.

13. CreateauniqueindexcalledVehicleIdxwiththeVehicleIdfield.14. Groupfieldsareoftenusedforaggregationorsearchqueries,soweshouldcreateanindexcalled

VehicleGroupIdxandaddtheVehicleGroupIdfieldtoit.Theindexmustnotbeunique.15. Completethetable'spropertiesasfollows:

Property Value

Label Vehicles

TitleField1 VehicleId

TitleField2 Description

Cachelookup Found

ClusteredIndex VehicleIdx

PrimaryIndex VehicleIdx

TableGroup Main

CreatedBy

CreatedDateTime

ModifiedBy

ModifiedDateTime

Yes

DeveloperDocumentation ConWHSVehicleTablecontainsvehiclerecords.

FormRef Thisisblankuntilwehavecreatedtheform

16. CreateafieldgroupOverview,labelledappropriately,anddraginthefieldsyouwishtoshowonthemainlistgridontheform.

17. Createafieldgroup,Details,andfindanappropriatelabel.Draginthefieldsthatshouldshowontheheaderoftheformwhenviewingthedetailsofthevehicle.

Itisusualtohavemanymorefieldsthanwehaveonthistable,whichnecessitatesthatseveralfieldgroupsareneeded.Whentheformiscreated,theformdesignerwillusethesefieldgroupstocreatetheform.Thedesignerwillusuallyhaveseveralfasttabs(collapsibleregions)andplaceoneormorefieldgroupsintothem.

18. Maintablesareusuallyreferencedinworksheettables,andOperationswillcreatealookupforusbasedontherelationontheforeigntable.Tocontrolthefieldsintheautomaticlookup,dragthefieldsyouwishtoseeintotheAutoLookupfieldgroup,andensurethatVehicleIdisfirst.

19. CreateaforeignkeyrelationfortheVehicleGroupIdfieldusingthefollowingproperties:

Parameter Value

Name ConWHSVehicleGroup

RelatedTable ConWHSVehicleGroup

CardinalityOneMore

Thefieldismandatory

RelatedTableCardinality ZeroOne

RelationshipType Association

OnDelete Restricted

20. Addanormalfieldrelationtotherelation,connectingtheGroupIdfields.21. Itiscommontoinitializemaintablesfromdefaults,heldinparameters.TheinitValuemethodis

calledwhentheusercreatesanewrecord.Right-clickontheMethodsnodeandselectOverride|initValue.

22. Inthecodeeditor,adjustthecodesothatitreadsasfollows:

publicvoidinitValue()

{

ConWHSVehicleParametersparm;

parm=ConWHSVehicleParameters::Find();

super();

this.VehicleGroupId=parm.DefaultVehicleGroupId;

}

23. Next,addtheFindandExistmethodsusingthetable'sprimarykeyfieldasusual.24. Finally,wewilladdafieldvalidationmethodtoensurethattheacquisitiondateisnotbeforetoday.

OverridethevalidateFieldmethodandaddthefollowingcodebetweencodetheret=super();lineandreturnret;:

switch(ret)

{

casefieldNum(ConWHSVehicleTable,AcquiredDate):

TimezoneclientTimeZone=

DateTimeUtil::getClientMachineTimeZone();

TransDatetoday=

DateTimeUtil::getSystemDate(clientTimeZone);

if(this.AcquiredDate<today)

{

//Theacquisitiondatemustbetodayorlater

ret=checkFailed("@ConWHS:ConWHS29");

}

break;

}

25. CreatealabelfortheerrormessagereturnedbycheckFailedandreplacetheliteralwiththelabelID.26. Oncecomplete,saveandclosethetablecodeeditoranddesignertabpages.

Howitworks...Wehaveintroducedacoupleofnewconceptsandstatementsinthisrecipe.

TheswitchstatementshouldalwaysbeusedonthevalidateFieldmethod,evenifweonlyeverintendtohandleonecase.Anifstatementmightseemeasier,butitwillmakethecodelessmaintainable.Thisgoesforanychecklikethis,wherethecaseshavethepossibilitytoincrease.

Thenextnewconceptisthatwecannowdeclarevariablesasweneedthem.Thishelpswithscope,butshouldn'tbeoverused.TheinitValueandvalidateFieldmethodsaregoodexamplesofexplainingwherethecodeshouldbedeclared.

TheAX2012systemGetDate()functionisdeprecatedinthisrelease.DateTimeUtilprovidesbetterhandlingfortimezones.Thedatecanbedifferentacrosstimezones,andcandifferbetweentheclient'smachine(thebrowser)andtheserverwhereOperationsishosted.

InthevalidateFieldmethod,wewillallowthestandardcodetorunfirst;thestandardcallwillvalidatethefollowing:

Thevaluevalidforthetype,suchasavaliddateinadatefieldIfthefieldisaforeignkey,doesthevalueexistintheparenttable?Ifthefieldismandatory,checkthatitisfilledin,orthatitisnotzerofornumericandEnumfields

There'smore...

Everyelement(table,tablefield,class,form,andsoon)hasanID.TablesandfieldsarecommonlyreferencedbytheirIDandnotbytheirname.ThevalidateFieldmethod,forexample,usesthefieldidastheparameterandnotthefield'sname.Aswecan'tknowtheID,Operationsprovidesintrinsicfunctions,suchastableNumandfieldNumtoassistus.Thepeculiarnatureofthesefunctionsisthattheydonotacceptastring,theywantthetypename.

Otherintrinsicfunctions,suchastableStr,fieldStr,andclassStr,simplyreturnthetypeasastring.Thereasonisthatthesefunctionswillcauseacompilationerrorshouldthetypebetypedincorrectly.Ifwedon'tusethem,notonlydowefailabestpracticecheck,butwemakeanyfuturerefactoringunnecessarilydifficult.

MoreonindexesTableindexesareaphysicalstructurethatareusedtoimprovereadperformance,ensureuniquenessofrecords,andfororderingofdatainthetable.Whenrecordsareinserted,updated,ordeleted,theindexisalsoupdated.Wemustthereforebecarefulwhenaddingindexes,astheycancarryaperformancehitwhenwritingdatabacktothetable.

Atypicalindexisanorderedcollectionofkeysandabookmarkreferencetotheactualdata.Findingarecordmatchingagivenkeyinvolvesgoingtotheappropriatelocationintheindexwherethatkeyisstored.Then,youwillhavetofollowthepointertothelocationoftheactualdata.This,ofcourse,requirestwoOperations:anindexseekandalookuptogettheactualdata.

Whenwesearchforarecord,theSQLServerisabletodeterminethebestindex,orindexes,touseforthatparticularquery.Ifwerealizethatweoftenrequirethesamesetoffieldsfromaspecificquery,wecancreateanindexthatcontainsthekeyswewishtosearchon,andthefieldswewishtofetch.Thisimprovesperformanceconsiderably,asSQLwillusethatindexandcanthensimplyreturnthevaluesthatalreadyexistintheindex.

WecanimprovethisfurtherbymarkingthefieldswesimplywishtoreturnasIncludedColumn(apropertyofthefieldsinaOperationsindex).So,inourcase,wemaywishtoselectthedescriptionfromthevehicletablewherethevehiclegroupisArtic,forexample.

Therefore,asolutioncanbetoaddtheDescriptionfieldtoourVehGroupIdxindexandmarkitasIncludedColumn.However,thereisabettersolutioninthiscase,whichistouseclusteredindexes.

Aclusteredindexissimilartothis,buttheclusteredindexwillcontaintheentirerecord,avoidingalookupinthedataforanyfieldinthetable.Clusteredindexesaresortedbytheirkeys;astheindexcontainstheentirerecord,itcanaddasignificantloadtotheSQLServerifrecordsareinserted,asopposedtobeingappendedattheendofthetable.

Forsetuptables,wherethenumberofrecordsissmallandchangesinfrequently,thisisn'taproblem,andthereadbenefitfaroutweighsanydrawback.Fortransactionaltables,wemustbecareful.Weshouldalwayshaveaclusteredindex,butthekeymustbesequentialandtherecordsmustbeaddedattheendofthetable.

Anexampleofthisisthesalesordertable,whichhasaclusteredindexbasedonSalesId.Thisisagreatchoiceaswewilloftenusethiskeytolocateasalesorderrecord,andthefieldisalsocontrolledbyanumbersequence;recordsshouldalwaysbeappendedattheend.However,shouldwechangethenumbersequencesothatrecordsareinserted"mid-table,"wewillexperienceadelayininsertingrecordsandwewillbeaddingunnecessaryloadtotheSQLServer.

SeealsoThefollowingisbasedonAX2012,butthecontentisstillrelevantinOperations:

IntrinsicFunctions[AX2012](https://msdn.microsoft.com/en-us/library/aa626893.aspx)

Thislinkgoesalittlebeyondwhatwehavedoneinthischapter,buthassomeveryusefulinformationabouttypes.

X++variablesanddatatypes(https://ax.help.dynamics.com/en/wiki/xpp-variables-and-data-types/)

Thefollowinglinkfocussesonmodellingaggregatedata,butalsoincludesimportantinformationaboutNon-ClusteredColumnStoreIndexes(NCCI),whichareinmemoryindexesusedforanalysingaggregatedata.

BIR100:Modelingandusingaggregatedata(https://ax.help.dynamics.com/en/wiki/modeling-and-using-aggregate-data/)

Creatingorderheadertables

Orderandlinetablesareusedwheneverweneedaworksheettoenterdatathatislateractedupon.Oncetheyhavebeenprocessed,theyshouldnolongerberequired.Reportsshouldactuponthetransactionsthattheordercreated,suchasinventorytransactions,salesledgertransactions,invoices,andmore.

GettingreadyAlthoughwewillbeusingthetablescreatedearlier,thepatterncanbefollowedwithyourownsolution.

Howtodoit...Wewillfirstcreatetheworksheetheadertable,whichwillbeavehicleserviceordertable:

1. CreateanewtablenamedConWHSVehicleServiceTable.2. CreateaprimarykeyEDT,ConWHSVehicleServiceId;thistime,extendNum.CompletetheLabelandHelpText

propertieswithappropriatelabels.3. DragtheEDTfromSolutionExplorertotheFieldsnodeofourtable,andrenameittoServiceId.4. CompletetheServiceIdfieldasanIDfield:Mandatory=Yes,AllowEdit=No,AllowEditOnCreate

=Yes.5. CompletetherelationinformationontheEDT.6. CreatetheprimarykeyindexasServiceIdxwithServiceIdastheonlyfield.7. SettheClusteredIndexandPrimaryIndexpropertiesasServiceIdx.8. DragtheConWHSVehicleIdEDTtoourtableandrenameittoVehicleId.9. MaketheVehicleIdfieldmandatory.Thedecisiontomakethefieldeditabledependsontheassociated

logic(referentialintegrity)andthebusinessrequirement.10. CreateaforeignkeyrelationforConWHSVehicleIdtoConWHSVehicleTable.VehicleId.

ThecardinalityshouldbeOneMoreasitismandatory.OnDeleteshouldbeRestrictedonforeignkeyrelationstomaintables.

11. DragtheDescriptionEDTontoourtablefromtheApplicationExplorer.12. CreateanewBaseEnumfortheservicestatus,asdefinedhere:

Property Value

Name ConWHSVehicleServiceStatus

Label Status(forexample@SYS36398)

Help Theserviceorderstatus

IsExtensibleTrue

Rememberthatwecan'tuse>or<comparisonsincodewiththisset.

Elements Label

None <empty>

Confirmed Confirmed

Complete Complete

Cancelled Cancelled

13. DragConWHSVehicleServiceStatustoourtableasServiceStatus.14. SaveanddragthenewEnumtoourtableandrenameittoServiceStatus.15. MaketheServiceStatusfieldreadonly.AllowEditandAllowEditOnCreateshouldbeNo.Statusfields

shouldbecontrolledthroughbusinesslogic.

16. CreatedateEDTsConWHSVehicleServiceDateReq'Requestedservicedate'andConWHSVehicleServiceDateConf'Confirmedservicedate'.ThedatesshouldextendTransDate.Labelappropriatelyanddragtothenewtable.

17. RenamethefieldstoServiceDateRequestedandServiceDateConfirmed.18. Completethetablepropertiesasshownhere:

Property Value

Label Vehicleserviceorders

TitleField1 ServiceId

TitleField2 Description

Cachelookup Found

ClusteredIndex ServiceIdx

PrimaryIndex ServiceIdx

TableGroup WorksheetHeader

CreatedBy

CreatedDateTime

ModifiedBy

ModifiedDateTime

Yes

DeveloperDocumentation ConWHSVehicleServiceTablecontainsvehicleserviceorderrecords.

FormRef Blankuntilwehavecreatedtheform

19. Createthefieldgroupsasfollows:

Group/fields Label

Overview

ServiceIdVehicleIdDescriptionServiceStatus

Overview

Details

ServiceIdVehicleIdDescriptionServiceStatus

Details

ServiceDates

ServiceDateRequestedServiceDateConfirmed

Servicedates

20. CreatethenowusualFindandExistmethodsusingServiceIdasthekey.21. Youcanalsocreateyourownvalidationontheservicedates.22. Finally,wewillintroducethevalidateWritemethod.Thisistoenforcetherequirementthatonly

serviceordersatstatusconfirmedorlesscanbechanged;themethodshouldbewrittenasfollows:

publicbooleanvalidateWrite()

{

booleanret;

ret=super();

ret=ret&&this.CheckCanEdit();

returnret;

}

publicbooleanCheckCanEdit()

{

If(!this.CanEdit())

{

//Serviceordercannotbechanged.

returncheckFailed("@ConWHS:ConWHS33");

}

returntrue;

}

publicbooleanCanEdit()

{

switch(this.ServiceStatus)

{

caseConWHSVehicleServiceStatus::None:

caseConWHSVehicleServiceStatus::Confirmed:

returntrue;

}

returnfalse;

}

23. Finally,wewillwriteamethodthatinitializesthedefaultsfromthemaintablerecord,thatis,vehicle,whenitisselected.Writethefollowingmethod:

publicvoidInitFromVehicleTable(ConWHSVehicleTable_vehicle)

{

this.Description=_vehicle.Description;

}

24. Then,overridemodifiedFieldasfollows:

publicvoidmodifiedField(FieldId_fieldId)

{

super(_fieldId);

switch(_fieldId)

{

casefieldNum(ConWHSVehicleServiceTable,

VehicleId):

this.InitFromVehicleTable(

ConWHSVehicleTable::Find(

this.VehicleId));

break;

}

}

25. Savethetableandclosetheeditortabs.

Howitworks...Therearefewnewconceptshere,andI'llstartwiththecodestructureattheendofthesteplist.

Themostimportantpartofthiscodeisthatwedidn'twritethis.ServiceStatus<=ConWHSVehicleServiceStatus::Confirmed.Thisisanextensibleenum,andwecan'tbesureofthenumericvaluethatthesymbolshave.

TheotherpartisthatwehavesplitwhatmayseemtobeasimpleifstatementinvalidateWriteintothreemethods.Thereasonisreusability.Itisnicertomakearecordread-onlyintheformthanitistothrowanerrorwhentheusertriestosave.So,wecanuseCanEdittocontrolwhethertherecordiseditableontheform,makingallcontrolsgreyedout.

Checkmethodsarewrittentosimplifythevalidationmethodswheretheyareused,andalsotomakethechecksreusable,ergoconsistent.Checkmethodsareexpectedtoreturnasilenttrueifthecheckpasses,ortodisplayanerrorshouldthecheckfail.TheerrorissenttotheuserusingthecheckFailedmethod,whichdoesnotthrowanexception.

ThenextistheInitFrommethod.Thisisaverycommontechniqueandshouldalwaysbeusedtoinitializedatafromforeigntables.Theoddthingisthatwedon'tcheckthatitexistsfirst.

Thisisdeliberate.RecordsinOperationsinitializesothatallthefieldsareempty,orzero(dependingonthefield'stype).So,iftherecordisnotfound,thevaluesthatareinitializedwillbemadetobeempty,whichisdesirable.Also,modifiedFieldoccursafterthefieldisvalidated.So,themethodwon'tbetriggeredshouldtheuserenterandinvalidvehicleID.Ifthevehicleisnotmandatory,wemayfindthevehicleIDisempty;however,again,thisisfine.

There'smore...TheOnDeletepropertyfortablerelationsissimilartothefunctionalitycontrolledbytheDeleteActionsnodeonthetable.ThedifferenceisthattheDeleteActionisplacedontheparenttable.Thisisaproblemiftheparenttableisastandardtable,andwearetryingtoavoidover-layering.UsingtheOnDeletepropertyisthereforecontrolledinamuchbetterlocation,eventhoughtheresultisthesame.

WehavethefollowingoptionsforbothDeleteActionsandtheOnDeleteproperty:

None

Restricted

Cascade

Cascade+Restricted

Nonehasnoeffect,andeffectivelydisablesthedeleteaction;thisisusefulifyouwanttospecificallystate"Donothing"sosomeoneelsedoesn'ttrytocorrectwhatseemstobeanomission.

Restrictedwillpreventtherecordfrombeingdeleted,iftherearerecordsintherelatedtablethatmatchtheselectedrelation.ThisoccurswithinthevalidateDeletetableevent,whichiscalledbythevalidateDeleteformdatasourceevent.

Cascadewilldeletetherecordintherelatedtablebasedontherelation;itisnousehavingasalesorderlinewithoutasalesorder.Thisisanextensiontothedeletetableevent.

Cascade+Restrictedisalittlespecial.Inatwo-tablescenario,itisthesameasRestricted;itwillstoptherecordfrombeingdeletedifarelatedrecordexists.However,iftherecordisbeingdeletedaspartofacascadefromatablerelatedtoit,therecordswillbedeleted.

Creatingorderlinetables

Thisrecipecontinuesfromthepreviousorderheadertablerecipe.Theexampleinthisrecipeisthatwewillhaveserviceorderlinesthatreflecttheworkrequiredonthevehicle.Theconceptsinthisrecipecanbeappliedtoanyorderlinetable;tofollowexactly,thepreviousrecipesshouldbecompletedfirst.

Howtodoit...Tocreatetheorderlinetable,followthesesteps:

1. CreateanewtablenamedConWHSVehicleServiceLine.2. DragthefollowingEDTsontothetable:

ConWHSVehicleServiceIdLineNumItemId(youmayreceiveanerrorifyouhaven'treferencedApplicationSuite)ItemNameConWHSVehicleServiceStatus

3. RemovetheConWHSVehicleprefixes.4. TheServiceIdandLineNumfieldsareusuallycontrolledfromcode,somakethemread-onlyand

mandatory(thisensuresthatthecodethatsetsthemhasrunbeforetheusersavestheline).

TheLineNumfieldisusuallyusedtoorderthelinesandcanbemadenotvisibleifthisisn'ttobedisplayedintheuserinterface.Allvisible(non-system)fieldsshouldeitherbeinafieldgroupormadenotvisible.

5. MakeItemIdmandatoryandonlyallowittobeeditedoncreation.6. CreateauniqueindexcalledServiceLineIdxandaddtheServiceIdandLineNumfields.Wewillusethisas

aclusteredindexasitwillnaturallysortthelinesontheform.7. AddarelationtoConWHSVehicleServiceTable,butservicelinesarecontainedwithinaserviceorder

record,socompleteitasfollows:

Parameter Value

Name ConWHSVehicleServiceTable

RelatedTable ConWHSVehicleServiceTable

Cardinality ZeroMore

RelatedTableCardinality ZeroOne

RelationshipType Composition

OnDelete Cascade

8. AddarelationtoInventTableonItemIdasfollows:

Parameter Value

Name InventTable

RelatedTable InventTable

Cardinality OneMore

RelatedTableCardinality ExactlyOne

RelationshipType Association

OnDelete Restrict

9. CreateanOverviewgrouptocontrolwhatappearsonthelinesandaddallfields.Inourcase,thisissufficient.Wewouldusuallyhavemanymorefieldsonaline,andwewouldorganizethefieldsintologicalgroupsthatareusedintheformdesign.

10. Updatethetable'spropertiesasfollows:

Property Value

Label Vehicleserviceorderlines

TitleField1 ItemId

TitleField2 ItemName

Cachelookup Found

ClusteredIndex ServiceLineIdx

PrimaryIndex SurrogateKey(default)

TableGroup WorksheetLine

CreatedBy

CreatedDateTime

ModifiedBy

ModifiedDateTime

Yes

DeveloperDocumentation ConWHSVehicleServiceLinecontainsvehicleserviceorderlinerecords.

11. TheFindandExistmethodswillneedtwokeysinthiscase,ServiceIdandLineNum.Theselectstatementclauseshouldbewrittenasfollows:

selectfirstonly*fromserviceLine

whereserviceLine.ServiceId==_id

&&serviceLine.LineNum==_lineNum;

12. Finally,wewillneedtoinitializetheItemNamefieldfromtheitemtable;first,createtheinitFromInventTablemethod.Wewillfollowthesamepatternaswedidearlierandwritethefollowinglinesofcode:

publicvoidinitFromInventTable(InventTable_inventTable)

{

this.ItemName=_inventTable.itemName();

}

13. OverridethemodifiedFieldmethodandcalltheprecedingmethodasshownhere:

publicvoidmodifiedField(FieldId_fieldId)

{

super(_fieldId);

switch(_fieldId)

{

casefieldNum(ConWHSVehicleServiceLine,ItemId):

this.initFromInventTable(

InventTable::find(this.ItemId));

break;

}

}

14. Oncecomplete,saveandclosetheeditors.

Howitworks...Thefirstnewconceptistheuseoftheclusteredindextocontroltheorderthattherecordsaredisplayedingridcontrols.ThisissimplyusingthefactthatSQLwillreturnrecordsintheorderoftheclusteredindex.Compositekeysarefineforthispurpose,butwejustwouldn'tusuallyusethemasaprimarykey.

WealsotouchedonSurrogatekeys,whichwehavesofaravoided.Theexplanationrequiresalittlebithistorytounderstand.ThesewereintroducedinAX2012asaperformanceaidandallowedfeaturesliketheledgeraccountlookupwhenenteringgeneralledgerjournals.TheproblemisthattheyarehardwiredtobeRecId.So,whenweaddedforeignkeyrelations,thefieldcreatedcontainedanunhelpfulRecId.Tosolvethis,analternatekeywasadded,whichisapropertyontheindexdefinition.Thisallowsamoremeaningfulrelationtobeusedforaforeignkey.TheprimarykeycouldonlybeuniqueindexesthathadtheAlternateKeypropertyset.

TheothertypeofkeyintroducedwastheReplacementkey.TheReplacementkeyisawaytoshowameaningfulkey,otherthanthenumericRecIdbasedSurrogateKey.

WhatSurrogateKeystillallowsustodoistouseRecIdastheforeignkey,butshowsmeaningfulinformationfromaFieldGroupontheparenttable.AnexampleisthatwecouldaddaforeignkeyrelationtoConWHSServiceOrderLine,whichshoulduseSurrogateKey.Whenweaddtheforeignkey,containingthemeaninglessnumber,weaddaReferenceGroupcontrolthatcandisplayfieldsfromafieldgroupontheConWHSServiceOrderLinetable;theuserisoblivioustothemagicalreplacementthatisgoingonbehindthescenes.

ThefinalpointbroughtoutinthisrecipeisintheinitFromInventTablemethod.Thepatternisstraightforward,butthecalltoinventTable.itemName()isamethod,hencetheparentheses.ThedeclarationforthemethodispublicItemNameDisplayitemName([Common]).AsalltablesderivefromCommon,wecanpassinanytable,whichisastrueasitispointless.Ifwelookatthemethod,itcanactuallyonlyhandleInventDim.

Readingthroughthemethodsisalwaysagoodinvestment,takingtimetounderstandthereasonwhythecodewaswrittenthatparticularway.

SeealsoForthehistoryoftheSurrogatekey,seehttps://msdn.microsoft.com/en-us/library/hh812105.aspx.

CreatingtheUserInterface

Inthischapter,wewillcoverthefollowingrecipes:

CreatingthemenustructureCreatingaparameterformCreatingmenuitemsCreatingsetupformsCreatingdetailsmaster(maintable)formsCreatingadetailstransaction(orderentry)formCreatingformpartsCreatetileswithcountersfortheworkspaceCreatingaworkspace

Introduction

Inthischapter,wewillmanyofthetaskswhencreatingauserinterface.ThiscontinuesonfromChapter2,DataStructures.AsdiscussedinChapter2,DataStructures,weusuallycreatetablesandformsaspartofthesameprocess;so,therecipesinthischapterwillinvolvecompletingafewpropertiesinthedatamodel.

Whencreatingforms,wemustprovideconsistencytotheusers,andtoaidwiththis,wewilluseformdesignpatterns.Theformdesignpatternisdeterminedbythetablegroup,asstatedinChapter2,DataStructures.Therearespecialcaseswhenwecanhavevariationsbut,forthemainpart,weshouldsticktothepatternssuggestedinthischapter.

CreatingthemenustructureThemenustructureiscarriedoverfromtheuserinterfaceconceptsinAX2012.InOperations,themenushavethesamestructure,butareopenedfromthe'burger'menubuttonjustbelowtheOffice365logoonthetopleftofthepage,asshowninthefollowingscreenshot:

EachmenuappearsundertheModulesoption,asshowninthefollowingscreenshot:

Allmenuitemsforforms,reports,andprocessesmustbeplacedinthemenustructure,evenifwelatercreateaworkspace.

ThemenustructuremayseemtohavebeenrelaxedslightlyfromtheerigidstructurerecommendedinDynamicsAX2012.InDynamicsAX2012,wealwayshavethefollowingmainheadingsinamenu:Common,Inquiries,Reports,Periodic,andSetup.IfwelookattheAccountsPayablemenuintheApplicationExplorer,wecanseethattheoldstructurehasbeenflattened,asshowninthefollowingscreenshot:

Theactualstructureisnoworganizedasfollows:

Workspaces:VendInvoiceWorkspace,VendPaymentWorkspaceTile.DetailsMaster(Main)tables:VendorsDetailsTransaction(Worksheets):PurchaseOrders,VendorInvoicesJournals:PaymentsInquiriesandReports:VendPackingSlipJournal,VendTransList,VendAccountStatementIntPeriodictasks:PeriodicTasksSetupandconfiguration:Setup,PolicySetup,andsoon

Themenustillneedsacommonstructureinordertomakeiteasierforuserstofindtheoptionsmoreeasily,butwearen'tconstrainedtoarigidstructure.Youmayarguethatthemenudoesnothaveaworkspacenode,thisisbecausethiswasaddedthroughamenuextension,asshowninthefollowingscreenshot:

GettingreadyWewilljustneedourprojectopeninVisualStudio.

Howtodoit...Tocreatethemenustructure,followthesesteps:

1. Addanewitemtotheproject(selectafoldernodeandpressCtrl+Shift+A).2. SelectUserInterfacefromtheleft-handlistandthenMenufromtheright.3. TypeConWHSVehicleManagementintheNamefieldandpressOK.4. Inthedesigner,createalabelforVehiclemanagementandenterthisasthemenu'sLabelproperty.5. Right-clickonnewmenuinthedesignerandchooseNew|Submenu.6. Completeasfollowsforthefollowingsubmenus:

Name Label

Workspaces @SYS:Platform_Menu_ColHeading_Workspaces

Vehicles Vehicles(newlabel)

ServiceOrders Serviceorders(newlabel)

PeriodicTasks @SYS76406

InquiriesAndReports @SYS3850

Setup @SYS333869

7. Wecanaddmoresubmenustohelporganizethestructure,shouldthisberequired.8. Saveandclosethemenudesigner.9. Wewillnowneedtoextendthemainmenusothatwecannavigatetoourmenu.10. IntheApplicationExplorer,navigatetoAOT|UserInterface|Menus.11. Right-clickonMainMenuandchooseCreateextension.

ThiscreatesanextensiontotheMainMenumenu,butdoesnotover-layerit,allowingourchangetositnicelyalongsidetheotherextensionofthesameelementwithouthavingtodoacodemerge.

12. RenamethenewiteminourprojectfromMainMenu.ExtensiontoMainMenu.ConWHSVehicleManagement.

Leavingthename,asitisgivenautomatically,isacommonomissionandisamistake;

allmenuextensionsmusthaveauniquename.Manypartnerswillinsistweprefixextension,suchasConWHSMainMenu.Extension.ThepointofthenameistomakeiteasytofindinApplicationExplorerandthatitisunique.

13. OpenMainMenu.ConWHSVehicleManagement.14. Right-clickontherootnodeandchooseNew|Menureference.15. SettheNameandMenuNamepropertiestoConWHSVehicleManagement.16. Saveandclosethedesigner.

Howitworks...Thisisastructuretowhichwewilladdthevariousmenuitems.Thisstructureispresentastheuseropensthemenufromtheleft-handmenubuttonfromwithintheclient-userinterface.

Thisbecomesmoreapparentaswecompletetheuserinterface.

Creatingaparameterform

Parameterformsshowsettingsgroupedintofieldgroupsandtabsusingatableofcontentsstyleform.Theyarealsousedtoshowthenumbersequencessetupforthatmodule.NumbersequencesarecoveredinChapter4,ApplicationExtensibility,FormCode-Behind,andFrameworks.Wearefollowingourvehiclemanagementsamplesolution,butthispatterncanbeappliedtoanyparametertable.

Howtodoit...Tocreatetheparameterform,followthesesteps:

1. DragtheConWHSVehicleParameterstablefromtheprojectontotheDataSourcesnodeinthetop-leftpaneofformdesigner.

2. Nametheformasperthetable'sname;inthiscase,ConWHSVehicleParameters.3. SelectUserInterfacefromtheleft-handpaneandFormfromtheright-handpane.4. Choosetoaddanewitemtotheproject.5. Datasourcesprovideadditionaloptionsofhowthistableshouldbehaveonthisform.Wedon'twant

theusertobeabletodeleteandcreaterecords,sochangethefollowingpropertiestoNo:AllowCreateAllowDeleteInsertAtEndInsertIfEmpty

6. Theformdesignerisbrokenintothreeareas:TheFormpane,Designpane,andPreview/Patternconformancepane,asshowninthefollowingscreenshot:

7. Let'sapplysomebasicpropertiesfortheform,whichareheldagainsttheDesignnode,asshownhere:

Property Value Comment

Caption Vehiclemanagementparameters

Thisisshowninthetitleoftheformandisusuallythetable'sname.

DataSource ConWHSVehicleParameters ChildnodesintheformwillusethisasthedefaultDataSourceproperty.

TitleDataSource

ConWHSVehicleParameters Thisformwillusethetitlefieldpropertiesfromthetableanddisplaythemontheform.

8. TheDesignnodestatesthatapatternisnotspecified,whichwemustdo.Toapplythemainformpattern,right-clickontheDesignnodeandchooseApplyPattern|TableofContents.

9. ThelowerpanechangestothePatterntabandshowsthepattern'srequiredstructure,asshowninthefollowingscreenshot:

Wewillrefertoeachnodeinthepatternstructureasapatternelementand,toaideaseofdesign,namethecontrolsbasedonthepatternelement'sname.

10. ThisenablesustoaddaTabcontrol,soright-clickontheDesignnodeandchooseNew|Tab.11. TheerrorisremovedfromthePatternpane,butshowsthatwehavenotabpageswithintheTOC

Tabs(Tab)patternelement.12. First,renamethenewcontroltoParameterTab,andthenaddanewTabPagebyright-clickingonitin

theDesignpaneandselectingNewTabPage.

13. Thefirsttabisusuallyageneralsettingstab,sonamethisnewcontrolGeneralTabPage.14. FindasuitablelabelforGeneralandenterthatintotheLabelproperty.15. Aswealterthedesign,thedesignerwillcontinuallycheckthatweareconformingtothepattern.It

hasfoundthefollowingissueswiththetabpagecontrol:

16. WemustaddtwoGroupcontrols,onefortitlefieldsthatprovidehelptotheuser,andtheotherforthefieldswewishtoshowinthispage.

17. Right-clickontheGeneralTabPagecontrolandselectNew|Group.

Oncethisisdone,reselecttheGeneralTabPagecontrolandnotethatthepatternconformancepanehaschangedfromarederrortoayellowwarning.Thismeansthatthatthecontrolsbelowthislevelhavepatternconformanceerrors.

18. RenamethenewgroupcontroltoGeneralTitleGroup.

Allcontrolsmustbeunique,andnaminginawaythathelpsusnavigatetothecontrol

easilywillhelpusgreatly.shouldwegetabuilderrorlateron.

19. ThepatternconformancepaneshowswemusthaveaStaticTextcontrolforthemaintitle,and0or1StaticTextcontrolsforasecondarytitle.Right-clickonGeneralTitleGroupandchooseNew|StaticText.

20. RenamethenewStaticTextcontroltoGeneralTitleText.21. CreateanewlabelforthetextGeneralparametersandentertheIDintotheTextproperty.22. ReselecttheGeneralTitleGroupcontrolandcheckifthepatternerrorsaregone.23. ReselectGeneralTabPage,aswenowhaveonepatternerrorforamissinggroup.AddanewGroup

control.24. Thenewgroupcontrolknowsthatitmustalsohaveapatternapplied,indicatedbythePattern:

<unspecified>textafterthecontrol'sname.Right-clickonthecontrolandchooseApplyPattern|FieldsandFieldGroups.

25. RenamethecontroltoGeneralFields.26. Ontheformpane,expandDataSources,ConWHSVehicleParameters,andthenFieldGroups.

27. Dragthefieldgroup,Defaults,ontotheGeneralFieldscontrolinthedesignpane,asshowninthefollowingscreenshot:

Wecanmanuallycreategroupsandthenaddtheappropriatefields.However,usingafieldgroup,anynewfieldsaddedtothegroupwillgetautomaticallyaddedtotheform,andtheLabelpropertywillbesetbasedonthefieldgroup'slabel.

28. Checkeachnodeforpatternconformanceerrors.29. Finally,wewillhaveaspecialtaskforparameterforms;toensurethatatleastonerecordexists,

clickontheMethodsnodeandchooseOverride|init.30. Createablanklinebeforethecalltosuper()andenterthefollowinglineofcode:

ConWHSVehicleParameters::Find();

Wedon'tcareaboutthereturnvalueinthiscase.WeareusingthefactthattheFindmethodforParametertableswillautomaticallycreatearecordifthetableisempty.

31. Saveandclosethecodeeditorandformdesigners.

Howitworks...

Whenwespecifytheformdesign'smainpattern,wearethenguidedastothecontrolsweshouldaddandwhere.Thishelpsusensurethattheformswedesignfollowabestpracticeuser-interfacedesign,whichinturnmakesourformseasiertouse,quickertotrain,andlesspronetousererror.

WecanstilloptoutofthepatternusingtheCustompattern;thisallowsustoaddanycontrolinanyorder.Thisshouldbeavoided,andtheformdesignshouldberedesignedsothatitusesstandardpatterns.

There'smore...TheformisbasedontheFormRunclass.Wecanseethisbyopening(double-click)classDeclarationfortheCustGroupform:

publicclassConWHSVehicleParametersextendsFormRun

ThisdiffersfromAX2012,wherethedeclarationwaspublicclassFormRunextendsObjectRun,whichwasprobablyalittlemorehonest,astheformisnotatype;thisiswhyitcanhavethesamenameasatable.

ThisisactuallyadefinitionfilethatFormRuninstantiateswithinordertobuildandexecutetheform.

OncethesystemhasbuilttheformdesignusingSysSetupFormRun,itwillperformtheinitializationtasksandruntheform.ThekeymethodsinthisareInitandRun.Thesecanbeoverriddenontheforminordertoperformadditionalinitializationtasks.

OneofFormRunInitmethod'skeytasksistoconstructtheform'sdatasources;thesearen'ttablereferencesbutFormDataSourceobjectsconstructedfromthetableslistedundertheDataSourcesnode.

InthecaseoftheConWHSVehicleParametersform,thesystemcreatesthefollowingobjectsfromtheConWHSVehicleParametersdatasourceforus:

ConWHSVehicleParametersastypeConWHSVehicleParameters(table)ConWHSVehicleParameters_DSastypeFormDataSourceConWHSVehicleParameters_QastypeQueryConWHSVehicleParameters_QRastypeQueryRun

Wearen'tnormallyconcernedwiththeQueryandQueryRunobjectsaswecanaccessthemthroughtheFormDataSourceobjectanyway.

Thedatasourcesaredeclaredasglobalvariablestotheformandprovidealayeroffunctionalitybetweentheformandthetable.Thisallowsustocontroltheinteractionbetweentheboundformcontrolsandthetable.

Let'soverridetheinitmethodonthedatasource,andthenoverridethemodifiedFieldeventonafield;thecodeeditorwillpresentthechangeasfollows:

[Form]

publicclassConWHSVehicleParametersextendsFormRun

{

publicvoidinit()

{

ConWHSVehicleParameters::Find();

super();

}

[DataSource]

classConWHSVehicleParameters

{

publicvoidinit()

{

super();

}

[DataField]

classDefaultVehicleGroupId

{

publicvoidmodified()

{

super();

}

}

}

}

Itaddsthedatasourceasaclasswithintheform'sclassdeclaration,andthenaddsthefieldasaclasswithinthedatasourceclass.ThisistheonlyplaceinOperationswherethisoccurs.Ifthedatasourceclasswereaclass,itwouldhavetoextendFormDataSource.TheForm,DataSource,andDataFieldattributesareaclueastowhat'sgoingonhere.AsallexecutablecodecompilestoCLRtypes,thecompilerusestheseattributesinordertocreatetheactualtypes.Thestructureiswrittenassuchforourconvenienceasapresentationofcode.

Let'stakethemodifiedFieldmethod.ThisisaneventthatoccursafterthevalidateFieldeventreturnstrue.Thecalltosuper()callsthetable'smodifiedFieldmethod.Wemaywonderwhythecalltosuper()hasnoparameter.Thishappensbehindthescenes,anditisusefulthatthisishandledforus.

Thispatternisfollowedforthefollowingmethods:

DataSourcemethod Callstablemethod

validateWrite validateWrite

write write(inturn,insertorupdate)

initValue initValue

validateDelete validateDelete

delete delete

DataField.validateField validateField(FieldId)

DataField.modifiedField modifedField(FieldId)

Thetable'sinitValue,validateField,modifiedField,validateWrite,andvalidateDeletemethodsareonlycalledfromformevents;thewritemethoddoesnotcallvalidateWrite.

Fromthis,wehaveachoiceastowhereweplaceourcode,andthisdecisionisveryimportant.Therule

tofollowhereistomakechangesashighaspossible:table,datasource,andformcontrol.

Itiscriticallyimportantthatcodeonaformmustonlybewrittentocontroltheuserinterfaceandshouldnotcontainvalidationorbusinesslogic.

Wecangofurtherwiththisandwriteforminteractionclassesthatallowuserinterfacecontrollogictobesharedacrossforms;forinstance,controllingwhichbuttonsareavailabletoalistpageandtheassociateddetailform.

SeealsoThegeneralformguidelines(https://ax.help.dynamics.com/en/wiki/general-form-guidelines/).

Creatingmenuitems

Menuitemsareareferencetoanobjectthatwewishtoaddtoamenu.Wehavethreetypesofmenuitems:Display,Output,andAction.Displayisusedtoaddforms,Outputisusedforreports,andActionisusedforclasses.

Menuitemsarealsoaddedasprivilegestothesecuritysystem.Usersthataren'tanadministratorwillnotbeabletoseethemenuitemsunlesstheyareassignedarole,duty,orprivilegethatgivesthemaccesstothemenuitem'srequiredaccesslevel.

GettingreadyWewilljustneedaform,report,orclassthatweneedtocreatethemenuitemfor.

Howtodoit...

1. Choosetoaddanewitemtotheproject.2. SelectUserInterfacefromtheleft-handpaneandFormfromtheright-handpane.3. Namethemenuitemtobethesameastheform;inthiscase,ConWHSVehicleParameters.4. Enterthelabel@SYS7764astheLabelproperty;thisisthelowest@SYSlabelthathasnospecificcontext

described.5. CreateahelptextlabelforMaintainsthevehiclemanagementsettingsandnumbersequences.AssigntheIDto

theHelpTextproperty.6. EnterConWHSVehicleParameterstotheObjectproperty.7. TheObjectTypepropertyisFormbydefault,sothisiscorrect.8. SavethemenuitemandopentheConWHSVehicleParameterstable,andthenenterConWHSVehicleParametersto

theFormRefproperty.9. Finally,opentheConWHSVehicleManagementmenuanddragtheConWHSVehicleParametersmenuitemontopof

theSetupnode.10. Saveandclosetheopentabs.

Howitworks...

Althoughthisrecipeislistedinisolation,itshouldalwaysbedoneaspartoftheprocess.Forexample,theprocessofcreatingaParametertableinvolvesfourmainsteps:

1. Createthetable2. Createtheform3. Createthemenuitem4. Addthemenuitemtothemenu

NearlyallofthetablesthatwecreateinOperationswillhaveaformthatisusedtomaintainit.So,thelastpartofthetable'sdesignistotellthetabledesignwhichformisusedforthatpurpose.ThisishowtheViewdetailsoptionworksintheclient.Theforeignkeyisusedtodeterminethetable,andthetable'sFormRefpropertyisusedtodeterminetheformtobeused.Theforeignkeydetailsarethenusedtofiltertheform.Allthishappensautomaticallybasedonthemetadata.

Menuitemsforformsandreportstriggerthesystemtobuildandexecutetheformorreportdefinition.TheformisrenderedinthebrowserusingsomeverycleverJavaScriptthatinterfaceswithserver,andreportsareessentiallyrenderedusingSSRS.Classesmusteitherhaveastaticentrypointmethodinordertobecalled.ThisiscoveredinChapter4,ApplicationExtensibility,FormCode-Behind,andFrameworks.

Creatingsetupforms

ThisfollowsasimilarpatterntotheParameterform.UsingthetableinChapter2,DataStructures,weknowweshoulduseSimplelistorSimplelistandDetails-ListGridpatterns.Ourtablehastwofields,sowewillusetheSimplelistformdesignpattern.ThisfollowsthepatternofcreatingatableoftypeGroup.

Howtodoit...

1. Choosetoaddanewitemtotheproject.2. SelectUserInterfacefromtheleft-handpaneandFormfromtheright-handpane.3. Nametheformasperthetable'sname;inthiscase,ConWHSVehicleGroup.4. DragtheConWHSVehicleGrouptablefromtheprojectontotheDataSourcesnodeoftheformdesigner.5. UsethesamelabelasthetablefortheCaptionpropertyoftheDesignnode.6. SettheTitleDataSourceandDataSourcespropertiestothetablename,ConWHSVehicleGroup.7. ApplytheSimpleListpatterntoDesign(right-clickandchooseApplyPattern|SimpleList).

Wewillusethepatternconformationerrorsasourto-dolist.Ifwedon'tconformtothepattern,theprojectwillnotbuild.However,justconformingtothepatternmaynotbeenough;thebuildwillalsospotsomeerrorsinproperties,whicharestatedasmandatorybythepattern.

8. AddanActionPanecontrol;renametoFormActionPaneControlastherewillonlybeoneonthisform.

Wedon'tneedtospecifytheDataSourceproperty,asthiswilldefaultfromtheDesignnode.

9. ReselecttheDesignnode.ThepatternhighlightsthatweneedaCustomFilterGroup,whichisaGroupcontrol.So,addanewcontroloftypeGroup.

10. RenamethecontroltoCustomFilterGroup,foreaseofreference.

Apartfromnavigatingformbuilderrors,thisbecomesmoreimportantforcomplicatedpatterns,aswewillbeabletoseeifthepatternisusingthecorrectcontrol.Thenameisnothowthepatternknowswhichcontrolsmaptothepattern'sstructure.

11. Wecanseethatthenewgroupcontrolneedsapattern;assignCustomandQuickFilters.12. Wewillnowhavemoretodoforthiscontrol.Thepatternhighlightsthatwemusthaveone

QuickFilterControl.Right-clickontheCustomFilterGroupcontrolandchooseNew|QuickFilterControl.

WewillneedtolinkthistoaGridcontrol,sowewillcompletethiscontrolalittlelater.

13. Right-clickontheDesignnodeandselectNew|Grid.14. WecanrenamethecontroltoFormGridControlastherewillonlybeonegridcontrolwiththispattern.15. TheDataSourcepropertydoesnotinheritfromtheparentnode,andwemustspecifythisas

ConWHSVehicleGroup.16. WecreatedanOverviewfieldgroupforthistable,sosettheDataGrouppropertytoOverview.

Renamethecontainercontrolsbeforeaddingfieldstothem,orsettingtheDataGroupproperty,asthefieldswillbeprefixedwiththecontainercontrol'sname.

17. GobacktotheQuickFiltercontrolandsetTargetControltothegridcontrol'snameandDefaultColumntothedesireddefaultcontrolofthetargetcontrol.

18. Doublecheckthatpatternpaneforerrors,andthensavetheform.19. Next,createamenuitemusingthesamenameastheformandthesamelabelasweusedinthe

Captionproperty.20. Completethetable'sFormRefpropertywiththemenuitemname.21. Finally,addthemenuitemtothesetupmenusoitliesundertheParametersmenuitem.

Howitworks...Thestepsweresimilartotheparameterform,althoughtherewerejustafewmoretocomplete.Wecanseethatthepatternisactuallyato-dolist,althoughpatternerrorswillpreventtheprojectfrombeingbuilt.

There'smore...

Ifyouwanttoactuallytesttheform,youcandosobyfollowingthesesteps:

1. BuildthepackageusingDynamics365|BuildModels;selectthepackagefromthelist.2. Ifweaddedtablesorfieldssincethelastdatabasesynchronization,synchronizethedatabasesby

right-clickingontheprojectandchoosingSynchronize<yourprojectname>withdatabase.3. Opentheproject'sproperties.4. SettheStartupObjectTypeoptiontoMenuItemDisplay.5. SettheStartupObjectoptiontothemenuitemtoopen,forexample,ConWHSVehicleGroup.6. SpecifyInitialCompany;USMFisausefulcompanyforwhenweareusingthedeveloperVMsthatuse

Microsoft'sdemodata.7. ClosethepropertyformandpressF5.

Thisisalsohowwedebugcode,andshouldabreakpointbeencountered,VisualStudiowillmovetotheforeandallowustodebugthecode.

Creatingdetailsmaster(maintable)forms

ForthosethatareusedtoDynamicsAX2012,thisstyleofformreplacestheListpageandseparatestheDetailsformusedformaintables,suchasProductsandCustomers.Listpagesareeffectivelydeprecatedforthispurpose.Itmayseemalittleconfusingatfirst,aswearedevelopingtwoviewsofaformatthesametime.However,oncewehavedonethisafewtimes,itbecomesmoreautomatic.

Thepatternpanewillbeourguideinthisrecipe,helpingussimplifytheprocess,andremindinguswhenwehaveforgottenakeystep.Wearecontinuingtheformdesignforthemaintable,ConWHSVehicleTable.However,thisrecipecanbeusedasapatternforanymaintable.

Howtodoit...Tocreatethedetailsmasterform,pleasefollowthesesteps:

1. Choosetoaddanewitemtotheproject.2. SelectUserInterfacefromtheleft-handpaneandFormfromtheright-handpane.3. Nametheformasperthetable'sname;inthiscase,ConWHSVehicleTable.4. DragtheConWHSVehicleTabletablefromtheprojectontotheDataSourcesnodeoftheformdesigner.

5. SelecttheConWHSVehicleServiceTabledatasourceandsettheInsertIfEmptyandInsertAtEndpropertiestoNo.

InsertIfEmptywillcreateanewrecordiftherearenorecords,whichisundesirableformaintableandorderheadertables.TheInsertAtEndpropertycanalsobeundesirable;iftheuserpressesthedownarrowkeyonthelastrecord,anewemptyrecordwillbecreated.

6. UsethesamelabelasthetablefortheCaptionpropertyoftheDesignnode.7. SettheTitleDataSourceandDataSourcespropertiestothetablename,ConWHSVehicleTable.8. ApplytheDetailsMasterpatterntotheDesign.9. Asrequiredbythepattern,addanActionPanecontrolnamedFormActionPane.10. AddaGroupcontrolcalledNavigationListGroup.Thissectionisusedinthedetailsviewsothattheuser

canchangerecordswithoutgoingbacktothelistview.

ThetasksforNavigationListGrouparesimilartotheSimpleListpattern,sothestepswillbesummarized.

11. UndertheNavigationListGroupcontrol,createaQuickFiltercontrolandnameitNavgationQuickFilter.12. Then,createaGridcontrolnamedNavigationGrid.

Therewillbeanothergridandquickfilteraddedlater,andcontrolnamesmustbeunique.

13. SettheDataSourcepropertyofthegridcontroltoConWHSVehicleTableandaddtheVehicleIdandDescriptionfieldsbydraggingthemfromtheFieldsnodeofthedatasource.

Wecanaddwhicheverfieldswelikehere,butkeepthenumberoffieldstojustafew.

14. CompletethequickfiltercontrolwithTargetControlasthegridcontrolandDefaultColumnasdesired.

15. CheckingthenodesonandundertheDesignnode,wewillnowneedtocreateaTabcontrolforthe

PanelTab.CreateanewTabcontrolasfollows:

Property Value

Name PanelTab

ArrangeMethod Vertical

Youcanseeherethatwehaveatabpageforthedetailspanelandthelistpanel.Thesystemcontrolsthepagesthatarevisibleforus.

16. AddatabpagenamedDetailsPanelTabPagefirst,andthenListPanelTabPage.

ThepatternseemstomakeamistakeasweaddDetailsPanelTabPageandthinksthatweareaddingthelistpanel.Itcorrectsitselfoncewehavecreatedthesecondtabpagecontrol.

17. Let'scompletetheListPanelTabPagecontrolfirst.Asrequiredbythepattern,underthiscontrol,addaGroupcontrolnamedListQuickFilterGroupandaGridcontrolnamedListGrid.

18. Completethegridcontrol'sDataSourcepropertyandusetheOverviewfieldgroupfortheDataGroupproperty.

19. WewillcompleteListQuickFilterGroupinthesamewayaswedidfortheSimpleListpattern.ApplytheCustomandQuickFilterspatterntothecontrol.Then,addaQuickFiltercontrolandnameitListQuickFilter.CompletethecontrolbyreferencingittothegridcontrolwenamedListGrid.

20. ThepatternnowstatesthatwewillneedMainGridDefaultAction,whichisaCommandButtoncontrol.AddCommandButtontoListPanelTabPage,settingtheproperties,asshowninthefollowingtable:

Property Value

Name ListGridDefaultButton

Command DetailsView

Youmaynoticetherearemanycommandswecoulduse,sowemustbecarefulwhichcommandisselected!

21. OntheListGridcontrol,enterListGridDefaultButtontotheDefaultActionproperty.22. MovingontotheDetailsPanelpatternelement,reselecttheDetailsPanelTabPagecontrolandadda

groupcontrolnamedDetailsTitleGroup.

23. AddaStringcontrolfortheheadertitles,asshownhere:

Property Value

Name DetailsHeaderTitle

DataSource ConWHSVehicleTable

DataMethod TitleFields

ViewEditMode View

Skip Yes

ThetitleFieldsmethodisasystem-provideddatamethodthatusesthetitlefieldpropertiesfromthetable.Wecanwriteourown,shouldweprefer.

24. Checkingourprogressagainstthepatternpane,wewillseethatweneedtoaddtheDetailsTab(Tab)patternelement.AddanewTabcontroltotheDetailPanelTabPagetabpagecontrol,settingthepropertiesasfollows:

Property Value

Name DetailsTab

ArrangeMethod Vertical

Wewillnowcreateatabpagetoorganizethevariousfieldsontotheform,andeachtabpagecanbealistoffieldsorsomethingmoreelaborate.TheVendTableformisagoodexampleofamorecomplicatedform.

25. AddanewtabpagenameDetailsTabGeneralandapplytheFieldsandFieldGroupspattern.

ThefirsttabpageiscustomarilyatabpagelabelledGeneral.

26. Inthiscase,wecansimplydragtheDetailsfieldgroupfromtheConWHSVehicleTabledatasource,butfeelfreetoreorganizethefieldsintomoreappropriategroups.

Shouldnewfieldgroups(orfields)beadded,youcanrefreshthedatasourcebyright-clickingonthedatasourceandchoosingRestore.

27. Wewillneedtocreatethemenuitemusingthesamelabelasthetable's,butwewillneedtodefaultFormViewOptiontoGridsowecangetthelistviewwhentheformisopened.

28. Next,completetheFormRefpropertyoftheConWHVehicleTabletable.

29. Finally,addthemenuitemtoourmenu.

Howitworks...Theconceptisthesameasforanyform,wejusthavemorefeatures.Thepeculiarpartofthisformisthatwehavetwoviews--oneforthelistviewandtheothertoeditorviewthedetailsoftheform.

Thisisdonebyshowingandhidingthedetailandlisttabs.Itknowswhichcontroltoshoworhide,becausewefollowedthepattern--thisisoneofthereasonswhyapatternconformationerrorwillresultinthecompilation,shouldwetryandbuildtheproject.

TotestthisonthedevelopmentVMprovidedbyMicrosoft,buildtheprojectandusethefollowingURL:

https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=usmf&mi=ConWHSVehicleTable

Youcanusethispatterntoopenanydisplaymenuitem.

Creatingadetailstransaction(orderentry)form

Theseworksheetformsarethemostcomplicatedintermsofthestepsrequired,aswenowhavethreestatestodesign:list,header,andlinesviews.Tofamiliarizeyourselfwiththeendresult,openandusetheAllpurchaseordersformfromAccountsPayable|Purchaseorders|Allpurchaseorders.

ThefirstpartofthepatternisverysimilartotheDetailsMasterpattern,sowewillsummarizeslightly.Wewillcontinuethevehicleserviceordertable,butagain,therecipeiswrittensothatitcanbeappliedtoanyworksheettable.

Howtodoit...Tocreatetheform,followthesesteps:

1. Choosetoaddanewitemtotheproject.2. SelectUserInterfacefromtheleft-handpaneandFormfromtheright-handpane.3. Nametheformasperthetable'sname;inthiscase,ConWHSVehicleServiceTable.4. DragtheConWHSVehicleServiceTabletablefromtheprojectontotheDataSourcesnodeoftheform

designer.5. SelecttheConWHSVehicleServiceTabledatasourceandsettheInsertIfEmptyandInsertAtEndproperties

toNo.6. DragtheConWHSVehicleServiceLinetabletotheDataSourcesnode.7. SelecttheConWHSVehicleServiceLinedatasourceandsettheJoinSourcepropertyto

ConWHSVehicleServiceTable.

Wedon'tspecifyandjoininformationbeyondthename,asitwillusetheforeignkeyrelationdefinedinthechildtable.

8. OverridetheinitValuemethodonConWHSVehicleServiceLinesothatwecansettheServiceIdfieldasthisisnotsetforus.Usethefollowingcode:

publicvoidinitValue()

{

super();

ConWHSVehicleServiceLine.ServiceId=

ConWHSVehicleServiceTable.ServiceId;

}

9. Closethecodeeditorandgobacktotheformdesignedtab.10. Setthepropertiesasfollows:

Property Value

Caption Usethelabelfromthetable

DataSource ConWHSVehicleServiceTable

TitleDataSource ConWHSVehicleServiceTable

Thisissothattheheaderwillusethecurrentlinefortheheadersection.Thisisn'tmandatory,andisdoneheretodemonstratethatwehaveachoice.

11. ApplytheDetailsTransactionpatterntotheDesignnode.12. AddanActionPanecontrolnamedHeaderActionPaneandthenaGroupcontrolcalledNavigationListGroup

undertheDesignnode.

WewillhavetwoActionPanes,onefortheheaderandoneforthelines.

13. AddaQuickFiltercontroltotheNavigationListGroupcontrolnamedNavgationQuickFilter.14. Then,createaGridcontrolnamedNavigationGrid.15. SettheDataSourcepropertyofthegridcontroltoConWHSVehicleServiceTableandaddtheServiceIdand

DescriptionfieldsfromtheConWHSVehicleServiceTabledatasource.16. CompletethequickfiltercontrolwithTargetControlasthegridcontrolandDefaultColumnas

desired.17. UndertheDesignnode,createaTabcontrolforPanelTab.CreateanewTabcontrolandnameit

MainTab.SetArrangePropertytoVertical.18. AddatabpagenamedDetailsPanelTabPagefirst,andthenGridPanelTabPage.

ThenamesdifferfromtheDetailsMasterpattern.WewillnameourcontrolsafterthetextcontroldescriptionsinthePatternpane.

19. WewillcompletetheGridPanelTabPagecontrolfirst.Underthiscontrol,addaGroupcontrolnamedGridPanelQuickFilterGroupandaGridcontrolnamedListGrid.

20. Completethegridcontrol'sDataSourcepropertyandusetheOverviewfieldgroupfortheDataGroupproperty.

21. ForGridPanelQuickFilterGroup,applytheCustomandQuickFilterspatterntothecontrol.ThenaddaQuickFiltercontrol,andnameitListQuickFilter.CompletethecontrolbyreferencingittothegridcontrolwenamedListGrid.

22. ThepatternnowstatesweneedMainGridDefaultAction,whichisaCommandButtoncontrol.AddCommandButtontoGridPanelTabPagecalledMainGridDefaultAction.

23. SettheCommandpropertytoDetailsView.

Youmaynoticethattherearemanycommands,andwemustbecarefulwhichcommandisselected!

24. OntheMainGridcontrol,enterMainGridDefaultActiontotheDefaultActionproperty.25. TocompletetheDetailsPanel(TabPage)patternelement,reselecttheDetailsPanelTabPagecontroland

addagroupcontrolnamedDetailsPanelTitleGroup.26. AddaStringcontrol,settingthepropertiesasfollows:

Property Value

Name DetailsPanelHeaderTitle

DataSource ConWHSVehicleServiceLine

DataMethod titleFields

ViewEditMode View

Skip Yes

Optionally,createagroupundertheDetailsPanelTitleGroupcontrolcalledDetailsPanelStatusGroup,andthendragtheServiceStatusfieldfromtheConWHSVehicleServiceTabletable.RenamethefieldcontroltoDetailsPanelTitle_ServiceStatus.

27. WewillnowneedtocompletetheHeaderandLinePanels(Tab)patternelement.AddanewTabcontroltoDetailPanelTabPage,asfollows:

Property Value

Name HeaderAndLinePanelTab

ArrangeMethod Vertical

28. AddanewtabpageforLinesPanelusingthefollowingproperties:

Property Value

Name LinesPanelTabPage

Style DetailsFormDetails

TheStylepropertyaddsmoreinformationtothepatterninordertocontrolpresentationandbehavior.

29. Addasecondtabpagecontrol,shownasfollows:HeaderPanelTabPage.

Property Value

Name HeaderPanelTabPage

Style DetailsFormDetails

30. AddanewTabcontroltotheLinePanelTabPagecontrolnamedLineViewTab.31. Wewillnowaddthreetabpagestothiscontrol.32. AddtheLineViewHeaderDetails(TabPage)patternelementusingthefollowingproperties:

Property Value

Name LineViewHeaderTabPage

Label Vehicleserviceorderheader

Pattern ApplytheFieldandFieldGroupspattern

33. AddtheLineViewLines(TabPage)patternelementusingthefollowingproperties:

Property Value

Name LineViewLines

Label @SYS9664

34. AddtheLineViewLineDetails(TabPage)patternelementusingthefollowingproperties:

Property Value

Name LineViewLineDetailsTabPage

Label VehicleServiceorderlines

DataSource ConWHSVehicleServiceLine

35. AddanActionPanecontroltotheLineViewLinesTabPagecontrolnamedLinesActionPane,andunderthat,anActionPaneTabcontrolnamedLineActionRecordActions.

36. Right-clickonthenewLineActionRecordActionscontrolandselecttheNewButtonGroupcontrolunderthisnamedLineActionRecordActionsGroup.

Forthisactionpane,wewillneedtoaddbuttonstoaddandremovelines.TheseareonlyaddedautomaticallyinthemainheaderActionPanecontrol.

37. UndertheLineActionRecordActionsGroupbuttongroup,addaCommandbuttonnamedLineActionAddLine.38. Thisistoallowtheusertoaddlinestothelinesgrid.Setthefollowingproperties:

Property Value Description

NormalImage Delete Thisaddsawastebinsymbol

Label @SYS135131 Remove

Command New Thistriggersthenewrecordtaskforthedatasource

39. Then,addasecondCommandbuttonnamedLineActionRemove,settingthepropertiesasfollows:

Property Value Description

NormalImage Add Thisaddsasimpleplussymbol

Label @SYS319116 Addline

Command DeleteRecord Thistriggersthedeleterecordtask

SaveRecord No Wewanttoallowrecordsthatarenotyetsavedtobedeleted

40. Underthisbuttongroup,wewillneedagridforthelines.AddanewGridcontrol,settingthefollowingproperties:

Property Value

Name LinesGrid

DataSource ConWHSVehicleServiceLine

DataGroup Overview

Asweusuallyhavemanymorefieldsthatcanrealisticallyfitinthegrid,wehaveatabcontrolthatallowstheusertoseethesefieldsgroupedinalogicalorderusingtabpages.

41. CreateaTabcontrolunderLineViewLineDetailsTabPageandsetthefollowingproperties:

Property Value

Name LineViewDetailsTab

Label @SYS23823

DataSource ConWHSVehicleServiceLine

ArrangeMethod Vertical

42. Wewouldaddoneormoretabpagesbut,inourcase,wewillonlyneedone.43. AddaTabPagecontrolnamedLineViewDetailsTabDetailsandsettheLabelpropertyto@ConWHS:Details.44. DragIdentificationandDetailsfromtheConWHSVehicleServiceLinesdatasource.45. Wewillnowneedtocompletetheheaderviewoftheform,whichisgovernedbytheHeaderPanel

patternelement.Right-clickonHeaderPanelTabPageandchooseNew|Tab,andcompleteasfollows:

Property Value

Name HeaderDetailsTab

ArrangeMethod Vertical

DataSource ConWHSVehicleServiceTable

46. Wewouldusuallyhavemanyfieldgroupstoaddbut,inthiscase,wewilljustneedone.CreateanewTabPagecontrolnamedHeaderDetailsTabDetails.SettheCaptionpropertyto@ConWHS:Details.

47. DragtheDetailsandServiceDatesfieldgroupsfromtheConWHSVehicleServiceTabledatasource,butrenamethem,prefixedwithHeaderDetailsTabDetails.

48. Wewillneedtocreatethemenuitemusingthesamelabelasthetable's,butwewillneedtodefaultFormViewOptiontoGridsowecangetthelistviewwhentheformisopened.

49. Then,completetheFormRefpropertyoftheConWHVehicleServiceTabletable.50. Finally,addthemenuitemtoourmenu.

Howitworks...

Theprocess,albeitextended,isthesameastheDetailsMasterpattern.Therearesomeadditionalpropertiestosetinthiscasetohelpwiththeform'sbehavior.Therearealotofsteps,anditiseasytogetlostandpotentiallysetthewrongproperty.Thisiswhywenamethecontrolsafterthepatternelementname.

Thisformpatternwasdeliberatelyassimpleasitcanbe,andoncewearecomfortablewiththeprocess,itshouldbestraightforwardtoexpandthistomorecomplicateddatastructures.

Creatingformparts

Formpartsareusedfortwopurposes.Oneistoprovideapopupformasyouhoveroveraforeignkey,suchasthepopupwhenyouhovertheProductnumberwhenenteringsalesorpurchaseorders.Thisisgreatforusers,asitmeanstheydon'tnecessarilyhavetonavigateawayfromthetasktheyareperforming.

Anotheruseistocreateareusedformpartthatwecanplacewithinotherforms.Wecouldhaveaformpartthatcontainsproductinformation,whichwecouldaddtotheproductandsalesorderforms.Wecanspecifyalinkwhenweaddtheformpart,makingthemeasytoimplement.

GettingreadyWewillcreateasimpleformpart,whichisalistofvehicleserviceorders,sowewillonlyneedatablecreatedfromwhichthedatawillbedisplayed.

Howtodoit...Tocreateaformpartforopenvehicleserviceorders,followthesesteps:

1. CreateanewformcalledConWHSVehicleServiceOpenPart.2. DragtheConWHSVehicleServiceTabletabletotheDataSourcesnode.3. Setthepropertiesofthedatasourceasfollows:

Property Value

AllowEdit No

AllowCreate No

AllowDelete No

InsertIfEmpty No

InsertAtEnd No

4. SetthepropertiesontheDesignnodeasfollows:

Property Value

DataSource ConWHSVehicleServiceTable

ShowDeleteButton No

ShowNewButton No

5. ApplytheFormPartSectionListpattern.

YoucanuseanyofthepatternsstartingwithFormPart,eachofwhichisforadifferentstyle;forinstance,FormPartFactboxCardisusefultoapplytothePreview

Partpropertyofatableinordershowanicepopupwhentheuserhoversoveraforeignkey.

6. AddanewgroupcontrolnamedHeaderGroupandapplytheFiltersandToolbar-inlinepattern.7. AddagrouptothiscalledFilterGroupandaddaQuickFiltercontroltothis.8. ReselecttheHeaderGroupcontrolandaddanActionPanecontrolcalledToolbarActionPane.9. AddaButtonGroupcontrolnamedToolbarButtonGroup.10. DragtheConWHSVehicleServiceTableDisplayMenuItemontothebuttongroupandsetthepropertiesas

follows:

Property Value

Name ActionNew

Label @SYS2055(New)

NormalImage New

FormViewOption Details

OpenMode Edit

11. Dragthemenuitemasecondtimebut,thistime,setthepropertiesasfollows:

Property Value

Name ActionDetails

Label @ConWHS:Details

NormalImage Details

NeedsRecord Yes

SaveRecord No

CopyCallerQuery Yes

FormViewOption Details

OpenMode New

12. CreateanewgridcontrolundertheDesignnodenamedContentGrid.13. SettheDataSourcepropertytoConWHSVehicleServiceTableanddragthefields,asdesired,fromthedata

sourcetothegrid.

Youcouldconsidercreatingafieldgroupforthispurpose.Thefieldsonaformpartwillbelessthanthelistviewofthedetailsformastheyshouldonlyoccupyathirdofthewidthofthescreen.

14. SettheDefaultActionpropertyofthegridtoActionEdit.15. Createamenuitemusingthesamenameastheformpart,thatis,ConWHSVehicleServiceOpenPart.

Howitworks...FormPartsarejustforms,butthepatternforcesustodesigninaccordancewithhowtheformpartwillbeused;whichiswhytherearefourtypesofformpartpatterns.

Wewillusetheprecedingpartwhencreatingtheworkspaceformlater.FormPartFactBoxCardisalsoveryusefulandeasytocreate.Justusethepatterntocreatetheformpart,createamenuitemfromit,andcompletethetable'sPreviewPartRefproperty.

Later,wewillusequeriesandcompositesubqueriestoapplyfilterstoforms.Wecanusethetechniqueofspecifyingaquery,oroneofthequery'scompositequeries,toamenuitem.Thismeansthatwecanhaveoneformpart,butthemenuitemcanbeusedtoapplyafiltertotheform.

CreatetileswithcountersfortheworkspaceTilesareusedasanentrypointtoaformwhilsthavingtheabilitytoshowinformationaboutthedata.Itcanactasaprompttoactionandispresentedonatilesectionofaworkspace,asshowninthefollowingscreenshot:

ThepagethatopenswhenyoufirstsignintoOperationsisalsoalistofTiles,andeachopensaworkspace.Theworkspacewillhaveanimageresourcethatwewoulddesign;akeyguidelineisthatallgraphicsareminimalistic.Allsolutionsshouldlookandfeelasiftheywerepartofthestandardsolution.

Gettingready

Thisrecipecanbeusedtocreateatileforanyform,andisusuallydoneforDetailsMasterandDetailsTransactiontoshowbothallrecordsandcommonlyusedsubsetsofdata.

Inthiscase,wewillcreateatileforallvehicles,andthenatileforatypeofvehicle.

Howtodoit...Tocreateatile,followthesesteps:

1. Wewillfirstcreateaquerythatthetilewilluse.CreateanewoperationsartifactandselectQueryfromtheDataModelartifacts.

2. SetNametoConWHSVehicleTableAllandpressAdd.3. DragtheConWHSVehicleTabletabletotheDataSourcesnode.4. ChangetheDynamicsFieldspropertytoYes.

Thiswillensurethatthequeryalwaysreferencesallthefieldsinthetable,whichisimportantwhenitwillbeusedwithaform.

5. Saveandclosethequery.6. ChoosetoaddanewOperationsArtifactandselectTilefromtheUserInterfacenode.7. ChangeNametoConWHSVehiclesAllTileandpressAdd.8. Setthefollowingproperties:

Property Value

Query ConWHSVehicleTableAll

Label Allvehicles

MenuItemName ConWHSVehicleTable

NormalImage GenericDocument

CopyCallerQuery Yes

Next,wewillcreateatileforbikes.Wewillcreateacompositequery,whichwillallowustouseabasequeryandaddrangestoit.

9. Createanewartifact,andchooseCompositeQueryfromtheDataModelnode.10. SetNametoConWHSVehicleTableBikesandpressAdd.11. IntheQueryproperty,enterConWHSVehicleTableAll.12. Right-clickontheRangesnodeandselectNewCompositeQueryRange.13. IntheDataSourceproperty,selectConWHSVehicleTable.

14. SettheFieldpropertytoVehicleType.15. Wewillenterthecriteriabyenteringtheenum'ssymbolintheValueproperty;so,enterBikeinthe

Valueproperty.

Youmayalsoenterthenumericvalue,butthiscanonlybeusedfornon-extensibleenums.

16. CreateanewtilecalledConWHSVehicleBikesTile.17. CompletethepropertiesoftheTileasfollows:

Property Value

Query ConWHSVehicleTableBikes

Label Bikes

MenuItemName ConWHSVehicleTable

NormalImage <Blank>

CopyCallerQuery Yes

Type Count

18. Saveandclosealldesignertabs.

Howitworks...Tileshavetwobasicproperties,thesourcequeryandthetargetform'smenuitem.Whenthetileisclicked,thequeryisappliedtothetargetform,filteringthedata.TheCopyQueryCallerpropertyisimportanthere,andshouldthefilternotappeartowork,itisnearlyalwaysbecausethispropertywasnotset.

Thistechniqueofapplyingqueriescanalsobeappliedtomenuitems,andwecancreatemenuitemsthatwillopenthevehicletablefilterbasedonthequery.ThisisdonebyspecifyingtheQueryandCopyCallerQuerypropertiesonaduplicateofthemenuitems.

Whenexperimentingwithfilters,don'tfilterbasedondatathattheusercanchange;atileforaparticularvehiclegroupwouldbeaverygoodexampleofabadidea.

There'smore...Agoodtileisatilethattakesyoutoserviceordersduetomorrow,sowewillneedtofilterbasedonafunction.

Inthequeryrange'sValueproperty,wecanalsoenterqueryfunctions.ThestandardfunctionsaredefinedintheSysQueryRangeUtilclass.So,tofilterontoday'sorders,youwillenter(currentDate())inthequeryrange'sValueproperty.

WeusedtomodifythisclasstoaddnewfunctionsinDynamicsAX2012,butsinceoverlayeringisdiscouraged,wehaveabetterwaytodothisinOperations.

CreateanewclassandcallitConWHSQueryRangeUtil.Thenameisnotimportant.Tocreateamethodthatreturnsadateanumberofdaysfromtoday,usethefollowingcode:[QueryRangeFunction]publicstaticdateRelativeDate(intrelativeDays=0){utcdatetimecurrentDateTime;

currentDateTime=DateTimeUtil::applyTimeZoneOffset(DateTimeUtil::getSystemDateTime(),DateTimeUtil::getUserPreferredTimeZone());

returnDateTimeUtil::date(DateTimeUtil::addDays(currentDateTime,relativeDays));}

Tousetheprecedingfunctiontoreturntomorrow'sdate,addanewrangefortherequireddatefieldandenter(ConWHSQueryRangeUtil::RelativeDate(1))intheValueproperty.Tofilterrecordsonorbeforetomorrow,use..(ConWHSQueryRangeUtil::RelativeDate(1)).

Creatingaworkspace

Theworkspaceprovidesanareaforeverythingauserwillneedforataskorgroupoftasks.Theworkspaceshouldbeabletodisplayallofthekeyinformationwithoutscrolling,andisstructuredasahorizontalspacewiththefollowingsections:

TilesTabbedlistsofkeydataCharts(Optional)PowerBI(Optional)Relatedinformation,forexample,linkstokeyforms

Thedashboardisnormallycreatedoncewehavecompletedmostofthesolution;otherwise,wewillhavenothingtoadd.Thepatterncanbeeasilytransposedtoyourownrequirement.

Howtodoit...Tocreatetheworkspace,followthesesteps:

1. CreateanewformandnameitConWHSVehicleWorkspace.2. ApplytheWorkspaceOperationalpatterntotheDesignnode.

YoucanalsousetheWorkspace,whichprovidesasimplerdesignwherewewillonlyshowtilesandlinks.

3. CompletetheCaptionpropertyasVehiclemanagementworkspace.4. AddaTabcontrolandcallitPanoramaTab,followingtheideathatwewillnamecontrolsalongthe

linesofthepatternelementwearecreating.5. Createanewtabpage,namedPanoramaSectionTiles,andapplytheSectionTilespattern.6. Setthefollowingpropertiesonthenewtabpage:

Property Value

Caption Summary

ExtendedStyle panoramaItem_backgroundNone

7. Forthetabpage,createtheTileButtoncontrolsforeachofourtiles.Usethefollowingpropertiesasaguide:

Property Value

Name ConWHSVehiclesAllTile

Tile ConWHSVehiclesAllTile

CopyCallerQuery Yes

8. AddanewtabpagetothePanoramaTabcontrol,namedPanoramaSectionList,andapplytheSectionTabbedListpattern.

9. ProvideCaption,suchasServiceorders.

10. AddaTabcontrolnamedTabbedListTab.11. TotheTabbeListTabcontrol,addatabpageandnameitTabbedListPageOrders.12. CompletetheCaptionpropertyasOrders.13. AddanewFormPartcontrol,specifyingthefollowingparameters:

Property Value

Name ConWHSVehicleServiceOpenPart

MenuItemName ConWHSVehicleServiceOpenPart

RunMode Local

14. Right-clickonthePanoramaTabcontrolandselectNewTabPage.15. ApplytheSectionRelatedLinkspattern.

Here,wewilladdlinkstoformsthatareneededfortheworkspacestask;thesecanincludesetforms,andotherlinksthattheuserwilluse.

16. RenamethecontroltoPanoramaSectionLinksandsettheCaptionpropertytoLinks.17. CreateanewgroupandcallitLinksSetupGroup.SettheCaptionpropertytoSetup.18. DragthemenuitemsConWHSParametersandConWHSVehicleGroupstothenewgroup.19. Saveandclosethedesigntabs.20. Createamenuitemfortheform,settingtheLabelpropertytoVehiclemanagementworkspace.21. WewillneedaTiletoaddtothemainnavigationworkspace;createtheTileasfollows:

Property Value

Name ConWHSVehicleWorkspaceTile

MenuItemName ConWHSVehicleWorkspace

NormalImage Workspace_PurchaseReceiptAndFollowup(thisisacheat,moreonresourceslater)

Size Wide

TileDisplay BackgroundImage

22. LocatethenavpanemenumenuinApplicationExplorer,andright-clickonit.ChooseCreateextension.23. Renameittonavpanemenu.ConWHS.24. Openthemenuextension.Right-clickontherootnodeandchooseNew|Submenu.25. RenamethenewsubmenutoConWHSMenu.26. Right-clickonthesubmenuandchooseNew|MenuElementTile.27. CompletetheTilepropertyasConWHSVehicleWorkspaceTile.28. Saveandcloseallwindowsandperformabuild.

Howitworks...

Theformdesignhereisjustliketheotherforms,andmuchsimplerthantheDetailsMasterandTransactionpatterns.Thecleverpartofthisisthatwearethinkingtask-based.Wearecreatingaworkspacebasedonwhattheuserdoes.ThetraditionalformdesignfromDynamicsAX2012wastoprovideallthefeaturesthattheusercouldwant,whichresultedinalotoffeaturesthatmostusersneverused.Withworkspaces,theconceptisthatmostoftheworkspaceisusedregularly.Theusershouldn'thavetochangeworkspacetolocateaformforthattask.ItisalsoOKforausertousemorethanoneworkspace.Wewanttoprovideeverythingtheuserneeds,butwithoutclutter.

There'smore...Formoreinspirationonworkspaces,investigatetheReqCreatePlanWorkspaceform.Specifically,lookatthewayithandlesthewaythefiltercontrolsthetiles.

ApplicationExtensibility,FormCode-Behind,andFrameworksInthischapter,wewillcoverthefollowingrecipes:

CreatingahandlerclassusingtheapplicationextensionfactoryHookingupanumbersequenceCreatingacreatedialogfordetailstransactionformsCreatingaSysOperationprocessAddinganinterfacetoaSysOperationframework

IntroductionInthischapter,wewillgetstraightintowritingcode.Therecipeschosenforthischapterarecommontasksthatwillbeusedonmanydevelopmentprojects.

Asweprogressthroughthechapter,referencestocodeplacementismade.Codeplacementiscriticaltoamaintainableandextendablesolution.Wewillseethatcodecanbewrittenontheform,inaclass,orinatable.Theruleofthumbhereisthatwemustplacecodeaslowinthestackaspossible.Ifwewritecodeonaform,thatcodeisonlyavailabletothatformandcannotbereused.Thisisfinewhenwearehidingabutton,butdata(validation,andotherdataspecificcode)logicusuallybelongstoatable.Asthecodeontheformortablegetsmorecomplicated,thecodeshouldbemovedtoaclass.

TheSalesTableformandtableisanexample.InthiscasetableeventsarehandledbytheSalesableTypeandSalesLineTypeclassesandformlogicishandledbytheSalesTableFormclass.Thereasonhereisthattherecanbedifferenttypesofsalesorder,andthebestsolutionwastohaveabaseclassforthebasecodeandaspecializedclasstohandlethedifferentrequirementsofeachordertype.

CreatingahandlerclassusingtheApplicationExtensionfactoryFormhandlerclasseshavearoleinmorecomplicatedforms,suchasDetailsTransaction(orderentry)forms.Thesearethetwomainreasonswhywewouldconsiderdevelopingaformhandlerclass:

Weintendtocreateaseparateformforthecreationoftherecord,whichiscommononorderentryformsTheuserinterfacelogicisparticularlycomplicated,orvariesbytypeofrecord

Wecanalsohavetablehandlerclasses,forsimilarreasons.Thetablehandlerwillhavecodethatisrecord-specific,whereverthedataispresented.Thecodeintheformhandlerisformspecific.Theplacementofcodeisthereforeimportant.Codethatdetermineswhetherafieldiseditablewillbeinthetablehandlerclass,andtheformhandlerclasswillusethatmethodinordertomakethecontroleditable.

Wemaynotalwayshaveatablehandlerclass,andthesemethodscanbeplaceddirectlyonthetable;althoughwecan'tchangethemethoddeclarationsofpublicmethodsafterrelease(incaseotherpartieshaveusedthem),wecanchangetheinternallogicwithimpunity.

Inthisexample,wewillcontrolwhichfieldsonthevehicleservicetableareenabled,basedontheservicestatus.Itisasimpleexampleinordertodemonstrateapatternthatcanbeusedtoabstractcomplicatedscenarios,simplifyingcodecreation,andmaintenance.Wewillcreateatableandformhandlerclassusingastandardpattern.WewillthencreatethetablehandlerusingtheSysExtensionpattern.Thisallowsthecodetobeextendedbyotherpartieswithoutover-layering.

GettingreadyWeshouldhaveatableandformcreatedtotheDesignTransactionpatternforthisrecipe.Inourcase,wewilluseConWHSVehicleServiceTable.

Howtodoit...Let'screatethetablehandlerclassfirst,sincewewillneedthisinordertocompletetheformhandlerclass.TousetheSysExtensionpattern,wewillneedatleasttwoclasses:theattributeclassandtheclassthatwillbeconstructedusingthepattern.

1. Tocreatetheattributeclass,choosetoaddanewitemtotheproject,selecttheCodenode,andthenchooseClassfromtheartifactlist.

2. EnterConWHSVehicleServiceStatusAttributeintheNamefieldandpressAdd.3. Altertheclassdeclarationtobeasfollows:

classConWHSVehicleServiceStatusAttribute

extendsSysAttribute

implementsSysExtensionIAttribute

{

}

4. Wewillneedtostoretheattributewearehandlingasavariableintheclass,sodeclaretheservicestatusasfollows:

ConWHSVehicleServiceStatusstatus;

5. Overridethenewmethodsoitconstructsusingthestatusfield,asshowninthefollowinglinesofcode:

publicvoidnew(ConWHSVehicleServiceStatus_status)

{

status=_status;

}

6. Ifwesavethechangessofar,thecompilerwillcomplainthatcertainmethodsaren'timplemented;thequickwaytosolvethisistoclickinsidetheSysExtensionIAttributetextandpressF12.

7. Thisopensthecodeeditorfortheinterface,copiestheparmCacheKeymethodsanduseSingletonintoyourclass,soitreadsasfollows:

publicstrparmCacheKey()

{

//youcan'tcastanextensibleenumdirectly

//toanint,thefollowingwarningwillbegiven

//bythecompiler:

//Castfromextensibleenum'Extensible

//Enumeration(ConWHSVehicleServiceStatus)'to'int'

//potentiallyharmfulanddeprecated.

returnclassStr(ConWHSVehicleServiceStatusAttribute)

+';'+int2Str(enum2int(status));

}

publicbooleanuseSingleton()

{

returnfalse;

}

Readthemethoddocumentationforthesemethods,withspecificthoughttowhethertheclasswillbeimmutable.Returningfalseissafeasitwon'tconstructfromacachedinstance,butwillhaveanimpactonperformance.Beforemakingthistrue,youmustensurethatitisimmutable.

8. Next,wewillcreatethetablehandlerclasses.CreateanewclassnamedConWHSVehicleServiceTableType.

Thisnamingconventionisimportantsothatweknowthattheclassisahandlerclassforatableandwhichtableithandles.

9. WewillconstructtheinstancefromConWHSVehicleServiceTableandwill,therefore,storethisasaglobalvariableontheclass;addthisasfollows:

classConWHSVehicleServiceTableType

{

ConWHSVehicleServiceTableserviceTable;

}

10. Inordertoconstructtheclass,wewillneedtobeabletosetthevalueofserviceTableusinganaccessormethod(awaytogetandsetinternalvariables),whichiscalledaParmmethodinOperations:

publicConWHSVehicleServiceTable

ParmConWHSVehicleServiceTable(

ConWHSVehicleServiceTable

_serviceTable=serviceTable)

{

serviceTable=_serviceTable;

returnserviceTable;

}

11. WewillwritetheconstructorsothatitusestheSysExtensionpatterntoreturntheclassbasedonthevalueoftheServiceStatusfield.Thiscouldsimplybeaswitchstatementintheconstructmethod,butthefollowingmethodisnaturallyextensible:

publicstaticConWHSVehicleServiceTableTypeConstruct(

ConWHSVehicleServiceTable_serviceTable)

{

ConWHSVehicleServiceTableTypetableHandler;

tableHandler=

SysExtensionAppClassFactory::getClassFromSysAttribute(

classStr(ConWHSVehicleServiceTableType),

newConWHSVehicleServiceStatusAttribute(

_serviceTable.ServiceStatus));

tableHandler.ParmConWHSVehicleServiceTable(

_serviceTable);

returntableHandler;

}

YoucanalsousethegetClassInstanceListFromSysAttributemethodtocheckiftheattributeisbeinghandled.

12. Wewillnowneedtocreateamethodonthetablethatreturnstheprecedingclass;themethodisalwayscalledtype.OpenthecodeeditorfortheConWHSVehicleServiceTabletableandcreatethefollowingmethod:

publicConWHSVehicleServiceTableTypeType()

{

returnConWHSVehicleServiceTableType::Construct(this);

}

13. Thestandardpatternfortablehandlerclassesisthatalltableeventsaremovedtotheclass.Ensure

thatallofthefollowingtableeventsareoverriddenontheConWHSVehicleServiceTabletable:insert

update

delete

validateField

validateDelete

validateWrite

14. Tohandletheinsertandupdatemethods,wecanwritetwomethodsthatarecalledeithersideofthesuper()call.CreateplaceholdermethodsforInsert,Delete,andUpdateusingthefollowingpattern:

publicvoidInsertPre()

{

}

publicvoidInsertPost()

{

}

PleaseseetheHowitworks...sectionfordetailsonwhythisdiffersfromothertablehandlers.

15. Fortherest,thequickestwayistocopyalltableeventmethodstotheclassandrefactorthem,asshowninthefollowingpieceofcode:

publicbooleanValidateWrite()

{

booleanret=true;

ret=ret&&this.CheckCanEdit();

//morecheckscanbeaddedusingthesamepattern

returnret;

}

publicvoidModifiedField(FieldId_fieldId)

{

Switch(_fieldId)

{

casefieldNum(

ConWHSVehicleServiceTable,VehicleId):

serviceTable.InitFromConWHSVehicleTable(

ConWHSVehicleTable::Find(

serviceTable.VehicleId));

break;

}

}

publicbooleanValidateDelete()

{

returntrue;

}

publicbooleanCheckCanEdit()

{

If(!this.CanEdit())

{

//Serviceordercannotbechanged.

returncheckFailed("@ConWHS:ConWHS33");

}

returntrue;

}

publicbooleanCanEdit()

{

switch(serviceTable.ServiceStatus)

{

caseConWHSVehicleServiceStatus::None:

caseConWHSVehicleServiceStatus::Confirmed:

returntrue;

}

returnfalse;

}

DonotbetemptedtocallserviceTable.validateWrite()intheInsertorUpdatemethods,orvalidateDelete()intheDeletemethod.Theform'sFormDataSourceobjectmustdothis.

WewillnowcompletethechangesrequiredtoConWHSVehicleServiceTable.

1. RemovetheCheckCanEditandCanEditmethodsfromthetable;CanorMaymethodsshouldalwaysbeonthehandlerclassifoneexists.

2. Alterthetable'seventmethodssothattheyusethehandlerclassinstead;toinsert,update,anddelete,usethefollowingpattern:

publicvoidinsert()

{

this.type().InsertPre();

super()

this.type().InsertPost();

}

Whenoverridinginsert,delete,orupdateconsiderperformance,thiswillforcetheoperationtobedoneoneatatime,andnotasaset.Sodelete_from,update_recordsetandinsert_recordsetcommandswillnolongerbeasetbasedoperation,significantlyaffectingperformance.Also,ifyouremovesuper()fromthecall,thetableeventdelegateswillnotfire.

3. Forthevalidateandmodifiedmethods,usethefollowingpieceofcode:

publicbooleanvalidateField(FieldId_fieldIdToCheck)

{

booleanret=super(_fieldIdToCheck);

if(ret)

{

ret=ret&&

this.type().ValidateField(_fieldIdToCheck);

}

returnret;

}

publicbooleanvalidateWrite()

{

booleanret=super();

if(ret)

{

ret=this.type().ValidateWrite();

}

returnret;

}

publicbooleanvalidateDelete()

{

booleanret=super();

if(ret)

{

ret=this.type().ValidateDelete();

}

returnret;

}

publicvoidmodifiedField(FieldId_fieldId)

{

super(_fieldId);

this.type().modifiedField(_fieldId);

}

4. WenowneedtocontrolwhetherornottheVehicleId,ServiceDateRequested,andServiceDateConfirmedfieldscanbeedited.CreatethreemethodsontheConWHSVehicleServiceTableTypeclass,asshownhere:

publicbooleanCanEditVehicleId()

{

returntrue;

}

publicbooleanCanEditServiceDateRequested()

{

returntrue;

}

publicbooleanCanEditServiceDateConfirmed()

{

returntrue;

}

5. Eachofthechildclasseswilloverridetheprecedingmethods,returnthecorrectstatusforthestatus,andcreatefourclasses,whicharenamedasfollows:

ConWHSVehicleServiceTableTypeNone

ConWHSVehicleServiceTableTypeConfirmed

ConWHSVehicleServiceTableTypeCompleted

ConWHSVehicleServiceTableTypeCanceled

6. Foreachclass,addextendsConWHSVehicleServiceTableTypetotheclassdeclaration:

ClassConWHSVehicleServiceTableTypeNone

extendsConWHSVehicleServiceTableType

7. OverridetheCanEditmethodsineachclasssothatthefollowingresultwillbeachieved:

Status/action None Confirmed Completed Canceled

CanEdit True True True False

CanEditVehicleId True True False False

CanEditServiceDateRequested True False False False

CanEditServiceDateConfirmed True False False False

8. Repeatthispatternforeachoftheotherthreeclasses.

YoumayalsonoticethatthecodeinConWHSVehicleServiceTableType.CanEdit()isnow

redundant,andweshouldjustreturntrue(orfalse,asdesired).Itmayseemthatthecurrentcodeisn'tdoinganyeffectiveharm,butitlookslikeitisdoingsomething,anditshouldbechanged.

9. ThefinalstepistodecoratethefourclasseswiththeConWHSVehicleServiceStatusAttributeattributeclass,butwewillneedtobuildtheprojectfirst.Ctrl+Shift+Bmaysufficeinthiscase,ifwehavebuiltrecently.Also,inordertoallowintellitypetoworkforthenewattribute,restartVisualStudio.

10. Addthefollowingline,changingtheenumvalueasappropriate,totheverytopofeachofourfourclasses:

[ConWHSVehicleServiceStatusAttribute(

ConWHSVehicleServiceStatus::Cancelled)]

Thiscompletesthetablehandlerfornow,sowecanmoveontotheformhandlerclass:

1. CreateanewclassnamedConWHSVehicleServiceTableForm.

Again,thenamingconventionisimportantsothatweknowthattheclassisahandlerclassforaformandwhichformisbeinghandled.

2. Thisisthebaseclassandwillcontaincodecommontoallclassesthatextendit,whichwewillconstructfromaFormDataSourceobjectthattheformwillhaveconstructedautomatically(inourcase,ConWHSVehicleServiceTable_DS).Wewill,therefore,needtostorethisasavariableglobaltothecall,asshownhere:

classConWHSVehicleServiceTableForm

{

protectedFormDataSourceserviceTableDS;

}

Wearedeclaringthisasprotectedtoshowthatwearedeliberatelymakingthevariableavailabletothisclassandallclassesthatextendit.

3. Wewillalsoneedawaytosetthismethod,whichcouldbedonedirectlyifitwasmadepublic;so,wewillneedtoprovideanaccessormethod.InOperations,thesearecalledtheparmmethodsandarecreatedasfollows:

publicFormDataSourceParmServiceTableDS(

FormDataSource

_serviceTableDS=serviceTableDS)

{

serviceTableDS=_serviceTableDS;

returnserviceTableDS;

}

Inthiscase,thereislittledifferencebetweenmakingthevariablepublicandusingitasapropertydirectly.However,oncewemakesomethingpublic,wemaynotbeabletoundoit,especiallyifitisusedinotherpackagesorbyotherparties.

4. Althoughwewon'tusethismethoduntillater,weshouldwriteitnow.Oneofthemainreasonstowriteaformhandlerclassistohandletheinteractionbetweenacreatedialogandthemaindetails

form.Thisisdonebyaddinganaccessormethodtothehandlerclasssothatthenewrecordcanbepassedbetweenthedialogandthedetailsform.DefinethevariableandParmmethods,asshownhere:

publicConWHSVehicleServiceTableParmServiceTableCreated(

ConWHSVehicleServiceTable

_serviceTable=serviceTableCreated)

{

serviceTableCreated=_serviceTable;

returnserviceTableCreated;

}

YoumaynoticethatthismethodiswritteninotherpartsofOperationswithouttheCreatedsuffix,butthiscanleadtoconfusionandthevariablecanbeusedforthewrongpurpose.

5. Next,wecanwriteourconstructor,whichwillbeasfollows:

publicstaticConWHSVehicleServiceTableForm

NewFromDataSource(FormDataSource_serviceTableDataSource)

{

//Checkfirstthatthetablethatthe

//datasourceisboundtoissupported

if(_serviceTableDataSource.table()

!=tableNum(ConWHSVehicleServiceTable))

{

//Table%1isnotsupported

throwerror(strFmt("@SYS31187",

tableId2Name(_serviceTableDataSource.table())));

}

//nopointconstructingtheclass,beforethispoint.

ConWHSVehicleServiceTableFormform

=newConWHSVehicleServiceTableForm();

form.ParmServiceTableDS(_serviceTableDataSource);

returnform;

}

Thisisaprettystraightforwardconstructor,butyoumaywonderwhyweconstructfromthedatasourceandnotthetable.Thereasonisthatthedatasourceispassedbyreference,sowecanalwaysgetthecurrentrecordusingthecursor()method.

6. Tosimplifytheclass'usage,writethefollowingmethod:

publicConWHSVehicleServiceTablecurrentRecord()

{

returnserviceTableDS.cursor()

asConWHSVehicleServiceTable;

}

Thecursor()methodreturnstherecordastheglobalbasetypeforalltables,whichisCommon.Althoughwedon'tneedtouseASinthiscase,usingtheASperformsthecastatthatpoint,andisagoodhabitwhenusinggenerictypessuchasCommonandObject.

7. Ourrequirementisthatwecontrolwhethercertainfieldsareeditablebasedonthecurrentstatus.ThestandardmethodtodothisistocreateamethodcalledEnableFields.Tocontrolourthreefields,writethefollowingcode:

publicvoidEnableFields()

{

ConWHSVehicleServiceTableserviceTable;

booleancanEdit;

serviceTable=this.currentRecord()

canEdit=serviceTable.type().CanEdit()

serviceTableDS.allowEdit(canEdit);

serviceTableDS.allowDelete(canEdit);

//iftherecordcan'tbeedited,nopoint

//checkingeachfield.

if(canEdit)

{

RefFieldIdfieldId;

booleancanEditField;

FormDataObjectdsField;

//thisiswrittenthiswaytomakethecodeeasier

//toreadbymakingeachlineappearononeline

ConWHSVehicleServiceTableTypehndlr;

hndlr=serviceTable.type();

fieldId=fieldNum(ConWHSVehicleServiceTable,

VehicleId);

canEditField=hndlr.CanEditVehicleId();

dsField=serviceTableDS.object(fieldId);

dsField.allowEdit(canEditField);

fieldId=fieldNum(ConWHSVehicleServiceTable,

ServiceDateRequested);

canEditField=hndlr.CanEditServiceDateRequested();

dsField=serviceTableDS.object(fieldId);

dsField.allowEdit(canEditField);

fieldId=fieldNum(ConWHSVehicleServiceTable,

ServiceDateConfirmed);

canEditField=hndlr.CanEditServiceDateConfirmed();

dsField=serviceTableDS.object(fieldId);

dsField.allowEdit(canEditField);

}

}

Eachprecedingblockwouldnormallybewrittenasoneline,butthisbecameunreadableasitwrapped.Onelineperblockispreferableasitmakesiteasiertoreadandlesspronetocopyandpasteerrors.Forexample,serviceTableDS.object(fieldNum(ConWHSVehicleServiceTable,

VehicleId)).allowEdit(hndlr.CanEditVehicleId());

8. Theactivemethodonthedatasourceistriggeredwheneveranewrecordbecomesactive,soweshouldhookintothismethod.Wewillfirstcreateamethodtohandlethisinourformhandlerclassandwritethemethodasfollows:

publicvoidHandleActivePost()

{

this.EnableFields();

}

9. Next,opentheConWHSVehicleServiceTableforminthedesigner.10. Right-clickontheMethodsnodeandchooseOverride|Init.11. Wewillneedtodotwothings:declareanobjectoftypeConWHSVehicleServiceTableFormandthenconstruct

it.Wewillconstructtheformhandlerafterthesuper()call,becausetheConWHSVehicleServiceTable_DSobjectisconstructedbythecalltosuper().Writethecodeasfollows:

ConWHSVehicleServiceTableFormformHandler;

publicvoidinit()

{

super();

formHandler=

ConWHSVehicleServiceTableForm::NewFromDataSource(

ConWHSVehicleServiceTable_DS);

}

12. Finally,tohookuptheHandleActivePostmethod,right-clickontheMethodsnodeoftheConWHSVehicleServiceTabledatasourceandchooseOverride|active.

13. Alterthemethodsothatitreadsasfollows:

publicintactive()

{

intret=super();

formHandler.HandleActivePost();

returnret;

}

14. Saveandcloseallwindows,andbuildthepackage.15. OpentheD365Oinyourbrowser,whichishttps://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=usmfon

thestandarddevelopmentVM,atthetimeofpublication.16. Navigatetotheserviceorderform,creatingaserviceorderifnecessary.17. ClosetheformanduseSQLServerManagementStudiotoeditServiceStatustodifferentvaluesinthe

ConWHSVehicleServiceTabletable.Eachtimeyouopentheform,checkthatthefieldsbehavecorrectlyforeachchangeinstatus.

Howitworks...Therewerealotofnewconceptsinthisrecipe.Thefirstwastheuseofthetablehandlerclass,forgettingtheattribute-basedconstructorfornow.

Thetablehandlerisusefulwhenthelogicgetsverycomplicatedandwouldbenefitfrombeingabstractedintoaclassstructure.

IfyoucomparetheInsert,Update,andDeletemethodstothewaythiswasdoneinSalesTableandSalesTableType,youwillseeakeydifference.InSalesTable,thecalltosuper()wasremovedandthehandlerclasscallstheequivalentdomethoddirectly.SincethestandardInsert,Update,andDeletemethodssimplycalltheequivalentdomethodanyway,thismayseemfine.

Thereisadrawback,whichdetractsfromtheaimofmakingourcodeextensible.Thisisthatthetableevents,thosethatliveundertheEventsnodeinthetabledesigner,donotfireunlesssuper()iscalled,effectivelydisablingtheeventsforinsert,delete,andupdate.ThisiswhythetableeventOnInsertedonSalesTablewillnotfire.

TheSysExtensionframeworkisgenius,butshouldbeusedwithcare.Byusingthis,wehaveincreasedtheoverheadofwritingrecordstothetable.Thepracticaldifferencemaybesmallinthiscase,butshouldwehaveusedthisontransactionaltableslikeInventTrans,theresultcouldbesignificant.

Theclassisinstantiatedbylookingattheclassesthatextendthebaseclass(asspecifiedinthecalltoSysExtension::getClassFromSysAttribute)forclassesthathavetheappropriateattributedecoration.

Thismeansthatanotherpartycouldextendtheenumandwritetheirownclasstohandleit;noover-layeringisrequired,andwecanshipupdatesregularlywithoutcausingourcustomerunduepainastheyapplythem.

TheformwashookedupusingapatternthatcanbeseenthroughoutOperations,although,thehandlerissometimesconstructedfromthetableandnotthedatasource.Thismeansthatcallstothehandlerclassmustincludethecurrentrecord,whichisnotrequiredinthepatternusedinthisrecipe.

There'smore...Let'ssaywedonotwantadefaultvalueinourbaseclass,andwanttoforcethatanyclassthatextendsthebaseclasstoimplementamethodwewoulduseaninterface.Inourexample,wecouldthrowanerrorshouldabaseclassnotbecaught.Thisisfine,butitwouldbeniceifitwascaughtbythecompiler.

Interfaceshavemanyuses,andsomewillbeexploredfurtherinlaterchapters;however,inthiscase,wewillneedtoensurethatclassesthatinheritfromourbaseclassimplementtherequiredmethods.

Thisisoneofthemanyfeaturesthatinterfacesprovide.Ifwedeclarethataclassimplementsaninterface,itwillnotcompileuntilallmethodsintheinterfaceareimplemented.However,thereisn'tawaytoforceasubclasstoimplementtheinterface.Ifthebaseclassimplementstheinterface,thebaseclassmusthavethemethodsand,byinheritance,thechildclasseswillbeseentoimplementthemethods.

Wecouldmakethebaseclassabstract,butthislimitswhatwecandoaswehavetiedthemtogetherthroughinheritance.Implementinganinterfacemerelyenforcesthattheclassimplementsitsmethods,thereisnoinheritance.

InolderversionsonD365O,thestandardRunBaseabstractclassusedtouseabstractmethods,suchaspackandunpack,toforcethedevelopertoimplementthesemethods,butnowMicrosofthaschangedthissothatitusesaninterfaceinstead.IfyoulookatthecodeinRunBase,itnowimplementsthreeinterfaces,ofwhichoneisSysSaveable,whichextendsSysPackable.ThisforcesanyclassthatimplementsSysSaveabletocreatethepackandunpackmethods.

Thebenefitsofinterfacesgofurther.Althoughwecanneverinstantiateaninterface(itisn'taclass,itisacontract),wecanassignaninstanceofaclassthatimplementsittoit.Thisbringsusbacktoourscenario.Wewanttoenforcethatthesubclassesimplementamethodthatisn'tinthebaseclass;ifitwereinthebaseclass,therewouldbeadefaultvalue.

Interfacesarecreatedjustlikeclasses;justchooseInterfacefromthelist.Inourexample,theinterfacewouldbewrittenasfollows:

interfaceConWHSVehicleServiceTableCheckable

{

publicbooleanCanEdit()

{

}

publicbooleanCanEditVehicleId()

{

}

publicbooleanCanEditServiceDateRequested()

{

}

publicbooleanCanEditServiceDateConfirmed()

{

}

}

Wemustfirstremovethefourmethodsfromthebaseclass,aswedon'twantadefaultinthisscenario.Also,ifthemethodexistsinthebaseclass,thecompilerwillconsiderthattheinterfacemethodsare

implemented.

Then,foreachofourfourConWHSVehicleServiceTableTypechildclasses,addimplementsConWHSVehicleServiceTableCheckable.Forexample:

classConWHSVehicleServiceTableTypeCancelled

extendsConWHSVehicleServiceTableType

implementsConWHSVehicleServiceTableCheckable

Tousetheinterfaceinstead,changetheConWHSVehicleServiceForm.EnableFieldsmethodsothatthehndlrvariableisdeclaredandinstantiatedasfollows:

ConWHSVehicleServiceTableCheckablehndlr;

hndlr=serviceTable.type()asConWHSVehicleServiceTableCheckable;

Thisshouldbedeclaredatthetop,andusedthroughthemethod.

ThefinalchangeistoConWHSVehicleServiceTable.CheckCanEdit(),whichshouldnowusethenewpattern,forexampleasfollows:

publicbooleanCheckCanEdit()

{

if(!(thisisConWHSVehicleServiceTableCheckable))

{

Returntrue;//orthrowerror!

}

ConWHSVehicleServiceTableCheckablecheckable;

checkable=thisasConWHSVehicleServiceTableCheckable;

if(!checkable.CanEdit())

{

//Serviceordercannotbechanged.

returncheckFailed("@ConWHS:ConWHS33");

}

returntrue;

}

Thiswouldresultinanastyclienterrorshouldthereturnedclassnotimplementtheinterface,whichisn'tdesirable!WecouldusetheDictClassclasstocheckifthereturnedobjectdoesimplementitfirstandgiveanerrorthatwillmakesomesense.Abettersolutionistousetestdrivendevelopmentpractices.Withthismethod,wewillcreateatestcasethattestswhetherthemethodsreturntheexpectedresultforeachpossiblestatus,andanyerrorwouldbeblockedbythebuildserver.ThisiscoveredinChapter11,UnitTesting.

Seealso...Formoreinformationoninterfaces,checkouthttps://msdn.microsoft.com/en-us/library/aa892319.aspx.

HookingupanumbersequenceThenumbersequenceframeworkisusedonmostDetailsMaster(Maintables)andDetailsTransaction(Worksheettables)forms,forexample,thesalesordernumberisgeneratedthroughanumbersequence.Theseusedtobehookeduptotheformdirectly,orintheformhandlerclass.Thismadesensepreviouslyasuserinterfaceevents(newrecord,deleterecord,abandonanewrecord,andsoon)wouldneedtobehandled.Theproblemwiththisisthatifwehavetwoformsthathandlethesametable,wemayneedtowritethecodetwice.

Thenewpatternisthatitishandledonthetableortablehandler,butcalledfromtheformorformhandlerclass.

Wewillfirstneedtocreateaclassthatdefinesournumbersequences,andthenwritethecodetohandlethem.

GettingreadyTodothis,weshouldhaveatableandformcomplete,ideallyusingthehandlerclassesforthetableandform.

Howtodoit...

Tocreatethenumbersequencedefinitionclass,followthesesteps:

1. First,wewillneedtoaddanelementtotheNumberSeqModulebaseenum;so,locatethisenum,right-clickonit,andchooseCreateextension.

2. RenamethenewbaseenumextensiontoNumberSeqModule.ConWHS.3. Openitinthedesignerandaddanewelement.SettheNamepropertytoConWHSVehicleManagementandthe

LabelpropertytoVehiclemanagement.4. Saveandclosethedesigner,andcreateanewclassname:ConWHSNumberSeqModule.5. ChangethedeclarationsothatitextendsNumberSeqApplicationModuleandoverridesthefollowing

methodssothattheyreadasfollows:

publicNumberSeqModulenumberSeqModule()

{

returnNumberSeqModule::ConWHSVehicleManagement;

}

///<summary>

///Appendsthecurrentclasstothemapthatlinks

///modulestonumbersequencedatatypegenerators.

///</summary>

[SubscribesTo(classstr(NumberSeqGlobal),delegatestr(

NumberSeqGlobal,

buildModulesMapDelegate))]

staticvoidbuildModulesMapSubsciber(Map

numberSeqModuleNamesMap)

{

NumberSeqGlobal::addModuleToMap(

classnum(ConWHSNumberSeqModule),

numberSeqModuleNamesMap);

}

protectedvoidloadModule()

{

}

ThosemigratingfromDynamicsAX2012mayrememberthemanualjobthatmustberuntoinitializethenumbersequence.ThisisnowdoneautomaticallybysubscribingtoNumberSeqGlobal.buildModulesMapDelegate.

6. TocompletetheloadModulemethod,wewilldefineeachnumbersequencefromtheEDT,andisdoneinblocksofcode.ThefollowingcodedefinessequencesforConWHSVehicleIdandConWHSVehicleServiceId:

protectedvoidloadModule()

{

NumberSeqDatatypedatatype;

datatype=NumberSeqDatatype::construct();

//Vehiclenumber

datatype.parmDatatypeId(

extendedtypenum(ConWHSVehicleId));

//Uniquekeyfortheidentificationofvehicles.

//Thekeyisusedwhencreatingnewvehicles

datatype.parmReferenceHelp(

literalstr("@ConWHS:ConWHS42"));

datatype.parmWizardIsContinuous(false);

datatype.parmWizardIsManual(NoYes::No);

datatype.parmWizardIsChangeDownAllowed(NoYes::No);

datatype.parmWizardIsChangeUpAllowed(NoYes::No);

datatype.parmWizardHighest(999999);

datatype.parmSortField(1);

datatype.addParameterType(

NumberSeqParameterType::DataArea,

true,false);

this.create(datatype);

//Vehicleserviceorder

datatype.parmDatatypeId(

extendedtypenum(ConWHSVehicleServiceId));

//Uniquekeyfortheidentificationofserviceorders.

//Thekeyisusedwhencreatingnewservicesorders

datatype.parmReferenceHelp(

literalstr("@ConWHS:ConWHS43"));

datatype.parmWizardIsContinuous(false);

datatype.parmWizardIsManual(NoYes::No);

datatype.parmWizardIsChangeDownAllowed(NoYes::No);

datatype.parmWizardIsChangeUpAllowed(NoYes::No);

datatype.parmWizardHighest(999999);

datatype.parmSortField(2);

datatype.addParameterType(

NumberSeqParameterType::DataArea,

true,false);

this.create(datatype);

}

Thenextpartistoupdatetheparametersform,sothatwecanmaintainthenewsequence:

1. Wewillnowneedtoupdatetheparametersformsothatwecanmaintainthem.Wecansavetimeherewithsomecopyingandpasting.OpenthedesignforourConWHSVehicleParametersform,andthenthedesignerfortheInventParametersformfromtheApplicationExplorer.

2. IntheformdesignforInventParameters,expandDataSources.3. Right-clickontheNumberSequenceReferencedatasource,andchooseCopy.4. ChangetabtoourConWHSVehicleParametersformdesigner,right-clickontheDataSourcesnode,and

selectPaste.5. Wewillneedtorefactorthecodethatwasbrought,butwewillneedtosetupthenumbersequence

handlingcode.Double-clickontheclassDeclarationnodeoftheMethodsnodeontheform.6. Justafterthefirstbrace,enterthefollowinglines:

BooleanrunExecuteDirect;

NumberSeqReferencenumberSeqReference;

NumberSeqScopescope;

ConWHSNumberSeqAppModulenumberSeqApplicationModule;

TmpIdReftmpIdRef;

containernumberSequenceModules;

7. Wewillnowneedtocreatecodetoinitializethenumbersequenceclass,whichisdonebythefollowingmethod:

privatevoidnumberSeqPreInit()

{

runExecuteDirect=false;

numberSequenceModules=

[NumberSeqModule::ConWHSVehicleManagement];

numberSeqApplicationModule=new

ConWHSNumberSeqAppModule();

scope=NumberSeqScopeFactory::createDataAreaScope();

NumberSeqApplicationModule::createReferencesMulti(

numberSequenceModules,scope);

tmpIdRef.setTmpData(

NumberSequenceReference::configurationKeyTableMulti(

numberSequenceModules));

}

8. Wewillrequireasecondmethodthatperformssomefurtherinitialization,butrequiresthatthedatasourcebesetup.Thismust,therefore,runafterthesuper()callintheinitmethod.Writethepostinitinitializationcodeasfollows:

privatevoidnumberSeqPostInit()

{

booleansameAsActive=

numberSeqApplicationModule.sameAsActive();

numberSequenceReference_ds.object(

fieldNum(NumberSequenceReference,

AllowSameAs)).visible(sameAsActive);

labelSameAs.visible(sameAsActive);

}

9. Theprecedingtwomethodsshouldbeplacedaboveandbelowthesuper()callintheinitmethod,asdemonstratedinthefollowingpieceofcode:

publicvoidinit()

{

ConWHSVehicleParameters::Find();

NumberSeqApplicationModule::loadAll();

this.numberSeqPreInit();

super();

this.numberSeqPostInit();

}

10. WhenwecopiedthedatasourcefromInventParameters,itbroughtoverthecodeaswell.TheActivemethodwasoverriddeninInventParametersforaspecialcase,anditisnotrequired.DeletetheActivemethod.

11. Let'scheatagainandcopythetabpagefromInventParameters.Select(oropen)theformdesignerforInventParameters.

Normally,Iprefercreatingeverythingmanually,butaddingthenumbersequenceelementstoaformsavesalotoftimeandisrelativelyriskfreefromcopyandpasteerrorsoromissions.

12. Right-clickontheTabcontrolandchooseCopy.13. Next,gobacktoourform'sdesigner,right-clickontheParameterTabcontrolandchoosePaste.14. Expandthecontrols,NumberSeqandNumberSeqBody.Then,deletetheActionPanecontrol.15. WithintheNumberSeqtabpage,expandtheHeadergroupcontrol.16. ChangetheTextpropertyoftheStaticText10controltoSetupnumbersequencesforvehiclemanagement

documents.

17. Forconsistency,andtoavoidpotentialnamingconflicts,renamethefollowingcontrols:

Originalcontrolname Correctcontrolname

Header NumberSeqHeader

StaticText10 NumberSeqHeaderText

GridContainer NumberSeqQuickFilter

Grid NumberSeqGrid

18. Finally,wedon'tneedthetabpagetobeautomaticallydeclaredasaglobalvariable:selecttheNumberSeqtabpagecontrolandchangetheAutoDeclarationpropertytoNo.

19. Saveandclosealldesignersandcodeeditors.20. OpenthecodeeditorfortheConWHSVehicleParameterstable.21. Addanewmethodthatwillreturnthenumbersequencereference,asshowninthefollowing

method:

publicstaticNumberSequenceReferenceNumRefServiceId()

{

returnNumberSeqReference::

findReference(

extendedTypeNum(ConWHSVehicleServiceId));

}

Itisconventiontoplaceastaticmethodpersequenceontheparameterstable,andotherdeveloperswillexpecttofindthesehelperfunctionsthere.

22. Buildtheprojectandlookoutforcompilationerrorsandcorrectasrequired.

Thefinalstageistointegratetheformwiththenumbersequenceframework:

1. OpenthecodeeditorfortheConWHSVehicleServiceTableTypehandlerclass.2. Atthetopofourclass,declareavariableglobaloftypeNumberSeqFormHandler,asshownhere:

NumberSeqFormHandlernumberSeqFormHandler;

3. Next,createamethodtoconstructaninstance,ifitisnotalreadyinstantiated,asperthefollowingcode:

protectedNumberSeqFormHandlernumberSeqFormHandler(

FormRun_formRun,FormDataSource_serviceTableDS)

{

if(!numberSeqFormHandler)

{

RefRecIdlocalNumSeqId;

RefFieldIdserviceIdField;

localNumSeqId=

ConWHSVehicleParameters::

NumRefServiceId().NumberSequenceId;

serviceIdField=

fieldNum(ConWHSVehicleServiceTable,

ServiceId);

numberSeqFormHandler=

NumberSeqFormHandler::newForm(localNumSeqId,

_formRun,

_serviceTableDS,

serviceIdField);

}

returnnumberSeqFormHandler;

}

4. Next,wewillneedtowritethemethodsthatcontrolwhathappenswhenthevariousdatasourceeventmethodsarerun.Writethemethodsasshownhere:

publicvoidformMethodClose()

{

if(numberSeqFormHandler)

{

numberSeqFormHandler.formMethodClose();

}

}

publicvoidformMethodDataSourceCreate(

FormRun_element,

FormDataSource_serviceTableDS)

{

this.numberSeqFormHandler(

_element,

_serviceTableDS).formMethodDataSourceCreate();

}

publicvoidformMethodDataSourceDelete(

FormRun_element,

FormDataSource_serviceTableDS,

boolean_forced=false)

{

this.numberSeqFormHandler(

_element,

_serviceTableDS).formMethodDataSourceDelete(

_forced);

}

publicvoidformMethodDataSourceLinkActive(

FormRun_element,

FormDataSource_serviceTableDS)

{

this.numberSeqFormHandler(

_element,

_serviceTableDS).formMethodDataSourceLinkActive();

}

publicbooleanformMethodDataSourceValidateWrite(

FormRun_element,

FormDataSource_serviceTableDS)

{

booleanret=true;

if(!this.numberSeqFormHandler(

_element,_serviceTableDS).

formMethodDataSourceValidateWrite())

{

ret=false;

}

returnret;

}

publicvoidformMethodDataSourceWrite(

FormRun_element,

FormDataSource_serviceTableDS)

{

this.numberSeqFormHandler(

_element,

_serviceTableDS).formMethodDataSourceWrite();

}

Thiscodedoesseemalot,butitislargelythesameinmostimplementationsand,withsomerefactoring,itcansimplybecopied.TheSalesTableTypeclassusesthispattern.

5. ThenexttaskistooverridecertainmethodsontheConWHSVehicleServiceTabletableinordertocallthemethodswehavejustwritten.

Whenreadingthefollowingsteps,itmayseemquickerandeasiertocallConWHSVehicleServiceTable.type().formMethodDataSourceWrite().Thiswouldcarryasignificantperformanceoverhead,asthehandlerwouldbeconstructedwheneveramethodwascalled.

6. CreateaglobalvariabletotheformbyopeningtheclassDeclarationnodeoftheformandtypingthefollowing,justafterthefirstopeningbrace:

ConWHSVehicleServiceTableTypeserviceTableType;

7. Wenowneedtohookupthedatasourceeventmethodstoourhandlerclass.Thenamingschemeforthenumbersequencemethodstellsuswhichtouseoneachdatasourceeventmethod.ThefirstisformMethodClose,sowewillneedtooverridetheClosemethodatformlevel.Doso,andenterthefollowingcode:

publicvoidclose()

{

if(!serviceTableType)

{

serviceTableType=

ConWHSVehicleServiceTable.type();

}

serviceTableType.formMethodClose();

super();

}

8. TherestoverridemethodsontheConWHSVehicleServiceTabledatasource.Tosavetime,overridethefollowingmethods:

CreateDeleteLinkActiveValidateWriteWrite

9. Thesemethodsshouldbewrittenasfollows:

publicvoidcreate(boolean_append=false)

{

super(_append);

if(!serviceTableType)

{

serviceTableType=

ConWHSVehicleServiceTable.type();

}

serviceTableType.formMethodDataSourceCreate(

element,this);

}

publicvoiddelete()

{

if(!serviceTableType)

{

serviceTableType=

ConWHSVehicleServiceTable.type();

}

serviceTableType.formMethodDataSourceDelete(

element,this);

super();

}

publicvoidlinkActive()

{

if(!serviceTableType)

{

serviceTableType=

ConWHSVehicleServiceTable.type();

}

serviceTableType.formMethodDataSourceLinkActive(

element,this);

super();

}

publicbooleanvalidateWrite()

{

booleanret;

ret=super();

if(!serviceTableType)

{

serviceTableType=

ConWHSVehicleServiceTable.type();

}

ret=ret&&

serviceTableType.formMethodDataSourceValidateWrite(

element,this);

returnret;

}

publicvoidwrite()

{

if(!serviceTableType)

{

serviceTableType=

ConWHSVehicleServiceTable.type();

}

serviceTableType.formMethodDataSourceWrite(

element,this);

super();

}

10. Saveandclosealldesigners,andbuildthepackage.11. Althoughweshouldtestateachstage,thisrequiressettingupanumbersequence.Thefollowing

stepsarearoughguide,justsowecantesttheform'sbehavior.12. Inthewebclient,opentheVehiclemanagementworkspace,andclickonparameters.

ThisistotriggertheNumberSeqApplicationModule::loadAll()method.

13. Onceopen,checkthatthetworecordsappearinthegridintheNumbersequencestab.

Iftheydonot,weeithermissedaddingthecalltoNumberSeqApplicationModule::loadAll()inourinitmethodorthedelegatesubscriptioninourConWHSNumberSeqModuleclassisincorrect.

14. NavigatetoOrganizationaladministration|Numbersequences|Numbersequences.15. ClickonNew.16. EnterConWHSServintheNumbersequentcodefield(theprefixhelpsgroupsequencetogether)and

VehicleserviceidasName.17. SetScopetoCompany,andenterthecurrentcompanyIDasCompany.18. ConfiguretheSegmentsgridasshowninthefollowingscreenshot:

19. ChangethevalueforLargestto999999intheGeneraltab.20. Thenextpartisthefirsttest;expandReferencesandclickonAdd.21. SelectthemodulefromtheAreatab;inourcase,Vehiclemanagement,andthenServiceId.ClickonOK.22. Next,openVehiclemanagementworkspaceandcreateanewserviceorderrecordtotestthatthevarious

eventsworkcorrectly.

Howitworks...

Theprocessisdoneinthreeparts:

WritingtheclassthatregistersourEDTinthenumbersequenceframeworkUpdatingtheparameterformtoallowustomaintainthenumbersequencesHookinguptheformsothatitusesthenumbersequence.

Thenumbersequenceframeworkisnotjustawaytogetanewnumberinaprescribedformat.Italsoautomaticallyhandleswhathappensshouldtherecordnotbesaved,andwhatshouldhappenifwedeletearecord.

NumbersequencesetupWheneverweneedanumbersequence,wewillalwaysusethenumbersequenceframeworktodeclareandmaintainthenumbersequenceweneed.ThefirstpartwastocreateaclassthatextendstheNumberSeqApplicationModuleclass.Thisallowsustodefinethenumbersequences,andalsosetuptheeventthatwillgeneratethenumbersequencesetupdata.

Thismakesthenumbersequenceavailable,andwecancreatethesequenceusingtheNumberSequenceformfoundinOrganizationaladministration|NumberSequences|NumberSequences.Wecancreateonemanuallyandrefertothesequencedefinedinourclass,orusetheGeneratebutton.

IfwetakealookattheNumberSeqModule::ConWHSVehicleManagementcode,wecanseethattheextensionisseamlesslyappliedtothebasetype,eventhoughtheextensionisnamedNumberSeqModule.ConWHS.Thiswouldonlyworkinthispackage,orpackages,thatreferenceit.WehavenotmodifiedNumberSeqModule.ThesecondpointtonoteisthattheConWHSprefixweaddedtotheelementisrequired,asallelementsinsideanenummustbeuniqueacrossallextensionstoit.

Itistheconventiontobeabletoseeandmaintainthenumbersequencesfromthemoduletowhichtheybelong.Thisiswhywemodifiedourparametersform.Thisnicelyshowsthelinktothenumbersequencereferenceandthenumbersequencecodethatdefineshowthenumberswillbegeneratedandmaintainedbytheframework.

Thescopeinourcasewasonemodule,asdefinedbytheenum;however,wecanincludemore,shouldwehavemorecomplicatedrequirements.LookingattheimplementationintheInventParametersformdemonstrateshowthiswouldbedone.

TheTmpIdReftemporarytableisusedtogeneratethequeryusedbytheNumberSequenceReferencedatasource.ThiswasdoneintheexecuteQuerymethod.

ThereisanewconceptdemonstratedinthecodefortheNumberSequenceReferencedatasource.TheNumberSequenceCodeIdfieldisactuallyaRecIdrelation,butisdisplayedashumanreadable.ThisisdonebyusingacontroloftypeFormReferenceFieldGroupControl.Thedatasourcefieldmethods,lookupReferenceandresolveReferencewillfacilitatethisprocess.

Hookingupthenumbersequence

Thecodetotiethevariousformdatasourceeventstothenumbersequenceframeworkwasdoneinitsmostabstractedmethod.Thiswouldallowtheserviceordertobeextended.Forexample,ifweaddedaserviceordertypefield,wecouldhaveadifferentsequencepertypeofserviceorderwithoutmuchreworktothecode.

Inthesemethods,weusedthekeywordselementandthis.Onaform,thethiskeywordisthemostconfusingoneasitchangesdependingonwhereitisused.Thisisbecausewehavenestedclassdefinitionsinsidethemainformclass.So,inthecontextwhereweusedit,thismeantConWHSVehicleServiceTable_DS.Withintherootformmethods,thismeanstheFormRunobjectthatiscreatedwhentheformrun,butwetendtouseelementinstead,asthisisalwaystheFormRuninstance.

There'smore...WecoulddothesamethingtoConWHSVehicleTable.

Wedon'tneedaformhandlerclassinthiscase;wecanaddthemethodsdirectlytotheConWHSVehicleTableform.

DeclaretheNumberSeqFormHandlerclassglobaltotheform,andwritetheinitializemethodalsoatformlevel.Callthemethodafterthesuper()callininit.

Ineachdatasourcemethodwechangedintherecipe,justcallnumberSeqFormHandler(element,ConWHSVehicleTable_DS).<method>directly.TheCustTableformisagoodexampleofthis.

Creatingacreatedialogfordetailstransactionforms

Mostorderformsuseadialogwhencreatinganewrecord.Thisisbecausethefieldsthatareneededforordercreationarenotthesameasthosedisplayedontheheadersectionoftheform.Thedialogcanbedesignedspecificallytobringallthefieldstheuserwillwant,allinviewwithouthavingtohuntthevariousfasttabsforthefield.

Thecomplexityintheprocessistheinterplaybetweentheorderform'sdatasourceandthedialogusedtocreatetherecord.Thepatternisthesamefor'createforms'.

GettingreadyWeshouldhaveaDetailsTransactionformcompleted,withaformandtablehandler.

Howtodoit...Tocreatethecreatedialog,followthesesteps:

1. Createanewform,suffixingthedetailsformnamewithCreate;forexample,ConWHSVehicleServiceTableCreate.

2. Dragthetablesassociatedwiththeheaderrecordtotheform'sDataSourcenode;inourexample,theConWHSVehicleServiceTabletable.

3. SetthepropertiesfortheConWHSVehicleServiceTabledatasourceasfollows:

Property Value

AllowDelete No

AllowNotify

No:wewantthistoactasasinglerecorddialoganddisablemostoftheeventsthatthedatasourceperformsforus

AutoSearch No

InsertAtEnd No

InsertIfEmpty No

DelayActive No

Wewillneedtocontrolthebehaviorofthedatasourceinthiscase,asitwillbecalledfromanother,sowehavetodisablecertainuseroptionsandevents.

4. Overridetheform'sinitmethodandwritethefollowingpieceofcode:

//globaltotheform/element

ConWHSVehicleServiceTableFormserviceTableForm;

ConWHSVehicleServiceTableTypeserviceTableType;

///<summary>

///theformmustbecalledwithaformhandlerclass

///</summary>

publicvoidinit()

{

if(element.args())

{

if(element.args().caller()is

ConWHSVehicleServiceTableForm)

{

serviceTableForm=element.args().caller();

}

}

if(!serviceTableForm)

{

//Formwasincorrectlycalled

throwerror("@SYS22996");

}

super();

serviceTableType=ConWHSVehicleServiceTable.type();

}

5. Continuetooverridetherunandcloseformmethodswiththefollowinglinesofcode:

publicvoidrun()

{

ConWHSVehicleServiceTable.clear();

ConWHSVehicleServiceTable_DS.create();

super();

}

publicvoidclose()

{

if(serviceTableType)

{

serviceTableType.formMethodClose();

}

super();

}

6. WewillnowneedtoadjustsomeoftheConWHSVehicleserviceTabledatasource'smethods;startthisbyoverridingtheresearchmethodandwritingthefollowingpieceofcode:

publicvoidresearch(boolean_retainPosition=false)

{

//super(_retainPosition)Disabletherefreshfeature.

}

publicvoidreread()

{

//AllowtheDStorereadonlyifsaved

if(ConWHSVehicleServiceTable.RecId)

{

super();

}

}

voidwrite()

{

//thisensuresthattheformcloseif

//thenumbersequencecan'tbeused

//andpassesanullrecordback

try

{

serviceTableType.formMethodDataSourceWrite(

element,this);

super();

}

catch(Exception::Error)

{

ConWHSVehicleServiceTable.RecId=0;

serviceTableForm.ParmServiceTableCreated(

ConWHSVehicleServiceTable);

element.close();

throwException::Error;

}

this.reread();

this.refresh();

//updatethehandlerformwiththenewrecord.

serviceTableForm.ParmServiceTableCreated(

ConWHSVehicleServiceTable);

}

voidcreate(booleanappend=false)

{

//onlyallowcreateifthecurrentrecord

//hasn'tbeensaved

if(!ConWHSVehicleServiceTable.ServiceId)

{

super(append);

serviceTableType.formMethodDataSourceCreate(

element,this);

}

}

7. ApplytheDialog-basicpatterntotheDesignnode.8. CompletetheDesignproperties,butsettheCaptionpropertytoNewvehicleserviceorder.9. Completetheformlayoutaccordingtothepattern.Usethefollowingasaguide:

WithintheDialogCommitContentpatternelement,usetheFieldsandFieldGroupspattern,andaddfieldsandfieldgroupsasdesired.TheOKandCancelbuttonsinthepatternareCommandButtons,andthepatternwillhidetheCommandproperty.MaketheOKbuttonthedefaultbutton.

10. Next,opentheConWHSVehicleServiceTableFormclassandaddthefollowingmethods:

publicstrCreateFormName()

{

returnformStr(ConWHSVehicleServiceTableCreate);

}

publicbooleancreate()

{

Argsargs=newArgs();

FormRuncreateFormRun;

ConWHSVehicleServiceTablecurrentRecord;

currentRecord=this.currentRecord();

args.name(this.CreateFormName());

args.caller(this);

createFormRun=classfactory.formRunClass(args);

createFormRun.init();

createFormRun.run();

if(!createFormRun.closed())

{

createFormRun.wait();

}

if(createFormRun.closedOk())

{

returntrue;

}

else

{

serviceTableCreated=currentRecord;

returnfalse;

}

}

11. OpentheConWHSVehicleServiceTableform,openthecodefortheConWHSVehicleServiceTable.activemethod,andchangeitsoitreadsasfollows:

publicvoidcreate(boolean_append=false)

{

ConWHSVehicleServiceTablenewServiceTable;

if(formHandler.create())

{

newServiceTable=

formHandler.ParmServiceTableCreated();

if(newServiceTable)

{

super(_append);

ConWHSVehicleServiceTable.data(

newServiceTable);

this.setCurrent();

}

}

}

12. Nowweshouldsaveall,build,andtest.YoushouldtesttheNewbuttonontheVehicleserviceform,andalsotheNewbuttonontheworkspace.

Howitworks...WhentheuserclicksNew(ortheformisopenedwithamenuitemthathasOpenModesettoYes),theformtriggersataskthatcallsthecurrentdatasource'sCreatemethod.Inourcase,wemustletthedatasourcedothis;however,atthispoint,wewillopenthecreatedialogusingthecreatemethodwewroteontheformhandler.

Thecreatemethodisastandardwaytocallanyformfromcode.Neverusetheformnameasastringliteralwithoutusinganintrinsicfunction;inthiscase,formStr.

Thiscallsthecreatedialogthatissetupsothenormalformeventsnolongerfire.Wewillinitializetheformfromtheformhandlerclassinordertopassbackthenewrecord.

Oncecontrolreturnsbacktoourdetailsform,wewillreplacethedataoftherecordwiththerecordcreatedbythecreatedialog;thiswasdoneinthedata([Common])method.

Thekeyactivitiescanbeseeninthisdiagram:

CreatingaSysOperationprocessThisframeworkprovidesasimplemethodtoallowustowriteroutinesthatcanbesynchronousorasynchronouswithnofurtherefforttoallowthistohappen.

Thecomplexityincreatingaroutinethatisruninabatchprocessishowtostorethevariousparametersthattheroutinemayrequire.ThiswasdoneinthepreviousversionusingtheRunBaseBatchframework.Thisolderframeworkstoredthisdatainalooselytypedblob,andrequiredspecialhandlingshouldthedeveloperaddorchangetheparameters.Ithadotherproblems,includingthefactthatthedataandprocessweretightlycoupled.

TheSysOperationframeworkprovidesanew,andsimplerwaytocreateprocesses.Itdecouplesthedata(orparameters)fromtheprocessbyusingadatacontract.Thedatacontractisaclassthatistheparameterfortheentrypointtotheclassthatperformstheprocess.Theframeworkaddsfurtherhelpinautomaticallycreatingadialogfromthedatacontract,whichwillusetheEDTsinthecontracttoprovidethelabel,andevendrop-downlistsbasedonthetablereferenceontheEDT.

Tousetheframework,wewillneedthefollowingthreeclasses:

Controller:ThisisaclassthatextendsSysOperationServiceController,whichcontrolsboththeUIandinstantiationoftheprocessingclassDatacontract:ThisisaclassthatcontainsthepropertiesrequiredbytheprocessclassProcessingclass:Thisistheclassthatactuallydoesthework

Wecanusethistocreateanyprocess,andthepatternbelowcanbereusedforyourownneeds.Inthisrecipewewillcreateaprocessthattheusercanusetochangethevehicle'sgroup.

Howtodoit...

Thefirstpartofthisistocreatethedatacontract,whichisdonebyfollowingthesesteps:

1. CreateanewclassnamedConWHSVehicleGroupChangeContract.2. Completethedeclarationasfollows:

[DataContract]

classConWHSVehicleGroupChangeContract

extendsSysOperationDataContractBase

{

ConWHSVehicleIdvehicleId;

ConWHSVehicleGroupIdvehicleGroupId;

}

3. Thenaddthedatamemberpropertymethods,asshownhere:

[DataMember]

publicConWHSVehicleIdvehicleID(ConWHSVehicleId

_vehicleId=vehicleID)

{

vehicleID=_vehicleID;

returnvehicleID;

}

[DataMember]

publicConWHSVehicleGroupIdvehicleGroupId(

ConWHSVehicleGroupId

_vehicleGroupId=vehicleGroupId)

{

vehicleGroupId=_vehicleGroupId;

returnvehicleGroupId;

}

Now,tocreatetheclassthatperformstheupdate,let'sfollowthesesteps:

1. CreateanewclassnamedConWHSVehicleGroupChange.2. Withintheclass,declarethedatacontractasaglobalvariabletotheclass,asfollows:

publicConWHSVehicleGroupChangeContractcontract;

Thisallowsotherobjectstosetthispropertydirectly,whichneedstobethoughtthroughcarefully.ItissafeinthiscaseaswecalltheRunmethodwiththecontract,andcallValidatewithintheRunmethod.ThisallowsacallertosetthecontractpropertyandcallValidatebeforeRuniscalled.

3. Thisclasswillhavethefollowingmethodsasaminimum:

publicRun(<datacontract>)

publicbooleanValidate()

public<class>Construct()

4. Createthesemethodsasfollows:

publicbooleanValidate(){

If(contract.VehicleId()=="")

{

//VehicleIdmustbespecified

returncheckFailed("@ConWHS:ConWHS45");

}

If(!ConWHSVehicleTable::Exist(contract.VehicleId()))

{

//Vehicle%1doesnotexist

returncheckFailed(strFmt("@ConWHS:ConWHS46",

contract.VehicleId()));

}

If(contract.VehicleGroupId()=="")

{

//Vehiclegroupisrequired

returncheckFailed("@ConWHS:ConWHS47");

}

If(!ConWHSVehicleGroup::Exist(

contract.VehicleGroupId()))

{

//Vehiclegroup%1doesnotexist

returncheckFailed(strFmt("@ConWHS:ConWHS48",

contract.VehicleGroupId()));

}

returntrue;

}

publicvoidRun(ConWHSVehicleGroupChangeContract)

{

if(!this.Validate())

{

return;

}

ttsBegin;

this.UpdateVehicleGroup();

ttsCommit;

}

privatevoidUpdateVehicleGroup()

{

ConWHSVehicleTablevehicle=

ConWHSVehicleTable::Find(

contract.VehicleID(),true);

if(vehicle.RecId!=0)

{

vehicle.VehicleGroupId=contract.VehicleGroupId();

vehicle.update();

}

}

Thenextpartoftheprocessistocreatethecontrollerclass,whichisdoneasfollows:

1. CreateanewclasscalledConWHSVehicleGroupChangeController.2. ModifyclassDeclarationsothatitextendsSysOperationServiceController.3. Next,overridetheCaptionmethodandchangethistoreturnVehiclegroupchangeasalabel.Thissetsthe

caption,ifitisaddedtoabatchqueue.4. DothesamefortheparmDialogCaptionmethod;thissetsthetitleofthedialogthattheframeworkcreates

fromthedatacontract.5. Finally,wewillneedanentrypoint,whichisamainmethod.Themethodshouldbecreatedasperthe

followinglinesofcode:

publicstaticvoidmain(Args_args)

{

ConWHSVehicleGroupChangeControllercontroller;

controller=newConWHSVehicleGroupChangeController(

classStr(ConWHSVehicleGroupChange),

methodStr(ConWHSVehicleGroupChange,

Run),

SysOperationExecutionMode::Synchronous);

controller.startOperation();

}

6. SavethechangesandcreateActionMenuIteminourproject.7. NamethemenuitemasConWHSVehicleGroupChangeControllerandsettheObjectTypepropertytoClassand

theObjectpropertytoConWHSVehicleGroupChangeController.8. AddthistothePeriodicTasksubmenuoftheConWHSVehicleManagementmenuandbuildtheproject.

Howitworks...Wewillgetadialogwithafieldcreatedautomaticallyfromthecontract.Wecanevensubmitittothebatchqueue.Prettycool,andverylittleadditionaleffort!

Thiswitchcraftdoesdeserveanexplanation.

AsourclassextendsSysOperationServiceController,wegetalotoffunctionality,thefirstbeingthatitcanconstructadialogfromthedatacontract.However,howdiditknow?

Whenthenewmethodexecuted,theframeworklookedattheRunmethodanddeterminedthecontractfromthemethod'sinputparameter.

ThecalltostartOperationcausedthesystemtobuildtheUIandhandlethecodeexecutionbasedonwhattheuserdoes.ThedialogisconstructedusingtheEDTsspecifiedinthedatamembermethods.SinceweconstructedtheEDTscorrectlywithreferencetothemaintable,itcanalsoprovideasimplelookupforus.

Thisexplainswhythenewmethodrequirestheclassandmethodnames,butthethirdparameterisanexecutionmode.Thiscomesintoitsownifweexecutethecontrollerprogrammatically.Synchronousmeansitwillruninlinewiththecurrentprocess,unlesstheuseriselectedtoRuninthebackground.Ifthiswaschosen,theexecutionmethodwouldhavechangedtoScheduledBatch.

TheclassStrandmethodStrintrinsicfunctions,asexplainedinthepreviouschapters,areusedtocheckwhethertheelementthefunctionsrefertoexistsatcompiletime.

Ifyourunthisprocesstwice,youwillseethepreviousoptionsarerememberedandactasdefaults.Thisisbecausethecontractisserializedanddeserializedtotheuser'susagedata.

There'smore...

ThereisanothermethodtowritetheMainmethodinthecontrollerclass.Thisistospecifytheclassandmethodinthemenuitem'sParameterproperty,forinstance,ConWHSVehicleGroupChange.Run.WecanthenusetheinitializeFromArgsmethodtobuildthecontroller.Thisisincommonuse,butsincethispropertyisfreetext,thecompilerwillnotpickupthatthisvalueisinvalid.

ExecutingcodeusingthebatchframeworkToforcetheprocessthroughthebatchframework,wewillusetheexecutionmodes:ReliableAsynchronousandScheduledBatch.

Bothofthesemethodssubmitjobstothebatchserverforexecution,wheretheReliableAsynchronousmethodauto-deletesthejobsafterexecution.

Thejobsdonotexecuteimmediately,butwithinaminuteofsubmission;thebatchserverpollsforwaitingjobseveryminute.Weshouldusethismethodtoperformasynchronousorscheduledjobsthatrequireheavyprocessing.

Programmaticallyspeaking,wewouldn'twanttousetheScheduledBatchmode.Thiswouldbesetbasedontheuserchoosingtorunasabackgroundtask.

TosubmitthetaskasReliableAsynchronous,simplychangethecodethatconstructsthecontrollertousethisexecutionmode.Onexecution,youmaynoticethattheinformationmessageindicatesthatnothingwasdone,butifyouwaitforaboutaminuteandcheckthevehiclerecord,youwillseethatithassucceeded.Youcanalsoseethebatchhistoryforthis.

CallingaprocessfromaformTheexampleinthisrecipesimplydemonstratestheframework,buttheexampleisnotparticularlyuseful.Todoso,itwouldbebettertocalltheprocessfromthevehicleformanddefaulttheparameterstothecurrentvehicles.

Todothis,wewillneedtofetchtheconstructedcontractandupdateitwiththerecordsetbythecallerfromtheArgsobject.

Thefollowingcodedoesthis:publicstaticvoidmain(Args_args){ConWHSVehicleGroupChangeControllercontroller;ConWHSVehicleTablevehicle;

switch(_args.dataset()){casetableNum(ConWHSVehicleTable):vehicle=_args.record();break;}if(vehicle.RecId==0){//Activebufferrequired.throwerror("@SYS25821");}controller=newConWHSVehicleGroupChangeController(classStr(ConWHSVehicleGroupChange),methodStr(ConWHSVehicleGroupChange,Run),SysOperationExecutionMode::Synchronous);

ConWHSVehicleGroupChangeContractcontract;contract=controller.getDataContractObject('_contract');if(!contract){//Function%1wascalledwithaninvalidvaluethrowerror(strFmt("@SYS23264",classStr(ConWHSVehicleGroupChangeController)));}contract.VehicleGroupId(vehicle.VehicleGroupId);contract.VehicleId(vehicle.VehicleId);controller.startOperation();if(FormDataUtil::isFormDataSource(vehicle))

{//Thiswillcallthetable'sdatasource'sresearch//methodtorefreshthedatafromthetable.//ThetrueparameterwillkeepthecurrentrecordFormDataUtil::getFormDataSource(vehicle).research(true);}}

Wewillthenneedtoaddtheactionmenuitemtoourform.OnthemainFormActionPanecontrol,addanewActionPaneTab,andsettheCaptionpropertytoVehicles.CreateabuttongroupwiththeTextpropertysetto@SYS9342.

Finally,dragtheactionmenuitemtothebuttongroupandsetthefollowingproperties:

Property Value

DataSource ConWHSVehicleTable:Thissetsargs.Record()

NeedsRecord Yes:Thisdisablesthemenuitemifthereisnocurrentrecord

SaveRecord Yes:Thisisdefault,butwecouldendupwithanerrorduetooptimisticconcurrency

MultiSelect No:Ifmultiplerecordsareselected,disablethebutton

Buildandtestthenewfeaturewithatleasttwovehiclestoprovethatthevaluesarenotdefaultingfromyouruser'susagedata.

UsingthedatacontracttomakechangestothedialogLet'stakeastepbacktothepointwherewewerecallingthemainmethodofConWHSVehicleGroupChangeControllerdirectly.

Thesystemverycleverlyconstructstheuserinterface;itdoesthissimplybyaddingthedatacontract'sdatamemberdirectlytothedialog.Therearesomesimplechangeswecanmaketothedatacontracttocontrolhowtheyaredisplayed.

Wecanmakethesechangesbyalteringthedecorationatthetopofeachdatamember.Thefollowingexamplesareusefulforthis:

SysOperationLabel("..."), Thissetsthelabeldisplayedonthedialog

SysOperationHelpText("..."); Thissetsthehelptextfortheresultingcontrol

SysOperationDisplayOrder("...") Thiscausesthecontroltobeplacedfirstintheresultingdialog

Thecompletedcodeforthedatamembermethodsisasfollows:[DataMember,SysOperationControlVisibility(false)]publicConWHSVehicleIdVehicleId(ConWHSVehicleId_vehicleId=vehicleId){vehicleId=_vehicleId;returnvehicleId;}

//Newvehiclegroup//Pleaseselectanewvehiclegroupforthevehicle[DataMember,SysOperationLabel(literalStr("@ConWHS:ConWHS50")),SysOperationHelpText(literalStr("@ConWHS:ConWHS51")),SysOperationDisplayOrder('1')]publicConWHSVehicleGroupIdVehicleGroupId(ConWHSVehicleGroupId_vehicleGroupId=vehicleGroupId){vehicleGroupId=_vehicleGroupId;returnvehicleGroupId;}

BuildandtestthenewformandfeelfreetotesttheotherSysOperationattributes.Wedon'tneedtheAttributesuffixinOperations.

AddinganinterfacetotheSysOperationframeworkWecandoalotbyjustdecoratingthecontractdatamethodsbut,sometimes,weneedmorecontrol.ThisrecipestepsthroughaddingmorecontroltotheuserinterfacecreatedbytheSysOperationframework.

GettingreadyWejustneedanexistingSysOperationprocessclassthatwewishtoaddacustomizedinterfaceto.

Ifyouarefollowingonfromthepreviousrecipe,removetheSysOperationControlVisibilityattributefromtheVehicleIddatamethod.

vehGroupIdField.registerOverrideMethod(<br/>methodStr(FormStringControl,validate),<br/>methodStr(ConWHSVehicleGroupChangeUIBuilder,<br/>validateVehicleGroupId),<br/>this);

Nearlydone!However,wehaven'thandledhidingthevehicleIDfieldifitwascalledfromavehiclerecord.Wecan'tdeterminethisfromwithintheUIbuilderclass;onlythecontroller'smainmethodknowsthis.ThismeanswewillneedamechanismtotelltheUIbuilderwhetherornottohidethefield.SinceweattachtheUIbuildertothedatacontract,wewilladdahiddenfieldtothedatacontract,asfollows:

1. First,changethedecorationinclassDeclarationofConWHSVehicleGroupChangecontractsothatitisassociatedwiththeUIBuilder:

[DataContract,SysOperationContractProcessing(

classStr(ConWHSVehicleGroupChangeUIBuilder))]

2. ThenaddavariabledeclarationtoclassDeclaration,asfollows:

NoYesIdhideVehicleId;

3. Addadatamembermethodforthevariablewiththevisibilityattribute:

[DataMemberAttribute,

SysOperationControlVisibilityAttribute(false)]

publicNoYesIdHideVehicleId(

NoYesId_hideVehicleId=hideVehicleId)

{

hideVehicleId=_hideVehicleId;

returnhideVehicleId;

}

4. Finally,wewillneedtomodifyourcontroller'smainmethodtohandlethecasewhereitwascalledfromavehiclerecord,whichisdonelikethis:

publicstaticvoidmain(Args_args)

{

ConWHSVehicleGroupChangeControllercontroller;

ConWHSVehicleTablevehicle;

switch(_args.dataset())

{

casetableNum(ConWHSVehicleTable):

vehicle=_args.record();

break;

}

controller=newConWHSVehicleGroupChangeController(

classStr(ConWHSVehicleGroupChange),

methodStr(ConWHSVehicleGroupChange,

Run),

SysOperationExecutionMode::Synchronous);

ConWHSVehicleGroupChangeContractcontract=

controller.getDataContractObject(

'_contract');

if(!contract)

{

//Function%1wascalledwithaninvalidvalue

throwerror(

strFmt("@SYS23264",

classStr(ConWHSVehicleGroupChangeController)));

}

controller.initParmDefault();

controller.loadFromSysLastValue();

controller.parmShowDialog(true);

contract.HideVehicleId(false);

if(vehicle.RecId!=0)

{

contract.VehicleGroupId(vehicle.VehicleGroupId);

contract.VehicleId(vehicle.VehicleId);

contract.hideVehicleId(true);

}

controller.saveLast();

controller.startOperation();

//vehicle.IsDataSource()isdeprecated

if(FormDataUtil::isFormDataSource(vehicle))

{

FormDataUtil::getFormDataSource(vehicle).

research(true);

}

}

5. Saveallandbuildtheprojecttotesthowthisworks.

Howitworks...Byusingthismethod,wehaveaddedacouplingtotheprocessclassstructurebymeansofthebindingtotheUIbuilder.Inordertoavoidthis,wewoulduseinheritancetodefineadatacontractbaseclassandextenditforthepurposesofthisbinding.

Theprocessmayseemalittlecomplicatedatfirstbut,whenwebreakthisdown,itwillbecomeclearerwhatisactuallyhappening.

ThefirsttaskwastodeclaretotheDialogFieldvariablesthatwewantaddedtotheresultingdialog,andthentobindthemtothedatacontractdatamethods.However,inorderforthistowork,wehadtobindtheUIbuilderclasstothedatacontract.WedidthisbyaddingtheSysOperationContractProcessingattributetothedatacontract.

Oncethisisdone,theDialogFieldvariablesarenowboundtothedatacontractmethods.Wethenwroteamethodtovalidatethevehiclegroupcontrol,andboundthistothecontrol'svalidateeventusingthecontrol'sregisterOverrideMethodmethod.

Oncethisisalldone,thedialogcreatedbytheframeworkcanbevalidatedinteractively,allowingtheusertocorrectanyerrorswithouthavingtostartagain.

BusinessIntelligenceInthischapter,wewillcoverthefollowingrecipes:

CreatingaggregatedimensionsCreatingaggregatemeasurementsCreatingaggregatedataentitiesCreatingandusingkeyperformanceindicators

IntroductionBusinessintelligence(oftenreferredtoasBIoranalytics)hasevolvedagreatdealinthisrelease;manyelementsofBIthatweredevolvedtoexternalapplicationsarenowfirstclasscitizensofthedevelopmentenvironment.WewillnowdefinedimensionsandmeasurementsinanOperationsprojectthatarebuiltandreleasedasanyotherOperationsproject.Ourroleinthisistoprovidethebasiswhichthebusinessintelligencedesignerwillusetodeveloppowerfulbusinessintelligencesolutions.

WhendevelopingaBIsolution,itisusualtocreateanewprojectthatreferencesthepackagesitanalyzes.Thisallowsthesolutionstobedevelopedatthesametime,andalsoallowstheanalyticsandotherprojectstobedeployedindependently.Ofcourse,ifthedatastructureschangeinawaythatbreakstheanalyticssolution,thebuildserverwillusuallyhighlightthis.

Whenplanningthistypeofwork,theanalyticsshouldbedesignedalongwiththemainsolutiondesignandnotasanafterthought,eventhoughwewillstartanalyticslaterintheprojectlifecycle.Thisresultsinthedatastructuresbeingcorrectlydesignedwithanalyticsandreportinginmind;whenwecometodeveloptheanalyticselementoftheproject,weshouldbeabletodosowithoutchangestotheunderlyingtables.Changestounderlyingtablescouldbeconsideredlikeaddingabasementgaragetoyourhouse.

Therecipesinthischapterarebasedoncreatingananalyticsprojectforcustomerinvoiceanalysis.

Creatingaggregatedimensions

Aggregatedimensionsareusedtodefineattributesthatsplicethedatatheyareassociatedwith.Thedimensionisthereforebasedonatableorviewandwillhaveoneormoreattributes,whicharethewayswewillallowthedatatobespliced.Definingthedatainthiswayallowsexceptionallyfastdataanalysis.ThisisbecausethedataisexpandedintoadatawarehousedatabaseautomaticallybyOperations.

Insomecases,youmaywanttocreateaviewtosimplifytheprocess,inwhichcase,usetheviewastheTableproperty.

Gettingready

Beforewestart,itisbesttocreateanewmodelforanewpackageandaddreferencesasrequired.Theseareusually,ApplicationPlatform,ApplicationFoundation,ApplicationSuite,andanyotherwhoseelementsweintendtouse.Whendeliveringanimplementationproject,itisusuallybesttocreateonepackageforanalytics.

Howtodoit...Dimensionsshouldbebasedonviewsinordertodenormalizethedata.Thisistolettheusersseenameanddescriptionfields,andnottheidentitycolumns,sothefirstpartistocreateorextendaviewtoallowthisdatatobeeasilyaccessed.

Tocreatetheview,followthesesteps:

1. CreateanewviewcalledConWHSVehicleTableExpanded.2. AddtheConWHSVehicleTabletableastherootdatasource,andConWHSVehicleGroupasachilddatasource,as

showninthefollowingscreenshot:

3. EithersettheUseRelationspropertyontheConWHSVehicleGroupdatasource,oraddarelationmanually.4. Addallnon-systemfieldsfromtheConWHSVehicleTabletable,andtheDescriptionfieldfrom

ConWHSVehicleGroup.RenamethisfieldtoVehicleGroupDescriptionandsettheLabelpropertytoVehicleGroup.5. Saveandclosetheview.

Thefollowingstepscreatethedimensionattributebasedontheview:

1. Intheproject,addanewitem,andintheAddNewItemdialog,selectAnalyticsfromtheleft-handlistandAggregateDimensionfromtheright.

2. NamethenewitemasConWHSVehicles.Thishastobeuniqueamongstotheraggregatedimensions.3. SettheTablepropertytoConWHSVehicleTableExpanded,asthisisthetablethathasthedataweneed.

Thesystemhasdeterminedthatthemaindatasourceoftheviewhasaprimarykeyandhasusedthistocorrectlycreatetheattributeusedasauniquekey;donotdeletethiskey.Allattributesthatarecompany-specificshouldincludetheDataAreaIdfield.

4. Right-clickonthe@AttributesnodeandchooseNewDimensionAttribute.5. ChangeNameandtheNameFieldpropertiestoVehicleGroupDescription.6. Right-clickontheattributeandchooseNewDimensionFieldReference.7. SpecifyVehicleGroupDescriptionastheDimensionFieldproperty,andthenaddDataAreaId.

Addingafieldreferencewillautomaticallysettheattribute'sNameproperty,whichiswhyweaddthefieldsinthisorder.

8. RepeatthisfortheVehicleTypeandDescriptionfields.9. Right-clickontheHierarchiesnodeandchooseNewDimensionAttributeHierarchy.10. Right-clickonthenewattributehierarchyandchooseNewDimensionHierarchyLevel.Addalevel

forSourceAttributeVehicleTypefirst,andthenVehicleGroup.11. Theresultshouldlooklikethefollowingscreenshot:

12. Saveandclosethedesigner.

Howitworks...

Thisisusedasadefinitionwhenwecreatetheaggregatemeasures.Theviewwasanecessarystepinordertosimplifyaccesstothedatainawaythatuserscanrelate.Wewillthendefineattributes.Althoughweaddtheaggregatedimensiontotheaggregatemeasure,theuserwillusetheattributestosplicethedata.

Webasedtheattributeonthevehicletable,sowecaneasilyrelatethistotheservicetable,whichiswhatwewillbeanalyzing.Thehierarchyisnotmandatory,anditisusedwhenwewanttopredefinetheanalytichierarchiesatthistime.

SeealsoFormoreinformationonAnalyticsinOperations,pleasevisitAnalytics(https://ax.help.dynamics.com/en/wiki/analytics/)

CreatingaggregatemeasuresThesecanbethoughtofascubesfrompriorreleasesofOperationsandformthatbasistowhichweanalyzeourdata.Theycontaintheaggregatedatathatthedimensionsspliceorpivoton.

GettingreadyYoumayfindthatthedrop-downsusedinpropertiesmaynotworkinthisrecipe;toresolvethis,simplybuildtheprojectbeforeyoustart.

Howtodoit...First,wewillneedtocreateaviewthatflattenstheservicedataintooneview.Todothis,followthesesteps:

1. CreateanewviewcalledConWHSVehicleServiceExpanded.2. AddtheConWHSVehicleSErviceTabletableastherootdatasourceandConWHSVehicleServiceLineasachilddata

source,asshowninthefollowingscreenshot:

3. EithersetUseRelationspropertyontheConWHSVehicleServiceLinedatasource,oraddarelationmanually.

4. Addallnon-systemfieldsfromtheConWHSVehicleServiceTabletable,andtheItemIdfieldfromConWHSVehicleServiceLine.

5. Saveandclosetheview.

Wecannowcreatetheaggregatemeasure,whichisdonebyfollowingthesesteps:

1. Intheproject,addanewitem,andintheAddNewItemdialog,selectAnalyticsfromtheleft-handlistandAggregateMeasurementfromtheright.

2. NamethenewitemasConWHSVehicleServiceMeasure.

YoumaynoticethatmanyexistingaggregatemeasuresaresuffixedwithCube;thisisthelegacynamingconventionfromAX2012.

3. ThedesignerwillopenwithadefaultmeasuregroupnodenamedMeasureGroup1.RenamethistoServiceInformation.

4. SettheTablepropertytoConWHSVehicleServiceExpanded.5. Wewilladdthreemeasures,acountofservicerecords,acountofitemsused,andacountof

vehicles.Right-clickontheMeasuresnodeandchooseNewMeasure;addthemusingthefollowingsettings:

Name DefaultAggregate Field

ServiceRecords Count ServiceId

Items Count ItemId

Vehicles Count VehicleId

Toaddsumaggregatesforvalueorquantityfields,settheDefaultAggregatepropertytoSum.

6. Let'saddtheaggregatedimensionsfirstbyexpandingtheDimensionsnodeandnoticingthatithasalreadycreatedtwodimensions:CompanyandDate_.

TheunderscoreistoavoidproblemsshouldthisbedeployedtoSQLServerAnalysisServices(SSAS).Ifyousavewithoutthisunderscore,youwillgetawarning.ThisalsooccursforotherTransact-SQLkeywords,suchasDescription.

7. RenametheDate_dimensionandthesub-nodetoServiceDate,asshowninthefollowingscreenshot:

8. ItwillhaveguessedtherelationfortheCompanydimensionautomatically;however,ithasnoideahowtorelatetheDate_dimensiontotheview.Selecttherelationnode(thefinalnodeundertheServiceDatedimension)andsettheRelatedFieldpropertytoServiceDateRequested.

9. ThecompletedServiceDateandCompanydimensionsshouldlooklikethefollowingscreenshot:

10. Let'saddourConWHSVehiclesaggregatedimension:dragtheConWHSVehiclesaggregatedimensionfromtheprojecttotheDimensionsnode.

11. Inthiscase,ifyouexpandthenewdimension,youwillseethatthattheVehicleIdrelationissetforus;ifthisisnotset,wemustspecifyit.

12. Let'saddastandarddimensiontoouraggregatemeasure.FromApplicationExplorer,locatetheReleasedProductsaggregatedimensionfromAnalytics|Perspectives|AggregateDimensions.

13. DragReleasedProductstotheDimensionsnode.14. Thistime,therelationisnotset.Todothis,settheDimensionAttributepropertytoReleasedProducts.

ThiswasselectedasthisisthekeyattributeandhasItemIdasthekeyattribute.Conventionguidesustoknowthattheattributewiththesamenameastheaggregatedimensionisthekeyattribute.Ofcourse,wecansimplyopentheaggregatedimensioninthedesignertocheck.

15. SelectthenewrelationandtheRelatedFieldpropertytoItemId.Thecurrentstateofourdesignshouldbeasinthefollowingscreenshot:

16. Finally,let'saddacustomattributetotheaggregatemeasure.Thisisnotnormallydone,becausetheycan'tbereusedelsewhere.Right-clickonthe@AttributesnodeandselectNewDimensionAttribute.

17. Completeasbefore,theresultshouldbeasperthefollowingscreenshot:

18. Saveandclosethedesigner.19. Buildtheprojectandperformdatabasesynchronization.

Next,wewillconfiguretheentityrefreshbatchjob.

1. Inorderforthedatatobedeployedandautomaticallyrefreshed,navigatetoSystemadministrationandchooseSetup|EntityStore.

2. SelecttheentitiesthatshouldberefreshedandclickonRefresh.3. IntheConfigurerefreshdialog,expandRuninthebackground,clickonRecurrence,andconfigureas

perthefollowingscreenshot:

Onlymakethisasfrequentasitneedstobe.Asthiswillrunfortheselectedentities,wecanconfigureentitiessothattheyarerefreshedatdifferentrates.

4. PressOKontheDefinerecurrencedialog,andOKontheConfigurerefreshdialog.

Howitworks...Theaggregatemeasuredefinesthevalueswewishtodisplayandthedimensionsbywhichwewillfilterandsplicethedata.Itmayseemoddthatwedon'thavemeasuressuchasVehiclesServicedThisYear,butthisisn'tafunctionofameasure.ThisisaccomplishedbyacombinationoftheServiceDatedimensionandtheVehiclesmeasure.

Theentityrefreshcreatesandmaintainsthedatainthedatawarehousedatabase.ThisisAxDWondevelopmentenvironments,andfortheclouddeployedsandbox,youwillgetthisfromLCS.

IfyouopentheSQLServerManagementStudio,thetablesarecreatedasfollows:

Youcanquerythisdatatodiagnosewhymeasuresorkeyperformanceindicators(KPIs)maynotworkasexpected.

CreatingaggregatedataentitiesAggregatedataentitiesallowustouseaggregatemeasuresinthesamewaywewoulduseatable.TheycanalsobeusedtoexposetheaggregatedatathroughOData.Thisexamplewillbeusedtocreateachartformpartinthenextrecipe.

GettingreadyWewillneedanaggregatemeasureforthis,sowearefollowingonfromtheprevioustworecipes.

Howtodoit...Tocreatetheaggregatedataentity,followthesesteps:

1. CreatenewitembychoosingAnalyticsfromtheleftpaneoftheAddNewItemdialogandAggregatedDataEntityfromtheright.NametheentityConWHSVehicleServiceDataEntity.

2. DragtheConVehicleServiceMeasureaggregatemeasuretotheDataSourcenode.3. ExpandtheMeasuresnodeanddragallthreemeasurestotheFieldsnode.4. Wecanalsoadddimensionsasfields;inourcase,wewillneedtheServiceDatedimension.Drag

ServiceDatetotheFieldsnode.5. Tomakethismoreusefulasanaggregatedataentity,wewillchooseaspecificattributeofthis

dimension.SettheAttributepropertytoMonth.6. Thisaggregatedataentityshouldbegroupedbyvehicle;toaddtheVehicleIDtothefieldlist,right-

clickontheFieldsnodeandselectNewMappedField.7. Setthepropertiesofthis,asshownhere,andsetthemintheorderlistedasfollows:

Property Value

Name Vehicle

MeasureGroup ServiceInformation:thisiswhatwerenamedfromMeasure1whenwecreatedtheaggregatemeasure.

Dimension ConWHSVehicles

Attribute ConWHSVehicles

ExtendedDataType ConWHSVehicleId:thisshouldbesetautomatically

8. Wewillnowneedtobuildtheproject,andthensynchronizethedatabase.

Howitworks...

Aggregatedataentitiesaresimilartotheotherentities,inthat,theycanbeseenastables,usedtoexportdatatoExcel,andtherefore,canbeusedforanalyticsoutsideofPowerBI.Theyareactuallystoredasviews.

Wecanalsousethemasdatasourcesinchartcontrols,givingmoreeasilydigestibleinformationtotheuserwithoutnavigatingaway.

CreatingandusingkeyperformanceindicatorsKeyperformanceindicators,inOperationsprovideasimplewaytocreatemetricsthatcanbeviewedwithinOperations.Inourcase,weshallcreateaKPIthattargetsanumberofserviceordersinamonth.

GettingreadyFirst,weshouldhavecreatedanaggregatemeasurebeforewestartthis.

Howtodoit...FollowthesestepstocreatetheKPI:

1. Createanewitem;selectAnalyticsfromtheleft-handlistandKeyPerformanceIndicatorfromtheright.

2. SettheNamefieldtoConWHSVehicleServiceThisMonthandpressAdd.3. DragtheConWHSVehicleServiceMeasureaggregatemeasurefromtheprojectontotheKPI.Thissetsthe

Measurementpropertyforus.4. Completetheremainingproperties,asfollows:

Property Value

Label Serviceorders

BadThreshold 20

GoodThreshold 1

ScoringPattern LessIsBetter

MenuItemName Mustleavethisblank

5. OntheValuenode,setthepropertiesasfollows:

Property Value

MeasureGroup ServiceInformation

Measure ServiceRecords

WearebasingourKPIonthenumberofserviceordersinamonth.

6. ExpandtheValuenode,andaddanewrangetotheRangesnodeandconfiguretheproperties,as

shownhere:

Property Value

Dimension ServiceDate

Attribute Month

Name MonthRange

ThedefaultPeriodisCurrent,andtheKPIwillknowthatwemeanthecurrentperiodbasedontheAttributeproperty.

7. Wecannowaddtrends.UndertheTrendnode,createanewtrendandsetthepropertiesasshownhere:

Property Value

Dimension ServiceDate

Attribute Week

Name WeeklyTrend

ItemCount 10,thismeans10weeksinthiscase

Label Weeklytrend

8. Now,weshouldcreatethetoptrend,whichshowsthetopncontributoryfactorstothevalue.Completethenewtrendasfollows:

Property Value

TrendType TopTrend

Dimension ConWHSVehicles

Attribute VehicleGroupDescription

Name TopTrend

Label Vehiclegroups

ItemCount 5,thismeansthetop5vehiclegroups

9. Finally,let'saddabottomtrend,whichisdoneasfollows:

Property Value

TrendType BottomTrend

Dimension ConWHSVehicles

Attribute VehicleGroupDescription

Name BottomTrend

Label Vehiclegroups

ItemCount 5,thismeansthebottom5vehiclegroups

Wewillchooseadifferentmeasureforthis,butwecanusethesamemeasureforboth.

10. Youcancreatemultipletrendsofeachtype,andtheywillappearasoptionstotheuser.Tryandcreateatoptrendforvehicletypes.

11. Saveandclosethedesigner.

TotestourKPIweshouldcreateatilefortheKPIsowecanaddittoaworkspace.Thisisdonebythefollowingsteps:

1. CreateanewtilecalledConWHSVehicleServiceThisMonthTile.2. DragtheConWHSVehicleServiceThisMonthKPIontothetileinthedesigner.ThissetstheTypepropertyto

KPIandtheKPIpropertytoConWHSVehicleServiceThisMonth.3. SetLabeltoVehicleserviceordersandtheSizepropertytoWide.4. Saveandclosethedesigner.

Addthetiletothevehiclemanagementworkspaceform:

1. Wewillneedtoaddthistoaworkspace.LocatetheConWHSVehicleWorkspaceforminApplicationExplorer.

2. Right-clickontheformandchooseCreateextension.3. ChangethesuffixsothatitisnamedConWHSVehicleWorkspace.ConAnalytics.4. Opentheforminthedesigner.5. Right-clickonthePanoramaSectionTilestabpageandchooseNew|TileButton.6. SettheNameandTilepropertiestoConWHSVehicleServiceThisMonthTile.

Buildtheprojectandsynchronizethedatabase:

1. Right-clickontheprojectandchooseProperties.2. ChangeSynchronizeDatabaseonBuildtoTrue.3. PressOK.4. Buildtheproject.

TotesttheKPI,followthesesteps:

1. OpentheOperationsclient;inourcase,wecanjustopenthefollowingURL:

https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=usmf&mi=ConWHSVehicleWorkspace

2. IfyoureceiveanerrorthattheKPIdoesnotexist,rebuildtheprojectwiththeSynchronizeDatabaseonBuildprojectpropertysettoTrue.

3. TheKPIwilldisplaywithnodata;tosetthisup,navigatetoSystemadministration|Setup|Datacache|Datacacheparameters.

4. SelecttheCacherefreshtabpage.5. Ifyouhaveabatchgroupsetupforsystemjobs,enterthisintheBatchgroupfield;otherwise,leave

itblank.6. ClickonInitializebatchjob.7. OpenSystemadministration|Inquiries|Batchjobs.8. Thebatchjob,Datacacherefreshbatch,willbeinthelistandwillremainexecuting.Thismayseem

unusualforbatchjobs,astheynormallyhavearecurrence;however,thisjobrunscontinuously.9. Ifthebatchjobisnotexecuting,ensurethattheMicrosoftDynamics365forOperationsBatchManagement

Servicewindowsserviceisrunning.10. Waitaroundaminuteforthedatatoprocessandtesttheworkspaceagain.

Testingtheworkspace:

1. First,youshouldcreatesometestdatasowecantestifourKPIworkscorrectly.Wewillprobablyneedaround30ordersformultiplevehicletypesandgroups.

2. TheKPItilecontainsaconcisesummaryoftheKPIandwillresemblethefollowingscreenshot:

3. Ifyouclickonthetile,youaretakentotheKPIview.4. Inthisview,theusercanchangethedefaultswesuppliedintheKPI,changethetrendviews,and

applydifferentfilterstothosesupplied.Theviewshouldlooklikethefollowingscreenshot:

Howitworks...ThesimpleKPIdefinitionisapowerfultool.TheKPIdefinitionhasenoughinformationtodefineboththetileandtheKPIworkspace.

There'smore...PowerBIisabusinessintelligencetoolusedtodesignandviewBIreports.AgoodresourcetolearnaboutPowerBIisavailableforthefollowinglink:GuidedLearningforPowerBI(https://powerbi.microsoft.com/en-us/guided-learning/?WT.mc_id=PBIService_GettingStarted)InordertoleveragePowerBI,afewmorestepsarerequired.

Thebasicstepsareasfollows:

1. EnsurethatyouhavesetupyourO365accountswithaccesstoPowerBIFreeorPowerBIPro.Tosignupforafreeaccount,usethislink:

https://powerbi.microsoft.com/en-us/documentation/powerbi-admin-free-with-custom-azure-directory/

2. RegisterthePowerBIappandgetaClientIDandApplicationKey.Thedetailsofthisarelistedhere:

https://powerbi.microsoft.com/en-us/documentation/powerbi-developer-register-a-web-app/

3. OpenOperationsandnavigatetoSystemadministration|Setup|PowerBIandcompletetheformwiththeClientIDfromthepreviousstep;anexamplesetupisshowninthefollowingtable:

Field Value

AzureADauthorityURL https://login.windows.net(default)

AzureADPowerBIresourceURI https://analysis.windows.net/powerbi/api(default)

AzureADtenant contoso.com-thetenantyousignedupwith.

ClientID Fromthepreviousstep

Applicationkey Fromthepreviousstep

RedirectURL https://<productionurl>/oauth

PowerBIAPIAddress https://api.powerbi.com/beta/myorg

Alsoseethefollowinglink:https://ax.help.dynamics.com/en/wiki/configuring-powerbi-integration/

InordertoauthoranddistributePowerBIreports,usethefollowinglink:https://blogs.msdn.microsoft.com/dynamicsaxbi/2016/06/23/authoring-and-distributing-power-bi-reports-with-dynamics-ax7/

Whenfollowingthisforacloudsolution,whereyouwanttoseerecentdatafromthesandboxenvironment,doitonthesandboxserver.YoucanconnectusingtheAxDWdatabasedefinedinLCS,butsuffixedwithdatabase.windows.net.ConnectusingtheDatabasetab,astheAxDWaccountwon'thaveaccesstotheserver.

SecurityInthischapter,wewillcoverthefollowingrecipes:

CreatingprivilegesCreatingdutiesCreatingsecurityrolesCreatingpolicies

IntroductionWhendesigningthesecuritymodel,therearethreemethodsdependingonthesolution.ForISVsolutions,thesecurityshouldbedesignedintothemainpackageandshippedtogether.Thisalsogoesforpartnersolutionsfornewcustomerimplementationswhenthepackageisadiscreetpackageoffunctionality.Sometimes,customersneedrolescustomizedfortheirownneeds,andtheserolesmayencompassprivilegesforbothnewandexistingfunctionalities.Inthiscase,anewpackageshouldbedeveloped.Havingaseparatepackagealsoaidsworkplanningefficiency.

Shouldtheendusercustomerdeveloptheirownsecuritymodel,thisshouldalwaysbeinanewpackage;although,thesamefunctionalitycanbeachievedwithintheapplication.

ThesecuritymodelinOperationsisbrokendownintothefollowingstructure:

RolesDutiesPrivilegesPolicies

TheprimarystructureofthesecuritymodelincludesProcesscycles,Roles,Duties,andPrivileges.WewillexplorethesefirstbeforetakingalookatPoliciesandCodePermissions.

EntrypointsdefinetheaccesslevelgrantedtomethodsofentrytoOperationsfunctionality,suchasmenuitemsandservices.

Permissionsdefineaccesstotablesandfields,servermethods(suchasaservicecallwherethecodewon'trunundertheuser'ssecuritycontext),andformcontrols(buttons,fields,orothercontrolsplacedonaform).

Asapartofthefunctionalrequirementsdefinition,thebusinessprocessesareanalyzed,alongwiththerolesthatperformthem.ThebusinessprocesseswillthenbemappedtotheOperationssystemprocesses.Thisisusedformanypurposes,includinggapfitlevel,trainingplans,testingplans,andsoon.ThismethodofanalyzingrolesandprocessesalsofitsnicelyintotheOperationssecuritymodel,allowingthesecuritymodeltobedesignedbasedonthis.

Thesecuritymodelthatwedesignandimplementshouldalwaysbesimpletouse,followingthepatternofthestandardrolesprovidedbyMicrosoft.Animportantdesignprincipleistothinkoftheuser'sroles,andnottothinkofspecificusers,whichshouldresultinthefollowingoutcomes:

ReducednumberofrolesLesscomplicatedassignmentofuserstorolesRolesareeasiertomaintain,withreducedriskoferrors,suchastheunintendedassignmentofaprivilegetoauser

Creatingprivileges

Privilegesarenormallycreatedforeachmenuitem(display,output,oraction)foranaccesslevel.Everymenuitemshouldbeinaprivilege,butyoucanaddmorethanonemenuitemtoaprivilegeiftheymustneverbeassigneddifferentpermissions,suchasmenuitemsthatpointtothesameform.Thisisthemostgranularlevel,andwillbegroupedintodutiesandroleslater.

Sincetheprivilegeassignstheaccesslevel,wewillusuallyhavetwo--toprovideviewonly,andtomaintain(full)accessrights.

GettingreadyWewilljustneedanOperationsprojectopeninVisualStudio.

Howtodoit...

Tocreateaprivilegetoprovideviewaccesstothevehicleform,followthesesteps:

1. Choosetoaddanewitemtotheproject.2. IntheAddNewItemdialog,selectSecurityfromtheleft-handlistandSecurityPrivilegefromthe

right.3. EnterConWHSVehicleTableViewintheNamefieldandclickonAdd.4. CompletetheDescriptionproperty;thisshoulddescribetothesecurityadministratorwhatthis

privilegegrantsaccessto.5. CompletetheLabelpropertybygivingitashortdescriptiontothesecurityadministrator,suchasView

vehiclerecords.

Itcanbeagoodideatoincludeatextorevenaprefixinthetexttouniquelyidentifywhichmodulethissecurityelementrelates.ThesecurityadministratorwillonlybeabletoeasilyseetheLabelandDescriptionproperties,anditcouldbeeasytoconfusethesecurityelementwithanother.

6. Inthedesigner,dragtheConWHSVehicleTablemenuitemontotheEntryPointsnode.7. Changetheentrypoint'sAccessLevelpropertytoRead.8. Tocreatetheprivilegetomaintainthevehicletable,createanewprivilegenamed

ConWHSVehicleTableMaintain.9. CompletetheDescriptionproperty.10. CompletetheLabelproperty.Forexample,Maintainvehiclerecords.11. Inthedesigner,dragtheConWHSVehicleTablemenuitemontotheEntryPointsnode.12. Changetheentrypoint'sAccessLevelpropertytoDelete.13. Shouldtheformhaveanyassociateddataentities,suchasthosethatallowustoedittheform'sdata

inExcel,theyshouldalsobeaddedtotheprivilegeundertheDataEntityPermissionsnodewiththeappropriateaccesslevel.

Howitworks...

Theprivilegesaresimplyawaytograntpermissionstoanentrypoint,whichcanbeservices,toaduty,role,orevendirectlytoauser.Typically,weonlyaddentrypointstoaprivilege,suchasmenuitems.Inordertograntaccesstotheuser,thesystemappliestheaccessleveltoformcontrolsanddatasources.WhenwesettheNeededPermissionpropertyonformcontrols,itcanhavetheeffectofhidingthecontroliftheprivilegedoesn'tgranttheneededpermission.

Sincewecan'textendsecurityprivileges,wewouldalwayscreateanewprivilege.Thisisnotarealrestriction,andhelpsenforcegoodpractice;wecan'tgetmoregranularthanaprivilege,intermsofassigningpermissionstoauser.Weshouldneverover-layer(customize)anexistingsecurityprivilege,asthereisnoneedasdutiesandrolesareextensible.

There'smore...

Onthevehicleform,wehaveabuttonthatallowsustochangethevehiclegroup,andthisshouldbehiddenfortheviewprivilege.Whenaddingcontrolstoforms,wherethesystemcan'tdeterminetheneededpermission,wemustsetitonthecontrolwhilstdesigningtheform.Inourcase,thebuttonwascreatedwithaNeededPermissionpropertyofUpdate.So,thebuttonwillbehiddenwhentheprivilegesgrantedtotheuserarelessthanUpdate.

Insomecases,wemaywishtoelevatethepermissionlevelabovethatoftheprivilege'saccesslevel.Thisisaveryspecialcase,andisdonebyfollowingthesesteps:

1. ExpandtheEntryPointsnodeandtheConWHSVehicleTableentrypoint.2. Right-clickontheControlsnodeandchooseNewControl.3. IntheNameproperty,typethenameofthecontrol,suchasConWHSVehicleGroupChangeController.4. SelecttheappropriateGrantpropertyandtograntaccess,makeitUpdate.

WecanalsousetheFormControlPermissionsnodetograntthistypeofpermissiontoformsnotassociatedwiththemenuitem.

ImpactonlicensingOperationsislicensedbasedonnamedusers.Basedonyourorganization'srequirements,amixofclientaccesslicense(CAL)typescanbebought.

Theuser'sCALtypeisdeterminedbytheentrypoints(effectively,themenuitems)towhichtheyhavereadaccessorhigher.Foreachmenuitem,wehavetwopropertiesthatcontrolwhichlicensetypewillberequired:

ViewUserLicense:ThisiswhenauserisgivenReadaccesstothismenuitemMaintainUserLicense:ThisiswhenauserisgivenUpdateorhigheraccesstothismenuitem

TheCALtypeisdeterminedbythehighestuserlicensetypetowhichtheuserisassigned.Wearenotforcedtoentervaluesinthesemenuitemproperties,butitwouldbeabreachofthelicenseagreementtocreateamenuitemtoopenastandardformifwedon'tmatchtheuserlicensetypeoftheoriginalmenuitem.Microsoftreservestheright,attheirexpense,toinspectthesystem.

Securityadministratorsshouldplanacyclicapproachtosecurity.CheckwiththeNamedUserLicenseCountsreportandadaptthesecuritysetuptoensurethatyouarecomplyingwiththelicense.Checkingandmaintaininglicensecomplianceisthelicenseholder'sresponsibility.

SeealsoSecurityanddataentities(https://docs.microsoft.com/en-us/dynamics365/operations/dev-itpro/data-entities/security-data-entities)

CreatingdutiesAdutyisacollectionofoneormoreprivileges.Theparadigmisthatwearecreatingalistofdutiesthattherolewillperform,and,therefore,addtherequiredprivilegeinorderfortheroletobeabletoperformthatduty.

Itiscommontohaveonlyoneprivilegeinaduty,butmorecanbeadded,forexample,thesetupformsmaybeaddedtooneduty.

Dutynamesaresuffixedwithaverbtodenotetheactionthedutywillallowtheroletoperform;commonsuffixesareMaintain,Inquire,andApprove.Inordertodeterminethecorrectsuffix,lookatthestandarddutiesandsuffixyourdutiesusingthesamenamingconvention.

Howtodoit...

Tocreateaduty,followthesesteps:

1. Choosetoaddanewitemtotheproject.2. IntheAddNewItemdialog,selectSecurityfromtheleft-handlistandSecurityDutyfromtheright.3. EnterConWHSVehicleTableInquireintheNamefieldandclickonAdd.4. CompletetheDescriptionproperty;thisshoulddescribetothesecurityadministratorwhatthisduty

does,suchas"Respondstoinquiriesintovehiclerecords".5. CompletetheLabelpropertybygivingashortdescriptionforthesecurityadministrator,suchas

Inquireintovehiclerecords.6. Inthedesigner,dragtheConWHSVehicleTableViewsecurityprivilegeontothePrivilegesnode.7. Repeatthisforalldutiesrequired,forexampleaConWHSVehicleTableMaintaindutythatwillhavethe

ConWHSVehicleTableMaintainprivilege.

Howitworks...

Youcanconsiderthatadutyisacollectionofsecurityprivileges,whichitis;however,whendesigningthesecuritymodel,wewoulddothistheotherwayaround--wedesigntherolewiththerequiredduties,andthenaddtherequiredsecurityprivilegetosupporttheduty.

There’smore…

Whenextendingthestandardapplication,weoftencreatenewformsandthereforenewmenuitems.Thesecurityprivilegemaythereforeberequiredforanexistingduty.Inthiscasewewouldcreateanextensionoftherequiredduty.Todothis,wewouldrightclickonthatduty,andchooseCreateextension,rememberingtochangethe.extensionsuffixtoonetherelatestothemodelweadeveloping:applicationelementsbelongtoamodel,andmodelsbelongtoapackage.Wecanthenjustdragtheprivilegefromtheprojectontotheduty.Thissameprocesswouldbefollowedshouldwewishtoaddanewdutyandanstandardrole-oranexistingroleinadifferentpackage.

CreatingsecurityrolesAroleisacollectionofdutiesandprivileges.Theroleiswhatweassociatewithauser,whichcanbedoneautomaticallybasedontheemployee'sinformation,suchastheirpositioninthecompany.ThesecurityrolesshouldbethoughtofintermsofthepersonasthatfirstappearedwithDynamicsAX2012.Thechangeisintendedtomovethethinkingawayfromcreatinggroupsoffunctionalitytodesigntherolesbasedonhowtheorganizationisstructured.Forexample,aSalesmanagerwouldbeinaSalesmanagerrole,whichwillhavedutiesassigned.Thedutieshaveprivileges,whichinturngiveaccesstothesalesmanagerinordertoperformthatrole.

Inourcase,wecouldconsiderthattheyhavethreeroles:vehiclemanagementsupervisor,vehicleservicesupervisor,andvehicleserviceentryclerk.Whendefiningtheroles,wedosobydefiningthedutiesthateachrolewillhave.Thenamingconventionissimilartootherobjects,andsuffixedwiththetypeofrole.ThesetypesincludeSupervisor,Manager,Clerk,orothersshouldthesenotfittherequiredrole;forexample,ConWHSVehicleManagerwouldhavetheMaintaindutiesforvehiclemasterdata.

Wecanaddprivilegesdirectlytoarole,butweshouldbestrictandonlyadddutiesdirectlytoarole.Thisassistsinmaintenance,and,additionally,itisabestpracticechecktohaveallprivilegesinoneormoreduties,andalldutiesmustbeinoneormoreroles.

Wecanalsoaddrolesasasub-role.Thismayhelpinrarecases,but,again,trytoavoidthis.Itmakesmaintenancealittlemorerestrictiveforthesecuritymanager.Theymaywanttograntorrestrictaccesstothesub-rolewithoutchangingtherightsoftheparentrole.

Howtodoit...

Tocreatearole,followthesesteps:

1. Choosetoaddanewitemtotheproject.2. IntheAddNewItemdialog,selectSecurityfromtheleft-handlistandSecurityRolefromtheright.3. EnterConWHSVehicleManagerintheNamefieldandclickonAdd.4. CompletetheDescriptionproperty;thisshoulddescribetothesecurityadministratorwhatthisduty

does,suchasManagersvehiclemasterdata.5. CompletetheLabelpropertybygivingashortdescriptiontothesecurityadministrator,suchas

Responsibleforthemaintenanceofvehiclerecordsandassociatedsetupdata.6. Inthedesigner,dragthedutiesthatprovidefullrightstothevehicleandsetupmenuitemsontothe

Dutiesnode.7. Repeatthisforallrolesrequired.

Howitworks...

Technically,thisisstraightforward.Thecomplicatedpartisdesigningthesecuritymodelwiththecustomerinordertohaveacommonviewofsecurityfromahumanresourceperspective.Whensecurityrolesaresynchronizedwiththeorganizationalhierarchy,securitybecomesmoreofahumanresourcemanagementprocessthananITadministratorrole.

Seealso...Role-basedsecurity(https://docs.microsoft.com/en-us/dynamics365/operations/dev-itpro/sysadmin/role-based-security)

Creatingpolicies

Theterm"Securitypolicies"isaslightmisnomer.ItisalsoknownunderamoreaccuratetermofExtensibleDataSecurity(XDS).ItisanevolutionofrecordlevelsecuritythatwasdeprecatedfromAX2012:youcouldstilldothis,butitwasn'tarecommendedapproach.

Inthisscenario,wewillcreateapolicythatonlyallowsaccesstovehiclesoftypetruck.Inthisscenario,wehaveateamthatonlyhasaccesstotruckswhencreatingserviceorders.

Howtodoit...

Tocreatearole,followthesesteps:

1. Choosetoaddanewitemtotheproject.2. IntheAddNewItemdialog,selectDataModelfromtheleft-handlistandQueryfromtheright.3. EnterConWHSVehicleTruckPolicyintheNamefieldandclickonAdd.4. Inournewquery,dragtheConWHSVehicleTabletabletotheDataSourcesnode.5. AddtheVehicleTypefieldtotheFieldslistbyright-clickingontheFieldsnodeandchoosingNew|

Field.6. DragthefieldontotheRangesnode.7. OnthenewVehicleTyperange,changetheValuepropertytoTruck.

Whendeployed,thiswillbeupdatedforus.Wecanalsousetheenum'svalueasthetypeisnotextensible.

8. Saveandclosethequerydesigner.9. Addanewitemtotheproject.10. IntheAddNewItemdialog,selectSecurityfromtheleft-handlistandSecurityPolicyfromtheright.11. EnterConWHSVehicleTruckPolicyintheNamefieldandclickonAdd.12. SetLabeltoVehiclemanagementclerkvehicletableaccesspolicy.

13. SetHelpTexttoRestrictsaccesstothevehicletablesothatonlytruckscanbeselected.

14. SetPrimaryTabletoConWHSVehicleTable.ThistellsOperationsthenameofprimarytableintheQueryproperty.

15. EnterConWHSVehicleTruckPolicyintotheQueryproperty.16. LeaveUseNotExistJoinasNo.Otherwise,thiswouldhavetheeffectofmakingthepolicyallow

inactivevehiclesonly.17. SetConstrainedTabletoYes.18. LeaveOperationasSelect;weareintendingthispolicytocomeintoeffectwhenselectingrecords.19. UsingtheCreatingsecurityrolesrecipe,createaroleforaTruckserviceentryclerkcalled

ConWHSVehicleTruckServiceClerk.20. SetContextTypetoRoleNameandenterConWHSVehicleTruckServiceClerkintheRoleNameproperty.21. Finally,settheEnabledpropertytoYes.

Howitworks...Whentheuserinthepolicy'sroleopensaform,orwhenadrop-downisdisplayed,thesystemwillcreateaquerythatcombinestheform'sdatasourcewiththepolicy'squerydefinitionasanExistsorNotExistsjoin.Thequerycannotbechangedbytheuser,andisenforcedatkernellevel.

Here'sawordofcaution:sincethedatapolicyisdefinedusingaquery,itcouldaddsignificantserverload,especially,whenthequeryisnotwrittenefficiently.Inordertousepolicies,thereshouldbeaclearbusinesscase,anditmaybemoreappropriatetohaveasecondarylistpageinstead.Thepoliciesshouldbebasedonbusinessprocessrules,whereaccessmustberestrictedforsecuritypurposes.SeethelinkinSeealsoformoredetails.

So,creatingapolicythatisbasedondatatheuserenters,suchasvehiclesforaparticularvehiclegroup,indicatesthatitisn'tapolicybutafilteronthevehicleslistpage.Policiesarenormallybasedonrulesthatareunaffectedbyusersetup,suchasanenum.

Thiscouldveryeasilyhaveanimpactonperformance,sotherangesandjoinswecreateinthequerymustbewrittencorrectlyandcoveredbyappropriateindexes.

There'smore...Wecanalsoconstrainrelatedtablesandviews;forexample,restrainingaccesstovehicleservicerecordsthatarenotlinkedtovehiclesoftypeTruck.

Wecandothisintwoways--tablesthathavearelationdefined,andthosethatdon't,whichincludesviews.

Toaddaconstrainedtable,followthesesteps:

1. Right-clickontheConstrainedTablesnodeandchooseNew|ConstrainedTable.2. SettheConstrainedpropertytoYes.

Constrainedtablescanbenested,andwecanaddintermediarytablestoeventuallygettothetablewewanttoconstrain.Wemaynotwanttheseintermediarytablestobeconstrained,andwouldthereforeleavethisvaluetobeNo.

3. SettheNamepropertytoConWHSVehicleServiceTable.4. SelectConWHSVehicleTableintheTableRelationproperty.

Toaddaview,oratablewithoutarelation,usetheConstrainExpressionoptioninstead.Inthiscase,wehaveaValueproperty,whereweentertherelation.Forexample,ConWHSVehicleTable.VehicleId==ConWHSVehicleServiceTable.VehicleId.Thevalueisnotvalidated,sowemustensurethatitiscorrect.

Seealso...BestPractices,TipsandTricksforImplementingXDS(ExtensibleDataSecurity)policies(https://blogs.msdn.microsoft.com/daxserver/2013/06/26/best-practices-tips-and-tricks-for-implementing-xds-extensible-data-security-policies/)Securityarchitecture(https://docs.microsoft.com/en-us/dynamics365/operations/dev-itpro/sysadmin/security-architecture)SecurityPoliciesPropertiesforAX2012(https://msdn.microsoft.com/en-us/library/gg731857.aspx)

ThisisstillrelevantinDynamics365forOperations

LeveragingExtensibility

Inthischapter,wewillcoverthefollowingrecipes:

ExtendingstandardtableswithoutcustomizationfootprintCreatingdataeventhandlermethodsHowtocustomizeadocumentlayoutwithoutanover-layerExtendingstandardformswithoutcustomizationfootprintUsingaformeventhandlertoreplacealookupCreatingeventhandlermethodsCreatingyourownqueryfunctions

IntroductionThefocusonthischapterishowtoextendthestandardsoftwarewithoutanover-layer.TothosethatcomefromDynamicsAX2012orprior,itmaynotseemabigissue:over-layeringwastheonlychoice,andwhencomparedtootherproductsinthespace,itwasoneofitskeyfeatures.Whenweover-layer,thesystemcopiesthecodeintothepackage'slayerandthedevelopermakeschangestothiscopy.ItisallseamlessandhasworkedwellsinceAxapta1.5.

Theproblemiswhenweneedtoservicetheapplication,suchasdeployingahotfix,wewillneedtocheckforconflictsandpotentiallyupdateourcode.Also,ifwehaveanISVsolutionthatover-layersapackage,wewillneedanupdatefromtheISVifthehotfixmodifiesanover-layeredelement.

Theotherdownsideofover-layeristhatwecan'thavetwoISVsolutionsthatover-layerthesameelement.Thishasalwaysbeenaproblem,anditcanonlybesolvedbymanuallymerginginahigherlayer.

ThesolutionMicrosofthasforthisiscalledextensibility.Thisiswherewestorethechangeswewanttomakeinadeltachangeformatinaseparateobject.ThismeansthattwoISVsandthecustomercouldalladdfieldstotheSalesLinetable,andnotversion-lockthecustomer.Allofthesefieldsaremergedbythesystem,andthetablewillhaveallfieldsinallpackagesaddedtothetableinSQL.Thereisnolongeraneedtohaveseparatetableswiththeadditionalfieldsandcodeinordertoavoidover-layeringatable.

ExtendingstandardtableswithoutcustomizationfootprintTableextensionsprovideawaytoaddfieldstoatablewithoutover-layering.Thismeansthatwedon'tneedtoperformacodemergewhenthebasepackageischanged.

Inthisrecipe,wewilladdafieldtotheSalesConfirmHeaderTmptable,whichwewilluseintheHowtocustomizeadocumentlayoutwithoutanover-layerrecipe.

Itiscommontohavethemaindevelopmentworkinonepackage,andreportsinahigherpackage,asthiscanhelpwithdeployingupdates.Whendecidingwhichpackagethistableextensionshouldbedonein,weneedtoconsiderthescopeofitsusageshouldbe.Ifwecreatetheadditionalfieldsinareportingpackage,whichreferencesamaindevelopmentpackage,themaindevelopmentpackagewillnotbeabletousethisfield.

Sometimesareportdrivestherequirementforadditionalfields.So,itcanseemnaturaltoaddthenewfieldstoatableextensioninthereportingpackage.Thismeansthatwewillneedatableandformextensioninthereportingpackage,whichmayseemfineuntilapieceofcodeinthemaindevelopmentpackageneedsthefield.Thereferencesinpackagesonlygoesinonedirection,themaindevelopmentpackagecannotseetheadditionalfieldsinthereportingpackage-eventhoughtheyphysicallyexistinthebasetable.

Intheabovetypeofscenario,thefieldsshouldbeaddedtoanewprojectinthemaindevelopmentpackage.LookbacktoChapter1,StartingaNewProject,forthediscussiononpackages.

Intheexampleinthisrecipe,thefieldisspecifictoareportingpackage,becausethefieldisaddedtoatemporarytablethatisonlyusedforthereportgeneration.

GettingreadyCreateanewprojectinanewmodeltoreport,forexample,ConReports,asanewpackage,whichisanextensionpackage.

Howtodoit...

Toaddafieldtoatableasanextension,followthesesteps:

1. LocatetheSalesConfirmHeaderTmptableintheApplicationExplorer.2. Right-clickonitandchooseCreateextension.3. Rememberingthatallelementnamesmustbegloballyunique,renamethenewextensionfrom

SalesConfirmHeaderTmp.extensiontoSalesConfirmHeaderTmp.ConReports.4. Openthetableextensioninthedesigner.5. LocatetheEDTnameintheApplicationExplorer.6. DragtheNameEDTtotheFieldsnode.7. RenamethefieldtoConSalesPoolName.8. SettheLabelpropertyto@SYS84547(Salespool).9. Saveandclosethedesigner.

Howitworks...Thisrecipeiscloselylinkedtothenexttworecipes,andsomeexplanationisrequiredastohowweknewwhichtablestomodify.Tokeepthisinformationinoneplace,thisexplanationisintheThere'smore...sectionoftheHowtocustomizeadocumentlayoutwithoutanover-layerrecipe.

Thiscreatesanentitythatonlystoresthechangestotheelementbeingextended.Thisistrueofallextensions.Thereisnocopyingfromlayertolayer;itisareferenceandalistofchanges.

Theactualsourceofthetableextensionisshowninthefollowingpieceofcode:

<?xmlversion="1.0"encoding="utf-8"?>

<AxTableExtensionxmlns:i="http://www.w3.org/2001/XMLSchema-instance">

<Name>SalesConfirmHeaderTmp.ConReports</Name>

<FieldGroupExtensions/>

<FieldGroups/>

<FieldModifications/>

<Fields>

<AxTableFieldxmlns=""

i:type="AxTableFieldString">

<Name>ConSalesPoolName</Name>

<ExtendedDataType>Name</ExtendedDataType>

<Label>@SYS84547</Label>

</AxTableField>

</Fields>

<Indexes/>

<Mappings/>

<PropertyModifications/>

<Relations/>

</AxTableExtension>

Whentheprojectisbuiltandthedatabasesynchronizationisdone,thephysicaltableSalesConfirmHeaderTmpwillhavethebasefieldsplusthefieldsinallofitsextensions.Thisiswhythefieldneedstobeprefixed.

There'smore...Youcanalsoextendthefieldgroups.Thisallowsustoaddfieldstoafieldgroupthatisusedinformstokeepthepresentationconsistentandreducethenumberofchangesandcodemaintenanceeffort.

Wecanalsoaddindexesandrelations.

TheindexesarecreatedwithinthephysicaltableinSQL,sothesemustalsobeprefixedtoensurethatwedon'tcauseanycollisions(duplicatename).

Creatingdata-eventhandlermethodsThedata-eventhandlershandlethedelegatesexposedoneverytable.ThesedelegatesarelistedundertheEventsnode.

Theseeventsdonotfireiftheassociatedmethod(forexample,insert)isoverriddenonthetableandsuper()isnotcalled.

Howtheeventhandlermethodsareorganizedisuptothedeveloper;theyjustneedtobeplacedlogicallysothatotherswillfindthemeasily.

Inourcase,theeventhandlerisusedpurelyforareportandpopulatingafieldoninsertthatisusedinareport.So,wewillplacethesemethodsinahelperclass.

GettingreadyThisrecipecontinuesfromthepreviousrecipe.

Howtodoit...

Tocreateadataeventhandler,followthesesteps:

1. CreateanewConReportManagerclass.2. Openthetabledesignforthetableinquestion;inourcase,double-clickonthe

SalesConfirmHeaderTmp.ConReportstableextension.3. ExpandEventsandlocateonInserting.

TheonInsertedeventistoolateaswewanttofillinafieldbeforetherecordiswritten;ifwesubscribedtoonInserted,thedatawillnotbesaved.

4. Right-clickontheeventandchooseCopyeventhandlermethod.Thiswillcreateacodesnippet,andplaceitinthepastebuffer.

5. OpentheConReportManagerclass,andpasteinthecodegeneratedbystep4intotheclassbody,asshownhere:

classConReportManager

{

///<summary>

///

///</summary>

///<paramname="sender"></param>

///<paramname="e"></param>

[DataEventHandler(tableStr(SalesConfirmHeaderTmp),

DataEventType::Inserting)]

publicstaticvoidSalesConfirmHeaderTmp_onInserting(

Commonsender,DataEventArgse)

{

}

}

Lookattheeventdeclaration:itreferencesthetable,nottheextension.Thisisimportant,astheextensionisnotatable,andthesystemcorrectlychangesthesubscriptiontothecorrecttable.

6. Next,wewillneedtowritethecode,andtheobviouscodetowritemayseemtobeasfollows:

SalesConfirmHeaderTmpheader=sender;

SalesTablesalesTable;

selectSalesPoolId

fromsalesTable

whereSalesTable.SalesId==header.SalesId;

header.ConSalesPoolName=SalesPool::find(

SalesTable.SalesPoolId).Name;

Thiswouldbewrong,asweassumethatthesalesorderrecordwillneverbedeleted-andonceinvoiced,theycanbedeleted.

WhatweshoulddoisaddSalesPoolIdtotheCustConfirmJourtable,whichisthepermanentrecordofthatconfirmation.

7. CreateanextensionofCustConfirmJour,calledCustConfirmJour.ConReports.8. DragtheSalesPoolIdEDTfromtheApplicationExplorer,andrenameittoConSalesPoolId.9. Right-clickontheonInsertingeventandchooseCopyeventhandlermethod.10. PastethemethodintothebodyofourConReportManagerclass.11. Adjustthemethodsothatitreadsasfollows:

///<summary>

///Handlestheinsertingeventof<c>CustConfirmJour</c>

///</summary>

///<paramname="sender"></param>

///<paramname="e"></param>

[DataEventHandler(tableStr(CustConfirmJour),

DataEventType::Inserting)]

publicstaticvoidCustConfirmJour_onInserting(

Commonsender,DataEventArgse)

{

CustConfirmJourjour=sender;

jour.ConSalesPoolId=jour.salesTable().SalesPoolId;

}

Therecordispassedbyreference,andwemustnotcallinsertorupdate.

12. Finally,adjustourSalesConfirmHeaderTmphandlersoitreadsasfollows:

///<summary>

///Handlesinsertingevent<c>SalesConfirmHeaderTmp</c>

///</summary>

///<paramname="sender">Thecallingrecord</param>

///<paramname="e"></param>

[DataEventHandler(tableStr(SalesConfirmHeaderTmp),

DataEventType::Inserting)]

publicstaticvoidSalesConfirmHeaderTmp_onInserting(

Commonsender,DataEventArgse)

{

alesConfirmHeaderTmpheader=sender;

CustConfirmJourjour;

selectConSalesPoolId

fromjour

wherejour.RecId==header.JournalRecId;

header.ConSalesPoolName=

SalesPool::find(jour.ConSalesPoolId).Name;

}

13. Saveandclosethedesigner.14. Totestthis,performafullbuildincludingdatabasesyncandcreateasalesconfirmation.Then,

checkthattheSalesPoolIdfieldwaspopulatedintheCustConfirmJourtable.

Howitworks...

Theeventhandlerswewroteinthisrecipeareboundwhenthepackageisbuilt--nochangesaremadetothebasepackagesandwecanshipthepackageasadeployablepackagetoadifferentsystemanditshouldallworkfine.

Thesubscribersarenotcalledinanyorder,andthiscannotbereliedupon.So,thecodemustbewritteninawaythatdoesn'tmakethisassumption.Whenconsideringtransactiondurability,theeventsarecalledwithinthetransactionofthecaller--andthrowinganexceptionwillcausethewholetransactiontobeaborted.

There'smore...Addingfieldsasanextensionisrelativelystraightforward,butwealsoneedtohandleeventssuchasmodifiedFieldandvalidateField.

Iftheseareoverriddenonthetable,wecoulduseaPreorPost-eventhandler.However,thesearenotalwaysoverridden,andthesetypesofeventhandlersshouldalwaysbethelastchoice;thesearediscussedlaterinthischapter.

Tohandletheseevents,wewouldsubscribetotheappropriatedataevent.Therearetwomethodsforeach:apresent-continuous(ing)methodandapasttense(ed)method.

Theingmethodsarecalledbeforetheeventfires,andtheedmethodfiresaftertheevent.Theyonlyfireonsuper();so,ifthedeveloperdoesn'tcallsuper()inthemethod,theeventwillnotfire.ThisisthecaseonSalesLine,SalesTable,PurchLineandPurchTable,forexample.

LocatethetableInventTableintheApplicationExplorer,rightclickonitandchooseOpendesigner.ExpandtheEventsnode,andrightclickontheonValidatedFieldevent(whichisactuallyadelegate).Let'susetheCopyeventhandlermethodoptionwhichwillplacethefollowingcodeinthepastebuffer:///<summary>//////</summary>///<paramname="sender"></param>///<paramname="e"></param>[DataEventHandler(tableStr(InventTable),DataEventType::ValidatedField)]publicstaticvoidInventTable_onValidatedField(Commonsender,DataEventArgse){}

Thishelps,buttheDataEventArgsobjectdoesn'tcontainanythingaboutthefieldbeingvalidated,oramethodbywhichwereturnfalseshoulditnotbevalid.

Youcanthenchangethecodeusingthefollowingpattern:///<summary>///Validatesthefieldsonthe<c>InventTable</c>table///</summary>///<paramname="sender"></param>///<paramname="e"></param>[DataEventHandler(tableStr(InventTable),DataEventType::ValidatedField)]publicstaticvoidInventTable_onValidatedField(Commonsender,DataEventArgse){ValidateFieldEventArgsfieldArgs=e;

Booleanok;switch(fieldArgs.parmFieldId()){casefieldNum(InventTable,ProdPoolId)://if(<condition>)//{//ok=checkFailed("message");//}break;}if(!ok){fieldArgs.parmValidateResult(false);}}

WechosetousetheonValidateFieldeventdelegatebecausewewouldneverhandlefieldvalidation,ormodifyeventsontablesthatwedon'tenterdatainto.Asdiscussedinpreviouschapters,theseeventsaretriggeredfromtheformcontrol,passedthroughtothedatasource,andfinallythetable.

ShouldweaddafieldtoanextensionofInventTable,thecodeintheeventhandlerwouldbethesame;thatisthetableStr,methodStr,andfieldNumfunctionswouldalluseInventTableandnotthenameoftheextension.Theeventhandlerissimplytriggeredwhentheeventhappens,thecodewewritewillbeabletoseeallfieldsaddedtothecurrentpackageandfieldsinallpackagestowhichthecurrentpackagereferences.

TheotherspecializedclassesunderDataEventArgsthatareusefulareasfollows:

Class Usage

ValidateFieldEventArgs

onValidatedField

onValidatingField

ValidateEventArgs

onValidateWrite

onValidatingWrite

onValidateDelete

onValidateingDelete

ModifyFieldEventArgs

onModifiedField

onModifyingField

Howtocustomizeadocumentlayoutwithoutanover-layerTheexamplehereistoaddanextensionfieldtoaprint-managedstandarddocumentwithoutover-layeringthereport.Wewillusethesalesorderconfirmationreporttoaddthesalesorderpool'snametothereport.

Therearetwomaintypesofreports:listingreportsanddocuments.Thedocuments,suchasthesalesorderconfirmationdocument,usetemporarytablestomakethelayouteasiertowrite.Anyreportcanusethistechnique,butitismorecommonondocumentlayoutsandcomplicatedlistingreports.

Itisoftengoodpracticetohaveaseparatemodelforreports.Reportscanuseelementsthatwehavewrittenacrosspackages,forexample,wemayhaveanextensionpackageandanISVpackagethathaselementswewishtoreporton,butwedon'twanttolinkourpackageasadependencyontheISVpackage.

Wewon'tcovertheactualreportdesigninthisrecipe,asreportdesignisbeyondthescopeofthisbook;butwewillcoverallotherareas.

Wehavealreadyaddedthefieldsandeventhandlersinthepriortworecipes;wejustneedtoaddthefieldtothereport.

Howtodoit...

Toaddafieldtothereport,followthesesteps:

1. LocatetheSalesConfirmreportintheApplicationExplorer.2. Right-clickonthereportandchooseDuplicateinproject.3. RenamethereporttoConSalesConfirm.4. OpenthereportinthedesignerandexpandtheDatasetsandSalesConformHeaderDSnodes.Then,

lookforthenewextensionfieldintheFieldsnode.Ifthisdoesnotappear,right-clickoneachdatasetandchooseRestore.

5. Youcannowproceedtodesignthereportasperyourrequirements.

Thenextstageisintegratingthereportsothatournewreportisusedinsteadofthestandardreport.Todothis,followthesesteps:

1. WewillneedtoaddaneventhandlertoadelegateexposedbyMicrosoftonthePrintMgmtDocTypeclasscalledgetDefaultREportFormatDelegate.Thefollowingcodedoesthis,thecommentedoutsectionsarethereforreferenceshouldyouwishtoextendotherdocuments:

///<summary>

///AllowstheSSRSReportusedforthePrint

///managementbasedreportstobeoverridden

///</summary>

///<paramname="_docType">ThePrintMgmtDocumentType

///</param>

///<paramname="_result">TheEventHandlerResult</param>

[SubscribesTo(classstr(PrintMgmtDocType),

delegatestr(PrintMgmtDocType,

getDefaultReportFormatDelegate))]

publicstaticvoidDefaultReportFormat(

PrintMgmtDocumentType_docType,

EventHandlerResult_result)

{

switch(_docType)

{

casePrintMgmtDocumentType::SalesOrderConfirmation:

_result.result(

ssrsReportStr(ConSalesConfirm,Report));

break;

casePrintMgmtDocumentType::WHSPickListShippingLoad:

casePrintMgmtDocumentType::WHSPickListShippingShipment:

casePrintMgmtDocumentType::WHSPickListShippingWave:

//_result.result(

//ssrsReportStr(ConPickListShipping,

//Report));

break;

casePrintMgmtDocumentType::WHSPickListProd:

//_result.result(

//ssrsReportStr(ConPickListProduction,

//Report));

break;

casePrintMgmtDocumentType::SalesOrderPackingSlip:

//_result.result(

//ssrsReportStr(ConSalesPackingSlip,

//Report));

break;

casePrintMgmtDocumentType::SalesOrderInvoice:

//_result.result(

//ssrsReportStr(ConSalesInvoice,Report));

break;

casePrintMgmtDocumentType::SalesFreeTextInvoice:

//_result.result(

//ssrsReportStr(ConFreeTextInvoice,Report));

break;

casePrintMgmtDocumentType::CustAccountStatement:

//_result.result(

//ssrsReportStr(ConCustAccountStatement,

//Report));

break;

casePrintMgmtDocumentType::PurchaseOrderRequisition:

//_result.result(

//ssrsReportStr(ConPurchPurchaseOrder,

//Report));

break;

casePrintMgmtDocumentType::PurchaseOrderReceiptsList:

//_result.result(

//ssrsReportStr(ConPurchReceiptsList,

//Report));

break;

casePrintMgmtDocumentType::CustCollectionLetter:

//_result.result(

//ssrsReportStr(ConCustCollectionJour,

//Report));

break;

}

}

2. Saveandclosethedesignersandbuildtheproject.Youwillneedtodeploythereportsoncethisisdoneinordertotestit.Todothis,right-clickonthereportandchooseDeployreports.

Howitworks...

Eventhoughthedatasetinthereportisthebasetable,itshowsourextensionfields.Itwillalsoshowallextensionfieldsforthepackagesthatthereportspackagereferences.Theprocessofreportdesignisstraightforwardfromthatpointonwards.

TheintegrationhasbeenwellthoughtoutbyMicrosoftandhas,therefore,exposedadelegatethattheyhandle.Inthemethodthatcallsthisdelegate,thecodedeterminesifithasbeenhandledandwillusetheresultargumentsforthereportname.

Theremore...Someknowledgeofthedatabasewasrequiredinordertoknowwhichtableswehadtoaddextensionfieldsto,andjustreadingthecodeisnotonlydaunting,butalsoimpractical.Thisexamplewaschosenspecificallyasitisn'teasytofind.Thisrecipeismucheasierwhenthefieldistobeaddedtoaform.WecanseethemenuitemintheURLintheclient,andfromthere,findtheformandthetable.

ThetemporarytablesusedwerefoundsimplybyopeningtheSalesConfirmreportandlookingatthedatasourcesituses.Weknewwhichreportitwas,basedonconsistentnamingconventions.

SalesdocumentsstartwithSales,andarefollowingbythedocumenttype,whichisasfollows:

SalesQuotationSalesConfirmSalesPackingSlipSalesInvoice

PurchasingdocumentsstartwithPurch,andarealsosuffixedwiththedocumenttype:

PurchPurchaseOrderPurchReceiptsListPurchPackingSlipPurchInvoice

ThesedocumentsarecreatedthroughtheFormLetterframework.Thepatternisthateachdocumenthasaheaderandlinestablethatformsthepermanentdata,allowingthereportstobereproducedexactlyastheywerewhenfirstprinted,eveniftheorderhasbeenchangedordeleted.Thesearereferencedasjournals.Thetablesfollowasimilarconvention,asshowninthefollowingtable:

Document Journalheadertable Journallinetable

SalesQuotation CustQuotationJour CustQuotationTrans

SalesConfirm CustConfirmJour CustConfirmTrans

SalesPackingSlip CustPackingSlipJour CustPackingSlipTrans

SalesInvoice CustInvoiceJour CustInvoiceTrans

PurchPurchaseOrder VendPurchOrderJour None

PurchReceiptsList VendReceiptsListJour VendReceiptsListTrans

PurchPackingSlip VendPackingSlipJour VendPackingSlipTrans

PurchInvoice VendInvoiceJour VendInvoiceTrans

WhenweusedtheCopyeventhandlermethodoptionontheevent,thismerelycreatedavalidmethoddeclarationforuse.Itdoesnotmodifythesourcetableoraddanyreference.Thecodecanthereforebeaddedmanually;itwasjusttosavetime.Thereferenceiscreatedwhentheprojectisbuilt.

CreatingeventhandlermethodsWehavealreadycreatedaneventhandlerinthepreviousrecipe.

Sofar,wehavecreatedaneventhandlerfordataeventsusingtheDataEventHandlerdecorationandadelegateeventhandlerusingtheSubscribesTodecoration.Wecanalsoaddhandlersdirectlytoanypublicmethod.

Therearetwotypes:apre-eventandapost-eventhandler.OneexampleofwherewemayneedtodothisistheSalesTable.insert()method.Thisdoesn'tcallsuper(),sowecan'tuseadataeventhandler.

TheactualinsertoccurswithintheSalesTableTypeclassintheinsert()method.Ifyouwantaccesstothesalestablerecord,youneedtoaddthehandlertothetableastherecordbeinginsertedisaprivatevariabletotheclass.

Eventhandlerslikethisareusedtointegratespecificsolutions,sothehandlerwillbeinaclassinthespecificpackage.

GettingreadyWewilljustneedaDynamics365forOperationsprojectandaclassthatrequirestheeventhandleropeninthedesigner.

Howtodoit...Tocreateapre-eventorpost-eventhandler,followthesesteps:

1. Openthedesignerforthetableorclassthatwewanttoaddahandlerfor,suchastheSalesTabletable.2. Locatethemethodwewishtohandleandright-clickonit:selectCopyeventhandlermethod|Pre-

eventhandler(orPost-eventhandlerasrequired).Thiscreatesamethoddeclarationintothepastebuffer.

3. Pastethemethoddeclarationintothetargetclassforapre-eventhandleronSalesTable.Insert(),whichwillbeasfollows:

///<summary>

///

///</summary>

///<paramname="args"></param>

[PreHandlerFor(tableStr(SalesTable),

tableMethodStr(SalesTable,insert))]

publicstaticvoidSalesTable_Pre_insert(

XppPrePostArgsargs)

{

}

4. Whatwedoatthispointdependsontherequirements;thecommonmethodsinargsareasfollows:

Method Use

booleanexistsArg(str) Doesthehandledmethodhavetheparameter

AnyTypegetArThig(str)

Thisreturnsthemethod'sparametervalueusingtheparameter'sname.ItisreturnedasanAnyTypeobject.

Forexample,ifwearehandlingSalesTine.insert(Boolean_skipMethod=false),wecangetthevalueof_skipMethodusingargs.getArg('_skipMethod').

Thereisnovalidation;wewilluseexistsArgtocheckfirst.Thisisnotanintrinsicfunction,andwecan'tcauseacompilationerrortohighlightanyruntimeerrors.

AnyTypegetArgNum(int) Thisgetsthehandledmethod'sparameterbyusingitspositionfromtheleft.

AnyTypegetThis()

Thisgetstheinstanceoftheobject,inourcase,thecurrentSalesTablerecord.

Thereisnocompilervalidationonthetype,sowemustcheckthismanually.Again,wecan'tuseintrinsicfunctionstoforceacompilationerror.

AnyTypegetReturnValue

Thisisonlyusefulonpost-eventhandlersandgetsthevaluethemethodhasreturned.

WewouldusuallyusethisinconjunctionwiththesetReturnValuemethod.

setReturnValue(AnyType)Thisisonlyusefulonpost-eventhandlersandletsusoverridethevaluethemethodreturns.

setArg(str,AnyType) Thisallowsustosetthemethod'sparameternamedinthismethod.

setArgNum(int,AnyType) Thisallowsustosetthemethod'sparameterbyitspositionfromtheleft.

5. Oncedone,saveandclosethedesigner.

Howitworks...Pre-eventandpost-eventhandlersworkinthesamewayastheothereventhandlers,butthesecarryariskofregression.Theeventswehandlethroughthistechniquearenotdelegates,andthedeveloperdidnotspecificallywritethemethodtobehandledinthisway:otherwisetheywouldhavewrittenadelegate.ThereasonwearerestrictedtopublicmethodsisbecausetheseareconsideredasapublicAPI,andwillnotbechangedordeprecatedwithoutaproperprocedure.Protectedmethodscanbechangedwithoutwarning,andwecould,therefore,findthatourcodenolongerworks.

Privatemethodsareprivateforareason:youcan'tguaranteetheinternalclassstate,orhowthesewillbecalled.

Thisalsoreinforcesthatweneedtobecarefulwhenassigningpublic,protected,orprivatetoamethod.Alwaysmakeyourmethodsasprivateaspossible,asthisnotonlyensuresthatyourcodecan'tbecallederroneously,butitalsomakesiteasierforotherdeveloperstouseyourcodecorrectly.

ExtendingstandardformswithoutcustomizationfootprintAdjustingthelayoutofaform,asanextension,hasbeenmadeveryeasyforus.Thisiscoveredinthefirstpartoftherecipe.Wewillalsocoveranewtechniquetoworkwithformcode.

GettingreadyWejustneedaDynamics365forOperationsprojectopen.

Howtodoit...Towriteaformextensionforthesalesorderform,SalesTable,followthesesteps:

1. LocatethedesiredformintheApplicationExplorer,rightclickonitandchooseCreateextension.Thiswilladdanewformextensiontoourproject.

2. Locatethenewformextensioninourproject,andrenameitsoitwillremaingloballyunique,forexample,SalesTable.Con.

3. Opentheformextensioninthedesigner.4. Wecannowdraganyfieldorfieldgroup,includingextensionfieldsthatareavailabletothecurrent

package,tothedesign.5. Wecanalsochoosetochangepropertiesofthecontrolsontheform'sdesign.Therulehereisthat,if

itletsyouchange,itwillwork.

Rememberthatwhilstdoingthis,allchangesshouldbeatthelowestlevelpossibleasthisensuresconsistencyandminimizesmaintenanceeffort.

Thiscoversthedesign,butnotthecode.Wecanright-clickonmostmethodsanduseCopyevent-handlermethodtocreateapre-eventorpost-eventhandler.Thismaysufficeinsomecases,butwecangofurther.IntheNovemberupdate,wecannowcreateextensionclassesthatactasanextensiontotheform'scode.

Thefollowingexampleexplainsoneofthemanybenefitsthatmaynotimmediatelybeobvious.

Tocreateanextensionclassforaform'scode,followthesesteps:

1. Createanewclassthatmustendin_Extension;so,foraSalesTableextension,useConSalesTable_Extension.

Forthistowork,weonlyneedtohavetheExtensionOfdecorationandensurethatthenameendsin_Extension.

2. Theclassdeclarationshouldreadasfollows:

[ExtensionOf(formStr(SalesTable))]

finalclassConSalesTable_Extension

3. Inthisexample,wewilldeterminethecallerrecordbyhandlingtheinitializedeventandusethestoredrecordthatisscopedtotheformwhenweclosetheform.Thecodeforthisisasfollows:

[ExtensionOf(formStr(SalesTable))]

finalclassConSalesTable_Extension

{

PrivateCustTableconCustTable;

[FormEventHandler(formstr(SalesTable),

FormEventType::Initialized)]

publicvoidinitializedFormHandler(xFormRunformRun,

FormEventArgse)

{

Argsargs=formRun.args();

switch(args.dataset())

{

casetableNum(CustTable):

conCustTable=args.record();

break;

}

}

[FormEventHandler(formstr(SalesTable),

FormEventType::Closing)]

publicvoidinitializedFormHandler(xFormRunformRun,

FormEventArgse)

{

if(conCustTable.RecId!=0)

{

if(FormDataUtil::isFormDataSource(

conCustTable))

{

FormDataUtil::getFormDataSource(

conCustTable).research(true);

}

}

}

}

Wedonotnormallyprefixvariablesinourowncode,butintheprecedingcaseweprefixedCustTablewithCon.Theextensionclassislikeadecoration,andthevariableswecreatemustbeuniqueinthescopeoftheforminstance.

Ifyouwanttowriteamethodtooverrideaform-controlevent,justasamodifiedeventofItemId,wewillperformthefollowingsteps:

1. AddthefollowinglinestotheinitializedFormHandlermethodcreatedearlier:

FormDataObjectitemIdDataObject;

itemIdDataObject=this.SalesLine_DS.object(

fieldNum(SalesLine,ItemId),1);

itemIdDataObject.registerOverrideMethod(

methodStr(FormDataObject,modified),

methodStr(ConSalesTable_Extension,

modifiedItemIdHandler),this);

2. Thenaddtheoverridehandler,asfollows:

///<summary>

///Handlesthemodifiedeventofthe

///ItemIddatasourcefield

///</summary>

///<paramname="itemIdFieldObject"></param>

publicvoidmodifiedItemIdHandler(

FormDataObjectitemIdFieldObject)

{

//writewhatshouldhappenhere

}

FormDataObjecthasthesettingsforthedatasourcefield,suchasAllowEdit.ItalsohasadataSource()methodtofetchtheFormDataSourceobject.Thiswouldbeneededifthemethoddidn'talreadyhaveaccesstotheform'sdatasources,whichitdoes,astheyaredeclaredpublic.

3. Thisisit.Weshouldexperimentandgetusedtothisparadigmbeforeusingitforreal.

Howitworks...Theformextensionworkslikeanyotherextension;theyaredeltachangesthatareappliedwhenthepackageisdeployed.

Thenewconcepthereistheextensionclass.Thetermextensionisusedspecificallytoavoidconfusionwiththeextendskeyword.Extensionclassesareautomaticallyinstantiatedinplaceofthestandardclassandallowthemtobeaugmented.Thereisnoaccesstoprivatemethods,butwecanaddstatevariablesandaccessthemalongwiththeform'spublicvariablesandmethods.Critically,wedohavefullaccesstothedatasourcesontheform.Theeventhandlerscanbecreatedasinstancemethods,whichgrantsthemaccesstotheextensionclass'svariablesandmethods,butalsothepublicvariablesandmethodsintheclasswecreatedtheextensionfor.

Theclassmustbecreatedasfinal,becauseitisonlyinstantiatedinthecurrentpackage.Therecanbeextensionclassesinotherpackages,whichwecannotaccess,andshouldnotwantto.Thewholepointisforustoknowthatinthispackage,thatform'sclasswillbeinstantiatedasthisclass.

There'smore...Therearemanymoretypesofextensionsthatwecanperform,butallfollowthesametheme.Ensurethatthenamewegiveisnamedcorrectlyandisunique(andcertainlydoesn'thavethesuffixof.extension).

YoucantellwhichobjectscanhaveextensionduetotheApplicationExplorerhavinganodeformthen;forexample,MenuItemsisfollowedbytheMenuItemExtensionsnode.Orsimply,ifyoucanselectCreateextension,youcan!

Developinginthiswayrequiresaslightparadigmshiftinordertomakeitseemnatural.Itseemsthatthosenewtowritingthiswayarethinkingfromacustomization(over-layer)viewpointandthesolutionseemstobemoreofabruteforceworkaroundthanarealsolution.

Forexample,whenwritinganeventhandler,theeventonlyhasaccesstothepublicvariablesandmethodsofthecaller.Wecan,however,usereflectiontogainaccesstothecaller'sprivatevariablesandmethods.Ifwearedoingthis,itusuallymeanswearethinkingaboutthesolutioninthewrongway.ThetoolinginVisualStudioisimprovedwitheachrelease,andtheabilitytomakechangesviaextensionhasincreasedsignificantlyeachtime.

Reflectionisalargetopic,anddiscouragedbymany,asitwillmakeourcodevulnerabletoregressionasMicrosoftreleaseupdates.Formoreinformationonthistopic,pleaseseethefollowinglink:

https://msdn.microsoft.com/en-us/library/f7ykdhsy(v=vs.110).aspx.

AlthoughthepurposeofthisbookistoproviderecipestoextendDynamics365forOperationsthroughthemeansofextension,itisimportanttokeepinmindthereasonforavoidingover-layering:maintainabilityandserviceability.Ifwemakeahackychangetoavoidanover-layer,(likereflectionandinsomecasespre-eventorpost-eventhandlers),wecanactuallymakethecodelessmaintainable.Worsethanthat,thesystemmaycompileandpassyourtestscriptsandstillfailwhendeployedtoproductionaswehavemadeanassumptionabouttheinternalstateofaclass.ThepointIamtryingtomakeistokeepinmindthereasonsweavoidover-layeringstandardcode,whichisn'tjusttoavoidit.Itistorealizethemanybenefitsintermsofeaseofdeployment,andmaintainabilityofcodeasupdatesarereleased.

Usingaformeventhandlertoreplacealookup

Thisisacommonrequirementofaneventhandler,anddeservesitsownrecipe.PriortoUpdate3inNovember2016,theseeventhandlershadtobeplacedinautilityclass;wecannowhaveaclassthatactsasanextensiontotheform'scode-behind.

Thiswascoveredinthepreviousrecipe,andifyouareusingthistechnique,theeventhandlershouldbewrittentherewithoutthestatickeyword.

GettingreadyIdeally,weshouldhaveaformextensionclasswrittenforthis;otherwise,wejustneedaDynamics365forOperationsprojectopen.

Howtodoit...

Toaddacustomlookuptoacontrolonastandardform,pleasefollowthesesteps

1. Createanewclassthatcouldbeagenericsalesorderutilityclass,orideallyaformextensionclass(seepreviousrecipeforthis).

Thenamingiskeysothatwecaneasilyfindit.Thereisnoobviouslinkthatwehavedonethis,sonaminganddocumentationiscritical.

2. Thefirstexampleistomakethesalesordercreatedialogusingalookupform.Asstandard(intheNovemberrelease),thelookuponlycontainstheaccountnumberandaccount.Thisexamplebindsthecustomlookuptothecustomeraccount'slookupevent,asfollows:

///<summary>

///Overridethecustomerlookuponthesalesordercreate

///dialoginordertousealookupwiththeaddress

///</summary>

///<paramname="sender">callingcontrol</param>

///<paramname="e">formevents</param>

[FormControlEventHandler(formControlStr(SalesCreateOrder,

SalesTable_CustAccount),

FormControlEventType::Lookup)]

publicstaticvoidSalesTable_CustAccount_OnLookup(

FormControlsender,

FormControlEventArgse)

{

FormStringControlcustAccountCtrl=sender;

CustTableselectedCustomer;

FormRunformRun;

FormcustTableLookupForm;

custTableLookupForm=new

Form(formStr(CustTableLookup));

FormControlCancelableSuperEventArgseventArgs=e;

ArgsformArgs=newArgs();

formArgs=newArgs();

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),

tableStr(CustTable)),

formArgs);

custAccountCtrl.performFormLookup(formRun);

eventArgs.CancelSuperCall();

}

Ifyouwishtocreateyourownlookup,itisalsofine.JustcreateanewformusingLookup-basicpattern.

Let'suseanexamplewherewearewritingacustomproductlookup:

1. Weneedtobindtheselectcontrol(theIDcolumn,suchastheItemIdfieldinourexample).2. Addthedatasourceasusual(orwriteaviewandusethat)andcompletethedesignpattern.When

addingfieldstothegrid,changetheAutoDeclarationpropertyoftheIDcolumntoYes.ThecontrolwillprobablybecalledGrid_ItemIdbydefault,whichisfine.

3. OverridetheinitmethodsowecantelltheformthattheGrid_ItemIdcontrolwillcontainthedatathatwewanttoreturn.Thisisdonewiththefollowingcode:

voidinit()

{

DictFielddictField;

FormStringControlcallerControl;

if(!element.args())

{

throwerror(strfmt("@SYS22862",element.name()));

}

super();

element.selectMode(Grid_ItemId);

}

4. Next,weshouldhandlethefilteringofthecontrolbasedonthecallingcontrol,whichisdonewiththefollowingcode:

publicvoidrun()

{

FormStringControlcallerControl;

booleanfilterLookup=false;

callerControl=SysTableLookup::getCallerStringControl(

element.args());

filterLookup=SysTableLookup::filterLookupPreRun(

callerControl,

Grid_ItemId,

InventTable_DS);

super();

SysTableLookup::filterLookupPostRun(filterLookup,

callerControl.text(),

Grid_ItemId,

InventTable_DS);

}

InventTable_DSisthedefaultdatasourcenameifweaddedtheInventTabletable.Replacethisasrequiredwiththedatasourcenameoftheactualtableused.

5. Weshouldnowcreateamenuitem,andaddittoanappropriatesecurityprivilege.

LookupsinOperationsdon'talwaysuseaformand,infact,theyareusuallyautomaticbasedontheautoLookupfieldgroup.Wecanwritesuchalookupprogrammatically:

1. Createaclass,ConGeneralHandlers,forexample,andaddthefollowingmethod:

///<summary>

///Performsalookuponthe<c>InventTable</c>table.

///</summary>

///<paramname="_lookupCtrl">

///The<c>FormStringControl</c>controlthatthe

///lookupwillbeattachedto.

///</param>

///<paramname="_itemId">

///Theitemthatisusedtofilterthelookup.

///</param>

publicstaticvoidlookupItemId(

FormStringControl_lookupCtrl,

ItemId_itemId)

{

SysTableLookupsysTableLookup

Queryquery=newQuery();

QueryBuildRangequeryBuildRange;

QueryBuildDataSourcequeryBuildDataSource;

sysTableLookup=SysTableLookup::newParameters(

tableNum(InventTable),

_lookupCtrl)

queryBuildDataSource=query.addDataSource(

tableNum(InventTable));

if(_itemId)

{

queryBuildRange=queryBuildDataSource.addRange(

fieldNum(InventTable,ItemId));

queryBuildRange.value(queryValue(_itemId));

}

sysTableLookup.addSelectionField(

fieldNum(InventTable,ItemId));

sysTableLookup.addLookupMethod(

tableMethodStr(InventTable,

itemName));

sysTableLookup.addLookupMethod(

tableMethodStr(InventTable,

modelGroupId));

sysTableLookup.addLookupMethod(

tableMethodStr(InventTable,

itemGroupId));

sysTableLookup.addLookupField(

fieldNum(InventTable,

ItemType));

sysTableLookup.parmQuery(query);

sysTableLookup.performFormLookup();

}

2. Wemayneedtoreplacethesupercallonthelookupmethodonthefieldwithinadatasource;thisisdonebyright-clickingontheMethodsnodeforthedatasourcefieldinquestionandchoosingOverride|Lookup.Removethesuper()callsoyoudon'tgettwolookups!

3. Touseitasbeforeinaneventhandlerisdoneasfollows:

///<summary>

///Overridetheitemlookuponthesalesorderform

///</summary>

///<paramname="sender">ItemIdcallercontrol</param>

///<paramname="e">argssowecancancelsuper</param>

[FormControlEventHandler(formControlStr(SalesTable,

SalesLine_ItemId),

FormControlEventType::Lookup)]

publicstaticvoidSalesLine_ItemId_OnLookup(

FormControlsender,

FormControlEventArgse)

{

FormStringControlitemIdCtrl;

FormControlCancelableSuperEventArgseventArgs;

eventArgs=e;

itemIdCtrl=sender;

ConGeneralhandlers::lookupItemId(

itemIdCtrl,

itemIdCtrl.text());

eventArgs.CancelSuperCall();

}

4. ThesetechniquescanbeusedasdesiredtoadjustorreplacethelookupsinDynamics365forOperationswithoutanycustomizationtothestandardcode.

Howitworks...Thefirstpartistouseacustomform.Thebindingtothelookupeventisdoneusingaspecificeventhandlerforthispurpose.Theseeventshaveoneveryimportantfeature,apartfromallowingustobindtoaformcontrolevent;inthat,theypasstheargumentsobjectasaFormControlEventArgsobject.

WecanthencastthisasaFormControlCancelableSuperEventArgsobject.WhenwecalltheCancelSuperCall()method,ittellsthecallingcontrolnottocallitssuper.Inthecaseofalookupevent,wewouldotherwiseendupwithtwolookups:ournewlookupandthenthestandardlookup.

Apartfromgatheringinformation,theotherkeypartisthatweconstructtheFormRunobjectusingafactory,whichcreatesFormRunsothatitbehavesasalookupandnotjustaform.

Thesecondpartwastocreateourownlookup,whichwaslargelyrepeatingwhatwehavealreadylearnt.Justfollowthepattern.However,thepatterndoesneedalittlehelp,soitunderstandshowtobehaveitself.Thisiswhywehavetooverridetheinitandrunmethods.Evenso,thisisalotlesseffortthaninpriorreleases.

Thethirdpartwastocreatealookupprogrammatically.Thisisoftenpreferableforsimplelookups,andwecouldalsoaddchilddatasourcesandfilterstothequery.Thiswasdoneintwomethods,asitiscommontouselookupslikethisinourowncode.Sothelookupmethodcouldbeusedasthelookupmethodonadatasourcefield.

CreatingyourownqueryfunctionsQueryfunctionsareusedinuserqueries.Oneoftheissuestheycansolveiswhensubmittingbatchroutines,whereaqueryrangewouldbebasedonthecurrentdate.

OnesuchfunctioniscurrentDate().Thisisusedinaqueryrangeas(currentDate()).Thesystemseestheroundbracketsandknowstolookforaqueryfunction.Wheneverthequeryisexecuted,thesystemwillusethecurrentsystemdate.

InprioreditionsofAX,wewouldaddpublicstaticmethodstotheSysQueryRangeUtilclass.Thiswouldmeananover-layerand,consequently,Microsofthasprovidedawaytoaddnewqueryfunctionswithoutover-layering.

Howtodoit...

Tocreateaqueryfunction,followthesesteps:

1. CreateanewclassintheprojectandnameitConQueryRangeFunctions.2. Toaddaqueryfunctiontotheclass,inthiscase,togetthecurrentworkerrecordID,writethe

followinglinesofcode:

[QueryRangeFunction]

publicstaticstrCurrentWorkerNum()

{

RefRecIdcurrentWorkerRecId=

HcmWorkerLookup::currentWorker();

HcmWorkerworker;

if(currentWorkerRecId==0)

{

//ifwereturnandemptystring,itwillmatchall

//records.Soreturnastringthatwillbothcause

//thenorecordstobefoundinthequeryrange,

//butalsotogivetheuseraclueastowhat

//happened.Youshouldcreatethisasalabel,

//withoutspaces,usthefollowingasanexample.

return'#NotFound#';

}

selectPersonnelNumber

fromworker

whereworker.RecId==currentWorkerRecId;

returnworker.PersonnelNumber;

}

YoumaynoticethatittriestoaddAttributetotheend,ifyouuseintelli-type,andthatsomeofthemethodsinSysQueryRangeUtilalsoendinAttribute.Thisistheoldconventionandisnolongerrequired.

3. Thesecondpartofthetrickishowtouseit.Buildtheprojectandopenhttps://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=USMF&mi=HcmWorkerListPageinyourbrowser.

4. SelecttheOPTIONSactionpanetab,asshowninthefollowingscreenshot:

5. ClickonAdvancedFilter/SortandaddarangeforPersonnelnumber,asshowninthefollowingscreenshot:

6. TypethefollowinglineofcodeintotheCriteriacolumnandpressOK:

(ConQueryRangeFunctions::CurrentWorkerNum())

7. Youshouldsee,whenusingtheContosodemodataintheUSMFcompany,thatonlytheuserJuliaFunderburkisshown.

8. IfyouclickontheheadingforPersonnelnumber,ourqueryfunctioncodeisalsolistedasamatchescriterion.Asthesefunctionsareusuallyusedwhensubmittingroutinesorprintingreports,theadvancedfilterdialogismorecommonlyused.

Howitworks...Whenthepackageisbuilt,theQueryRangeFunctionattributedirectsthecompilertomakethemavailableforuseinquerydialogs.ThesyntaxinthebracketsisnotX++,itisinterpretedatruntimeandjustlookssimilar.Theroundbracketinstructsthecompilertoexpectafieldorfunctionname,andanyfunctionnotinthebaseSysQueryRangeUtilclassrequiresthe(<classname>::<functionname>())format.

Theerrorcheckingislimitedandtheerrorsaren'thelpful.Forexample,ifweforgetoneofthecolons,wewillgetanerrorsuggestingthatarightparenthesisiswrong.Thereisalsonohelptotheuserastohowtousethemfromwithintheuserinterface;so,wemustprovidedocumentationwiththesefunctions.

Apartfromthat,wegetaverypowerfulwaytoprovidetheuserswithsomeverypowerfulfunctionsthattheycanusetosimplifyandautomatetheirprocesses.

DataManagement,OData,andOfficeInthischapter,wewillcoverthefollowingrecipes:

CreatingdataentitiesExtendingstandarddataentitiesImportingdatathroughdataimport/exportReading,writing,andupdatingdatathroughOData

IntroductionMicrosoftDynamics365forOperationsisacloudsolution,andevenwhenitisavailableon-premise,theobvioushighavailability/disasterrecoverysolutionwouldbetoswitchtoMicrosoftAzure.So,evenwhenoursolutionisdesignedtobeon-premise,weshouldn'twriteourintegrationstoaccesslocalareanetworkresources.

AllintegrationsshouldhaveaserviceendpointthatwillbeaccessedbyDynamics365forOperations.

Tofacilitatewritingintegrationsthatareagnosticofthelocalnetworkresources,MicrosofthasevolvedtheDataImport/ExportFramework(DIXF)inthisrelease,tohelpresolvemanyoftheintegrationsissueswewilloftenface.ItalsoopensupamuchmoreintegratedwayinwhichwecancommunicatewithMicrosoftOffice.

Inthischapterwewillcovertheusageandextensibilityoptionsfordataentities,andalsohowtointeractprogrammaticallywithourdataentitiesthroughOData.

Creatingadataentity

Inthistask,wewillcreateadataentityforourvehicletable,whichwewillextendinordertodemonstratehowdataentitiescanbeused.WewillalsousethistoallowustomaintainvehicledatathroughtheOfficeadd-inandmakeitapublicODataentity.

GettingreadyWewilljustneedtohaveaDynamics365forOperationsprojectopen,andatableforwhichwewanttocreateadataentity.

Howtodoit...Tocreatethedataentity,followthesesteps:

1. Intheproject,addanewitem.WithintheAddNewItemdialog,selectDataModelfromtheleft-handlist,andthenDataEntityfromtheright.

2. EnterConWHSVehicleTableEntityasNameandpressAdd.3. WewillthengetaDataEntityWizarddialogandselectConWHSVehicleTableinthePrimarydatasource

drop-downlist.

Asyouscrolldown,thedrop-downlistcanresize,causinganitemtobeselectedbymistake,itisthereforeeasiertousethePageUpandPageDownkeystolocatethetable.

TheEntitycategoryisnotcorrectbydefault;usethefollowingtabletoselectthecorrectcategory:

Tablegroup Entitycategory

Main,Group Master

Worksheet(alltypes) Document

Transaction(alltypes) Transaction

Parameter Parameter

Reference Reference

4. ThedialogmadeaguessthatConwasaprefixandstrippedthisfromthePublicentitynameandPubliccollectionnamefields;theprefixshouldbeputbacktoavoidthechanceofnamingcollision.

Thedefaultswillcreateapublicinterfaceforaccessbyotherapplications,suchasMicrosoftOffice,astagingtableforusewiththeDataImport/ExportFramework,andsecurityprivilegesinordertocontrolwhohasaccesstothisentity.

5. ClickonNext.

Onthispage,wecanaddrelateddatasourcesandvirtualfields.Inourcase,thisis

notrequired,andwewillcoverthisoptionintheThere'smore...section.

6. IfwecheckConvertlabelstofieldnames,itwillusethefield'slabelsforthefieldnames.Thisisnotusuallydesirable;thelabelmayneedthecontextoffieldgroupinorderforustoknowwhichfielditrelatesto.Donotcheckthischeckbox.

Agridiscreatedbasedonthetable'sdefinition,whichwillbeusedasthesettingsusedtogeneratethedataentity.Thesesettingsareusuallycorrect;inourcase,thegridisasfollows:

Wecanmakeadditionalfieldsmandatory;however,ifwedecidetouncheckamandatoryfield,wewillgetanerrorunlessitisspecifiedincodebeforetheactualrecordisinsertedorupdated.Thiscanbeusefulwhenthemandatoryfieldisinferredfromanotherfieldinthedataentity.

7. ClickonFinish.

Thewizardhascreatedthefollowingobjectsforus:

Element Description

ConWHSVehicleTableEntity Thisisthedataentity

ConWHSVehicleTableStagingThisisatableusedtostagedatawhenimportingviatheDataImport/ExportFramework

ConWHSVehicleTableEntityMaintain Thisisasecurityprivilegetoallowusfullaccesstothedataentity

ConWHSVehicleTableEntityView Thisisasecurityprivilegetoallowview-onlyaccesstothedataentity

8. Buildtheprojectandsynchronizeitwiththedatabase.9. Openthemainformforthedataentity;inourcase,theVehiclesform,whichcanbeaccesseddirectly

usingthefollowingURL:

https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=usmf&mi=ConWHSVehicleTable

10. Onthetoprightofthescreen,theOfficeiconhasanewoption,OPENINEXCEL,asshowninthe

followingscreenshot:

11. IfyouhoverthemouseovertheVehicletable(usmf)link,youwillseethatitisourentity,asshowninthefollowingscreenshot:

12. Thisisthepublicentitynamewespecifiedinthewizard,andwecanchangeitbychangingthePublicEntityNamepropertyonthedataentity.

13. ClickontheVehicletable(usmf)link,andthenclickDownloadintheOpeninExceldialog.14. OnceMicrosoftExcelopens,youmaygetthefollowingwarning:

15. ClickonTrustthisadd-in.16. Next,clickonSignin,andsigninusingthesameaccountyouusedforloggingintoDynamics365for

Operations.

Oncesignedin,itwillpopulateasheetwiththedatafromtheConWHSVehicleTabletable,butonlyaddthemandatoryfields.Totesttheentity,weshouldaddafewfields.

17. Intheadd-in,clickonDesignandthenclickontheediticonnexttotheConWHSVehicleTabletable,asshowninthefollowingscreenshot:

18. Inthenextpage,selectallofthefieldsintheAvailablefieldslistandpresstheAddbuttonthatisjustabovetheSelectedfieldslist.

19. ClickonUpdate,andthenclickYestothewarning.20. ClickonDone,whichtakesusoutofthedesignexperience,andthenpressRefreshtofetchthenew

data.21. Oursheetshouldnowhavethedatafromthevehicletable,asshowninthefollowingscreenshot:

Theheadingsarelabelsinyouruser'slanguage,andtheenumeratedtypesarealsotranslated.

22. Editoneormoreofthefieldsusingtheadd-intoselectthevalueswhentheyhaveadrop-downlistordatepicker.DonotchangetheVehicleIdvalue,butyoucantestthisyourselfinadifferenttesttoseewhathappens.

23. ClickonNewintheadd-ininordertoaddanewvehicle,andcompletethesheetasrequired.Oncedone,theresultshouldbesimilartothefollowingscreenshot:

TheCompanycolumnwasleftblank,andweshouldactuallyremovethiscolumnfromthesheet.Ifyourememberthatthelinkhadthecompanywithinthelink'sname,thisconnectionisboundusingthatcompanyID.

24. Oncedone,clickonPublish;theentitywillberefreshedwiththecompanyIDthatwasactuallyusedwhentherecordsarecreated.

25. Finally,refreshtheVehiclesforminDynamics365foroperations,andyouwillseetherecordswithinthevehicleslistpage.

26. ClosetheExcelworksheet.27. Ideally,wewouldwanttocontrolwhichfieldsareavailable,soopentheConWHSVehicleTableEntitydata

entityinthedesigner.

Thepropertiesareverysimilartothoseofatable,andthenodesinthedesignsharethoseofbothqueriesandtables.Infact,thisiscreatedintheSQLserverdatabaseasaviewand,ifwesynchronizedthedatabase,wecouldviewthedatainSQLServerManagementStudio.

28. AddthefieldsyouwouldliketoseebydefaulttotheAutoReportfieldgroup.29. YoumayalsohavenoticedthattheVehicleGroupIdfielddidnothaveadrop-downlistinExcel,andthe

foreignkeyrelationdoesnothelpinthiscase.Wewillneedacustomlookup,asshowninthe

followingpieceofcode:

///<summary>

///Acustomlookupforvehiclegroupids

///</summary>

///<paramname="_fields">

///Thisisthefields'metadataprovidedbythe

///officeadd-in

///</param>

///<returns>

///Aserializedlistofvehiclegroupids

///</returns>

[SysODataAction(

'ConWHSVehicleTableEntityVehicleGroupLookup',

false),

SysODataCollectionAttribute('_fields',Types::String),

SysODataFieldLookup(fieldStr(ConWHSVehicleTableEntity,

VehicleGroupId))]

publicstaticstrLookupVehicleGroupId(Array_fields)

{

RefFieldNamevehicleGroupIdFld;

vehicleGroupIdFld=fieldStr(ConWHSVehicleTableEntity,

VehicleGroupId);

//Buildafieldandvaluemapfromthe_fieldsArray

MapfieldMap;

fieldMap=OfficeAppCustomLookupHelper::getFieldMap(

tableStr(ConWHSVehicleTableEntity),_fields);

//Determinethecompanythattheofficeadd-inis

//connectedto,otherwiseitwillreturndatafromDAT

DataAreaIddataAreaId=curExt();

RefFieldNamedataAreaIdFld;

dataAreaIdFld=fieldStr(ConWHSVehicleTableEntity,

DataAreaId);

if(fieldMap.exists(dataAreaIdFld))

{

dataAreaId=fieldMap.lookup(dataAreaIdFld);

}

//Constructtheresultobject,andaddourID

//fieldtothelistasthefirstelementinthearray

OfficeAppCustomLookupListResultresult;

result=newOfficeAppCustomLookupListResult();

result.determinationFields().value(1,

vehicleGroupIdFld);

//declaretheresultStringhere(latest)asitneeds

//tobeinscopeforwhenitisset,andreturnedto

//thecaller

strresultString;

//Checkthatthekeyfieldisinthesuppliedmetadata

if(OfficeAppCustomLookupHelper::fieldsBound(

result.determinationFields(),fieldMap))

{

intcounter=1;

//changetothecompanytheofficeadd-in

//isconnectedto

changecompany(dataAreaId)

{

ConWHSVehicleGroupvehicleGroups;

//Addthevehiclegroupidstothevaluearray

whileselectVehicleGroupId

fromvehicleGroups

orderbyVehicleGroupId

{

result.items().value(counter,

vehicleGroups.VehicleGroupId);

counter++;

}

}

resultString=result.serialize();

}

returnresultString;

}

30. Rebuildtheprojectandtesttheadd-inagain;youwillgetthefieldsthatyouaddedtothefieldgroupalongwiththedrop-downlistontheVehiclegroupcolumn.

Howitworks...Whenwecreatedtheentitywithapublicinterface,itactuallycreatesaservicethatofficecommunicateswith.TheExcelfilewedownloadedwasjusttoallowconnectiontothedataentityusingOData.Wearen'treadingrecordsdirectly,therecordsarereadfromDynamics365forOperationsandarewrittenbackwhenwepublishthechanges.

TheauthenticationgoesthroughourMicrosoftOffice365account,andwhenhostedinAzure,theadd-intakescareofthecomplexitiesofthisintegrationforus.Itissecure(anditalsohonorsXDSdatapolicies),yetavailableeverywhere.

Untilwegottowritingthelookup,theprocesswasremarkablyeasy,ifwetakeintoaccounttheresultweachievewithsuchlittleeffort.It,therefore,stoodoutthatwehadtowritequiteacomplicatedmethodforthelookup.

Themethoddoeslookalittledaunting;however,whenbrokendown,itbecomeseasiertounderstand.

ThefirstkeypartofthemethodistheSysODataFieldLookupdecoration,whichishowitknowsforwhichfieldthelookupisbound.

Asweareusingastaticversionofthepattern,the_fieldsparameterprovidesbothmetadataandthevalueofeachfieldinthedataset.ThisisconvertedintothefieldMapmapforeaseofuse.

Wewillneedthisinordertoworkoutwhichcompanyweareworkingin,sowewillreturndatafromthatcompany.Thisdiffersfromotherstaticmethods;inthat,itiscalledwithacompanycontext.ThisisdonebylookingupthevalueofthedataAreaIdfieldasfollows:

dataAreaIdFld=fieldStr(ConWHSVehicleTableEntity,DataAreaId);

if(fieldMap.exists(dataAreaIdFld))

{

dataAreaId=fieldMap.lookup(dataAreaIdFld);

}

Wehavetocheckifitexistsbeforewelookitup;ifnot,itwillthrowanerrorifthekeydoesn'texistinthemap.

Beforeaddingthevaluestothearray,weshouldcheckthattheVehicleGroupIdfieldisbound(isinthe_fieldsarray).WeusedOfficeAppCustomLookupHelper::fieldsBound()forthis.Inourcase,itissemanticallyequivalenttocheckingthatthefieldexistsinthefieldMapmap.

Theactuallookupdataisconstructedbythefollowinglineofcode:

result.items().value(counter,vehicleGroups.VehicleGroupId);

Theresult.items()functionisanarrayorstringvalues.

Toreturnthedata,ithastobeserializedtoastring,whichwasdonebythefollowinglineofcode:

resultString=result.serialize();

There'smore...Thereareafewspecialmethodsthatareusedwhenwritingadataentity,whichareasfollows:

Method Description

updateEntityDataSource Thisiscalledwhenupdatinganexistingrecord

deleteEntityDataSource Thisiscalledwhendeletingarecord

insertEntityDataSource Thisiscalledwheninsertinganewrecord

initializeEntityDataSource Thisiscalledwhenarecordisinitialized.

Thesemethodsareusedsothatwecanupdaterelatedrecordsthatarenotdirectlyaffectedbythetablesintheentity.AgoodexampleiswhenatableisrelatedtoDirPartyTable.Wecan'tsimplydeletetherelatedpartyrecordinthiscasewhentheparentisdeleted,asitmaybeusedinotherroles.

ThefollowingaresamplemethodsforafictitiousConWHSHaulierTableEntity,whichhasamaindatasource,ConWHSHaulierTable,andachilddatasource,DirPartyBaseEntity.

InthismethodwewillcheckiftheentityistheDirPartyBaseEntitydatasource,andifitis,itexecuteslogictohandletheDirPartyTableandLogisticsPostalAddresstables.Thesetablesarepartofcomplicatedstructures,andthehelperensuresthattheyareinsertedcorrectly:publicbooleaninsertEntityDataSource(DataEntityRuntimeContext_entityCtx,DataEntityDataSourceRuntimeContext_dataSourceCtx){booleanret;

switch(_dataSourceCtx.name()){casedataEntityDataSourceStr(ConWHSHaulierTableEntity,DirPartyBaseEntity):DirPartyBaseEntityHelperpartyHelper;partyHelper=newDirPartyBaseEntityHelper();partyHelper.preInsertEntityDataSource(_entityCtx,_dataSourceCtx,dataEntityDataSourceStr(ConWHSHaulierTableEntity,

LogisticsPostalAddressBaseEntity));

ret=super(_entityCtx,_dataSourceCtx);

if(ret){partyHelper.postInsertEntityDataSource(_entityCtx,_dataSourceCtx,dataEntityDataSourceStr(ConWHSHaulierTableEntity,LogisticsPostalAddressBaseEntity));}break;default:ret=super(_entityCtx,_dataSourceCtx);}returnret;}

Thefollowingcodehandlesthedeletionlogicandwillcorrectlyhandletheupdatetotheglobaladdressbook(DirPartyTable),removingthelinkscorrectlyforus:publicbooleandeleteEntityDataSource(DataEntityRuntimeContext_entityCtx,DataEntityDataSourceRuntimeContext_dataSourceCtx){booleanret;

switch(_dataSourceCtx.name()){casedataEntityDataSourceStr(ConWHSHaulierTableEntity,DirPartyBaseEntity)):DirPartyBaseEntityHelperpartyHelper;partyHelper=newDirPartyBaseEntityHelper();partyHelper.deleteEntityDataSource(_dataSourceCtx);break;default:ret=super(_entityCtx,_dataSourceCtx);}returnret;}

Thefinalmethodinthissetisthecodetohandlewhathappenswhenrecordsareupdated,thisisidenticaltotheinsertEntityDataSourcemethodexceptthatthemethodiscalledupdateEntityDataSource.

Thefollowingmethodiscalledwhentherecordisinitialized;thecodeinthismethodisusedcorrectlytoinitializetheglobaladdressbookdatastructures:publicvoid

initializeEntityDataSource(DataEntityRuntimeContext_entityCtx,DataEntityDataSourceRuntimeContext_dataSourceCtx){super(_entityCtx,_dataSourceCtx);

if(_dataSourceCtx.name()==dataEntityDataSourceStr(ConWHSHaulierTableEntity,DirPartyBaseEntity)){//Takescareofmaintainingthereferencetoexisting//partiesifthisrecordprovidesapartynumber.Thisis//because,eventhoughwemaybeinsertingthecustomer//record,thepartymayalreadyexist.DirPartyBaseEntity::initializeDirPartyBaseEntityDataSource(_entityCtx,_dataSourceCtx);}}

Youcanseeexamplesofhowtheseareusedinmanyofthestandardentities;agoodexampleisCustCustomerEntity.

Seealso

ThefollowinglinksprovidesomeguidanceonrelatedfeaturesandbackgroundtodataentitiesandOData:

AddtemplatestoOpenlinesinExcelmenu(https://ax.help.dynamics.com/en/wiki/add-templates-to-open-lines-in-excel-menu/)OpenlinesinExcelfromjournalsanddocuments(https://ax.help.dynamics.com/en/wiki/open-lines-in-excel-from-journals-and-documents/)CreateOpeninExcelexperiences(https://ax.help.dynamics.com/en/wiki/off101-office-integration-enable-users-to-edit-data-in-excel/)

Theprecedinglinkshowshowtocreatealookupusingapatternthatdiffersfromhowitisusedwithinstandardsoftwareandinthisrecipe.Ichosetousethepatternfromthestandardsoftwareinsteadoftheversioninthisguide.

Officeintegrationtroubleshooting(https://ax.help.dynamics.com/en/wiki/office-integration-troubleshooting/)Securityanddataentities(https://ax.help.dynamics.com/en/wiki/security-and-data-entities/)

Extendingstandarddataentities

ExtensibilityisbecomingmoreandmorepervasiveinthedevelopmentparadigmofDynamics365forOperations,anditisimportanttobeabletohaveextensibledataentities;otherwise,wewouldhavetowritenewonestobeabletouseafieldweaddedtoatableasanextension.

Inthisexample,wewillcreateanextensionfortheReleasedproductcreationentity,namedEcoResReleasedProductCreationEntity.

Gettingready

Partofthisrecipeistoaddanextensionfieldsowecanimportdataintoit,sothefirstpartistocreateanextensionfortheInventTabletablewithanewfield.Thisisoptional,butisincludedinordertodemonstratehowthisisdone.

Tofollowthisoptionalstep,createatableextensionfortheInventTabletableandaddanewfieldoftypeNamecalledConWHSAdditionalName.Also,addthistoaformextensionsowecanseetheresults.

Howtodoit...

Tocreateadataentityextension,followthesesteps:

1. IntheApplicationExplorer,expandDataModelandthenDataEntities.Right-clickonEcoResReleasedProductCreationEntityandselectCreateextension.

2. Renamethesuffixfrom.Extensionto.ConWHS,oryourpackagename.

AfterexaminingtheDataSourcesnode,wecanseethatitisnotbaseddirectlyonInventTable,butonadataentity.Wewill,therefore,needtocreateanextensionforthisbeforewecanaddthefield.

3. LocateEcoResReleaseProductEntity,createanextension,andrenameitappropriately.4. ExpandtheDataSourcesnodesandthenInventTable,andthenlocatetheextensionfield

(ConWHSAdditionalName).Right-clickonthefieldandchooseCopy.5. CollapsetheDataSourcesnodeandright-clickontheentity'srootFieldsnode.Right-clickonthe

FieldsnodeandselectPaste.

Alternatively,youcanright-clickontheFieldsnode,chooseNew|MappedField,andcompletethepropertysheet.

6. Saveandclosethedataentity.7. OpentheEcoResReleasedProductCreationEntity.ConWHSdataextensioninthedesignerandexpandtheData

SourcesandEcoResReleasedProductEntitynodes.8. LocatetheConWHSAdditionalNamefieldandusecopyandpastetoaddittothedataentity'sFieldsnode.9. SincethiswillbeusedwithintheDataImport/ExportFramework,wewillneedtoaddthefieldto

thestagingtable.Thiscannotbedoneautomaticallyasweareworkingwithanextension--thesystemcan'taddfieldstothestandardtableasthatwouldresultinanover-layer.

10. LocatetheEcoResReleasedProductCreationStagingtableandcreateanextension;nameitappropriately.11. AddtheConWHSAdditionalNamefieldaswedidonInventTable;youcanusecopyandpastetodothis.

WecanworkoutwhichtableisthestagingtablebylookingattheDataManagementStagingpropertyoftheoriginaldataentity,whichisEcoResReleasedProductCreationStaginginthiscase.

12. Forcompleteness,addtheConWHSAdditionalNamefieldtothestagingtablefortheEcoResReleasedProductCreationEntitydataentity.

13. Saveallandbuildtheproject.

YouwillneedtoaddareferencetoDimensionstoyourpackageinorderforthebuildtosucceed.

14. Finally,synchronizetheprojectwiththedatabase.

Howitworks...

Thedataentityextensionworksinasimilarwaytoanyotherextension--itstoresadeltachangeinanXMLdefinitionfilethatismergedwiththebaseentitywhentheprojectisbuilt.Shouldwelookattheentitywithintheclient,whichwedointhenextrecipe,wewillseethatthefieldsexistasiftheywerepartoftheentity.

Asitwasamappedfield,wedon'tneedtowriteanyspecialcodeinordertopersistthisthroughthestagingtabletothetargettable.Sincethisdataentityisnotpublic,itcan'tbeusedwiththeOfficeadd-inorthroughOData;itisintendedtobeusedwithintheDataImport/ExportFramework.

There'smore...

Theextentbywhichapplicationobjects(suchasforms,tablesanddataentities)areextensiblewillevolvewitheachrelease.Currently,wecanadddatasources,ranges,relations,fieldgroups,andtablemapstodataentities.Wecannotaddnewmethodsorchangeexistingones,asthiswouldresultinnopracticaldifferencetoanover-layer(exceptitcouldbeworse,asnotoolingwouldexistforconflictmanagement).Instead,wewouldsubscribetothemanyeventdelegates.

ImportingdatathroughDataImport/ExportFrameworkThisrecipeisusuallyasystemadministrationfunction,butisneededinordertotestourdataentities.Italsogivesusmoreinsightintounderstandinghowourdataentitieswork.

GettingreadyThisrecipefollowsonfromthepreviousone,wherewearetestingadataentityextension.

Howtodoit...ToimportandexportdatausingtheDataImport/ExportFramework,followthesesteps:

1. OpenDynamics365forOperationsinInternetExplorer;usethefollowinglinkonaDevelopmentvirtualmachine:

https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=USMF

2. Thefirststep,aftermakinganychangetothedataentities,istorefreshtheentitylist.OpentheDatamanagementworkspace,selectFrameworkparameters,andclickonRefreshentitylistintheEntitysettingstabpage.Thishappensasynchronously,andyouwillgetamessagewhenitiscomplete;youmayneedtoclickinandoutofformsforthemessagetoappear.

3. Next,wewillneedtocheckthatthefieldsexistandaremappedcorrectlyontheDatamanagementworkspace;clickonDataentities.

4. TypeinReleasedproductsintothefiltercontrolandselectEntity:"Releasedproducts"fromthedrop-downlist.

5. ClickonTargetfieldsandcheckthatthefieldexistsinthelist.Ifnot,youneedtorebuildtheprojectandsynchronizetheprojecttothedatabase.AfullbuildusingDynamics365|Buildmodelsmayberequiredifthispersists.

6. Then,clickonModifytargetmapping.Thegridmaybeempty,andifwehaven'tspecifiedanyspecificmappingpreviously,wecanusetheGeneratemappingbuttontocreatethedefaultfieldmappingforus.

7. Ifwehavespecifiedspecificmappinginthispreviously,clickingonGeneratemappingwilldeleteit.YoucanclickonNewandcompletetheline,orusetheMAPPINGVISUALIZATIONtabpage.Inthisform,dragthefieldfromtheright-handlisttothecorrespondingfieldontheleft.

8. Nowthemappingisdone,let'sexportsomedata.OntheDatamanagementworkspace,clickonExport.

Itmayseemoddtoexportadataentitythatisusedtocreatedata,butthefollowingstepsareusedtocreateanimporttemplateusedlaterintherecipe.

9. Completetheform,asshowninthefollowingtable:

Field Value

Name ReleasedProductCreationExport

Targetdataformat EXCEL

Entityname Releasedproductcreation

Skipstaging Yes

Defaultrefreshtype Fullpushonly

Selectfields Allfields

10. Oncecomplete,clickonAddentity.11. Thiscreatesanewtile.Withthisselected,clickonExportfromthetop-buttonribbon.

Themessagesuggeststhatyourefreshthepage;itmeansthattherefreshicononthetop-rightoftheform,notthebrowserrefreshbutton.

12. Aftersometime,itwillreportthatitsucceeded.ClickonDownloadpackageandsayYestothemessagethatfollows.

13. ThisdownloadsaZIPfile,whichwecanuseasanimporttemplate.14. Copythespreadsheetand,inthecopy,removeallrowsexceptthefirsttwo;aswewillusethefirst

productasatemplatetosavehavingtolookupvalidvalues:weareonlytestingthatitworksfornow.

15. ChangetheItemNumberandProductNumbercolumnstoX1000(anitemnumberthathasnotyetbeenused).

16. FillintheConWHSAdditionalNamecolumnwithsomethinguseful.17. SavetotheDocumentsfolderandclosethespreadsheet.18. GobacktotheDynamics365forOperationsclient,andnavigatebacktotheDatamanagement

workspace.ClickonImport.19. Completethefieldsasfollows:

Field Value

Name ReleasedProductCreationImport

Sourcedataformat EXCEL

Entityname Releasedproductcreation

20. ClickonImport.21. CheckthemappingbyclickingonViewmaponthenewlycreatedtile,andnotethatthemappingis

doneforus.22. LocatethespreadsheetcreatedearlierandpressOpen.23. ClickonUpload.

24. ThiscantakeawhileonadevelopmentVM,sojustbepatientandclicktheformrefreshbuttoneverysooften.Eventually,youwillgetatileasshowninthefollowingscreenshot:

25. ClickonViewstagingdataandcheckthattheAdditionalnamefieldiscompleted.Ifnot,themappingwasprobablynotcompletedcorrectly.

26. Afterashorttimelater,thetilewillchangetoshowthattherecordsarenowinthetarget.Checkthisbyopeningthereleaseproductsform;checkthatallfieldswerepopulatedcorrectly.

Howitworks...

ThereisalottotheDataImport/ExportFramework,someofwhichwewillcoverfurtherinthenextrecipe.Inthiscase,wewillcreateasimpleexporttocreateatemplatethatwethenusedtoimportsometestdata.Themappingworkedbasedonnamematching,andthisisusuallythebestmethod,especiallyforloadingtestdata.

SeealsoDataentitiesandpackagesframework(https://ax.help.dynamics.com/en/wiki/using-data-entities-and-data-packages/)Dataentitieshomepage(https://ax.help.dynamics.com/en/wiki/data-entities-home-page/)

Reading,writing,andupdatingdatathroughOData

Inthisexample,wewillcreateasampleODataconsoleapplicationinordertodemonstratehowtoconnectandcommunicatethroughOData.

Inordertostart,youwillneedtocreateanapplicationwithinyourorganization'sAzureID.YouwillneedtheapplicationID--anofficialguideastohowtodothisisavailableathttps://ax.help.dynamics.com/en/wiki/dynamics-ax-7-services-technical-concepts-guide/

GettingreadyThefollowingdoesn'thavetobedoneintheDynamics365forOperationsdevelopmentvirtualmachine.However,itwillneedtohaveaccesstotheURL.

Howtodoit...

ToimportandexportdatausingtheDataImport/ExportFramework,followthesesteps:

1. Createanewprojectbut,thistime,chooseVisualC#fromtheTemplatesnodeandthenConsoleApplicationfromtheright.NametheprojectConODataTestandplaceitintheprojectfolderthatwesetupforsourcecontrol.EnsurethatthenamespaceisalsoConODataTest.

2. WewillnowneedtoinstallsomeNuGetpackages.WithinVisualStudio,navigatetoTools|NugetPackageManager|PackageManagerConsole.

"NuGetisthepackagemanagerfortheMicrosoftdevelopmentplatformincluding.NET.TheNuGetclienttoolsprovidetheabilitytoproduceandconsumepackages.TheNuGetGalleryisthecentralpackagerepositoryusedbyallpackageauthorsandconsumers."-(https://www.nuget.org/)

3. InthePackageManagerconsole,typethefollowingcommands:

Install-PackageMicrosoft.Bcl.Build

Install-PackageMicrosoft.Bcl

Install-PackageMicrosoft.Net.Http

Install-PackageMicrosoft.OData.Core-Version6.15.0

Install-PackageSimple.OData.Client-Version4.24.0.1

Install-PackageMicrosoft.Data.OData

Install-PackageMicrosoft.OData.Client

Install-PackageMicrosoft.IdentityModel.Clients.ActiveDirectory

Simple.OData.Clientisaddedasanalternativeandisnotusedinthisexample.Thisiswhythespecific6.15.0versionofMicrosoft.OData.Corewasused.IfSimple.OData.Clientisnotbeingused,theversionneednotbespecified.

4. RestartVisualStudio,otherwise,thebuildwillfailwithoutanymessageastowhy.

5. Next,wewillneedanadd-inforVisualStudioinordertoreadthemetadataandgeneratetypesforus.NavigatetoTools|ExtensionsandUpdates.

6. ClickonOnlineontheleftandtypeODataClientCodeintheSearchVisualStudioGallerytextbox.

7. SelectODatav4ClientCodeGeneratorfromthelistandclickonDownload.

8. Onceinstalled,youwillbeaskedtorestartVisualStudio.

9. Oncerestarted,choosetoaddanewitemtotheproject.SelectODataClientfromthelist.NameitOdataClient.ttandclickonAdd.

Attfileisatransformationfile,anditspurposewillbecomemoreapparentasweprogress.

10. Towardthetopofthisfile,youwillseethefollowingline:

publicconststringMetadataDocumentUri="";

11. Changeitsothatitreadsasfollows:

publicconststringMetadataDocumentUri="https://usnconeboxax1aos.cloud.onebox.dynamics.com/data/$metadata";

12. Checkthatyouhaveaccesstohttps://usnconeboxax1aos.cloud.onebox.dynamics.com/inthebrowserandthatyoucanlogin.Minimize(donotclose)thebrowserandgobacktoVisualStudio.

13. WiththeODataClient.ttfileopeninthecodewindow,clickonSaveorpressCtrl+S.Youwillreceiveasecuritywarning,clickonOKtoproceed.Thiswilltakeafewminutestoproceed,asitisageneratedclientandtypecodeforallpublicentities.

Oncecomplete,youwillseeanewODataClient.csfilenestedundertheODataClient.ttfilewithmanymethods.Thefileisaround40MB,soopeningitwilltakeawhileandisbestavoided.ShouldyoufindthatVisualStudiostartstoperformslowly,ensurethatyouhaveclosedODataClient.ttandODataCilent.csandthenrestartVisualStudio.

14. Wecannowstartwritingthecodeforourtest;addanewclasstotheprojectnamedODataTest.

15. Addthefollowingusingstatementstothetopofthefile:

usingSystem;

usingSystem.Threading.Tasks;

usingMicrosoft.IdentityModel.Clients.ActiveDirectory;

usingMicrosoft.OData.Client;

usingConODataTest.Microsoft.Dynamics.DataEntities;

16. Tosimplifytheprocess,createthefollowdatacontractclassesinordertoaidpassingparameters.Addthemjustbelowtheusingstatements,andabovetheODataTestclassdefinition:

classODataUserContract

{

publicstringuserName;

publicstringpassword;

publicstringdomain;

}

classODataApplicationContract

{

publicstringapplicationId;

publicstringresource;

publicstringresult;

}

publicclassODataRequestContract

{

publicstringcompany;

}

Wewouldusuallyuseget/setmethodshere,butIusedpublicvariablestosavespaceforthistest.

17. Intheclassbody,declarethefollowingclassvariables:

classODataTest

{

publicconststringOAuthHeader="Authorization";

publicODataUserContractuserContract;

publicODataApplicationContractappContract;

publicODataRequestContractrequest;

stringauthenticationHeader;

publicstringresponse;

TheOAuthHeaderandauthenticationHeadervariablesarekeytotheprocessofauthenticationwithDynamics365forOperations.

18. ThecodetoauthenticatewithAzureADisasfollows:

privateAuthenticationResultGetAuthorization()

{

UriBuilderuri=newUriBuilder("https://login.windows.net/"+userContract.domain);

AuthenticationContextauthenticationContext

=newAuthenticationContext(uri.ToString());

UserPasswordCredentialcredential=new

UserPasswordCredential(

userContract.userName,userContract.password);

Task<AuthenticationResult>task=authenticationContext.AcquireTokenAsync(

appContract.resource,

appContract.applicationId,credential);

task.Wait();

AuthenticationResult

authenticationResult=task.Result;

returnauthenticationResult;

}

19. Now,createapublicmethodthatwillbecalledinordertologon:

publicBooleanAuthenticate()

{

AuthenticationResultauthenticationResult;

try

{

authenticationResult=GetAuthorization();

//thisgetstheauthorizationtoken,this

//mustbesetontheHttpheaderforallrequests

authenticationHeader=

authenticationResult.CreateAuthorizationHeader();

}

catch(Exceptione)

{

response="Authenticationfailed:"+e.Message;

returnfalse;

}

response="OK";

returntrue;

}

20. EachmethodthatmakesacalltoOperationsmustsetuparesourcesinstance,whichhasaneventhandlerinordertosettheauthenticationkey.Thismethodshouldbewrittenasfollows:

privateResourcesMakeResources()

{

stringentityRootPath=appContract.resource+"/data";

UrioDataUri=newUri(entityRootPath,

UriKind.Absolute);

varresources=newResources(oDataUri);

resources.SendingRequest2+=new

EventHandler<SendingRequest2EventArgs>(

delegate(objectsender,

SendingRequest2EventArgse)

{

//Thiseventhandlerisneededtoset

//theauthenticationcodewegotwhen

//weloggedon.

e.RequestMessage.SetHeader(OAuthHeader,

authenticationHeader);

});

returnresources;

}

21. Thenextthreemethodsaretodemonstratereading,updating,andcreatingrecordsthroughOData:

publicSystem.Collections.ArrayListGetVehicleNameList()

{

System.Collections.ArrayListvehicleNames;

vehicleNames=newSystem.Collections.ArrayList();

varresources=this.MakeResources();

resources.ConWHSVehicleTables.AddQueryOption(

"DataAreaId",request.company);

foreach(varvehicleinresources.ConWHSVehicleTables)

{

vehicleNames.Add(vehicle.Description);

}

returnvehicleNames;

}

publicBooleanUpdateVehicleNames()

{

varresources=this.MakeResources();

resources.ConWHSVehicleTables.AddQueryOption(

"DataAreaId",request.company);

foreach(varvehicleinresources.ConWHSVehicleTables)

{

vehicle.Description=vehicle.VehicleId

+":ODatadidit";

resources.UpdateObject(vehicle);

}

try

{

resources.SaveChanges();

}

catch(Exceptione)

{

response=e.InnerException.Message;

returnfalse;

}

returntrue;

}

publicBooleanCreateNewVehicle(

ConWHSVehicleTable_newVehicle)

{

varresources=this.MakeResources();

_newVehicle.DataAreaId=request.company;

resources.AddToConWHSVehicleTables(_newVehicle);

try

{

resources.SaveChanges();

}

catch(Exceptione)

{

response=e.InnerException.Message;

returnfalse;

}

returntrue;

}

22. Finally,wecanwriteourmainmethod.OpentheProgram.csfileandensurethatwehavethefollowingusingstatementsatthetop:

usingSystem;

usingConODataTest.Microsoft.Dynamics.DataEntities;

23. Writethemainmethodasfollows(usetheapplicationIDyougeneratedfromyourAzureADapplication):

staticvoidMain(string[]args)

{

ODataApplicationContractappContract;

appContract=newODataApplicationContract();

appContract.resource="https://usnconeboxax1aos.cloud.onebox.dynamics.com";

appContract.applicationId="<yourapplicationId>";

ODataUserContractuserContract=new

ODataUserContract();

Console.WriteLine("UsetheO365accountthatyouusetologintoDynamics365forOperations");

Console.Write("O365Username:");

userContract.userName=Console.ReadLine();

Console.Write("O365Password:");

userContract.password=Console.ReadLine();

Console.WriteLine("Thisisyourtenant,suchasyourdomain.comor<yourtenant>.onmicrosoft.com");

Console.Write("O365Domain:");

userContract.domain=Console.ReadLine();

ODataTesttest=newConODataTest.ODataTest();

test.userContract=userContract;

test.appContract=appContract;

if(!test.Authenticate())

{

Console.WriteLine(test.response);

}

test.request=newConODataTest.ODataRequestContract();

test.request.company="USMF";

System.Collections.ArrayListvehicleNames=test.GetVehicleNameList();

foreach(varvehicleNameinvehicleNames)

{

Console.WriteLine(vehicleName);

}

Console.WriteLine("Changingvehicledescriptions");

test.UpdateVehicleNames();

ConWHSVehicleTablevehicle=newConWHSVehicleTable();

Console.WriteLine("CreateanewVehicle");

Console.Write("VehicleId:");

vehicle.VehicleId=Console.ReadLine();

Console.Write("Vehiclegroup:");

vehicle.VehicleGroupId=Console.ReadLine();

Console.Write("Description:");

vehicle.Description=Console.ReadLine();

test.CreateNewVehicle(vehicle);

Console.WriteLine("Pressentertocontinue.");

Console.ReadLine();

}

24. Toseewhatisgoingon,addsomebreakpointsandusethedebuggerandrunit(pressF5);don'tstepintocodethatwouldopenODataClient.cs;thiscantakealongtimetoopen.

Howitworks...Todescribethis,itisbesttostepthroughthekeypartsofthecode.

ThefirstpartwasclickingonsaveontheODataClient.ttfile.ThiscreatedtheclassbyreadingtheURLweenteredinthemetadataDocumentURIvariable.Whenthefileissaved,ittriggersthegenerationofcodeusingmetadatafromDynamics365forOperations.IfyouclickonSave,andthencancelthesecuritywarning,theODataClient.csfilewillbeemptied.

Withinthecodewewrote,thefirstkeypartistheauthentication,whichworksbyauthenticatingwithAzureADandfetchinganauthentication,whichisusedwitheachsubmissionrequest.TheauthorizationcodewasdeterminedintheGetAuthorizationmethod.

ThelogonURIisalwayshttps://login.windows.net/plusyourdomain.IfyourO365accountwasjulia@contoso.com,theURIwouldbehttps://login.windows.net/contoso.com.

Wecan'tsendausernameandpassworddirectly;wehavetocreateacredentialusingthefollowingcode(fromtheActiveDirectorynamespace):

UserPasswordCredentialcredential=new

UserPasswordCredential(

userContract.userName,userContract.password);

Theauthenticationisperformedbythefollowingcode:

Task<AuthenticationResult>task=authenticationContext.AcquireTokenAsync(

appContract.resource,

appContract.applicationId,credential);

task.Wait();

AuthenticationResultauthenticationResult=task.Result;

ThisdiffersfromthecurrentMicrosoftsamplecode,aswenowhavetouseanasynchronouscall;usingatask,thetask.wait()lineessentiallymakesthecodeexecutesynchronously.ThisisOKforsamplecode,butitismoreusefultouseanasynchronouscallwhenwritingaproductionapplicationaswecancreateamoreuser-responsiveapplication.

TheAuthenticatemethodcallstheGetAuthorizationmethodanddoestwothings.First,wehaveatrycatchstatementtoreturnanicermessagetotheuseruponfailure(wewillpopulatetheresponsepublicvariableandreturnfalse);secondly,thisiswheretheauthenticationHeadervariableisset.This,wewillneedfortherequestswemake.

WithintheProgramclass,intheMainmethod,wewillconstructODataUserContractandcompletethevariablesforusername,password,anddomainusingtheConsole.ReadLine()method.WewillconstructtheODataTestclassandcallAuthenticate().

Onceauthenticated,wecanworkwiththedata.Thethreetestmethods,GetVehicleNameList,UpdateVehicleNames,andCreateNewVehicle,allhaveasimilarpattern.

EachmethodmakesacalltoMakeResources,whichisdonebythefollowinglinesofcode:

stringentityRootPath=appContract.resource+"/data";

UrioDataUri=newUri(entityRootPath,

UriKind.Absolute);

varresources=newResources(oDataUri);

resources.SendingRequest2+=new

EventHandler<SendingRequest2EventArgs>(

delegate(objectsender,

SendingRequest2EventArgse)

{

e.RequestMessage.SetHeader(OAuthHeader,

authenticationHeader);

});

TheentityRootPathvariableistheDynamics365forOperationsURLplusdata;onthedevelopmentvirtualmachine,thisishttps://usnconeboxax1aos.cloud.onebox.dynamics.com/data.WecanspecifyHTTPheadersontherequest,sowehavetoaddaneventhandler.This,essentially,settheAuthorizationheaderproperty,whichiswhatOAuthHeaderconstantsequatesto.

Eachofthemethodscanthendowhattheyneedto;allrequestsaretousetheresourcesobjectand,wheneachrequestismade,theeventhandlerwillbecalledtosettheauthorizationheaderproperty.

Thefollowinglinesetsthecompany;however,ifweomitthis,itwillusetheuser'sdefaultcompany,whichmaybedesirableinsomecases:

resources.ConWHSVehicleTables.AddQueryOption(

"DataAreaId",

request.company);

ThefirstexamplewastoreadConWHSVehicleTableEntityintoalist,whichwasdoneusingthefollowingcode:

//System.Collections.ArrayListvehicleNames;

foreach(varvehicleinresources.ConWHSVehicleTables)

{

vehicleNames.Add(vehicle.Description);

}

Therequesttotheserverisdonewhenitreachesresources.ConWHSVehicleTables,whichiswhenthedataisactuallyread.ThevehicleNames.Add(...)functionsimplyaddsourchosenfieldtoalist.Youmaywonderwhytheresource'sobjectknowstocallthecollectionConWHSVehicleTablesandthesingletonConWHSVehicleTable.ThisisdeterminedfromthePublicCollectionNameandPublicEntityNamepropertieswespecifiedwhenwecreatedtheConWHSVehicleTableEntitydataentity.

Thenextexamplewashowtoupdatedataandwasdonebywritingthefollowingpieceofcode:

foreach(varvehicleinresources.ConWHSVehicleTables)

{

vehicle.Description=vehicle.VehicleId+

":ODatadidit";

resources.UpdateObject(vehicle);

}

try

{

resources.SaveChanges();

}

catch(Exceptione)

{

response=e.InnerException.Message;

returnfalse;

}

returntrue;

Theresoures.UpdateObject(object)methodrecordsthatwehavemadeachange,butdoesnotwritethisback.Thechangesareactuallysavedbytheresources.SaveChanges()method.ThiswillcallDynamics365forOperation'svalidationlogicfortherecordand,ifthisfails,itwillthrowanexception.Thee.InnerException.MessagesisactuallyJSON,andyoucantraversethisbygivingthemessagebacktotheusers.

Thenextexampleistocreateanewvehicle.Thisisverysimplecode.WewilljustcreateaninstanceoftheConWHSVehicelTableclass,populateitwiththerequiredvalues(VehicleId,DataAreaId,VehicleGroupId,andsoon),andcalltheappropriateAddTomethod.Inthiscase,itisAddToConWHSVehicleTables(ConWHSVehicleTable).

ThereisalotwecandowithOData,andthebestwaytolearnistousethisexample.

SeealsoThefollowinglinkcontainssimpleexamplesforOData,Soap,andJSON.Youmaywishtochangethecodetomatchthepatternsinthisrecipeasyouusethem:

Microsoft/Dynamics-AX-Integration(https://github.com/Microsoft/Dynamics-AX-Integration/tree/master/ServiceSamples)Datamanagementandintegrationthroughdataentities(https://ax.help.dynamics.com/en/wiki/data-management-and-integration-through-data-entity/)OData(https://ax.help.dynamics.com/en/wiki/odata-in-dynamics-ax-7/)NuGet(http://www.nuget.org/)

ConsumingandExposingServicesInthischapter,wewillcoverthefollowingrecipes:

CreatingaserviceConsumingaDynamics365forOperationsSOAPserviceConsumingaDynamics365forOperationsJSONserviceConsuminganexternalservicewithinDynamics365forOperations

IntroductionThischapterfocusesonhowtocreateservicesinDynamics365forOperations,andhowtoconsumetheminexternalapplications.ThisdoesnotincludeODataservices,whichwascoveredinthepreviouschapter.

Themethodinwhichservicesarecreatedhasn'tchangedsignificantlyfromDynamicsAX2012(thepriorreleasetoDynamics365forOperations).Thekeydifferenceisthatwebmethodsdon'tneedtobemarkedasanentrypoint.

TheconsumptionofDynamics365forOperations'serviceshaschangedconsiderably.Thismeansthatanycurrentintegrationfromexternalappswillneedchangestothemethodofauthenticationandhowtheinputcontractsarecreated.Havingsaidso,thechangesshouldbeakintoarefactoringexerciseforcustomservicesaccessedthroughSOAP.

TheApplicationIntegrationFramework(AIF)hasbeenremovedinthisrelease,sodocumentservicesarenotavailable;thisisreplacedbyOData.Customservicesarestillavailable,butnolongerrequiretheAIF.TheyareaccessedusingthepathdefinedbythelevelsServiceGroup,Service,andOperation,andwewillseethislaterinthischapter.

CompletelynewtothisreleaseisJSON,anditisthepreferredmethodofinteractionwiththeAPI.JSONmaybenewtomostX++developers,butthereisalotoftechnicalguidanceavailableonlineonhowtouseit.

Finally,wewillconsumeanexternalservicefromwithinDynamics365forOperations.

Creatingaservice

Therearethreepartstocreatinganewservice:

CreateaclassthatcontainsthebusinesslogicCreateaservicethathasoperationsthatreferenceoperationstotheclass'smethodsCreateaservicegroup

Theservicegroupisacollectionofoneormoreservices,andactsastheservicereferenceshouldweconsumeitwithinVisualStudio.WewillseehowthistranslatestoaURIinthenextrecipe.

Inthisexample,wewillhavetwoserviceoperations:onetogetalistofvehicles,andonetoupdateavehicle'sgroup.TheXMLdocumentationhasbeenomittedtosavespace.

GettingreadyWewilljustneedaDynamics365forOperationsvisualstudioprojectopen.

Howtodoit...

Tocreatetheservice,followthesesteps:

1. First,createanewclassthatwillholdourservicemethodsandnamethisclassConWHSVehicleServices.2. ThefirstservicewillbetoupdatetheVehicleservicegroup,forwhichwehaveacontractand

processingclassalreadycreated(ConWHSVehicleGroupChangeContract,ConWHSVehicleGroupChange),towritethismethod;writeamethodasfollows:

publicvoidChangeVehicleGroup(

ConWHSVehicleGroupChangeContract_contract)

{

ConWHSVehicleGroupChangechangeGroup=

newConWHSVehicleGroupChange();

changeGroup.Run(_contract);

}

Thiswillwork,buttheconsumerwillnotthankusforthelackoferrorhandling.WecouldeitherreturnaBooleanonsuccessorwriteareturncontract.Areturncontractisthemostusefulwaytoreturntheresultstatus.

3. Toallowameaningfulreply,wewillcreateadatacontractclass.CreateanewclasscalledConWHSMessageContract,andcompleteitasfollows:

[DataContract]

classConWHSMessageContract

{

booleansuccess;

strmessage;

[DataMember]

publicbooleanSuccess(boolean_success=success)

{

success=_success;

returnsuccess;

}

[DataMember]

publicstrMessage(str_message=message)

{

message=_message;

returnmessage;

}

}

4. Nowupdatethemethodsothatitreadsasfollows:

publicConWHSMessageContractchangeVehicleGroup(

ConWHSVehicleGroupChangeContract_contract)

{

ConWHSMessageContractmessage=

newConWHSMessageContract();

ConWHSVehicleGroupChangechangeGroup=

newConWHSVehicleGroupChange();

try

{

updateGroup.contract=_contract;

if(updateGroup.Validate())

{

updateGroup.Run(_contract);

message.Success(true);

}

else

{

message.Success(false);

//Couldnotupdatevehicle%1tovehicle

//group%2

message.Message(strFmt("@ConWHS:ConWHS53",

_contract.VehicleId(),

_contract.VehicleGroupId()));

}

}

catch

{

message.Success(false);

//Couldnotupdatevehicle%1tovehiclegroup%2

message.Message(strFmt("@ConWHS:ConWHS53",

_contract.VehicleId(),

_contract.VehicleGroupId()));

}

returnmessage;

}

5. Next,let'stryandgetalittleadventurousandreturnalistofvehicles.Inthismethod,wewillreturnalistofcontracts,eachrepresentingavehiclerecord.Wewillfirstneedacontracttostorevehicledata;createoneasfollows:

[DataContract]

classConWHSVehicleTableContract

{

ConWHSVehicleIdvehicleId;

Descriptiondescription;

ConWHSVehRegNumvehRegNum;

ConWHSAcquiredDateacquiredDate;

ConWHSVehicleTypevehicleType;

ConWHSVehicleGroupIdvehicleGroupId;

[DataMember]

publicConWHSVehicleGroupIdVehicleGroupId(

ConWHSVehicleGroupId_vehicleGroupId=

vehicleGroupId)

{

vehicleGroupId=_vehicleGroupId;

returnvehicleGroupId;

}

[DataMember]

publicConWHSAcquiredDateAcquiredDate(

ConWHSAcquiredDate_acquiredDate=acquiredDate)

{

acquiredDate=_acquiredDate;

returnacquiredDate;

}

[DataMember]

publicConWHSVehicleIdvehicleId(

ConWHSVehicleId_vehicleId=vehicleId)

{

vehicleId=_vehicleId;

returnvehicleId;

}

[DataMember]

publicConWHSVehicleTypeVehicleType(

ConWHSVehicleType_vehicleType=vehicleType)

{

vehicleType=_vehicleType;

returnvehicleType;

}

[DataMember]

publicConWHSVehRegNumVehRegNum(

ConWHSVehRegNum_vehRegNum=vehRegNum)

{

vehRegNum=_vehRegNum;

returnvehRegNum;

}

[DataMember]

publicDescriptionDescription(

Description_description=description)

{

description=_description;

returndescription;

}

}

Wecouldsimplyusethetableasthecontract,butusingadatacontractclassallowsusgreatercontroloverthedatapassedbetweenDynamics365forOperationsandtheexternalapplicationthatisusingtheservice.

6. ClosethecodewindowforthecontractclassandopenthecodewindowfortheConWHSVehicleServicesclass.

7. WecannotreturnanarrayofclassesinOperationsinthesamewaythatC#can;instead,weconstructaListandtellthecompilerthetypethatthelistcontains.ThisisdonebytheAifCollectionTypeattributeaddedtothestartofthemethod.Completethemethod,asshowninthefollowingpieceofcode:

[AifCollectionType('return',

Types::Class,

classStr(ConWHSVehicleTableContract))]

publicListGetVehicles()

{

ListvehicleList;

ConWHSVehicleTablevehicles;

ConWHSVehicleTableContractcontract;

vehicleList=newList(Types::Class);

whileselectvehicles

{

contract=newConWHSVehicleTableContract();

contract.vehicleId(vehicles.VehicleId);

contract.vehicleGroupId(vehicles.VehicleGroupId);

contract.vehicleType(vehicles.VehicleType);

contract.vehRegNum(vehicles.VehRegNum);

contract.acquiredDate(vehicles.AcquiredDate);

vehicleList.addEnd(contract);

}

returnvehicleList;

}

8. Wewillnowhaveourtwomethodsthatwillbecomeservicemethods.Tocreatetheservice,addanewitemtotheprojectbychoosingServicesfromtheleft-handlistandServicefromtheright.NametheserviceConWHSVehicleServicesandclickonAdd.

9. SelecttherootConWHSVehicleServicesnodeandsettheClasspropertytoConWHSVehicleServices(theclasswewrote).

10. Enteradescriptionfortheservice,suchasProvidesservicesforvehicles.11. TheExternalNamepropertyshouldbespecifiedasasimplerformoftheservicename.Asitwillbe

withinaservicegroup,wedon'tneedtheprefix.VehicleServicesisappropriateforthisproperty.12. EnteranamespaceintheNamespaceproperty,suchashttp://schemas.contoso.com/ConWHS.TheURLdoes

nothavetoexist,asitisusedasanamespacefortheservice.

13. Right-clickontheServiceOperationsnodeandselectNewServiceOperation.14. SelectoneofthetwomethodsintheMethodpropertyandmaketheNamepropertythesameasthe

Methodproperty.15. Repeatthisforthenextservicemethod.16. Saveandclosetheservicedesignerwindow.17. Createanewiteminourproject;thistime,ServiceGroupfromtheServiceslist.Nameit

ConWHSServices.18. CreatealabelforContosovehiclemanagementservicesandentertheIDintheDescriptionproperty.19. LocatetheConWHSVehicleServicesserviceintheprojectorApplicationExploreranddragitontothe

ConWHSServicesnodeinthedesigner;thisaddstheservicetotheservicegroup.20. RemovetheConWHSprefixfromtheNamepropertyasthisissuperfluous.21. Saveandclosealldesignersandbuildtheproject.

Howitworks...ThefirstnewconceptwashowDynamics365forOperationshandlescollections.InC#,wewouldreturnatypedcollectionoranarray(MyClass[]),andwecouldusetheforeachcommandtoiteratethroughit.

ThisisnotsupportedinX++,sowehavetoreturnaListinstead.Sinceweactuallywanttoreturnatypedcollectiontothecaller,wewillusetheAifCollectionTypeattributetotellthecompilerhowtodothis.

Thenextpartwastocreateaserviceandservicegroup,whichsimplyinstructsthesystemtogeneratepublicservicesexposingthemethodsweaddedtotheservice.

Wewillseehowthisisusedinthenextrecipe.

ConsumingaDynamics365forOperationsSOAPserviceInthisrecipe,wewillcreateanewC#projecttoconsumetheservicecreatedinthepreviousrecipe.

Beforewestart,weshouldunderstandtheAzureADauthenticationconceptsexplainedintheReading,writing,andupdatingdatathroughODatarecipe,inChapter8,DataManagement,Odata,andOffice.Manyoftheconceptsinthefollowingrecipesextendedtheconceptswecoveredinthischapter.

Inthisexample,wewillcreateaSOAPservicereference.

GettingreadyWearecontinuingtheCreatingaservicerecipe,whichmustbecompletedandbuiltbeforewecontinue.

Howtodoit...

ToconsumeaDynamics365forOperationsserviceusingSOAP,followthesesteps:

1. Createanewproject;thistime,chooseVisualC#fromtheTemplatesnodeandthenConsoleApplicationfromtheright.NametheprojectConServiceTestandplaceitintheprojectfoldersthatwesetupforsourcecontrol.EnsurethatthenamespaceisalsoConServiceTest.

2. WewillnowneedtoinstallsomeNuGetpackages.WithinVisualStudio,navigatetoTools|NugetPackageManager|PackageManagerConsole.

3. InthePackageManagerconsole,typethefollowingcommand:

Install-PackageMicrosoft.IdentityModel.Clients.ActiveDirectory

4. Right-clickontheReferencesnodeintheprojectandchooseAddServiceReference.5. Enterhttps://usnconeboxax1aos.cloud.onebox.dynamics.com/soap/services/ConWHSServicesintheAddressfieldand

clickonGo.

ThisistheOperationsURLwith/soap/services/added,followedbytheservicegroupname.

6. ExpandtheConWHSServicesnodeandselectVehicleService.7. ChangetheNamespacefieldtoConWHSandclickonOK.8. CreateanewclasscalledAuthenticate.

Wewillreusethisclass,sothissimplifiestheprocessandallowsustoreusethecodethroughouttheproject.

9. Wewillneedtwoclasses:thecontractclasssowecanpassdatatotheAuthenticateclass,andtheAuthenticateclassitself.

TheconceptsarethesameasdescribedintheReading,writing,andupdatingdatathroughODatarecipeofChapter8,DataManagement,Odata,andOffice;theyarejustseparatedinordertomakethecodereusable.

10. Writethefollowingpieceofcode:

classAuthenticationContract

{

publicstringUserName{get;set;}

publicstringPassword{get;set;}

publicstringDomain{get;set;}

publicstringApplicationId{get;set;}

publicstringResource{get;set;}

publicstringResponse{get;set;}

}

Thistime,wewillusepropertymethodsinstead,asthismethodallowsmoreflexibility.Forexample,ifweomittheset;argument,thepropertyisread-only:

publicclassAuthenticate

{

publicconststringOAuthHeader="Authorization";

stringbearerkey;

publicAuthenticationContractAuthentication

{get;set;}

publicstringBearerKey

{

get{returnbearerkey;}

}

publicBooleanGetAuthenticationHeader()

{

AuthenticationResultresult;

try

{

result=GetAuthorization();

//Thisgetstheauthorizationtoken,this

//mustbesetontheHttpheaderforall

//requests

bearerkey=

result.CreateAuthorizationHeader();

}

catch(Exceptione)

{

if(e.InnerException!=null)

{

Authentication.Response=

"Authenticationfailed:"+

e.InnerException.Message;

}

else

{

Authentication.Response=

"Authenticationfailed:"+

e.Message;

}

returnfalse;

}

Authentication.Response="OK";

returntrue;

}

publicUserPasswordCredentialGetCredential()

{

stringuri=this.GetSecurityURI();

UserPasswordCredentialcredential;

credential=

newUserPasswordCredential(

Authentication.UserName,

Authentication.Password);

returncredential;

}

publicstringGetSecurityURI()

{

UriBuilderuri;

uri=newUriBuilder(

"https://login.windows.net/"+

Authentication.Domain);

returnuri.ToString();

}

privateAuthenticationResultGetAuthorization()

{

UserPasswordCredentialcredential;

credential=GetCredential();

AuthenticationContextcontext

=newAuthenticationContext(GetSecurityURI());

Task<AuthenticationResult>task=

context.AcquireTokenAsync(

Authentication.Resource.TrimEnd('/'),

Authentication.ApplicationId,

credential);

task.Wait();

returntask.Result;

}

}

11. Youcanclosethecodeeditorforthisclass.12. CreateanewclasscalledSOAPUtil.

Thecodeinthisclasswas'inspired'byasampleutilityprovidedbythisURL:https://github.com/Microsoft/Dynamics-AX-Integration/blob/master/ServiceSamples/SoapUtility/SoapHelper.cs.

13. Settheusingdeclarationstothefollowinglinesofcode:

usingSystem;

usingSystem.Linq;

usingSystem.ServiceModel;

usingSystem.ServiceModel.Channels;

14. Tosimplifytheusage,wewillneedautilityclass.Thisshouldbewrittenasfollows:

publicclassSoapUtil

{

publicconststringOAuthHeader="Authorization";

publicstaticstringGetServiceURI(

string_service,

string_d365OURI)

{

stringserviceName=_service.Split('.').Last();

if(serviceName=="")

{

serviceName=_service;

}

return_d365OURI.TrimEnd('/')+"/soap/services/"

+serviceName;

}

publicstaticEndpointAddressGetEndpointAddress(

string_uri)

{

EndpointAddressaddress;

address=newEndpointAddress(_uri);

returnaddress;

}

publicstaticBindingGetBinding(

EndpointAddress_address)

{

BasicHttpBindingbinding;

binding=newBasicHttpBinding(

BasicHttpSecurityMode.Transport);

//Setbindingtimeoutandotherconfiguration

//settings

binding.ReaderQuotas.MaxStringContentLength=

int.MaxValue;

binding.ReaderQuotas.MaxArrayLength=int.MaxValue;

binding.ReaderQuotas.MaxNameTableCharCount=

int.MaxValue;

binding.ReceiveTimeout=TimeSpan.MaxValue;

binding.SendTimeout=TimeSpan.MaxValue;

binding.MaxReceivedMessageSize=int.MaxValue;

varhttpsBindingElement=

binding.CreateBindingElements().

OfType<HttpsBindingElement>().FirstOrDefault();

if(httpsBindingElement!=null)

{

//Largestpossibleis100000,otherwisethrows

//anexception

httpsBindingElement.MaxPendingAccepts=10000;

}

varhttpBindingElement=

binding.CreateBindingElements().

OfType<HttpBindingElement>().FirstOrDefault();

if(httpBindingElement!=null)

{

httpBindingElement.MaxPendingAccepts=10000;

}

returnbinding;

}

}

TheGetBindingmethodisthekeyonehere.Wewillauthenticatethroughwhatiscalledabearerkey,whichisimplementedbysettingtheAuthorizationheadervariable;so,wewillwriteabindingmanuallysothatthebindinginApp.Configdoesnotinterfere.

15. ClosethecodeeditorandcreateanewclasscalledUpdateVehicleGroup.16. Settheusingstatementsasfollows:

usingSystem.ServiceModel.Channels;

usingSystem.ServiceModel;

17. Writethefollowingmethod:

publicConWHS.ConWHSMessageContractUpdateSOAP(

AuthenticationContract_authContract,

ConWHS.ConWHSVehicleGroupChangeContract_change)

{

Authenticateauth=newAuthenticate();

auth.Authentication=_authContract;

ConWHS.ConWHSMessageContractmessage;

//Ifwefailtogettheauthorizationbearer

//key,stopandreturntheerrorthrough

//themessagecontract

if(!auth.GetAuthenticationHeader())

{

message=newConWHS.ConWHSMessageContract();

message.Success=false;

message.Message=auth.Authentication.Response;

returnmessage;

}

stringbearerKey=auth.BearerKey;

stringendPoint;

endPoint=SoapUtil.GetServiceURI(

"ConWHSServices",

_authContract.Resource);

EndpointAddressaddress;

address=SoapUtil.GetEndpointAddress(endPoint);

Bindingbinding=SoapUtil.GetBinding(address);

ConWHS.VehicleServiceClientclient;

client=newConWHS.VehicleServiceClient(

binding,address);

ConWHS.CallContextconContext;

conContext=newConWHS.CallContext();

conContext.Company="USMF";

conContext.Language="en-us";

conContext.PartitionKey="initial";

varchannel=client.InnerChannel;

//wedon'tusethecontext,itisusedtoaffect

//thechannelsothatwecansettheoutgoing

//messageproperties

//Usingisusedsothatitisdisposedof

//correctly.

using(OperationContextScopecontext

=newOperationContextScope(channel))

{

//Settheauthenticationbearerkey

HttpRequestMessagePropertyrequestMessage;

requestMessage=newHttpRequestMessageProperty();

requestMessage.Headers[SoapUtil.OAuthHeader]=

bearerKey;

OperationContext.Current.OutgoingMessageProperties[

HttpRequestMessageProperty.Name]=

requestMessage;

//setupthemessage

ConWHS.UpdateVehicleGroupupdate;

update=newConWHS.UpdateVehicleGroup();

update._contract=_change;

update.CallContext=conContext;

ConWHS.UpdateVehicleGroupResponseresponse;

message=newConWHS.ConWHSMessageContract();

response=

((ConWHS.VehicleService)channel).

UpdateVehicleGroup(update);

//theresponsecontainsthecurrentinfolog

//andthereturnresult,whichthereturntype

//wereturnedintheD365Omethod.

message=response.result;

}

returnmessage;

}

18. Let'sseeifitworks.IntheProgram.csfile,writethefollowingpieceofcodeastheMainmethod:

staticvoidMain(string[]args)

{

AuthenticationContractauthContract;

authContract=newAuthenticationContract();

authContract.ApplicationId="<yourapplicationId>";

authContract.Resource="https://usnconeboxax1aos.cloud.onebox.dynamics.com/";

Console.WriteLine("UsetheO365accountthatyouusetologintoDynamics365forOperations");

Console.Write("O365Username:");

authContract.UserName=Console.ReadLine();

Console.Write("O365Password:");

authContract.Password=Console.ReadLine();

Console.WriteLine("Thisisyourtenant,andshouldbeyourdomain,suchasyourdomain.com");

stringdefaultDomain=

authContract.UserName.Split('@').Last<string>();

Console.WriteLine("O365Domain:");

Console.Write("("+defaultDomain+"):");

authContract.Domain=Console.ReadLine();

if(authContract.Domain=="")

{

authContract.Domain=defaultDomain;

}

UpdateVehicleGroupupdate=newUpdateVehicleGroup();

ConWHS.ConWHSVehicleGroupChangeContractchange;

change=newConWHS.ConWHSVehicleGroupChangeContract();

change.VehicleId="X0002";

change.VehicleGroupId="Leased";

ConWHS.ConWHSMessageContractmessage;

message=update.UpdateSOAP(authContract,change);

if(message.Success)

{

Console.WriteLine("Success!");

}

else

{

Console.WriteLine(message.Message);

}

Console.ReadLine();

}

19. Buildandruntheproject.

Howitworks...WhenweaddedServiceReference,VisualStudiocreatedatypeforeachcontracttheservicereferenceuses,andaclientclassinordertointeractwiththeservicesreferencedbyit.ItcorrespondstotheServiceGroupwithinDynamicsAX365forOperations.ThisisalotmorehelpfulthanwhatisprovidedbyJSON,whichiswhythisrecipewaswrittenbeforetheJSONmethod.Inthenextrecipe,wewillseethatwecandeserializeSOAPtypesfromJSON.

TheAuthenticateclassisverysimilartotheclasswewroteinChapter8,DataManagement,Odata,andOffice,justalittlemoreelegant.Wearesimplygettinganauthenticationtoken(knownasabearerkey)thatisusedwhentherequestsaremade.

TheUpdateVehicleGroupsclasshasalittlemoretoit.Thefirstpartistogettheauthorizationcode,whichisjustavariationontheusedforaccessdatathroughOData.Itvariesmorefromthispointon.

Whenweconstructedtheclientwiththecode:client=newConWHS.VehicleServiceClient(binding,address);,weusedabindingthatweconstructedmanually.Wedidthisbecausewedon'twanttheApp.Configbindingstointerfere.ThebindingsinApp.Configaregeneratedautomaticallyforuswhenwecreateorupdatetheservicereference.Inourcase,wedon'twantanythingspecial;wejustwantabasicbinding.TheauthenticationisdonebyspecifyingsettingtheAuthorizationheaderpropertytothebearerkey(authenticationtoken).

ThenextnewpartisalegacyfromAX2012,theCallContextclass.Thiswasusedforsettingthecompany,language,andalsothecredentialstouse.Thisisnolongermandatory,andisfilledinforcompleteness.Partitionisstillactive,butisonlyusedforcertaintestingscenarios:theclientcannolongeraccessanyotherpartitionthan"initial".

Thenextpartlookscomplicated,butit'stheonlywayinwhichwecansettherequestheadervariables.Inthissection,wewillsetupaHttpRequestMessagePropertyinstanceinordertosettheauthorizationheadervariable:

HttpRequestMessagePropertyrequestMessage;

requestMessage=newHttpRequestMessageProperty();

requestMessage.Headers[SoapUtil.OAuthHeader]=bearerKey;

Thisisthenpassedtothecurrentoutgoingmessagepropertieswiththefollowingline:

OperationContext.Current.OutgoingMessageProperties[

HttpRequestMessageProperty.Name]=requestMessage;

Wewillneedtoensurethatthisiscleanedupafterwards,sothisiswhyweenclosedthecodeinthefollowingusingclause:

using(OperationContextScopecontext

=newOperationContextScope(channel))

Thecodethatdoestheactualworkisasfollows:

ConWHS.UpdateVehicleGroupupdate;

update=newConWHS.UpdateVehicleGroup();

update._contract=_change;

update.CallContext=conContext;

ConWHS.UpdateVehicleGroupResponseresponse;

message=newConWHS.ConWHSMessageContract();

response=((ConWHS.VehicleService)channel).UpdateVehicleGroup(update);

message=response.result;

WithintheConWHSservice,referenceisaclasscalledUpdateVehicleGroup,whichisthenameoftheservicemethodwewrote.Thedeclarationwasasfollows:

publicConWHSMessageContractUpdateVehicleGroup(

ConWHSVehicleGroupChangeContract_contract)

VisualStudiocreatedthisclassbecauseoftheinputparameter.TheclasscontainspropertiesfortheCallContextproperty,whichisalwayscreated,andalsoforthe_contractmethodinputparameter.

SeealsoTroubleshootserviceauthentication(https://ax.help.dynamics.com/en/wiki/troubleshooting-service-authentication/).Serviceendpoints(https://ax.help.dynamics.com/en/wiki/dynamics-ax-7-services-technical-concepts-guide/).Thisisalittlegeneric,butitcontainslinkstosomeusefulcodesamples.

ConsumingaDynamics365forOperationsJSONservice

Inthisrecipe,wewillextendthepreviousC#projecttoconsumetheserviceusingJSON.

TheprimarydifferenceisthatJSONwillnotcreatethecontractandclientclassesforus,wewillneedtowritethem.WewilluseaNuGetpackagetohelpwiththeserializationanddeserializationofC#classestoJSON.

GettingreadyWearecontinuingthepreviousrecipe,whichmustbecompletedandbuiltbeforewecontinue.

Howtodoit...

ToconsumeaDynamics365forOperationsserviceusingJSON,followthesesteps:

1. First,let'stakealookatwhatJSONlookslike;thiswillhelptherecipemakemoresenseasweprogress.OpenthefollowingURLusingInternetExplorer:

https://usnconeboxax1aos.cloud.onebox.dynamics.com/api/services/

2. Thiswillaskyoutoopenservices.json;clickonOpen,whichopensafileinVisualStudiothatcontainsalltheservicesexposed.Thefilewillcontainourserviceinthefollowingformat:

{"ServiceGroups":[{"Name":"ConWHSServices"},{"Name":"CuesServiceGroup"}

Now,openthefollowingURLonInternetExplorer:

https://usnconeboxax1aos.cloud.onebox.dynamics.com/api/services/ConWHSServices

3. Thistime,theJSONfileiscalledCONWHSServices.json,andcontainsthefollowing:

{"Services":[{"Name":"VehicleServices"}]}

4. YoucandothesamebyaddingVehicleServicestotheURL,whichwillopenaJSONfilewiththefollowinglinesofcode:

{"Operations":[{"Name":"UpdateVehicleGroup"},{"Name":"GetVehicles"}]}

5. TheJSONfortheGetVehiclesoperationisasfollows:

{"Parameters":[],"Return":{"Name":"return","Type":"ConWHSVehicleTableContract[]"}}

6. TheJSONforUpdateVehicleGroupisshownhere:

{"Parameters":[{"Name":"_contract","Type":"ConWHSVehicleGroupChangeContract"}],"Return":{"Name":"return","Type":"ConWHSMessageContract"}}

Thetakeawayhereistonotethatwehavethreelevels:ServiceGroups,Services,andOperations,whichcorrelatetohowwecreateserviceswithinDynamics365forOperations.

7. OpenthePackageManageconsolefromTools|NugetPackageManager,andtypethefollowingcommand:

Install-PackageNewtonsoft.Json

8. CreateaclassfortheC#classesthatwewillusetodeserializetheJSONintoandnametheclassJsonClient.

9. Setupthefollowingusingdeclarations:

usingSystem;

usingSystem.Collections.Generic;

usingNewtonsoft.Json;

usingSystem.Net;

usingSystem.IO;

10. ChangethenamespacetoConServiceTest.JsonasourclassesmaynotbeuniqueintheConServiceTestnamespace.

11. Removethedefaultclass,sothatweonlyhaveablanklineinsidethenamespacebraces.12. WewillfirstwritetheclassesfortheServiceGroupJSON,whichwas:{"ServiceGroups":

[{"Name":"ConWHSServices"},{etc.}".ThisisrepresentedinC#asfollows:

publicclassServiceGroups

{

[JsonProperty("ServiceGroups")]

publicList<ServiceGroup>ServiceGroupNames{get;set;}

publicclassServiceGroup

{

[JsonProperty("Name")]

publicstringServiceGroupName{get;set;}

}

TheJsonPropertydecorationmapsthemethodtotheJSONproperty.

13. WecancontinuethispatternfortheServicesandOperationslevels,asshowninthefollowingpieceofcode:

publicclassServices

{

[JsonProperty("Services")]

publicList<Service>ServiceNames{get;set;}

}

publicclassService

{

[JsonProperty("Name")]

publicStringName{get;set;}

}

publicclassOperations

{

[JsonProperty("Operations")]

publicList<Operation>OperationNames{get;set;}

}

publicclassOperation

{

[JsonProperty("Name")]

publicstringName{get;set;}

}

14. Let'swriteaclienttoaccesstheserviceanddeserializethedataintoournewclasses.InourJsonClient.csfile,createanewclasscalledClient.

15. Starttheclasswiththefollowingcode,inordertosetuptheglobalvariablesandconstructor:

publicclassClient

{

stringd365OURI;

Authenticateauth;

publicClient(string_d365OURI,Authenticate_auth)

{

d365OURI=_d365OURI;

auth=_auth;

}

}

16. Next,wewillwritetwohelperfunctions--thefirsttocreatearequestforthesupplieraddress,andtheothertoreadtheresponsefromtherequestintoastring.Thecodeisasfollows:

privateHttpWebRequestCreateRequest(string_address)

{

HttpWebRequestwebRequest;

webRequest=(HttpWebRequest)HttpWebRequest.Create(

_address);

webRequest.Method="POST";

//therequestwillbeempty.

webRequest.ContentLength=0;

webRequest.Headers.Set(JsonUtil.OAuthHeader,

auth.BearerKey);

returnwebRequest;

}

privatestringReadJsonResponse(HttpWebRequest_request)

{

stringjsonString;

using(HttpWebResponsewebResponse=

(HttpWebResponse)_request.GetResponse())

{

using(Streamstream=

webResponse.GetResponseStream())

{

using(StreamReaderreader=new

StreamReader(stream))

{

jsonString=reader.ReadToEnd();

}

}

}

returnjsonString;

}

17. Let'swritethemethodstoreadthemetadata(theservicegroups,services,andoperations).Thecodeforthisisasfollows:

publicServiceGroupsGetServiceGroups()

{

stringserviceGroupAddress=

Json.JsonUtil.GetServiceURI("",d365OURI);

HttpWebRequestwebRequest;

webRequest=

CreateRequest(serviceGroupAddress.TrimEnd('/'));

//Mustoverridethemetadatarequestcalls

//toGETasthisisnotREST

webRequest.Method="GET";

stringjsonString=ReadJsonResponse(webRequest);

ServiceGroupsserviceGroups;

serviceGroups=

JsonConvert.DeserializeObject<ServiceGroups>(

jsonString);

returnserviceGroups;

}

publicServicesGetServices(string_serviceGroup)

{

stringserviceGroupAddress=Json.JsonUtil.GetServiceURI(_serviceGroup,d365OURI);

HttpWebRequestwebRequest;

webRequest=CreateRequest(serviceGroupAddress);

//Mustoverridethemetadatarequestcalls

//toGETasthisisnotREST

webRequest.Method="GET";

stringjsonString=ReadJsonResponse(webRequest);

Servicesservices;

services=

JsonConvert.DeserializeObject<Services>(

jsonString);

returnservices;

}

publicOperationsGetOperations(

string_serviceGroup,

string_vehicleService)

{

stringservicePath=_serviceGroup.TrimEnd('/')

+"/"+_vehicleService;

stringserviceGroupAddress;

serviceGroupAddress=Json.JsonUtil.GetServiceURI(

servicePath,d365OURI);

HttpWebRequestwebRequest;

webRequest=CreateRequest(serviceGroupAddress);

//Mustoverridethemetadatarequestcalls

//toGETasthisisnotREST

webRequest.Method="GET";

stringjsonString=ReadJsonResponse(webRequest);

Operationsoperations;

operations=

JsonConvert.DeserializeObject<Operations>(

jsonString);

returnoperations;

}

18. Thenextmethodistomakeacalltogetthevehiclelist.Thistime,wearereusingtheConWHS.ConWHSVehicleTableContracttypethatwascreatedinthepreviousrecipe.Thecodeshouldbewrittenasfollows:

publicConWHS.ConWHSVehicleTableContract[]GetVehicles(

string_serviceGroup,

string_service,

string_operation)

{

stringservicePath;

servicePath=_serviceGroup.TrimEnd('/')

+"/"+_service.TrimEnd('/')

+"/"+_operation;

stringserviceGroupAddress;

serviceGroupAddress=Json.JsonUtil.GetServiceURI(

servicePath,d365OURI);

HttpWebRequestwebRequest;

webRequest=CreateRequest(serviceGroupAddress);

stringjsonString=ReadJsonResponse(webRequest);

ConWHS.ConWHSVehicleTableContract[]vehicles;

vehicles=JsonConvert.DeserializeObject

<ConWHS.ConWHSVehicleTableContract[]>(jsonString);

returnvehicles;

}

Youmaywonderhowthiscouldpossiblywork.HowcanthedeserializerpossiblyknowhowtoconverttheJSONfileintoaclasswithouttheJsonPropertydecoration?Thisisbecausethepropertymethodshavethesamenamesasthepropertymethods.

19. Let'stestthisnowandseewhathappens.IntheProgram.csfile,commentouttheSOAPcodefromUpdateVehicleGroupupdate=...andwritethefollowingpieceofcode:

Authenticateauth=newAuthenticate();

auth.Authentication=authContract;

if(auth.GetAuthenticationHeader())

{

Json.Clientclient=newJson.Client(authContract.Resource,auth);

Json.ServiceGroupsserviceGroups;

serviceGroups=client.GetServiceGroups();

foreach(Json.ServiceGroupserviceGroupin

serviceGroups.ServiceGroupNames)

{

Console.WriteLine(serviceGroup.Name);

}

Json.Servicesservices;

services=client.GetServices("ConWHSServices");

foreach(Json.Serviceserviceinservices.ServiceNames)

{

Console.WriteLine(service.Name);

}

Json.Operationsoperations;

operations=client.GetOperations(

"ConWHSServices","VehicleServices");

foreach(Json.Operationoperationin

operations.OperationNames)

{

Console.WriteLine(operation.Name);

}

GetVehiclesgetVehicles=newGetVehicles();

ConWHS.ConWHSVehicleTableContract[]vehicles;

vehicles=getVehicles.GetVehiclesJson(authContract);

foreach(ConWHS.ConWHSVehicleTableContractvehicle

invehicles)

{

Console.WriteLine(vehicle.vehicleId);

}

}

20. Closethecodeeditors,build,andruntheprojecttotestit.

Howitworks...TheJSONmethodactuallycarrieslessoverheadintermsofsettingupthecallsthanSOAP.Wedon'tneedtosetupanybindings;wejustneedtosettheAuthorizationheaderproperty,whichiseasilydoneusingthismethod.

TherestisgenericJSON,andthecodewewriteisthesameasifwewerewritingforanyotherapplicationthatisabletousethismethod.

ThedifficultpartissettinguptheC#classesinwhichtodeserializeto,orserializefrom.Itisn'tobviousatfirst,especiallywhenasimplelistofnames,suchasServiceGroups,requirestwoclasses.Thankfully,NewtonsofthavewrittenagreatNuGetpackagethatmeansalotoftheworkisdoneforus.WecanalsocheatalittlebyusingSOAPtogenerateclassesforuse,likewedidfortheGetVehiclesmethod.

Toexplainthisfurther,takethefollowingJSON:

{"Services":[{"Name":"VehicleServices"}]}

TheC#classforthiswasasfollows:

publicclassServices

{

[JsonProperty("Services")]

publicList<Service>ServiceNames{get;set;}

}

publicclassService

{

[JsonProperty("Name")]

publicStringName{get;set;}

}

TheouterpartoftheJSONcontainstheServicesproperty.ThispropertyisalistofNameproperties.WhentheJSONisdeserializedintotheServicesclass,itlooksforapropertythatiseithernamedServicesorhasthe[JsonProperty("Services")]decoration.Inourcase,publicList<Service>ServiceNames{get;set;}hastherequireddecoration.

Thisprocesscontinues.ThedeserializernowcreatesaListoftypeService.ItwilliteratethroughtheJSON,mappingtheNameJSONpropertytotheService.Nameproperty.Inthiscase,theJsonPropertydecorationisnotactuallyrequired,asthepropertynameisthesameasthemethodname.ThefactthatitmatchesonnameisthereasonthatwecanusetheclassescreatedbytheServiceReference.

TherequestisdonebysettinguptheHttpWebRequestobject,whichsetstheAuthorisationheaderpropertytoourbearerkeyandtheMethodpropertytoPOST;thisisthedefaultasmostcallswillbeRESTandthesemustusethePOSTmethod.

Formetadatacalls,wewillsettheMethodpropertytoGET,whichisrequiredfornon-RESTcalls.Wewillthendeserializedirectlyintotheclass.

TheGetVehiclesmethoddeserializesintoanarray;weknewthisbecausetheGetVehicles.jsonfilecontainedthefollowinglinesofcode:

{"Parameters":[],"Return":{"Name":"return",

"Type":"ConWHSVehicleTableContract[]"}}

There'smore...Let'ssaywewanttopassdatatoaJSONservice,asisrequiredbytheUpdateVehicleGroupsoperation.

OpenthefollowingURLinInternetExplorer:

https://usnconeboxax1aos.cloud.onebox.dynamics.com/api/services/ConWHSServices/VehicleServices/UpdateVehicleGroup

TheJSONfileitopenscontainsthefollowinglinesofcode:

{"Parameters":[{"Name":"_contract","Type":"ConWHSVehicleGroupChangeContract"}],"Return":{"Name":"return","Type":"ConWHSMessageContract"}}

TheactualinputJSONtherequestneedswillfollowthefollowingpattern:

{"_contract":{"VehicleGroupId":"Newvehiclegroup","VehicleId":"X0002","hideVehicleId":0,"parmCallId":"00000000-0000-0000-0000-000000000000","parmSessionIdx":0,"parmSessionLoginDateTime":"0001-01-01T00:00:00"}}

Forthis,wewillneedtoconstructaclasswithaJsonPropertyof_contract;thisisdonebythefollowingclass,whichshouldbeaddedtoJsonClient.cs:

publicclassUpdateVehicleParameter

{

[JsonProperty("_contract")]

publicConWHS.ConWHSVehicleGroupChangeContractContract

{get;set;}

}

TheprecedingclasswillnowserializetotheJSONthattheoperationneeds.

Wewillneedanewmethodinourclientclasstoperformtheupdate;thisisdonebywritingthefollowingpieceofcode:

publicConWHS.ConWHSMessageContractUpdateVehicleGroup(

ConWHS.ConWHSVehicleGroupChangeContract_change,

string_serviceGroup,

string_service,

string_operation)

{

stringservicePath;

servicePath=_serviceGroup.TrimEnd('/')

+"/"+_service.TrimEnd('/')

+"/"+_operation;

stringserviceGroupAddress;

serviceGroupAddress=Json.JsonUtil.GetServiceURI(

servicePath,d365OURI);

HttpWebRequestwebRequest;

webRequest=CreateRequest(serviceGroupAddress);

UpdateVehicleParameterparm;

parm=newJson.Client.UpdateVehicleParameter();

parm.Contract=_change;

stringjsonOutString=JsonConvert.SerializeObject(parm);

webRequest.ContentLength=jsonOutString.Length;

using(Streamstream=webRequest.GetRequestStream())

{

using(StreamWriterwriter=newStreamWriter(stream))

{

writer.Write(jsonOutString);

writer.Flush();

}

}

stringjsonString=ReadJsonResponse(webRequest);

ConWHS.ConWHSMessageContractmsg;

msg=JsonConvert.DeserializeObject

<ConWHS.ConWHSMessageContract>(jsonString);

returnmsg;

}

Thecomplicatedpartwasvisualizingtheinputparameterasaclassfromthatpointintheprecedingmethodislargelythesameasbefore.WewilljustwritetheJSONstringtothewebrequest'srequeststream.

Now,addanewmethodintheUpdateVehicleGroup.csfileintheUpdateVehicleGroupclass.Themethodshouldreadasfollows:

publicConWHS.ConWHSMessageContractUpdateJSON(

AuthenticationContract_authContract,

ConWHS.ConWHSVehicleGroupChangeContract_change)

{

Authenticateauth=newAuthenticate();

auth.Authentication=_authContract;

ConWHS.ConWHSMessageContractmessage;

if(!auth.GetAuthenticationHeader())

{

message=newConWHS.ConWHSMessageContract();

message.Success=false;

message.Message=auth.Authentication.Response;

returnmessage;

}

Json.Clientclient=newJson.Client(

_authContract.Resource,auth);

message=client.UpdateVehicleGroup(

_change,

"ConWHSServices",

"VehicleServices",

"UpdateVehicleGroup");

returnmessage;

}

Thisistosimplifyusage.Finally,addasectiontotheMainmethodofProgram.cs,justbelowtheexistingJSONtestcode:

ConWHS.ConWHSVehicleGroupChangeContractjsonChange;

jsonChange=newConWHS.ConWHSVehicleGroupChangeContract();

Console.Write("VehicleId:");

jsonChange.VehicleId=Console.ReadLine();

Console.Write("Newvehiclegroup:");

jsonChange.VehicleGroupId=Console.ReadLine();

ConWHS.ConWHSMessageContractjsonMessage;

UpdateVehicleGroupjsonUpdate;

jsonUpdate=newUpdateVehicleGroup();

jsonMessage=jsonUpdate.UpdateJSON(

authContract,jsonChange);

if(jsonMessage.Success)

{

Console.WriteLine("Success!");

}

else

{

Console.WriteLine(jsonMessage.Message);

}

Thisisjustthestart.Youcanexperimentfurther;forexample,deserializingtheDataSetobjectssoyoucanpresentdatadirectlytogridsinyourapps.

Seealso...NewtonsoftJSONSamples(http://www.newtonsoft.com/JSON/help/html/Samples.htm)

ConsuminganexternalservicewithinDynamics365forOperationsThetechniquehasn'tchangedsubstantiallysinceDynamicsAX2012.Thekeydifferenceisthatwewillneedtomanuallycraftthebinding.WewillstillneedtocreateaC#projecttoconsumethewebservice,andthenuseitasareferencewithinourDynamics365forOperationsproject.

TheexampleserviceisaweatherserviceprovidedbyWebserviceX.net(www.webservicex.net).Thereisnorecommendationhere;itwassimplythefirstoneinwhenIsearchedforweatherwebservices.Theaimistocreatearecipethatyoucanuseforyourwebservices:thechosenserviceinthiscaseisn'trelevant.

Whenselectingaservicetouse,wemustcheckthelicensetermsandconditions,asnotallarefreetouse;justbecausethelicensetermsaren'tenforced,itdoesnotmeanthatitisfreetouse.

GettingreadyWewillneedanexistingDynamics365forOperationsprojectavailable.

Howtodoit...Tocreatetheservicewrapperfortheservice,followthesesteps:

1. CreateanewC#ClassLibraryprojectnamedWeatherService.2. Renametheclass1.csfiletoWeatherService.cs.3. Createanewservicereferencebyright-clickingontheReferencesnodeinSolutionExplorerand

choosingAddServiceReference.

4. Enterhttp://www.webservicex.net/globalweather.asmx?WSDLintheAddressfieldandclickonGo.5. EnterGlobalWeatherintheNamespacefield,andclickonOK.6. OpentheWeatherService.csfileandenterthefollowingpieceofcode:

usingSystem.ServiceModel;

usingWeatherService.GlobalWeather;

namespaceContoso.Weather

{

publicclassWeather

{

publicconststringserviceAddress

="http://www.webservicex.net/globalweather.asmx";

publicstaticstringGetWeatherForCity(

string_country,string_city)

{

//Wecan'tmodifyD365O'sconfigfile,sowe

//needtoaddthebindingmanually

varbinding=newBasicHttpBinding();

binding.MaxReceivedMessageSize=int.MaxValue;

varaddress=

newEndpointAddress(serviceAddress);

GlobalWeatherSoapClientclient=

newGlobalWeatherSoapClient(

binding,address);

returnclient.GetWeather(_city,country);

}

publicstaticstringGetCitiesByCountry(

string_country)

{

//Wecan'tmodifyD365O'sconfigfile,sowe

//needtoaddthebindingmanually

varbinding=newBasicHttpBinding();

binding.MaxReceivedMessageSize=int.MaxValue;

varaddress=new

EndpointAddress(serviceAddress);

GlobalWeatherSoapClientclient=

newGlobalWeatherSoapClient(

binding,address);

returnclient.GetCitiesByCountry(_country);

}

}

}

Thenamespaceshouldbebasedonyourorganization'sconventions,forexample,Contoso.Weather,orContoso.Services.)

7. Saveallandbuildtheproject.8. CopytheDLLfrombin/debug,forexample,C:\Projects\TFS\WeatherService\WeatherService\bin\Debug)tothe

projectfolderoftheDynamics365forOperationsproject(C:\Projects\TFS\ConWHSVehicleManagement\ConWHSVehicleManagement).

9. AddtheDLLtosourcecontrolusingVisualStudio'sSourceControlExplorer.

ThefollowingstepsshowhowtousetheDLLcreatedinthepreviousstepsinordertoaccesstheservicefromwithinDynamics365forOperations:

1. Closethesolution,andopenyourDynamics365forOperationsproject,inourcaseConWHSVehicleManagement.

2. Right-clickontheReferencesnodeandchooseAddreference.3. ClicktheBrowsetab,toselecttheDLLfilethatwecopiedtothisproject'sprojectfolder.Selectthe

DLLandclickonOK.4. CreateanewclassnamedConWHSWeatherService.5. Createtwomethodssowecaneasilyaccesstheservice,asshownhere:

publicstaticstrGetCities(str_country)

{

returnContoso.Weather.Weather::GetCitiesByCountry(

_country);

}

publicstaticstrGetWeather(str_country,str_city)

{

returnContoso.Weather.Weather::GetWeatherForCity(

_country,_city);

}

6. Totestthis,createaformcalledConWHSWeatherTestandusetheDialog-Basicformpattern.7. Right-clickontheMethodsnode,clickonNewFormMethod,andenterthefollowingpieceofcode:

strcityName;

strcountryName;

Notesresult;

publiceditstrCityName(boolean_set,str_cityName)

{

if(_set)

{

cityName=_cityName;

result=ConWHSWeatherService::GetCities(

countryName);

}

returncityName;

}

publiceditstrCountryName(boolean_set,str_countryName)

{

if(_set)

{

countryName=_countryName;

result=ConWHSWeatherService::GetWeather(

countryName,cityName);

}

returncountryName;

}

publicdisplayNotesResult()

{

returnresult;

}

8. ClosethecodeeditorandaddagroupcontrolundertheDesignnode.9. NametheMainFormGroupcontrol,andsettheCaptionpropertyasdesired.10. CreatethreeStringcontrolsasdefinedinthefollowingtable:

Name DataMethod Label MultiLine

CountryNameCtrl CountryName Country No

CityNameCtrl CityName City No

ResultCtrl Result Result Yes

YoumayalsowishtoadjustthewidthandheightofResultCtrl.

11. CompletethepatternbyaddingaButtonGroupcontrolwithtwoCommandbuttonsforOKandCancel.Theyaren'tneededforourtest,buttheformpatternweselectedrequiresthis.

12. SaveandclosetheformdesignerandcreateamenuitemnamedConWHSWeatherTest,labelledTestWeather.13. AddthemenutothePeriodicTaskssubmenuoftheConWHSVehicleManagementmenu.14. Buildtheprojectandtesttheform.

YoumayreceiveanerrorstatingthattheDLLorPDBfilecannotbecopiedtoC:\AOSService\PackagesLocalDirectory\CONWHSVehicleManagement\bin(orsimilarfolder).Inthiscase,stoptheAOSServiceapplicationpoolinInternetInformationServices(IIS)Manager.Youcanstartitoncethebuildhascompleted-therecycleoptionmaynothelpinthiscase.

15. Navigatetothemenuitemandenterthecountryname,suchasUnitedKingdom.16. TheservicecorrectlyreturnsanXMLstringcontainingalistofcities.17. Enterthecity,andyouwillreceiveforecastdata.

Howitworks...ThefirstpartoftherecipeisstraightforwardSOAP.Theonlyvarianceisthatwemanuallycraftthebinding.

WhenweaddedtheDLLtoourDynamics365forOperationsproject,itmakesthetypesintheDLLavailabletouse;thisiswhytheIntellisenseworkedwhenwewrotethetwomethodsinX++.

Theformisjustatestbed,nothingreallynewhere.

Whentheprojectisbuilt,itcopiestheDLLtothebinfolderofthepackage.InthecaseoftheConWHSVehicleManagementpackage,itisasfollows:C:\AOSService\PackagesLocalDirectory\CONWHSVehicleManagement\bin

Thiswouldcauseaproblemwhenabuildservertriestobuildtheproject,itwillneedtheDLL.Thisiswhywecopiedittoafolderthatisalreadyinsourcecontrol.Thereareothersolutionstothisbut,inmanycases,wemaynotalwayshavethesamesourcetoaDLL.

There'smore...

IfweputtogetherourknowledgeofJSON,wecouldcreateasolutionwhereweconvertedtheXMLtoJSONandserializedthistoC#classes.Thiswouldmaketheimplementationmucheasiertouse.

Therearemanywebservicesoutthere,ofwhichmanyarefreetouse.Theycaninclude:currencyexchangerates,unitconversions,orevenintegrationwithabot.

ExtensibilityThroughMetadataandDataDate-EffectivenessInthischapter,wewillcoverthefollowingrecipes:

UsingmetadatafordataaccessUsingInterfacesforextensibilitythroughmetadataMakingdatadate-effective

IntroductionInthischapter,thefocuswillbeonprovidingtechniquestomakeoursolutionmoreeasilyextendablebyotherdevelopersandalsotoprovidemorecontroltousers.

Usingmetadatafordataaccess

AlltablesandfieldswithinDynamics365forOperationshaveanID.ThetableIDcanbeusedtogenerateaninstanceofthetable,andwecanworkwiththattableasifwehaddeclareditasatypeinamethod,makingourcodemoregenericandmoreeasilyextendable.

Inthisexample,wewillwriteadatadefaultingframeworktodemonstratethis.Here,wewillcreateatableandaformthatallowsustostoredefaultsforatable,andaclassthatwillsetthedefaultswhenarecordiscreated.

Gettingready...Wewillneedasettingsformandtable,alongwithatableforwhichweshallsetthedefaultsfor.Thisexamplewillusethesamplevehiclemanagementsolutiondevelopedduringthecourseofthisbook.

Howtodoit...First,wewillcreatethesettingstableforthefieldsthatwewanttoallowdefaultsfor.Todoso,followthesesteps:

1. CreateanewtablenamedConWHSVehicleTableDefaults.

Thisparticularsamplewillbefixedtothevehicletable,butyoushouldbeabletoextendthistoworkwithanytableyoudesire.

2. LocatetheExtendeddatatypes(EDT)RefFieldName,andRefFieldLabelintheApplicationExploreranddragthemtotheFieldsnodeofthetable.MaketheRefFieldLabelfieldread-only.

3. DragtheEDTString255totheFieldsnodeandrenamethefieldtoDefaultValueStr.CreatealabelforDefaultvalue,andsettheLabelpropertytoit.

Thepatternforthistypeoftablewouldnormallycontinuebyaddingafieldforeachbasetype,andtheformwoulduseahandlertoshowtherelevantfieldbaseonthefield'sbasetype.Inthiscase,wewillstopatstring,soastonotdetractfromthemainfocusofthisrecipe.

4. Createfieldgroups,asshowninthefollowingtable:

Groupname Label Fields

Overview @ConWHS:OverviewRefFieldName

RefFieldLabel

DefaultValue @SYS40175 DefaultValueStr

5. CreateauniqueindexforRefFieldNameandmakeitthetable'sClusteredindex.6. Completethetableproperties,asshowninthefollowingtable:

Property Fields

LabelNewlabel

Vehicletabledefaults

TitleField1 RefFieldLabel

TitleField2 DefaultValueStr

CacheLookup EntireTable

ClusteredIndex FieldNameIdx

TableGroup Group

<Trackingfields> Asdesired,itmaybeusefultoknowwholastchangedit.

7. CreateastandardFindandExistmethod,asshowninthefollowingcode:

publicstaticbooleanExist(

RefFieldName_fieldName)

{

ConWHSVehicleTableDefaultstable;

if(_fieldName!='')

{

selectfirstonlyRecIdfromtable

wheretable.RefFieldName==_fieldName;

}

return(table.RecId!=0);

}

publicstaticConWHSVehicleTableDefaultsFind(

RefFieldName_fieldName,

boolean_forUpdate)

{

ConWHSVehicleTableDefaultstable;

if(_fieldName!='')

{

table.selectForUpdate(_forUpdate);

selecttable

wheretable.RefFieldName==_fieldName;

}

returntable;

}

8. TheRefFieldNamefieldwillbeafieldnamefromtheConWHSVehicleTable;sinceweworkwithIDs,itisusefultohaveahelperfunctionthatreturnsthisID.Createthefollowingmethod:

privateRefFieldIdGetFieldId(

RefFieldName_fieldName=this.RefFieldName)

{

returnfieldName2Id(tableNum(ConWHSVehicleTable),

_fieldName);

}

WewillallowthefieldnametobepassedsothatwecangetthefieldIDforanamenotcurrentlyupdatedtothecurrentrecord.

9. Next,createanewmethodthatsetsRefFieldLabelfromthefield'smetadata,asshowninthefollowing

code:

publicvoidInitFromFieldName()

{

RefFieldIdfieldId=this.GetFieldId();

if(fieldId==0)

{

this.RefFieldName='';

this.RefFieldLabel='';

return;

}

DictFielddField=newDictField(

tableNum(ConWHSVehicleTable),fieldId);

if(dField)

{

this.RefFieldLabel=dField.label();

}

}

10. Now,insertandoverridethemethodformodifiedFieldtocalltheInitFromFieldNamemethodasfollows:

publicvoidmodifiedField(FieldId_fieldId)

{

super(_fieldId);

switch(_fieldId)

{

casefieldNum(ConWHSVehicleTableDefaults,

RefFieldName):

this.InitFromFieldName();

break;

}

}

11. Thefieldselectedmustexist,beoftypeString,andbeeditableoncreate.Tomakethecodeeasiertoread,weshouldcreateacheckfunctionforthis,whichiswrittenasfollows:

publicbooleanCheckRefFieldName(

RefFieldName_fieldName=this.RefFieldName,

boolean_silent=false)

{

booleanok=true;

ErrorMsgmsg='';

RefFieldIdfieldId=this.GetFieldId(_fieldName);

If(fieldId==0)

{

ok=false;

//Field%1doesnotexistintable%2

msg=strFmt("@SYS75684",

this.RefFieldName,

tableStr(ConWHSVehicleTable));

}

DictFielddField=newDictField(

tableNum(ConWHSVehicleTable),fieldId);

if(dField)

{

if(dField.baseType()!=Types::String)

{

ok=false;

//Typeisnotsupported%1

msg=strFmt("@SYS73815",dField.baseType());

}

if(!dField.allowEditOnCreate())

{

ok=false;

//Thefield%1cannotbeselected.

msg=strFmt("@SYS70689",this.RefFieldName);

}

}

If(!ok&&!_silent)

{

returncheckFailed(msg);

}

returnok;

}

Themethodacceptsthe_silentparameteraswewillwanttousethesamelogictobuildthelookuplater.

12. WewillnowcallthecheckmethodintheprecedingcodetothevalidateFieldformtriggeredevent.OverridethevalidateFieldmethod,asshownhere:

publicbooleanvalidateField(FieldId_fieldIdToCheck)

{

booleanret;

ret=super(_fieldIdToCheck);

switch(_fieldIdToCheck)

{

casefieldNum(ConWHSVehicleTableDefaults,

RefFieldName):

ret=this.CheckRefFieldName();

break;

}

returnret;

}

13. Finally,wewillneedalookupfunctionthatonlyallowstheusertoselectfieldsthatarevalid.Thecodeisasfollows:

publicstaticvoidLookupFieldName(

FormStringControl_control)

{

FormRunformRun;

Argsargs;

Mapfieldmap;

DictTabledTable;

Counteridx;

RefFieldIdfieldId;

ConWHSVehicleTableDefaultsdefaultsRecord;

defaultsRecord=_control.dataSourceObject().cursor();

fieldMap=newMap(Types::String,Types::String);

dTable=newDictTable(tableNum(ConWHSVehicleTable));

for(idx=1;idx<=dTable.fieldCnt();idx++)

{

fieldId=dTable.fieldCnt2Id(idx);

RefFieldNamefieldName=fieldId2Name(dTable.id(),

fieldId);

if(defaultsRecord.CheckRefFieldName(fieldName,

true))

{

fieldMap.insert(fieldName,fieldName);

}

}

args=newArgs(formStr(SysPick));

args.parmEnumType(enumNum(SysPickListType));

args.parmEnum(enum2int(SysPickListType::Simple));

Args.parmObject(fieldMap);

formRun=classfactory.formRunClass(args);

_control.performFormLookup(formRun);

formRun.wait();

}

14. Next,createanewformnamedConWHSVehicleTableDefaultsanddragthenewtabletoitsDataSourcesnode.

15. CompletethedesignoftheformbasedontheSimpleListandDetails-ListGridformpattern.

16. Completetheformasperthepatternrequirements,usingtheOverviewgroupforNavigationListGroup.Thecompleteddesignshouldlooklikethefollowingscreenshot:

Remembertonamethecontrolsafterthetemplatename,asthismakesthedesigneasiertoworkwith.Thetemplatewillmakethewrongassumptionsaboutwhichcontrolmatchesthetemplatesectionuntilallthetemplatesectionshavematchingcontrols.

17. Wewillnowneedtooverridethelookupforthefieldname,expandtheDataSourcesnode,andlocatetheRefFieldNamefieldundertheConWHSVehicleTableDefaultsdatasource'sFieldsnode.

18. Right-clickonthefield'sMethodsnodeandselectOverride|lookup.19. Inthecodeeditorthatopens,removethelinesuper(_formControl,_filterStr);andreplaceitwiththe

followingline:

ConWHSVehicleTableDefaults::LookupFieldName(_formControl);

20. CreateadisplaymenuitemandaddittotheSetupsubmenuofConWHSVehicleManagement.21. Finally,completetheFormRefpropertyoftheConWHSVehicleTableDefaultstableandbuildtheproject,

includingdatabasesynchronizationaswehaveaddedanewtable.

Wehavenowcompletedthesetuptableandform,wecannowcreateahandlersothatthetable'sdefaultsareset.Todothis,followthesesteps:

1. CreateanewclassnamedConWHSVehicleTableDefaultingEngine.2. Writethefollowinglinesofcodetoinitializetheinstance:

privateConWHSVehicleTablevehicle;

publicConWHSVehicleTableVehicle(

ConWHSVehicleTable_vehicle=vehicle)

{

vehicle=_vehicle;

returnvehicle;

}

publicstaticConWHSVehicleTableDefaultingEngine

NewFromVehicleTable(ConWHSVehicleTable_vehicle)

{

ConWHSVehicleTableDefaultingEngineengine;

engine=newConWHSVehicleTableDefaultingEngine();

engine.Vehicle(_vehicle);

returnengine;

}

3. Then,writethemethodsthatperformtheupdate,whichareasfollows:

privatevoidProcessDefaults()

{

ConWHSVehicleTableDefaultsdefaults;

whileselectdefaults

{

RefFieldIdfieldId=fieldName2Id(

vehicle.TableId,

defaults.RefFieldName);

if(fieldId)

{

DictFielddField;

dField=newDictField(vehicle.TableId,

fieldId);

switch(dField.baseType())

{

caseTypes::String:

vehicle.(fieldId)=

defaults.DefaultValueStr;

break;

}

if(vehicle.validateField(fieldId))

{

vehicle.modifiedField(fieldId);

}

else

{

switch(dField.baseType())

{

caseTypes::String:

vehicle.(fieldId)='';

break;

}

}

}

}

}

publicbooleanValidate()

{

if(vehicle.RecId!=0)

{

//Cannotdefaultrecordsthatalreadyexist

returncheckFailed("@ConWHS:ConWHS61");

}

returntrue;

}

publicvoidRun()

{

if(!this.Validate())

{

return;

}

this.ProcessDefaults();

}

4. Thefinalmethodisthedataeventhandler,whichwilltriggerwhentherecordisbeinginitialized,anditiswrittenasfollows:

[DataEventHandlerAttribute(tableStr(ConWHSVehicleTable),

DataEventType::InitializingRecord)]

publicstaticvoidInitValueEventHandler(

Common_record,

DataEventArgs_eventArgs)

{

ConWHSVehicleTableDefaultingEngineengine;

engine=ConWHSVehicleTableDefaultingEngine::

NewFromVehicleTable(_record);

engine.Run();

}

5. Finally,buildandruntheproject.TestthecodebycreatingasetofdefaultsinthenewVehicletabledefaultvaluesform,andthencreateanewvehicleusingtheVehiclesform.

6. TryaddingtheVehiclegroupasadefaultwithavaluethatdoesn'texist;youshouldgetanerrorwhenanewrecordiscreated:Thevalue'invalidvalue'infield'Vehiclegroup'isnotfoundintherelatedtable'Vehiclegroups'.

Howitworks...Therearetwomainnewtopicsinthisrecipe.Thefirstistousethedictionaryclasses,DictTableandDictField,togainaccesstothetableandfieldproperties.Thiswasusedtobothvalidatethefieldandalsotobuildacustomlookupforfieldsthatcanbeedited.

TheothernewtopicisthatwecanusethefieldIDtoaccessthefieldonarecord;forexample,thecodevehicle.Description="Newdescription";isequivalenttothefollowing:

RefFieldIdfieldId=fieldNum(ConWHSVehicleTable,Description);

vehicle.(fieldId)="Newdescription";

Wehavetobecarefulbecausethefollowingcodewouldcompile,butfailwitharuntimeerror:

RefFieldIdfieldId=fieldNum(ConWHSVehicleTable,Description);

vehicle.(fieldId)=12.345;

ThisiswhyweusedtheDictField.baseType()method.

Youcanalsousethismethodinwhereclauses,butbeverycarefulbeforeyoudothis,inordertoavoidanyunpleasantruntimeerrorsthatmaynotpresentthemselvesatthepointoffailure.

WewillalsocreatealookupusingaMapobjecttocreatecustomlookupsthataren'tbasedondata.

AlthoughthecodecurrentlyonlyhandlesfieldsoftheStringbasetype,itcanbeeasilyextendedtohandleotherbasetypes.YoucouldevenextendthevalidationonConWHSVehicleTableDefaultstocheckifthevaluesarevalid.ThiswouldbedonebycreatingadummyConWHSVehicleTablerecord,populatingthefieldtheuserisaddingadefaultforandcallingthetable'svalidateFieldmethodwiththefieldID.

Tofullyunderstandwhatisgoingon,itisusefultousethedebuggertostepthroughthecode.Extendingthesampleinthisrecipeisalsoagreatwaytobecomecomfortablewithworkingwithmetadata.

Oneofthecommonmodificationsistosetdefaultdates,andwecouldusethathere,too.Thedefaultvaluewouldprobablybeanoffsetfromtoday'sdate,andnotaliteraldate.

UsingInterfacesforextensibilitythroughmetadataInterfacesenforcethatallclassesthatimplementthemalsoimplementthemethodsdefinedintheinterface.Thishasallthetraditionalbenefitsassociatedwiththem,butinDynamics365forOperations,wecangofurther.

WeshouldcreateaninterfacecalledMyRunnableIwithamethodcalledRun()andaclassthatimplementsitcalledMyRunningPerson(whichmusthaveamethodcalledRun()).WecanassignaninstanceofMyRunningPersontoavariableoftypeMyRunnableIandcallitsRun()method.Thisallowsgreaterflexibilityandextensibilityofourcode.

However,inDynamics365forOperations,wecancreateapluginframeworkwheretheclasstoinstantiateisconfiguredindata.Wecan,therefore,controlwhichclassgetsinstantiatedbasedonconditionsonlyknownatruntime.

Inordertofocusmoreonthewaythatwecanuseinterfacestocreateapluginpattern,wheretheclasstocallisstoredindata,theexamplewillbesimple.Wewillcreatea'colorable'interfaceandallowtheusertoselectwhichimplementationofthecolorableinterfacewillbeused.Youcouldcombinethisideawiththepreviouschaptertodeterminewhichwebservicetousetogettheweather,forexample.

Gettingready...Inthisexample,weshallcreateapatternbasedonnewelements,sowewillonlyneedaDynamics365forOperationsprojectopeninVisualStudio.

Howtodoit...

Tocreateapluginusinginterfacesthroughmetadata,followthesesteps:

1. First,createtheinterface,whichiscreatedasaclassinitially,usingthefollowinglinesofcode:

publicinterfaceConWHSVehicleColorable

{

publicColorColor()

{

}

}

2. Wewillneedaclassthatimplementsthisinterface.CreateaclassnamedConWHSVehicleGroupColorRed,whichwillsimplyreturnthestringliteralRed,asshownhere:

classConWHSVehicleGroupColorRed

implementsConWHSVehicleColorable

{

publicColorColor()

{

return'Red';

}

}

3. Tokeeplogicawayfromtheformandsimplifyusage,createaclassnamedConWHSVehicleGroupColorSetupForm.

5. Next,createastaticlookupmethod.Thepatternshouldbefamiliarfromthepreviouschapters.Thisisanexampleofcreatingalookupthatisnotbasedondata:

publicstaticvoidLookupColorableClasses(

FormStringControl_classNameControl)

{

FormRunformRun;

Argsargs;

MapclassMap;

ListclassList;

SysDictClassdClass;

ClassIdclassId;

ClassNameclassName;

DictClassimplementationClass;

dClass=newSysDictClass(

classNum(ConWHSVehicleColorable));

classList=dClass.implementedBy();

classMap=newMap(Types::String,Types::String);

ListEnumeratorle;

le=classList.getEnumerator();

while(le.moveNext())

{

classId=le.current();

implementationClass=newDictClass(ClassId);

if(!implementationClass.isInterface())

{

className=classId2Name(classId);

classMap.insert(className,className);

}

}

args=newArgs(formStr(SysPick));

args.parmEnumType(enumNum(SysPickListType));

args.parmEnum(enum2int(SysPickListType::Simple));

args.parmObject(classMap);

formRun=classfactory.formRunClass(args);

_classNameControl.performFormLookup(formRun);

formRun.wait();

}

ItisOK,inthiscase,tohavethisfunctioninacase-specificclass,astheusageisspecific.Itwouldusuallybebettertocreateautilityclassforthesefunctionsandpasstheinterfaceasamethodparameter.

5. Completetheformhandlerclassusingthestandardpattern,asshowninthefollowingcodesnippet:

FormDataSourcevehicleGroupSetupDS;

protectedvoidsetVehicleGroupSetupDS(FormDataSource_vehicleGroupSetupDS)

{

vehicleGroupSetupDS=_vehicleGroupSetupDS;

}

publicstaticConWHSVehicleGroupColorSetupForm

NewFromFormDS(FormDataSource_vehicleGroupSetupDS)

{

ConWHSVehicleGroupColorSetupFormform=

newConWHSVehicleGroupColorSetupForm();

form.setVehicleGroupSetupDS(_vehicleGroupSetupDS);

returnform;

}

6. Next,createatablenamedConWHSVehicleGroupColorSetupasagrouptable,withthefollowingfields:

Field Label EDT

VehicleGroupId Inherited ConWHSVehicleGroupId

Description Inherited Description

ColorClassName Colorableclass ClassName

7. Ensurethatthetableiscompletedtobestpractices,andensurethefollowing:

TheprimarykeyisVehicleGroupIdColorClassNameismandatoryAfieldgroupiscreatedfortheoverviewgridThestandardFindandExistmethodsarecreated

8. Writethefollowingstaticmethodonthetable:

publicstaticbooleanImplementsColorable(

ClassName_className)

{

ClassIdclassId;

DictClassdictClass;

classId=className2Id(_className);

if(ClassId==0)

{

returnfalse;

}

dictClass=newDictClass(ClassId);

if(dictClass)

{

Counteridx;

ClassIdtestClassId;

for(idx=1;idx<=dictClass.implementsCnt();

idx++)

{

testClassId=dictClass.implements(idx);

if(testClassId==

classNum(ConWHSVehicleColorable))

{

returntrue;

}

}

}

returnfalse;

}

9. Now,let'svalidatethattheclassselectedimplementstheinterface.OverridevalidateFieldandwritethefollowingcodesnippet:

publicbooleanvalidateField(FieldId_fieldIdToCheck)

{

booleanret;

ret=super(_fieldIdToCheck);

if(ret)

{

switch(_fieldIdToCheck)

{

casefieldNum(ConWHSVehicleGroupColorSetup,

ColorClassName):

If(!ConWHSVehicleGroupColorSetup::

ImplementsColorable(this.ColorClassName))

{

//%1mustimplement%2

ret=checkFailed(strFmt(

"@ConWHS:MustImplement",

this.ColorClassName,

classStr(ConWHSVehicleColorable)));

}

}

}

returnret;

}

10. CreateanewformforthetableusingtheSimpleListpattern,andfollowthepatterntocompletetheform.

11. Completetheformhandlerpatternbyaddingthefollowinglinesofcodetotheform'scode:

ConWHSVehicleGroupColorSetupFormformHandler;

publicvoidinit()

{

super();

formHandler=ConWHSVehicleGroupColorSetupForm::

NewFromFormDS(ConWHSVehicleGroupColorSetup_DS);

}

12. Toaddthelookup,expandDataSources,ConWHSVehicleGroupColorSetup,andFields.LocatetheColorClassNamefieldandoverridethelookupmethod.Writethefollowinglinesofcode:

publicvoidlookup(FormControl_formControl,str_filterStr)

{

ConWHSVehicleGroupColorSetupForm::

LookupColorableClasses(_formControl);

}

13. Wewillneedaclassthatwillexecuteourcolorableclass,nametheclassConWHSVehicleColorExecute,andaddthefollowingpieceofcode:

ConWHSVehicleGroupIdgroupId;

Colorcolor;

publicColorGetColor()

{

returncolor;

}

publicConWHSVehicleGroupIdParmVehicleGroupId(

ConWHSVehicleGroupId_groupId=groupId)

{

groupId=_groupId;

returngroupId;

}

publicstaticConWHSVehicleColorExecute

newFromVehicleGroupId(ConWHSVehicleGroupId_groupId)

{

ConWHSVehicleColorExecuteexecute;

execute=newConWHSVehicleColorExecute();

execute.ParmVehicleGroupId(_groupId);

returnexecute;

}

publicbooleanCanExecute()

{

If(!ConWHSVehicleGroupColorSetup::Exist(groupId))

{

returnfalse;

}

returntrue;

}

publicvoidRun()

{

If(this.CanExecute())

{

this.execute();

}

}

privatevoidexecute()

{

ConWHSVehicleGroupColorSetupsetup;

setup=ConWHSVehicleGroupColorSetup::Find(groupId);

if(!ConWHSVehicleGroupColorSetup::

ImplementsColorable(setup.ColorClassName))

{

return;

}

DictClassdictClass;

dictClass=newDictClass(

className2Id(setup.ColorClassName));

if(dictClass)

{

ConWHSVehicleColorablecolorable;

colorable=dictClass.makeObject();

color=colorable.Color();

}

}

14. Let'stestthisbyaddingabuttontooursetupform.OpentheConWHSVehicleGroupColorSetupFormclassandaddthefollowingmethod:

publicvoidTestColor()

{

ConWHSVehicleGroupColorSetupsetup;

setup=vehicleGroupSetupDS.cursor();

ConWHSVehicleColorExecuteexecute;

execute=ConWHSVehicleColorExecute::

newFromVehicleGroupId(setup.VehicleGroupId);

execute.Run();

info(execute.GetColor());

}

15. OpentheConWHSVehicleGroupSetupFormformandwritethefollowingmethod:

privatevoidTestColor()

{

formHandler.TestColor();

}

16. Closethecodeeditorandgobacktotheformdesigner.OntheFormActionPaneControlcontrolthatwecreatedaspartofcompletingtheform'spattern,addanActionPanetab;tothis,addButtonGroupandfinallyaButtoncontrol.

17. Overridetheclickedmethodandwritethefollowinglinesofcode:

publicvoidclicked()

{

super();

element.TestColor();

}

18. CreateamenuitemforourformandaddthistotheSetupmenuforourConWHSVehicleManagement.

19. Saveeverything,buildtheproject,andsynchronizethedatabasewiththeproject.

Howitworks...Thispatternachievesasafemethodtoallowafunctionalconsultant(orenduser)tochoosethebusinesslogicbasedondata.Thisisverypowerful,andallowsotherpartiestoaddtheirownplugins,whicharetobeusedinsteadofthosesupplied.

Tounderstandwhatisgoingon,thefirstthingtoknowisthatwecanwritethefollowingcode:

ConWHSVehicleColorablecolorable;

colorable=newConWHSVehicleGroupColorRed();

ColormyColor=colorable.Color();

Wecan'tinstantiateaninterface,butwecanassignaninstanceofaclasstoavariabledeclaredtobeaninterface,aslongastheclassimplementstheinterface.Anyerrorswillbehandledbythecompiler.Wewillconsidertheinterfacetobeacodecontract,andtheimplementskeywordbindstheimplementationtotheinterface.

Thereisanalternativetousinganinterface,inthat,wecouldwritethefollowinglinesofcode:

Objectobject;

object=newConWHSVehicleGroupColorRed();

ColormyObjColor=object.Color();

Althoughitwouldwork,inthiscase,thecodeisaterribleidea.Objectshouldalwaysbealastresort,andthiscodeispronetoregressionerrors.Theinterfaceisacontract;theobjecttechniqueallowsyoutowriteanymethodnameyoulike.

Ourexampleistoallowtheclasstobedefinedatruntime.Usingaswitchorattribute-basedconstructorrequiresustohardcodetherelationshipbetweenattributeorotherconditionandtheclasstoconstruct.Wewantausertobeabletodeterminethis.ThisisdoneusingDictClass.NotonlycanweuseDictClasstogaininformationaboutmetadata,butwecanalsousethemtoconstructaninstanceaclass.TheDictClassclassisinstantiatedusingaclass'sID,asshownhere:

DictClassdc=newDictClass(<MyClassId>);

Inourcase,theclassnameisstoredinatable,allowingaclassthatimplementsConWHSVehicleColorabletobeselected.

Therecipecanbebrokendownintosimplesteps,thefirstofwhichistodeterminetheclass'sIDfromthename.ThisIDisanimplementationspecificnumericobjectorelementidentifier,sowemustobtaintheIDthroughafunctioncall.StandardobjectalwayshavethesameIDs,butthisshouldneverbeassumed.Whenwedeployapackagetoanenvironmentforthefirsttime,theIDisset;itwillnotchangeuponsubsequentdeployments.Thisisveryimportantwhenyouconsiderhowextensibleenumerationswork,orifwedecidetostoreIDsinsetuptable--iftheIDchanged,thecodewillfailinunexpectedways.ThecodetodeterminetheobjectIDoftheclassnamethatweenteredinthesetuptableisasfollows:

ClassIdclassId=className2Id(setup.ColorClassName);

WecannowinstantiateavariableoftypeDictClass:

DictClassdc=newDictClass(classId);

Declareavariablethatisofthesametypeasinterface,ConWHSVehicleColorable:

ConWHSVehicleColorablecolourable=dc.makeObject();

WecannowcalltheColor()method.Themakeobject()methodcreatesaninstanceoftheclassitwasconstructedwith:theclassweenteredinoursetuptable.Shouldweenteraclassthatdoesnotimplementtheinterface,wewillgetanerroratthispoint,eveniftheclasshastheColor()method.Thecodecontainsvalidationondataentryandintheexecutionclass.Thismustbedoneinordertoavoidanyruntimeerrors.

Evenwithvalidationonentryandusage,wecan'tcompletelyavoidregressioninthiscase.Usinganinterface,however,doeshelpusreduceregressionerrors,forexample,itwouldhelpuscatcherrorscausedbyrefactoring.

Makingdatadate-effective

Dateeffectivetablesallowustocreateanewversionwheneverthedataischanged,andseethedateatanypointintime.Thissoundsgreat,andwillnodoubtfindrequeststomakealltablesdateeffective.Thereisapenalty.Thefirstisthatitbringsalittlecomplexitytotheprocessofdevelopingthetables,reports,anduserinterface.Theotheristhatitcreatesanewrecordeverytime,andcanaffectperformance.Weshouldonlyusethisifwereallyneedit.Greatexamplesincludethehumanresourcetablestoallowhistoryofnamechanges,previousandplannedpositions,addresses,andsoon.

Inourexample,wewillcreateanewtableforvaluessuchasodometerreadingsforavehicle.Wewillcreateatableforthisthatisdateeffectivesothatweonlyrecordanewversionwhenthesekeyfieldsarechanged.

Gettingready...ThisrecipeassumesthatwehavefollowedthechapterstocreateConWHSVehicleTable,butthepatternbehindthisrecipecanbeappliedtoanyrequirementwhereweneedtorecordthehistoryofchangestoarecord.

Howtodoit...Inthesesteps,wecreateatablethatputstheconceptofdate-effectivityintouse:

1. CreateanewintegerEDTnamedConWHSOdometerwithalabelOdometer.2. CreateanewtablenamedConWHSVehicleTableServiceData.3. DragtheConWHSVehicleIdEDTontotheFieldsnode,andrenameittoVehicleId.Configurethefield's

propertiestobeaprimarykey(cannoteditaftercreationandismandatory).Createaforeignkeyrelationship,butdonotcreateanindex.

4. DragtheConWHSOdometerEDTontotheFieldsnode,andrenametoOdometer.5. Wecannowchoosewhetherourchangescanbeupdateddaily,orforeverychangemade.Thisis

doneusingthetable'sValidTimeStateFieldTypeproperty.Wewanttoallowchangestobemadefrequently,andthateverychangeshouldbesavedasanewversion,sochooseUtcDateTime.

ThiswilladdtwofieldsoftypeUtcDateTimenamedFromDateandToDate.Theseareusedinordertotimestampeachversionoftheapparentrecord.

6. CreateanewprimaryindexnamedVehTimeIdxusingthefollowingsettings:

Property Value

AllowDuplicates No

AlternateKey Yes

ValidTimeStateKey Yes

ValidTimeStateMode NoGap

7. AddtheVehicleIdandFromDatefieldstotheindex.

WewillneedtoaddtheToDatefieldifwesetValidTimeStateModetoGap.

8. Setthetable'sReplacementKeypropertytothenewindex.9. TheFind()methodmustalsobewritteninordertofetchthecurrentversionbydefault,whichisdone

inthefollowingcodesnippet:

publicstaticConWHSVehicleTableServiceDataFind(

ConWHSVehicleId_vehicleId,

ValidFromDateTime_fromDateTime=

DateTimeUtil::utcNow(),

ValidToDateTime_toDateTime=_fromDateTime,

boolean_forUpdate=false)

{

ConWHSVehicleTableServiceDataserviceData;

serviceData.selectForUpdate(_forUpdate);

//thevalidtimestateisusedtoselect

//theversionatapointintime

selectfirstonly

validtimestate(_fromDateTime,_toDateTime)

serviceData

whereserviceData.VehicleId==_vehicleId;

returnserviceData;

}

10. WritetheExist()methodusingthesamepattern.11. Now,let'screateaformsothatwecanseewhathappens.NametheformConWHSVehicleTableServiceData.

SincetheformwillbeopenedfromaVehiclerecord,anditiskeyedontheVehicleIdfield,theformwillusuallybeadialogordetailsform.However,inordertodemonstratemoreclearlywhatisgoingon,wewilluseaSimpleListpatterninstead.

12. ApplytheSimpleListpatternandcompletetheformaccordingtothepattern.13. CreateamenuitemandaddittotheConWHSVehicleTableform.14. Saveeverythingandbuildtheprojectandsynchronizeitwiththedatabase.15. OpenD365OinawebbrowserandopentheVehiclesform.Fromthem,openournewVehicleservice

dataform.ChangetheOdometervalue,whilstsavingtherecordforeachchange.16. OpenSQLServerManagementStudio,pressConnectontheConnecttoServerform,andpressAlt+Nto

createanewquerywindow.Typethefollowing:

UseAxDB

SELECT

VEHICLEID,ODOMETER,

VALIDFROM,VALIDTO

FROMCONWHSVEHICLETABLESERVICEDATA

17. Theresultwillbesomethinglikethis:

VehicleId Odometer ValidFrom ValidTo

V000001 150 2017-01-3113:42:25.000 2017-01-3113:42:39.000

V000001 250 2017-01-3113:42:40.000 2154-12-3123:59:59.000

Itisclearlyworking,butwenowneedtoallowtheusertoseetherecordsasatdifferenttimes.Todothis,wewillneedtoaddDateEffectivenessPaneControllertotheform.

18. OpentheConWHSVehicleTableServiceDataform,andopenclassDeclarationfromtheMethodsnodeandenterthecodepresentinthenextstep.

19. First,wewillneedtoimplementtheIDateEffectivenessPaneCallerinterface,soaltertheclass

declarationasfollows:

publicclassConWHSVehicleTableServiceData

extendsFormRun

implementsIDateEffectivenessPaneCaller

20. Now,addthefollowingpieceofcode:

DateEffectivenessPaneControllerdePaneController;

//Asrequiredbytheinterface

publicDateEffectivenessPaneController

getDateEffectivenessController()

{

returndePaneController;

}

privatevoidenableAsOfDateButton()

{

dePaneController.setDropDialogButtonEnabled(true);

dePaneController.parmShowAllRecords(true);

}

publicDateEffectivenessPaneController

parmDatePaneController(

DateEffectivenessPaneController_

dePaneController=dePaneController)

{

dePaneController=_dePaneController;

returndePaneController;

}

publicvoidinit()

{

super();

dePaneController=DateEffectivenessPaneController::

constructWithForm(

element,ConWHSVehicleTableServiceData_ds,

false,//showplurallabels

true,//allowshowallrecords

true);//useDateTime

element.enableAsOfDateButton();

}

21. Buildandtesttheformagain,youshouldnowhaveanewAsatdatecontrol.ShouldtheerrorFormwascalledincorrectlybethrown,itisbecausetheIDateEffectivenessPaneCallerinterfacewasnotimplemented.

22. Thiswillsufficefornow,butwecantakeitfurther,andthisisdescribedintheThere'smore...section.

Howitworks...WhenD365OconstructstheConWHSVehicleTableServiceDatadatasource,itautomaticallyfilterstherecordstothecurrentversion.Thetransact-SQLthatdoesthisisasfollows:

SELECTT1.VEHICLEID,T1.ODOMETER,T1.VALIDTO,T1.VALIDTOTZID,T1.VALIDFROM,T1.VALIDFROMTZID,T1.RECVERSION,T1.PARTITION,T1.RECIDFROMCONWHSVEHICLETABLESERVICEDATAT1WHERE(((PARTITION=5637144576)AND(DATAAREAID=?))AND((VALIDFROM<=?)AND(VALIDTO>=?)))ORDERBYT1.VEHICLEID,T1.VALIDFROMOPTION(FAST54)

Thishelpsusunderstandwhywemadethepropertychangestothetable.Theindexisclearlyneededforperformance,andthedatasourceusesthepropertiesonthetabletodecideonthequerysenttoSQLServer.

Whenworkingincode,weneedtobesureweareselectingthecorrectrecord.Findmethodsareindispensableinthiscase,anditiscommontowritemultipleFindmethodsforeachtypeofusagescenario.

Weaddedthedateeffectivenesscontrollertoallowtheusertoseeotherversionsontherecord,oreventoshowallrecords.Theshowalloptionshouldonlybeusedwhenweareviewinginagridview,sincetheusercouldn'tseetheotherrecordsanyway.

Toexperimentfurtherwiththis,trycreatingatablethatfollowsthepatternoftheHcmWorkerEnrolledBenefittable.

There'smore...ItwouldbeanicerinterfaceifwecouldhavetheOdometerfieldonthevehicleform,whichwouldrequireustomixanon-dateeffectivetablewithadateeffectivetable.TheHcmWorkertableisagreatexampleofthis.

Toimplementthispattern,wewouldfirstaddConWHSVehicleTableServiceDatatotheConWHSVehicleTableform,joinedtotheConWHSVehicleTabledatasourceasanOuterjoin.ModifyinitValueofConWHSVehicleTableServiceDatainordertodefaultVehicleId.

CreateafieldgroupinasuitableplaceandaddtheOdometerfield.

Sofar,thereisnothingnew.Next,wewillneedtoconstructthedateeffectivenesscontroller,butcontrolthechilddatasources.Thisisdonebysubscribingtodelegatesonthecontroller.Thecodeissimilartothecodeinrecipedescribedpreviously.

Thecodeiswrittenasfollows.Thefirstpartistodeclarethedateeffectivenesscontroller,aswedidbefore:

DateEffectivenessPaneControllerdePaneController;

Next,wewillneedtodeclaretwoeventhandlermethodsthatwewillusetosubscribethecontroller'sevents,whicharedelegates.Wewillneedtofiltertheservicedatarecordbasedonthewhattheuserdidinthedropdialog,andthisisdonebyaddingthefollowingpieceofcode:

///<summary>

///EventHandlerforanApplybutton(clicked)eventinDateEffectivenessPaneController

///</summary>

publicvoiddatePaneController_ApplyClicked()

{

utcdatetimeasOfDateTime;

utcdatetimenowDateTime=DateTimeUtil::utcNow();

if(dePaneController.parmShowAsOfDate()==

DateTimeUtil::date(nowDateTime))

{

asOfDateTime=nowDateTime;

}

else

{

asOfDateTime=DateTimeUtil::newDateTime(

dePaneController.parmShowAsOfDate(),

Global::timeMax(),

DateTimeUtil::getUserPreferredTimeZone());

}

//adjustthequeryfortheouterjoineddatasources

ConWHSVehicleTableServiceData_DS.validTimeStateAutoQuery(

ValidTimeStateAutoQuery::AsOfDate);

ConWHSVehicleTableServiceData_DS.query().

validTimeStateAsOfDateTime(asOfDateTime);

}

///<summary>

///EventHandlerforaCurrentbutton(clicked)eventin

///DateEffectivenessPaneControllerto

///viewthecurrentlyactiveversionrecord.

///</summary>

publicvoiddatePaneController_CurrentClicked()

{

//adjustthequeryfortheouterjoineddatasources

ConWHSVehicleTableServiceData_DS.validTimeStateAutoQuery(

ValidTimeStateAutoQuery::AsOfDate);

ConWHSVehicleTableServiceData_DS.query().

resetValidTimeStateQueryType();

}

Thenextmethodsareastandardpartofthepatternandarethesameastherecipe:

//Asrequiredbytheinterface

publicDateEffectivenessPaneController

getDateEffectivenessController()

{

returndePaneController;

}

privatevoidenableAsOfDateButton()

{

dePaneController.setDropDialogButtonEnabled(true);

dePaneController.parmShowAllRecords(false);

}

publicDateEffectivenessPaneControllerparmDatePaneController(

DateEffectivenessPaneController_dePaneController=

dePaneController)

{

dePaneController=_dePaneController;

returndePaneController;

}

Theinitmethodissimilartotherecipe,exceptthatwedon'twanttoshowallrecords,andwejustwanttosubscribetothecontroller'sevents.Theinitmethodshouldreadasfollows:

publicvoidinit()

{

super();

dePaneController=

DateEffectivenessPaneController::constructWithForm(

element,ConWHSVehicleTable_ds,

false,//showplurallabels

false,//donotallowshowallrecords

true);//useDateTime

dePaneController.onApplyClicked+=

eventhandler(this.datePaneController_ApplyClicked);

dePaneController.onShowCurrentClicked+=

eventhandler(this.datePaneController_CurrentClicked);

element.enableAsOfDateButton();

}

Finally,adjusttheservicedateformsothatithastheValidFromfields.Thisallowstheformtobeusedtoseethehistoryoftheservicedatatable.

UnitTestingInthischapter,wewillcoverthefollowingrecipes:

CreatingaFormAdaptorprojectCreatingaUnitTestprojectCreatingaUnitTestforcodeCreatingatestcasefromataskrecording

IntroductionUnittestinghelpsensurethatthecodebothfulfillstherequirement,andfuturechanges(eveninotherpackages)donotcausearegression.Theunittestiswrittenasaseparatepackagethatreferencesthepackageitistesting.IfwefollowTestDrivenDevelopment(TDD),wewillwritethetestsearlyintheprocess(somewouldarguefirst).TDDchangesthewaywethinkwhenwritingcode.Shouldweneedtomakeachangetoaproject,weareforcedtoupdatethetestcasecode(asthetestswillotherwisefail)--thispromotesatest-centricapproachtodevelopment,andnaturallyreducesthetestcycles.Regressioninotherpackagesiscaughtbythebuildprocess;thebuildserverwilldownloadallchecked-incode,performabuild,andthenlookforteststoexecute.Anyteststhatfailarereportedandthebuild--dependingonthebuild'ssetup--willbemarkedasfailed.

Eachpartnerorcustomermayhavetheirownpoliciesforunittesting,somerequirethateverypieceofcodeistested,andotherswillrecommendthatonlykeypartsofthecodearetested.Itiscommonthatthecodeoftheunittestshavethreetimesthecodeofthecodebeingtested,whichmayseemwrongatfirst.Writingtestcasesisaninvestment,thebenefitofwhichisn'talwaysapparentatthetimeofwriting.Thisissometimesbecausetestingwhetherapieceofcodeworksisusuallyprettyeasy,theproblemisthatmanualtestsarenotrepeatable,andusuallyneglectedgecasesbytestingthemainscenario.Thebiggestwin,inmyopinion,isthereductionintheriskofregression.Thisiswhereanapparentminorchange(orahotfix)isapplied,whichaffectsaseeminglyunrelatedpartofthesystem.Thesetypesofregressioncaneasilymakeitintoproduction,sincethetestingproceduresmayonlytestthatpartofthesystem.Thisiscompoundedfurtherbythefactthatanyfixwilltakeatleastadaytodeploy,sincewearenowforcedtogothroughtestandthenproduction.

Thefollowinglinkprovidessomegoodadviceonachievingbalanceintestingsoftware:https://blogs.msdn.microsoft.com/dave_froslie/2016/02/03/achieving-balance-in-testing-software/

CreatingaFormAdaptorproject

Formadaptorsareusedinordertocreatetestcasesforuserinterfaceevents--theyprovideabridgebetweentheforminteractionsandcode.Theycanbecreatedwithinthemainproject,butthiscreatesalotofneedlessclassesthatwewillrarelywishtosee.

GettingreadyOpentheprojectthatweneedformadaptorsfor,which,inourcase,isConWHSVehicleManagement.

Howtodoit...TocreatetheFormAdapterproject,followthesesteps:

1. SelectDynamics365forOperations|CreatemodelfromthetopmenuandcompletetheCreatemodelformasfollows:

Field Value

Modelname ConWHSVehicleManagementFormAdaptor

Modelpublisher ContosoIT

Layer VAR

Modeldescription FormadaptersfortheConWHSVehicleManagementpackage

Modeldisplayname ConWHSVehicleManagementFormAdaptor

ThesuffixisimportantandshouldalwaysbeFormAdaptor.

2. ClickonNext.3. SelectCreatenewpackageandclickonNext.4. SelectConWHSVehicleManagementandTestEssentialsfromthePackages[Models]listandclickonNext.5. Uncheckthedefaultforcreatingaproject,butleavethedefaultoptionofmakingthenewmodelthe

defaultfornewprojects.ClickonOK.6. Right-clickonthesolutionnodeintheSolutionExplorerandchooseAdd|Newproject....7. IntheNewProjectwindow,ensurethattheDynamics365forOperationstemplateisselectedinthe

leftpane,andOperationsProjectisselectedintheright.EnterConWHSVehicleManagementFormAdaptorasNameandclickonOK.

8. SaveallandcloseVisualStudio.9. IfthepackagewascalledConWHSVehicleManagementFormAdapter,navigatetothefollowingfolderin

WindowsExplorer:

C:\AOSService\PackagesLocalDirectory\ConWHSVehicleManagementFormAdapter\Descriptor

10. OpentheConWHSVehicleManagementFormAdapter.xmlfileinNotepadandaddthehighlightedlineinordertotellthemodelwhichmodelisthesourcetobuildformadapters:

<AppliedUpdatesxmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays"/>

<Customization>Allow</Customization>

<Description>FormadaptersfortheConWHSVehicleManamentpackage</Description>

<DisplayName>ConWHSVehicleManagementFormAdapter</DisplayName>

<FormAdaptorSourceModel>ConWHSVehicleManagement</FormAdaptorSourceModel>

<Id>895571399</Id>

Donoteditanyotherpartofthisfile.TheoptiontosettheformadaptorsourcemodeldirectlyinVisualStudiomaybeaddedtolaterversions,sothisstepmightnotberequiredinthefuture.

11. OpenVisualStudioandtheConWHSVehicleManagementsolution,thenright-clickontheConWHSVehicleManagementFormAdaptorproject,andchooseProperties.

12. ChangetheGenerateFormAdapterspropertytoTrue.13. Right-clickontheConWHSVehicleManagementFormprojectandchoosePropertiesandchangetheGenerate

FormAdapterspropertytoTrue.14. SelectBuildmodels...fromtheDynamics365menuandbuildbothmodels.15. Thisshouldgenerateandaddaclassforeachform,suffixedwithFormAdaptor.Iftheydon'tappear,you

canlocatethemintheApplicationExplorerandaddthemmanually.

Howitworks...Wewillcreateanewpackageinordertoseparatetheclassesfromthemainpackage.Thisshouldalwaysbedone,aswecanendupwithalotofautomaticallygeneratedcodethatwouldbeadistraction.ThekeypartofthisprocesswastheXMLtagweenteredintheformadaptormodel'sDescriptorfile--thisiswhattoldthemainpackagethattheformadaptercodeshouldbegeneratedinourformadaptormodel.

ThenextpartwastoturnonGenerateFormAdaptorsinthepropertiesformforbothprojects.Thebuildwillthengenerateformadaptersintoourformadaptorproject.

Wewillusethislaterwhenwecreateaunittestfortheuserinterface.

CreatingaUnitTestprojectThetestprojectwouldideallybeanewproject(andmodel)insidethepackagewearetesting.Eachpackageshouldhaveonetestproject,and,ifwritinguserinterfacetests,weshouldhaveoneformadaptersalso.

GettingreadyOpentheprojectthatweintendtowritetestsfor.

Howtodoit...Tocreatetheunittestproject,followthesesteps:

1. SelectDynamics365forOperations|CreatemodelfromthetopmenuandcompletetheCreatemodelformasfollows:

Field Value

Modelname ConWHSVehicleManagementTest

Modelpublisher ContosoIT

Layer VAR

Modeldescription TestcasesfortheConWHSVehicleManagementpackage

Modeldisplayname ConWHSVehicleManagementTest

ThesuffixisimportantandshouldalwaysbeTest.

2. ClickonNext.3. SelectCreatenewpackageandclickonNext.4. SelectConWHSVehicleManagement,ConWHSVehicleManagementFormAdaptor,ApplicationFoundation,andTestEssentials

fromthePackages[Models]listandclickonNext.

Modelpackagesmayberequired,dependingonthecodeweneedtowrite.

5. Uncheckthedefaultforcreatingaproject,butleavethedefaultoptionofmakingthenewmodelthedefaultfornewprojects.ClickonOK.

6. Right-clickonthesolutionnodeintheSolutionExplorerandchooseAdd|Newproject....7. IntheNewProjectwindow,ensurethattheDynamics365forOperationstemplateisselectedinthe

leftpane,andOperationsProjectisselectedintheright.EnterConWHSVehicleManagementTestasNameandclickonOK.

Howitworks...

Thiswasbasicallyasimplerversionofthestepswetooktocreatetheformadaptorproject.Thisisdoneaftertheformadaptor,asweneedtoreferenceit;thisprojectwillcontaintestcasesformanuallycraftedunittestsandthosethatwilltesttheuserinterface.

CreatingaUnitTestcaseforcode

Inthisrecipe,wewillcreateatestcasefortheConWHSVehicleGroupChangeclass,wherewewilltesteachpartofthisclass.Thisincludeswhenitshouldfailandwhenitshouldsucceed.Theprocesswillinvolveprogrammaticallycreatingsometestdatainordertoperformtheupdate.

GettingreadyWewilljustneedtheunittestproject,whichwecreatedinthepreviousrecipe,open.Also,onthemainmenu,selectX64fromTest|TestSetting|DefaultProcessorArchitecture.

Howtodoit...

Tocreatetheunittestclass,followthesesteps:

1. CreateanewclassandnameitConWHSVehicleGroupChangeTest.Thesuffixisimportant.2. Inthecodeeditor,changethedeclarationsothatitextendsSysTestCase.3. Next,wewillneedsomeconstantsfortestcasesthatweeitherexpecttosucceedorfail;inthiscase,

wewillhavethefollowing:

constConWHSVehicleGroupIdgroupId='%VG01%';

constConWHSVehicleIdvehicleId='%V001%';

conststrnotFound='%ERROR%';

4. Thenextpartistosetupthetestcase,whichisdonebyoverridingthesetUpTestCasemethodwiththefollowingcode:

publicvoidsetUpTestCase()

{

super();

ConWHSVehicleGroupvehGroup;

ttsbegin;

vehGroup.initValue();

vehGroup.VehicleGroupId=groupId;

vehGroup.insert();

ttscommit;

ConWHSVehicleTablevehTable;

ttsbegin;

vehTable.initValue();

vehTable.VehicleId=vehicleId;

vehTable.VehicleGroupId=groupId;

vehTable.insert();

ttscommit;

}

Wecreatedavehiclerecordandavehiclegrouprecordusingtheconstantswedefinedearlier.Wewillexpectourteststofindtheserecordstosucceed.

5. Toreducetheamountofrepetitivecodethatisoftenfoundintestcases,writethefollowingmethod.Thiswillbecalledwithdifferentparametersfromatestmethod.Writethecodeasfollows:

privatevoidvehicleGroupExistTest(

ConWHSVehicleGroupId_groupId,

boolean_shouldBeFound)

{

booleanfound;

found=ConWHSVehicleGroup::Exist(_groupId);

strfoundMsg;

foundMsg="wasnotfound";

if(found)

{

foundMsg="wasfound";

}

strshouldBeFoundMsg;

shouldBeFoundMsg="shouldbenotbefound";

if(_shouldBeFound)

{

shouldBeFoundMsg="shouldbefound";

}

strmsg=strFmt("Vehiclegroup%1was%2whenit%3",

_groupId,foundMsg,shouldBeFoundMsg);

this.assertEquals(

_shouldBeFound?'Found':'Notfound',

found?'Found':'Notfound',msg);

}

TheassertEqualsmethodwillfailifthefirsttwoparametersarenotthesameandshowthemessage.Ifitsucceeds,nomessagewillbeshown.

6. Wecannowwriteourtestmethod.Thenamingisimportant,itdescribesitisatestandtheelementtowhichitistesting.Thecodeshouldbewrittenasfollows:

[SysTestMethod]

publicvoidtestValidate()

{

this.vehicleGroupExistTest(groupId,false);

this.vehicleGroupExistTest(notFound,true);

}

Wewillalwayswritethetestsothatitwillfailfirst,andthenalteritsothatitwillsucceed.

7. Let'sexecutethetests,andcheckthattheydo,indeed,fail.Todothis,buildtheprojectandchooseTest|Run|AllTests.Theresultshouldbeasshowninthefollowingscreenshot:

8. Changetheparameterssothattheyshouldsucceed;buildandthenclickonRunAll.Theresultshouldbeasfollows:

9. Dothesametotestthevehicletable.10. Wewilluseasimilarpatternforthevalidatemethodbywritingthefollowingcode:

privatevoidvalidateTest(ConWHSVehicleId_vehicleId,

ConWHSVehicleGroupId_groupId,

boolean_shouldBeValid)

{

ConWHSVehicleGroupChangeContractcontract;

contract.VehicleGroupId(_groupId);

contract.VehicleId(_vehicleId);

ConWHSVehicleGroupChangechange;

change=newConWHSVehicleGroupChange();

change.contract=contract;

booleanvalid;

valid=change.Validate();

strvalidMsg='failed';

if(valid)

{

validMsg='passed';

}

strshouldBeValidMsg;

shouldBeValidMsg='failed';

if(_shouldBeValid)

{

shouldBeValidMsg='passed';

}

strmsg;

msg=strFmt('Vehicle%1,group%2%3validation'+

'whenitshouldhave%4',

_vehicleId,_groupId,

validMsg,shouldBeValidMsg);

this.assertEquals(_shouldBeValid?'Passed':'Failed',

valid?'Passed':'Failed',msg);

}

11. WecannowcompleteourtestValidatemethod,whichshouldreadasfollows:

[SysTestMethod]

publicvoidtestValidate()

{

this.vehicleGroupExistTest(groupId,true);

this.vehicleGroupExistTest(notFound,false);

this.vehicleExistTest(vehicleId,true);

this.vehicleExistTest(notFound,false);

//Testblankvehiclegroup

this.validateTest('','',false);

this.validateTest(vehicleId,'',false);

this.validateTest(notFound,'',false);

//Testinvalidvehiclegroup

this.validateTest('',notFound,false);

this.validateTest(vehicleId,notFound,false);

this.validateTest(notFound,notFound,false);

//Testvalidvehiclegroup

this.validateTest('',groupId,false);

this.validateTest(vehicleId,groupId,true);

this.validateTest(notFound,groupId,false);

//Testblankvehicle

this.validateTest('','',false);

this.validateTest('',groupId,false);

this.validateTest('',notFound,false);

//Testinvalidvehicle

this.validateTest(notFound,'',false);

this.validateTest(notFound,groupId,false);

this.validateTest(notFound,notFound,false);

//Testvalidvehicle

this.validateTest(vehicleId,'',false);

this.validateTest(vehicleId,groupId,true);

this.validateTest(vehicleId,notFound,false);

}

12. IftheTestExplorerisstillopen,simplyclickonRunAll;thiswillbuildtheprojectforus.Theresultshouldbeasfollows:

Thewarningscomefromthetargetmethod,whereitcorrectlyfailedthevalidationcheck.

13. Asbefore,weshouldwritethevalidationcheckstofailfirst.Youcouldalsoinsertaruntimeerrortoseehowthisishandled.OnthelinethatdefinestheConWHSVehicleGroupChangeContractclass,commentoutthelinethatinstantiatesit.Whenitexecutes,youwillgetaveryverbosemessagestatingNullReferenceException,showingusthateventhesetypesoferrorswillbecaughtthroughunittesting.

14. Youcanthencontinuewritingthetestcasesfortherunmethod,whichshouldbedoneinthesamepatternasthevalidatemethodcheck.Youshouldalsopushyourselfwiththeotherassertmethods.

Howitworks...Mostofthecodewewroteisrelativelystraightforward.Theinterestingpartishowthesystemdiscoversthetestsandexecutesthem.

ThefirstpartwasthatwereferencedApplicationFrameworkandTestEssentialswhenwecreatedtheproject.ThediscoveryworksbylookingforaclassinthecurrentprojectthatextendsSysTestCaseandformethodsthathavetheSysTestMethodattribute.Thetestmethodmustbepublic,returnvoid,andhavenoparameters.Theyshouldstartwith(oratleastcontain)thewordtest,andreferencethemethodintheclasswearetesting.

Finally,whataboutthedatawecreated?Thetestframeworkwillautomaticallyteardownanydatawecreateduringthetestingsession.Thisoccursbetweentests,sodon'tassumeatestcanusedatacreatedorupdatedinaprevioustestcase.

Creatingatestcasefromataskrecording

Thispartofthetestistotesttheuserinterfaceinteractions.ThisisdonebycreatingataskrecordingfromwithinDynamics365forOperationsandimportingitintoaprojectinVisualStudio.

Wewilltesttheserviceordercreationlogicbycreatingataskrecording.

GettingreadyThiscontinuesfromthepreviousrecipes.

Howtodoit...

Tocreateaunittestforthevehicleserviceorder'sform,followthesesteps:

1. WemuststartfromthemainDynamics365forOperationswindow;otherwise,thegeneratedcodemayfail.

2. Onceatthemainmenu,clickonthesettingsicon(thecog)andchooseTaskrecorder.3. ClickonCreaterecordingintheTaskrecordersidebar.4. EnterConWHSServiceOrderTestintheRecordingnamefield,andadescriptionofwhatweexpectto

happen.Thiswillbecometheclassname.5. ClickonStart.6. Navigatetotheformtotest,inourcase,Vehicleserviceorders.7. Createanewserviceorder,andaddtwolines.Thesidebarwillrecordeachinteractionwiththe

form,soitcanpayofftorehearsethisfirsttominimizethenumberofstepsitcreates.

Toaddvalidation,right-clickonthefieldandchooseTaskrecorder|Validate|Currentvalue.

8. Oncedone,presstheStopbuttononthetopleftofthescreen.9. IntheTaskrecordersidebar,clickonSaveasdeveloperrecording.Savethisfilesomewhereyou

canfinditlater.10. GobacktoVisualStudio.11. Tosavetime,right-clickontheConWHSVehicleManagementTestprojectandchooseSetasStartUpProject.12. ChooseImportTaskRecodingfromtheDynamics365|Addinsmenu.

13. UsetheBrowsebuttontofindthetaskrecordingcreatedearlier.EnterthecompanyusedtocreatethetaskrecordingintheCompanyfield.Thefollowingscreenshotisanexample:

IfweusedtheNewProjectmethod,wewouldselecttheConWHSVehicleManagementTestmodelasModel.Whencreatingtheproject,ensurethattheSolutiondrop-downissettoAdd

tosolution.

14. Thiscreatedaclassnamedafterthetaskrecordingname,which,inourcase,isConWHSServiceOrderTest.15. Inmycase,Iaddedavalidationtothelinenumbercontrol,whichresultedinthefollowingcodein

thesetupDatamethod:

ConWHSVehicleServiceTable_LinesGrid_LineNum=0;

ConWHSVehicleServiceTable_LinesGrid_LineNum1=0;

Thisisclearlyabug,andwecansettheexpectedvaluesbyeditingthiscode.Inmycase,Iwillmakethem1and2,respectively.

16. Ifwebuildtheprojectnow,wewillgetmissingassemblyerrors.Thisisbecausethegeneratedcodereferencesstandardformadaptors.AddareferencetothefollowingpackagestotheConWHSVehicleManagementTestmodel:

ApplicationFoundationFormAdaptorApplicationPlatformFormAdaptorApplicationSuiteApplicationSuiteFormAdaptor

17. Ifwehadn'tcorrectedthelinenumbers,thetestrunwouldcorrectlystatethestatethatthevalidationforthelinenumberfailed.Thiswouldthenneedtobecorrectedincodeandthetestrerun.

Howitworks...Thisisaverypowerfultool,wherewecanaskconsultantsanduserstocreatethetestcasesbasedonwhattheyexpectforthedevelopertouseasatestcase.

Althoughthereisalotgoingonbehindthescenes,itworksbyreadingtheXMLfilecreatedbythetaskrecording.Wheneachstepisrecording,thesystemstorestheinformationabouttheformandcontrolsinawaythatcanbereferencedincode.Thetaskrecodingimportcreatesaclassthatusestheformadaptorinordertointeractwiththeformprogrammatically,usingthestepsintheXMLfile.

Althoughthiscodeisgenerated,wecaneditit.Inthecaseofvalidations,weusuallyhaveto.Forexample,theconsultantscouldhaveaddedavalidationfortheitemname.Sincewecanonlyusethecurrentvaluetovalidateagainst,wewillneedtoamendthecode.Inthecaseofanitemname,wewouldwritecodetofindtheexpectedvaluebasedontheitemIDtheuserentered.

Sincethesetestscanbeautomatedatthebuildserver,thedatausedtocreatethetestcaseandthatonthebuildserveritselfmustbethesame.Thisisdonebyimportingabackupofthedatabasefromthetestdatabase.Withthetechniquedescribedinthisrecipewecanthencreateintegrationteststhatarerunautomaticallyoneverybuild,thisismuchmorerobustthatrelyingsolelyonend-usertesting.Itisnaturalforustotestwhathaschanged,andthereforeveryeasytomissregressioninareasthathaven'tbeenchanged.Thetaskrecordingisnomoreeffortthatthetestingtheuserswouldhavetodoanyway,andwitharelativelysmalldevelopmenteffortwehavearepeatablesetofteststhatwillalwaysrun,regardlessofthechangemadeinthebuild.

AutomatedBuildManagementInthischapter,wewillcoverthefollowingrecipes:

CreatingaVisualStudioBuildAgentQueueSettingupabuildserverManagingbuildoperationsReleasingabuildtoUserAcceptanceTesting

IntroductionInthischapter,wewillcoverthestepsrequiredtosetupanduseabuildserver.WetoucheduponsomebenefitswithabuildserverinChapter11,UnitTesting,whereunittestscanbeexecutedtohelpreducetheriskofregression.

Weshallcovertwoscenarios.ThesearecloudhostedcustomerimplementationprojectdeployedviaLCS,andanon-premisebuildserver,whichisequivalenttoanAzureserverhostedunderyourownsubscription.

ShouldtheimplementationbehostedinAzuredeployedthroughaLCScustomerimplementationproject,allweneedtodoissetuptheBuildAgentPoolsandQueuesandthensupplytheparameterstothesetupforminLCS.Theprocessofdeployingabuildmachineiswelldocumentedandwewon'tduplicatethishere,especiallygiventhepaceatwhichupdatestoLCSarebeingmade.Evenifwedon'tsetupabuildservermanually,theinformationmayproveusefulinunderstandingissuesthatmayarisewiththeserver.

TherecipesinthischaptershouldbeusedinconjunctionwithreleasedMicrosoftdocumentation.Theaim(asalways)istoprovidepracticalhands-onguidance,intendedtoaugmentthealreadypublisheddocumentation.

CreatingaTeamServicesBuildAgentQueue

AgentqueuesactasbridgesbetweenVisualStudioTeamsServicesandthebuildagentthatisinstalledonthebuildserver.Wewillneedanagentqueuebeforeweconfigurethebuildserver.

AgentQueuesbelongtoAgentPools,andgiventhewaythatthebuildserversareprovisionedfromLCS,wewillhaveaone-to-onerelationshipforthis.Thisisbecauseaprojectwilltypicallyhaveitsownbuildserver(whichisnotlimitedtoone)andkeepingthequeuesandpoolsatone-to-onesimplifiesmanagement.ThisisespeciallyimportantforpartnersandISVswhohavemanyprojects.

GettingreadyYouwillneedtohavecreatedyourVisualStudioTeamServicessitebeforeyoustartthis.

Howtodoit...

TocreateanAgentQueue,followthesesteps:

1. OpentheVSTSsite,forexample,<yourdomain/tenant>.visualstudio.com.2. Clickonthesettingscogonthetoolbar,andthenAgentPools,asshowninthefollowingscreenshot:

3. OntheAgentsPoolstabpage,clickonNewpool....4. Thenameshouldrelatetotheproject;inmycase,IchoseB05712AX7DevCookBook.Chooseanamethatis

shortandeasytodeterminewhichprojectthepoolisfor.5. UncheckAuto-ProvisionQueuesinallprojectsandclickonOK.

Wewanteachbuildservertohaveitsownagentqueue;ifenabled,itwouldcreatethequeueforallVSTSprojects.

6. ClickonProjectsfromthetopbuttonribbonandselecttheVSTSproject,andthenselectAgentQueuesfromthesettingscogmenu.

7. ClickonNewqueue...andselecttheAgentpoolwecreatedearlier,andthenclickonOK.TheAgentqueuewillhavethesamenameasthepool.

Howitworks...

TheAgentqueueisusedduringthesetupofthebuildserverinordertoassociatetheagent,whichisinstalledonthebuildserver,withthequeue.Thisway,whenabuildistriggered(manually,orviaacheck-in),itknowswhichservertotriggerthebuildtobuildon.

Settingupabuildserver

Thebuildserverisaone-boxDynamics365forOperationsvirtualmachine,usuallywithdemodatathatisonlyeverusedtoproducebuilds.Eventhoughithasdata,andseemstohaveanapplicationrunninginIIS,itcannotbeused.

Ifwearecreatingabuildserverforacustomerimplementationproject,mostoftheworkisdoneforus.YouwilljustneedtospecifytheAgentQueuethatwecreatedinthepreviousrecipe.ThisrecipewillfollowtheISVscenariowherewemayinstallthebuildagentourselves.

GettingreadyYouwillneedabuildserverVMrunning,withaccesstotheinternet,andtheAgentQueuecreatedagainsttheproject.

Howtodoit...

Toconfigurethebuildserver,followthesesteps:

1. OpentheVSTSsiteandselectSecurityfromyouruseroptions(theiconwithyourinitialsorpicture).

2. ClickonAddunderthePersonalAccessTokenstab.3. Enterasuitabledescription,suchasB05712_Agent.4. Settheexpirybasedontheprojectlength,usuallyayearforOperationsprojects;youcanextendthis

shoulditexpire.5. EnsurethatAllScopesisselectedandclickonCreateToken.6. Makesureyoucopythetokenasyouwillnotbeabletoseeitagain!7. OpenaPowerShellprompt(pressWindows+RandtypePowerShell).8. Typethefollowingline:

Cd\DynamicsSDK

9. EnsurethatthecurrentdirectoryisC:\DynamicsSDKandtypethefollowinglinesofcode:

.\SetupBuildAgent.ps1-VSO_ProjectCollectionhttps://<yourDomain>.visualstudio.com/DefaultCollection-VSOAccessToken<youraccesstoken>-AgentNameB05712AX7DevCookBook01-AgentPoolNameB05712AX7DevCookBook

10. Theoutputshouldbeasfollows:

11. OpentheVSTSsite,andcheckthattheagentwasaddedtotheagentqueue;itwillbeshownasthefollowingscreenshot:

12. Westillneedabuilddefinitioncreatedforus,sogobacktoPowerShellandenterthefollowinglinesofcode:

.\BuildEnvironmentReadiness.ps1-VSO_ProjectCollectionhttps://<yourdomain>.visualstudio.com/DefaultCollection-ProjectNameB05712_AX7_DevelopmentCookbook-VSOAccessToken<Youraccesstoken>

13. OpentheVSTSsiteagainandopenyourproject.SelectBuild&ReleasefromthetopandthenBuildsfromthebuttonribbon,justbelowthemainbuttonribbon.Youshouldseethefollowing:

14. ClickonthethreedotsandchooseClone...aswewillcreateadefinitionforcontinuousintegration:everycheck-inwillperformabuildandrunourtests.

15. Afterafewseconds,thepagewillshowthedefinition,andweshouldnameitbasedontheoriginal,suffixedwithcontinuous.ThedefaultpageistheTaskspage,andwillshowthetasksthatthebuilddefinitionwillprocess.Thisisshowninthefollowingscreenshot:

16. SelecttheTriggerspageandenableContinuousIntegration.Thedefaultsareotherwisecorrect.17. SelecttheOptionspageandchangetheDefaultagentqueuesettingtothecorrectqueue(theonewe

createdearlierandwhichnowcontainsournewagent).18. SelectSavefromtheSave&queueoptionbutton.19. ClickonBuildsagaintoseethelistofbuilddefinitions,thenclickonthethreedotsicononAX7-

BuildMain,and,thistime,selectEdit.20. ClickonVariablesandselect+Addatthebottomofthepage.21. EnterBuild.CleanintheNamecolumnandAllintheValuecolumn.

Thisforcestheagenttoperformacleanbuild.

22. SelectTriggersandenableScheduled.Thisisbasedonyourcompany'sprocedures,andmostcompanieswishtorunacleanbuild(completefetchfromTFS)nightly.Thedefaultscheduleisusuallycorrect.

23. ClickonOptionsandchangetheDefaultagentqueuesettingtothecorrectqueue.24. Saveyourchangesandqueueanewbuild(SaveandQueue)--thiswilltakeanythingfrom15minutes

to2hours,dependingonthehardwaretheVMisrunningon.25. OntheQuerybuildforAX7-BuildMainoption,clickonQueue.26. Youshouldnowseeaconsolewindowshowingtheprogress(veryverboselyasthebuildis

performed).

Howitworks...Therewerefivepartstothisrecipe:

GettingaPersonalAccessTokenInstallingthebuildagentConfiguringthebuildagentUploadingabuilddefinitiontotheVSTSprojectConfiguringabuildagentforcleanandcontinuousintegration

WecouldhavedownloadedthebuildagentmanuallyfromtheAgentQueueform(youmayhavenoticedtheDownloadagentbutton),andthenconfigureditusingtheagent'sconfig.cmdscript.ThiswouldbeOK,butwewouldn'thavebeenabletodeployabuilddefinitiontotheprojectifwedid.Thenextpart,BuildEnvironmentReadiness.ps1,assumesthattheagentisinstalledinaparticularplaceonthedrive,andwillthereforefailtorun.

Weraneachcommandwithparametersratherthanenteringthematruntimebecausethescriptdoesn'taskfortheoptionalparametersanditwillusedefaults.

TheresultoftheconfigurationisthatwehaveanAgentlinkedtoourAgentqueuethroughitsconfiguration,andtwobuilddefinitionsthatarebothlinkedtotheAgentqueue.

Onceallthesearelinked,andwehavesetupthebuilddefinitions,thebuildserverisreadyforoperation.

There'smore...Weconfiguredbothbuilddefinitionstoexecuteanytestthattheprojectmaycontain,whichisthedefaultbehavior.Iftheprojectdoesnotcontaintests,wewillneedtodisabletheteststepsonthebuilddefinitions.

Todisablethetestexecution,selectBuildsfromtheBuild&ReleasesectionoftheVSTSprojectsite.ClickonthethreedotsiconandselectEdit.YouwillseethelistoftasksontheTaskstabpage.Thetesttasksareasfollows:

TestSetupExecuteTestsTestEnd

Foreachofthesetasks,uncheckEnabledfromtheControlOptionssection.

Ofcourse,wewouldalwayshavetestsforourprojects!

SeealsoDevelopertopologydeploymentwithcontinuousbuildandtestautomation:

https://ax.help.dynamics.com/en/wiki/developer-topology-deployment-with-continuous-build-and-test-automation/

DevelopmentandcontinuousdeliveryFAQ:

https://ax.help.dynamics.com/en/wiki/development-and-continuous-delivery-faq/

ManagingbuildoperationsThisrecipefocussesonwhathappenswhenabuildistriggered,andhowtodealwithsomecommonissues.Wewilltriggerabuildandthenmonitoritsprogress.

GettingreadyWemusthaveafullyfunctionalbuildserver,andabuilddefinitionthatwilltriggeroncheck-in.

Howtodoit...Tomanagethebuildoperations,followthesesteps:

1. Makeaminorchangetoanycodeinyourprojectandcheck-inthechanges.2. ThenopentheVSTSprojectandselectBuildfromtheBuild&Releasemenu.Youshouldsee

somethinglikethefollowingscreenshot:

3. The#<buildnumber>linkwilltakeustothedetailsofthebuild;clickonthislink.4. ThiswillopenthedetailsofthebuildoperationwiththeConsoleopenbydefault.Thisverbosely

listseverydetailoftheoperationasithappens.Anyerrorsorwarningsarealsolistedhere.5. Oncecomplete,checkforerrorsandlookattheSummarysection(clickontherootoftheprogress

treeontheleft).Youmayfindthatyouwillreceivethefollowingmessage:

EXEC(0,0):Warning:1:22:57PM:NoModelsreturnedbymodelinfoproviderfrommetadata

6. Youmayalsonoticethateventhoughnoerrorwasshown,notestswererun.Theconsolewillprovideanotherclue:

Notestassembliesfoundmatchingthepattern:'C:\DynamicsSDK\VSOAgent\_work\2\Bin\**\*Test*.dll'.

ItislookingforDLLsthatcontainTest!Wecanseethatnamingconventionsareimportant,albeitseeminglyalittleugly;thereisnowaythebuildagentcanseethattheclassintheDLLextendsSysTestCase.

7. Whydiditfail?OpenVisualStudioandselectTeamExplorer.Fromthere,selectSourceControlExplorer.

8. ThefirstcheckistocheckiftheDescriptorfolderisadded;inthefollowingscreenshottheDescriptorfolderisnotlistedundertheConWHSVehicleManagementTestfolder:

9. WeaddtheDescriptorfileaddedbyright-clickingonthepackage(thefirstConWHSVehicleManagementTestnodeinthiscase)andselectingAddItemstoFolder....AddtheDescriptorfolder,butnothingelse.

Whenhotfixesareapplied,whichwillneedtobeaddedtosourcecontrolsothatthebuildserverbuildsthem,itwillonlyaddthesourcefiles.Youwillmanuallyhavetoaddthemodels'descriptionfile(apackagecanhavemorethanonemodel,andthereisadescriptorforeachmodel).Onlyaddthedescriptorfileforthemodelsthathaveelementsaddedtosourcecontrol.

10. TheotherreasonisthatthemappingofTFSiswrong.Thebuildagentwillassume(andcreate)thestructureTrunk\Main\Metadataanddownloadchangesfromthisfolder.Ifwemappedthemetadatadifferently,thebuildagentwillnotfindanysourcecodetobuild.Dothisbyeditingyourworkspace(itwillusuallybetheservername)andmappingasfollows:

Beforeyoumakethischange,makesureeverythingischecked-inandthatyouhavemovedtheexistingfoldersagainstthe'wrong'foldertothecorrectfolderinTFS(Trunk\Main\Metadata).Failuretodothiswillresultinerrorscheckingcodeinandout,andyoucouldlosecode.

12. Triggerabuildeithermanuallyorviaacheck-in.OpenthebuilddetailsfromtheVSTSsiteandlookattheGetSourcesnode.Youshouldseeentriessimilartothefollowingcodesnippet:

2017-03-02T13:43:47.6277906ZC:\DynamicsSDK\VSOAgent\_work\2\s\Metadata:

2017-03-02T13:43:47.6287911ZGettingConWHSVehicleManagement

2017-03-02T13:43:47.6287911ZGettingConWHSVehicleManagementFormAdaptor

2017-03-02T13:43:47.6297916ZGettingConWHSVehicleManagementTest

2017-03-02T13:43:47.6498150Z

2017-03-02T13:43:47.6507910ZC:\DynamicsSDK\VSOAgent\_work\2\s\Metadata\ConWHSVehicleManagement:

2017-03-02T13:43:47.6517934ZGettingConWHSVehicleManagement

2017-03-02T13:43:47.6517934ZGettingDescriptor

13. Oncethebuildhasfinished,youshouldseethefollowingonthebuild'ssummarypage:

14. Youcanthenclickonthedetailedreport,usuallyinthecaseoffailures,toviewthedetailedtestresults:

Thedefaultistoshowonlyfailedtests;toviewall,clickontheoutcome(forexample,Failed)nexttothelabelOutcomeandchooseAll.

Howitworks...Whenthebuildagentisinstalled,itcreatesaTFSworkspacethatishardcodedtobeTrunk\Main\Metadata,theagentassumesamappedserver'slocalpackagesfolder.ItstartsbydownloadingallchangesfromthisfolderinTFStoalocalworkingfolder.Itthenusesthedescriptorfiletodeterminewhattobuild.

Oncethefileshavebeendownloaded,thefollowinghappens:

CheckiftheDynamicsBackupfolderexistsontheCdrive--thiswillbeonaspecificdriveifitisanLCSdeployedbuildserver,andwillnotbeontheCdrive.Ifitdoesnotexist,aSQLbackupismadetotheDynamicsBackup\DatabasesfolderandthelocalpackagesfolderiscopiedtotheDynamicsBackup\Packagesfolder.Ifthefolderexists,theSQLdatabaseisrestoredfromthisbackupandthelocalpackagesfolderisrecreatedfromthisfolder.Thesystemwillthenstartafullbuildofthesystem,andprocesseachstepinthebuilddefinition.MostcallPowerShellscriptsstoredintheC:\DynamcisSDKfolder.Oncecomplete,ituploadsthesourceandresultantdeployablepackagetothebuild.Thiswillthenbeappliedtoourtestserver,andeventuallyproduction.

Itispossible,asatUpdate6ofOperations,toincludeapartner'sorISV'spackagedirectlyonthebuildserver,andtoconfigurethebuildagenttoincludethatpackageinthebuild'sartifacts.

Thisisreferencedinthefollowinglink:

What'sneworchangedinDynamics365forOperationsplatformupdate6(April2017)(https://docs.microsoft.com/en-us/dynamics365/operations/dev-itpro/get-started/whats-new-platform-update-6)Theactualdetailsaretohandatthispoint,andevenwiththisfunctionalityMicrosoftdoencouragethatISVpackagesareinstalledonthedevelopmentmachinesandpushedthroughVSTS.Thisisacomplicatedsubject,asISVsnaturallywanttoprotecttheirintellectualpropertyandcustomersoftendesiretohavesourcecodeinordertoprotecttheirinvestment.

ReleasingabuildtoUserAcceptanceTesting

Attheendofthebuildprocess,adeployablepackagefilewasuploadedtothebuild.Thisfilecanthenbeappliedtoyouruseracceptancetestorsandboxserver.

Wecanapplythepackagemanuallyonthetestserver,butforLCSdeployedUserAcceptanceTesting(UAT)environments,thisisalwaysdoneviaLCS.Anyreleasetoproductionmustfirstbedeployedtoasandboxserverandmarkedasareleasecandidate.

GettingreadyInordertofollowthisrecipe,weneedtohaveanLCSOperationsserverdeployedthroughLCS.

Howtodoit...

Toapplythebuildtoatestserver,followthesesteps:

1. OpentheVSTSsite,andthenopenyourprojectandselectExplorerfromBuild&Release.2. Double-clickonthebuildthatyouwishtodeploytoUAT.3. Onthepagethatopens,selectArtifacts,andclickonExploreforthePackagesartifact,asshowninthe

followingscreenshot:

4. IntheArtifactsExplorertab,expandthePackagesnodeandclickonthearrowiconnexttothefilestartingwithAXDeployableRuntime.ClickonDownload.Theicononlyappearswhenyoumovethemouseoverit,asshowninthefollowingscreenshot:

5. DownloadthefileandthenopenLCS(https://lcs.dynamics.com/).6. OpenyourLCSprojectandthenopentheAssetLibrary.Dependingonthetypeofproject,thiswill

beundertheburgericonoratileontheproject.DonotusetheSharedAssetLibrary.7. InAssetlibrary,selectSoftwaredeployablepackagefromtheleft,andthenpresstheplussymbol.In

thedialog,enterthebuildname,(forexample,Build-2017-03-02),adescription,andthenclickonAddafile.

8. LocatethedeployablepackagewejustdownloadedandclickonUpload.Oncecomplete,clickonConfirmontheUploadSoftwaredeployablepackagefiledialog.

9. OpentheenvironmenttowhichthisshouldbedeployedtoandchooseMaintain|Applyupdates.ThiswillshowalistofSoftwaredeployablepackageassets.SelecttheassetandclickonApply.

Theupdateprocesswillstartimmediately.Thiswilltaketheserverofflineandapply

theupdate.Thiswilltakeseveralhourstocomplete.

10. Aftertestingiscomplete,gobacktothesandboxenvironmentandyouwillbepromptedtoconfirmthattheupdatewassuccessful.Afterthat,gobacktotheAssetlibrary,selectthedeployablepackage,andthenclickonReleasecandidate.Thisissothatitwillbeavailabletobedeployedtotheproductionenvironment.

Howitworks...

TheapplicationofdeployablepackagesisperformedbyaPowerShellscriptonthetargetserver.ThereareagentsinstalledonLCSdeployedserversthatallowLCStoperformthistask.

Theprocessesareasfollows:

Downloadthedeployablepackagetothetargetserverorservers(testandproductionenvironmentshavemultipleserversandthecomponentsinstalledoneachmayvary)ExtractthedeployablepackageApplytheupdatetotheserverusingthePowerShellscriptsincludedinthepackageAttheendoftheprocess,theservers'servicesarerestarted

Thisprocessisbestused,andis,infact,mandatoryforapplyingupdatestocustomerimplementationenvironments.Youcan'tapplyanupdatetoproductionserversthathasn'tbeenappliedtothesandboxserverthroughLCS.

ServicingYourEnvironmentInthischapter,wewillcoverthefollowingrecipes:

ApplyingmetadatafixesApplyingbinaryupdatesServicingtheBuildserverServicingtheSandbox-StandardAcceptanceTestenvironment

IntroductionThischapterfocusesontheservicesofthevariousenvironmentsusedinacustomerimplementationproject.WewillalsocovertheservicingofenvironmentstypicallyusedbyISVs.

Theimportantpartofthischapteristheprocess.TheactualdetailsofthetaskscanbefoundintheOperationsdocumentation.Therecipesarewrittentohelpthisprocessmakemoresenseastowhy,whichismainlytohelpmanagehowupdatesareappliedandtominimizetheriskofregression.

Forgeneralinformationontheupdateprocessandtheupdatepoliciespleaseseethefollowinglink:Dynamics365forOperationsversionsandupdatepolicy(https://docs.microsoft.com/en-us/dynamics365/operations/dev-itpro/migration-upgrade/versions-update-policy)

ApplyingmetadatafixesMetadatafixesareupdatestothesourcecodeofOperations.Forover-layeringwewouldusetheTheseareusedtocode-mergeover-layeredcode,buttheyarestillneededonextensionprojectsasthisispartoftheprocesstopushupdatesdownstreamtothebuild,test,andeventually,theproductionenvironment.

Theprocessofapplyingtheupdatesisstraightforward;itistheprocessthatisthemostimportantaspecttotakeawayfromthis.

LCScanseewhichmetadatafixesareavailableforeachenvironmentconnectedtotheproject.Whenworkingonacustomerimplementationproject,usetheSandbox(test)environmentasthereferenceVMtocheckforupdates.WhenworkingoninternalorISVprojects,useareferenceVMhostedinAzureforthis.On-premiseimplementationswillbeconnectedtoLCSandwillbehandledinthesamewayasAzure-basedimplementationprojects.

Thecycleisasfollows:

ChecktheavailabilityofmetadatafixesrequiredagainstareferenceVMorenvironmentDownloadthedesiredfixestoadevelopmentVMApplythehotfixessothattheyareplacedinTFSCheck-insothatabuildistriggered(andotherdeveloperscanbringdownthehotfixesapplied,maintainingthesamecodebaseacrossdevelopers)Deploytheresultantpackagetothetestenvironment(orreferenceVM)

Whenmovingtothelatestupdaterelease,wherenewVMsaredeployedwiththenewupdate,weremovethehotfixesfromsourcecontrolbeforeconnectingthenewdevVMsbacktothesourcecontrolprojectwiththeupgradedcode.Thisprocessmaychange,andthestandarddocumentationcoversthisverywell.Theprocessisdescribedinthefollowinglink:OverviewofmovingtothelatestupdateofDynamics365forOperations(https://docs.microsoft.com/en-us/dynamics365/operations/dev-itpro/migration-upgrade/upgrade-latest-update)

GettingreadyYouneedaccesstoanLCSprojectthatallowsthedownloadofmetadatahotfixes.LCSdeployedVMsandCustomerimplementationLCSprojectscandothis.EnsurethatVisualStudioisconnectedtothecorrectTFSproject.

Howtodoit...ToapplyametadatahotfixtoadevelopmentVM,pleasefollowthesesteps:

1. WithinthedesireddevelopmentVM,openhttps://lcs.dynamics.comandnavigatetoyourimplementationproject(ortheprojectthathostsyourAzurehostedVM).

2. ClickonFulldetailsontheSandbox:Standardacceptancetestenvironment(ortheVMifthisisnotacustomerimplementationproject).

3. Therearetwotilesformetadatahotfixes,whicharecalledX++updatesonthispage:Applicationx++updatesandPlatformx++updates.

Thenumberindicatesthenumberoffixestobeapplied.

4. Clickonthefirsttile.5. Youwillnowseealistofthefixes,andyoucanchoosewhichtoapply.Theyarelinked,whichwill

forceustoaddsetsofhotfixeswhereadependencyhasbeenidentified.ClickonSelectallandpressAdd.

Takenoteofthefixesincludedintheupdate,sothatthesecanbetestedforregressioninprocessofcodethatwehaveextended.

6. ClickonDownloadpackagefromthetopleftofthewindow,andthenDownloadonthenextpage.

Thereisnoneedtoselectthem;thesebuttonsaretoallowyoutoremovefixesfromthedownloadlist.

7. Onthefilethatisdownloaded,right-clickonit,chooseProperties,andunblockthefile.Renamethefilesothatitreflectsthetileusedtodownloadtheupdates,suchasAppXpp20170309.zip,andextractthefilecontents.

8. WithinVisualStudio,selecttheAddins|ApplyhotfixfromtheDynamics365menu.9. SelectApplyMetadataHotfix,andusetheBrowsebuttontoselectthefileextractedfromtheupdate

package.10. TheotherthreetextboxesaretakenfromVisualStudio;checkifthesearecorrectandclickon

Apply.11. Oncecomplete,youwillseemanypendingchangesreadytobecheckedintoTFS.12. Beforecheckingin,weneedtocheckthateachaffectedmodel'sdescriptorfileisalsoadded.Open

SourceControlExplorerfromtheTeamExplorerhometab.13. ExpandTrunk,MainandthenMetadata.14. ThefollowingscreenshotshowsthestructureofmodelsElectronicReportingApplicationandFoundation

sitwithintheApplicationSuitepackage:

15. Weneedtoensurethatthereisamodeldescriptorfileforbothofthesemodels;ifoneismissing,additbyright-clickingonthepackage'sfolderandchoosingAddItemstoFolder....

16. Addthefilesbydouble-clickingontheDescriptorfolderfromwithintheAddtoSourceControldialogandselectingthemissingfilesforthemodelsalreadyaddedtosourcecontrol.Donotaddanymodeldescriptorfilesformodelsthatarenotaddedtosourcecontrol.Inthefollowingscreenshot,theFoundationUpdateandSCMControlsfilesmustnotbeadded:

17. Onceallrequiredmodeldescriptorfilesareadded,performafullbuildandcheck-inthechanges.Thisbuildwillexecuteouttestscript,whichisespeciallyimportantastheywillhelpusidentifyregressionbeforetheusersstarttesting.

Ifanyover-layeringhasbeendone,thesechangesmustbemergedpriortobuildandcheck-in.

Howitworks...Theupdateworksbycheckingthelocalpackagesfolderforupdates,andapplyingthosethatarenotyetapplied.Thechangeismadeusingdeltachanges,whichiswhythefilesaresosmall.

Thereasonthatweaddthechangestosourcecontrolissothattheyarepushedtootherdevelopersandalsotothebuild.OnceahotfixhasbeenaddedtoTFS,theresultingdeployablepackageincreasesinsizefromafewmegabytestoaround600MB.Thisisbecausepackagesmustbedeployedintotal.

WehavetomanuallyaddthemodeldescriptorfiletoTFSifwewantthebuildservertobuildthepackage.Thisisusedbythebuildservertoworkoutwhatneedstobebuilt,andwhatwillendupasabuildartefactagainstthebuild.ShouldweaddmodeldescriptorfilesformodelsthatarenotinTFS,errorswillbeproduced;thisissolvedbyremovingthefilefromTFS.

There'smore...Theseupdatescanalsobeaddedusingacommandline,whichisdoneasfollows:

c:\AOSService\PackagesLocalDirectory\bin\SCDPBundleInstall.exe

-packagepath=<path>HotfixPackageBundle.axscdppkg

-metadatastorepath=c:\AOSService\PackagesocalDirectory

-tfsworkspacepath=C:\AOSService\PackagesLocalDirectory

-tfsprojecturi=https://<yourcompanysite>.visualstudio.com

Thisistypedasoneline,andissplitintheprecedingcodesothatitiseasiertoread.

Youmaywonderhowitknowswhichprojectitshoulduse,andyoumayhavethoughtthatVisualStudioautocompletedthisparameterwhenweusedtheGUImethod.

ItdoesthisusingtheworkspacemappingthatwasconfiguredinVisualStudio.ThisiswhatthetfsWorkspacePathparameterdoes--ittellsthecommandtolookupthemappingfromthecurrentTFSworkspace.ThefactthatwespecifythepathsforthelocalpackagesfolderandTFSworkspacepathseparatelyseemstobelegacy,andthesemustalwaysbethesame.

SeealsoInstallingametadatahotfix(https://docs.microsoft.com/en-us/dynamics365/operations/dev-itpro/migration-upgrade/install-metadata-hotfix-package)DownloadhotfixesfromLifecycleservices(https://docs.microsoft.com/en-us/dynamics365/operations/dev-itpro/migration-upgrade/download-hotfix-lcs)

ApplyingbinaryupdatesBinaryupdatescontainreplacementbinaryupdatestothetargetVM.ThebinaryupdatesdownloadedfromLCSaremergeddeployablepackagescontainingupdatestoone(andusually)morepackages.

TheyalsocontainupdatedVisualStudiotooling,whichhastobeinstalledseparately.

GettingreadyYouneedaccesstoanLCSprojectthatallowsthedownloadofmetadatahotfixes.LCSdeployedVMsandCustomerimplementationLCSprojectscandothis.

Howtodoit...

ToapplyabinaryhotfixtoadevelopmentVM,followthesesteps:

1. WithinthedesireddevelopmentVM,openhttps://lcs.dynamics.comandnavigatetoyourimplementationproject(ortheprojectthathostsyourAzure-hostedVM).

2. ClickonFulldetailsontheSandbox:Standardacceptancetestenvironment(ortheVMifthisisnotacustomerimplementationproject).

3. Thereisonetileforbinaryupdates,namedBinaryupdates;clickonthisfirsttile.4. Youwillnowseealistofthefixes,andyoucanchoosewhichtoapply.Theyarelinked,whichwill

forceustoaddsetsofhotfixeswhereadependencyhasbeenidentified.ClickonSelectallandselectAdd.

5. ClickonDownloadpackagefromthetopleftofthewindow,andthenDownloadonthenextpage.6. Onthefilethatisdownloaded,right-click,chooseProperties,andunblockthefile.Renamethefile

sothatitreflectsthetileusedtodownloadtheupdates,suchasBin20170309.zip,andextractthefilecontents.

7. WithinVisualStudio,selectAddins|ApplyhotfixfromtheDynamics365menu.SelecttheApplyBinaryHotfixtab,andthenbrowsetothefilebeforeclickingonApply.

8. SelectExtensionandUpdates...fromtheToolsmenu.9. ClickonDynamics365forOperationsVisualStudioToolsandthenclickonUninstall.10. CloseVisualStudio.11. InWindowsExplorer,navigatetothefolderextractedfromthedownloadeddeployablepackaged.12. NavigatethroughtheDevToolsServicefolder,andthenScripts.Double-clickonthe

Microsoft.Dynamics.Framework.Tools.Installer.vsixfile(the.vsixextensionmaynotbevisible)andfollowthepromptstoinstalltheupdatedextension.

Howitworks...Thebinaryupdatepackageisamergeddeployablepackage,whichusuallycontainsamergedsetofpackagesthatcanbe(technically)applieddirectlytoanyOperationsenvironment.

Thedialog,justlikethemetadatahotfixes,isoptional,andwecanuseacommandlinetoapplytheupdate.

ThisisdoneintheServicingthebuildserverrecipe.

SeealsoInstalladeployablepackage(https://ax.help.dynamics.com/en/wiki/installing-deployable-package-in-ax7/)

Servicingthebuildserver

AlthoughwecouldusethesameprocesstoapplybinaryupdatestothebuildserveraswedidonthedevelopmentVMs(albeitusingthecommandversion),thebuildserverisalittlespecial.

Theprocessofapplyingabuildisthattheenvironment(localpackagesfolderanddata)isrestoredfromabackupandmergedwithobjectsfromTFS.Ifwesimplyappliedtheupdate,thenextbuildwill,effectively,removeit.

GettingreadyYouneedaccesstoanLCSprojectthatallowsthedownloadofmetadatahotfixes.LCSdeployedVMsandCustomerimplementationLCSprojectscandothis.

Howtodoit...Toapplybinaryupdatestothebuildserver,followthesesteps:

1. Withinthebuildserver,openhttps://lcs.dynamics.comandnavigatetoyourimplementationproject(ortheprojectthathostsyourAzure-hostedVM).

2. ClickonFulldetailsontheSandbox:Standardacceptancetestenvironment(ortheVMifthisisnotacustomerimplementationproject).DonotusetheDevelopment/buildservertile.

3. ClickontheBinaryupdatestile.4. Inthelistoffixes,clickonSelectallandthenAdd.5. ClickonDownloadpackagefromthetopleftofthewindow,andthenDownloadonthenextpage.6. Onthefilethatisdownloaded,right-click,chooseProperties,andunblockthefile.Renamethefile

sothatitreflectsthetileusedtodownloadtheupdates,suchasBin20170309.zip,andextractthefilecontents.

7. OpenServices(Windows+Randtypeservices.msc).8. Stopthefollowingservices:

WorldWideWebPublishingServiceVSTSAgentMicrosoftDynamicsAXBatchManagementService

9. RenameJ:\AosService\PackagesLocalDirectorytoJ:\AosService\PackagesLocalDirectory_OLD.10. RenameI:\DynamicsBackuptoI:\DynamicsBackup_OLD.11. Openacommandpromptasadministratorandtypethefollowingcommand:

robocopyI:\DynamicsBackup_OLD\PackagesJ:\AosService\PackagesLocalDirectory/E

ThebackupisthebaselinethatisusedtomergewithTFSoneachbuild,soweneedtocopythistoourmainpackagesfolderbeforetheupdateisapplied.

12. Startthefollowingservices:WorldWideWebPublishingServiceMicrosoftDynamicsAXBatchManagementService

13. NavigateusingCDtotherootoftheextractedfiles,forexample,CDC:\Updates\Bin20170309\.14. Runthefollowingcommands:

AXUpdateInstaller.exegenerate-runbookid=Build<YYYYMMDD>

-runbookfile=Build<YYYYMMDD>

-topologyfile=DefaultTopologyData.xml

-servicemodelfile=DefaultServiceModelData.xml

AXUpdateInstaller.exeimport-runbookid=Build<YYYYMMDD>

-runbookfile=Build<YYYYMMDD>

AXUpdateInstaller.exeexecute-runbookid=Build<YYYYMMDD>

Therunbookisstoredinternally,soyoumustuseuniquenamesforeachupdate;weusethefollowingschemas:

Updatetype Namingscheme

Binaryplatformupdate

BinPlatform<YYYYMMDD>Forexample,BinPlat20160219

DonotuseDDMMYYYYorMMDDYYYYasthiscancauseconfusionbetweenthoseintheUSAandoutside.

Binaryapplicationupdate BinApp<YYYYMMDD>

Newbuild(areleaseofyourcode)

Build<YYYYMMDD>:thisdoesn'thavetomatchyourversionnumberingsystem;itjustneedstobeunique.

15. StarttheVSTSAgentserviceandtriggeranewbuild.ThispackagewillthenbeusedtoservicetheSandbox(standardacceptancetest)environment.

16. YoucandeletethefolderswerenamedasOLD.

Howitworks...

Theapplicationoftheupdateisthesameasbefore;theonlychangeisthatweneedtore-baselinethebuildserver'scleanenvironment.Thebaselineisactuallycreatedwhenthenextbuildruns.Sincewerenamedthebackupfolders,thebuildagentthinksthatthisisanewserverandwillrecreateanewbaselinebackupfromtheupdatedfilesinthelocalpackagesfolder.

ServicingtheSandbox-StandardAcceptanceTestenvironment

ThisprocessisnowdoneentirelywithinVisualStudioOnlineandLCS,andwedon'tneedtobeloggedintoaOperationsserver.

Theprocessistotakethelatestbuildfromthebuildserverandmergeitwithabinaryupdatebeforeapplyingittothetestserver.

GettingreadyYouneedaccesstoLCSandtheVisualStudioOnlinesitefromwhichthebuildsareexecutedandstored.

Howtodoit...

ToapplytheupdatestotheSandbox-StandardAcceptanceTestenvironment,followthesesteps:

1. NavigatetoyourVisualStudioonlineprojectandlocatethelatestbuild.2. Withinthebuilddetails,clickonArtifactsanddownloadthefilestartingwithAXDeployableRuntime.3. Thesizeofthefilevaries;onceitincludeshotfixestothebaseapplication,itcaninflatetoover600

MB.4. Next,downloadthebinaryupdatestoafolder;thisshouldbetakenfromthedevelopmentserverthat

startedthisprocessinordertoensurethattheupdatesarethesameasappliedtodeveloperandbuildservers.

5. Openhttps://lcs.dynamics.com/.6. OpenAssetlibraryfromthe"burger".7. SelectSoftwaredeployablepackage.8. IfanyaremarkedReleasecandidate,selectthemandclickonNotreleasecandidate.9. Clickonthe+icon.10. NamethepackageasBuild20170306.11. Enterthebuildnumberandversioninthedescription,andselectAOTdeployablepackageasPackage

type.12. Uploadthefile.13. Repeatthisforthebinaryupdate,forexample,Binary20170306.14. SelectbothupdatesandclickonMerge.UsetheMerge20170306pattern,andclickonConfirm.15. Clickontheprojectnameatthetoptoreturntotheprojectdetailspage.16. ClickonFulldetailsontheSandbox:StandardAcceptanceTestoption.17. ClickonMaintainandthenApplyupdates.Selectthemergedpackagefromthelistandclickon

Apply.ThisstartsimmediatelyandallOperationsserviceswillbestopped.YoucanuseMessageonlineuserstowarntheminadvanceofthis.

Howitworks...Althoughyoucantechnicallyusethecommand-linemethodforthisbyconnectingtotheserverthrougharemotedesktop,thesandboxenvironmentsinvolvedmorethanoneserver.TheLCSmethodappliestheupdatetoallservers.

Thismethoddoestakelonger(severalhoursatpresent)toprocess,sothisshouldbetimedwiththatinmind.

Theactualtechnicalprocessisthesameasthebinaryupdateweusedwhenapplyingtheupdatestothedevelopmentserver.

There'smore...Sometimes,thiscanfail.Itisusuallyafilepermissionissue.LCSfirstdownloadstheupdatetoafolderoneachserver,andthenexecutestherunbookaswedidwhenweservicedthebuildserver.

ThisisusuallyinF:\DeployablePackages.

Insidethefolder,thereisafoldercalledRunBookWorkingFolder;ifyounavigatedownthroughthefolders,finallyexpandingAOSService,youwillseealistofnumberedfolders.

Inthesefoldersarethelogsproducedbytheupdate.Thefailure,ifany,willbeinthelastfolder.Youcanopenthelogsandsearchfortheerrororfailkeywordstohelpdiagnosetheproblem.

Ifitisafileaccessissue,itwasprobablylockedbyaprocess.Locatethefileandsimplyrenameit.Checkthatthefilewascorrectlycreatedoncetheupdatecompletes.

Servicingproduction

Inordertoapplyanyupdatetolive,thepackagemustfirstbedeployedtotheStandardAcceptanceTestenvironment(LCSenforcesthisrule).Ifthenewupdatepassesuseracceptancetesting,gobacktotheAssetlibraryandlocatethemergedpackage.SelectitandclickonReleasecandidate.

Then,usethesameprocessaswedidforthesandboxservertoapplytheupdate.Thedifferenceinthiscaseisthatitcanbescheduled.SincethisrequiresMicrosoft'sDSE(ateamofengineersthathelpmaintainthecloudenvironments)tobeoncallforthis,theyneednoticewhichiscurrently8hours.Theupdatewindowis5hours,anditwilltake5hours.Itcantakelonger,soplanthiscarefullywiththecustomer.

SeealsoUpgradeDynamics365forOperationstothelatestplatformupdate(https://docs.microsoft.com/en-us/dynamics365/operations/dev-itpro/migration-upgrade/upgrade-latest-platform-update)Processforupgradingasandboxenvironment(https://docs.microsoft.com/en-us/dynamics365/operations/dev-itpro/migration-upgrade/upgrade-sandbox-environment)

WorkflowDevelopmentInthischapter,wewillcoverthefollowingrecipes:

CreatingaworkflowtypeCreatingaworkflowapprovalCreatingamanualworkflowtaskHookingupworkflowtotheuserinterfaceCreatingasampleworkflowdesign

IntroductionWorkflowinMicrosoftDynamics365forOperationshavetwomaintypesofelement,approvalsandtasks,centeredonadocument.Thisisthecenteroftheworkflowwheretasksaretriggeredbasedonwhattheuserdecides.Byadocument,itmeansarecordwithaformthatmaintainsit.Forexample,aNewcustomercreationworkflowwouldbebasedonthecustomertableusingthecustomerdetailsformasthedocument.

Theworkflowdesignercanthenuseconditionsbasedonfieldsanddisplaymethodsonthetablesusedintheworkflowinordertodecidewhathappens.Thissolvesmanyrequirementswhereagreatdealofconfigurabilityisrequired,butcanalsobemisunderstoodandusedinappropriately.ThesubmissionofaworkflowisusuallystartedwiththeuserpressingaSubmitbuttonontheform,whichisthenprocessedwithinaminutebythebatchserver.Theminimumtimeitcantakeforaworkflowtocomplete,iftheconditionsforautomaticcompletionaremet,isthreeminutes:uptooneminuteforsubmission,oneminuteforevaluation,andoneminutepersubsequentworkflowstep.This,therefore,can'tbeusedwhentheuserisexpectedfeedbackaspartofthedataentry.

Inthischapter,theworkflowdesignistocontroltheapprovalofanewvehicle,includingatasktoinspectthevehicleaspartoftheworkflow.

CreatingaworkflowtypeTheworkflowtypecanbeconsideredatemplateoradocumentdefinition.Theworkflowtypeactslikeanumbrellafortheassociateworkflowelements,suchasapprovalsandtasks.Whenthedesignerstartstodesignaworkflow,theyactuallyselectaworkflowtype,andthedesignsurfacewillallowthemtoselectworkflowelementswehaveassociatedwithit.

Wecreatetheworkflowtypefirstbecausetheworkflowtypecreationtoolingwillcreateelementsusedinotherworkflowelements.Wejustcomebacktothisinordertoaddthemtothelistofsupportedtypesbytheworkflowtype.

Becarefulwiththenaming,asthisprocessautomaticallycreatesmenuitemsandclasses.Theyareallprefixedwiththeworkflowtype'sname.Ifwehaveaclassthatexistsalready,itwilladda1tothename,whichisunpleasant.Forthisreason,themaximumlengthis20charactersforallworkflowtypes,approvals,tasks,andautomatedtasks.

WewillcreateaBaseEnumtopersisttheworkflowstatus.Inthisrecipe,wewillonlyuseoneofthestatusfieldstohandlethestartedandcanceledevents.TheBaseEnumwasdesignedwiththewholeworkflowinmind,whichincludeshandlingthestatusoftheworkflowapproval.

GettingreadyThisrecipecanapplytoanyrecordthathasamainformmanagingitsdata--thesearetypicallymainorworksheettabletypes.Inthisexample,wewillusethevehicletable.

Howtodoit...Tocreatetheworkflowtype,followthesesteps:

1. Ifwearecreatingaworkflowforanewmodule,oronethatcurrentlydoesn'thaveaworkflow,weneedtocreateanewworkflowcategory,whichmeansweneedtoaddamoduletoModuleAxapta.LocatetheModuleAxaptaBaseEnumandchooseCreateextensionfromtheright-clickcontextmenu.Renameasusual;changethe.extensionoraddaprefixtomakeitunique.

2. Tocreatetheworkflowcategory,weneedtoaddanewitemtoourprojectandchooseWorkflowCategoryfromtheBusinessprocessandWorkflowlist.EnterthenameConWHSVehicleManagementbeforeclickingonAdd.

3. CompletetheLabelandHelpTextproperties;thesewillbevisibletotheworkflowdesignerintheuserinterface.SettheModulepropertytothemodulename,whichisConWHSinourcase.

4. Beforewecreatetheactualworkflowtypeelement,weneedtocreatesomebaseelementsrequiredbytheworkflowapprovalelement.First,createanewquerycalledConWHSVehWF.Tothisquery,addtheConWHSVehicleTabletable,andsettheDynamicsFieldspropertytoYes.

5. Wecannowgoandgetintocreatingtheworkflowtype,choosetoaddanewitemtotheproject,andselectWorkflowTypefromtheBusinessprocessandWorkflowlist.NamethenewelementConWHSVehWFandclickonAdd.

6. CompletetheWorkflowTypedialogasshowninthefollowingscreenshot:

7. ClickonNext.8. Youwillseetheelementsthatwillbecreated;checkthatnonearesuffixedwith1(whichmeansthat

thepreferredelementnamealreadyexists),andclickonFinish.9. Sinceweusedaprefix,locatingtheelementswillberelativelyeasyforallmenuitems.Complete

theLabelandHelpTextproperties,creatinglabelsasrequiredusingnamedlabelidentifiers.10. Next,opentheConWHSVehWFDocumentclass,theworkflowdocumentclass,andalterasshownbythe

followingcode:

///<summary>

///TheConWHSVehWFDocumentworkflowdocumentclass.

///</summary>

[WorkflowDocIsQueueEnabledAttribute(true,

"@ConWHS:VehicleApproval")]

classConWHSVehWFDocumentextendsWorkflowDocument

{

///<summary>

///Returnsquerynamefortheworkflowdocument.

///</summary>

///<returns>

///Nameofthequery<c>ConWHSVehWF</c>.

///</returns>

publicqueryNamegetQueryName()

{

returnquerystr(ConWHSVehWF);

}

///<summary>

///Providesdayssinceacquiredonworkflow

///conditioneditorinworkflowconfigurationform.

///</summary>

///<paramname="_companyId">

///Thecompanyonwhichtheworkflowisrunning.

///</param>

///<paramname="_tableId">

///ThetableIDofthetablewhichisassociated

///withtheworkflow.

///</param>

///<paramname="_recId">

///TherecordIDofthetablewhichisassociated

///withtheworkflow.

///</param>

///<returns>

///Thedayssincethevehiclewasacquired.

///</returns>

publicDaysparmDaysSinceAcquired(

CompanyId_companyId,

tableId_tableId,

recId_recId)

{

Daysdays;

if(_tableId==tableNum(ConWHSVehicleTable))

{

ConWHSVehicleTablevehicle;

selectcrosscompanyAcquiredDate

fromvehicle

wherevehicle.DataAreaId==_companyId

&&vehicle.RecId==_recId;

days=vehicle.AcquiredDate-systemDateGet();

}

returndays;

}

}

Weaddedtheparmmethodasanexampleofhowtoadddisplaymethodsthatcanbeusedintheworkflowexpressionbuilderandasaplaceholderintheworkflowtextpresentedtotheuser.

11. TherewillbetwonewActionMenuItemscreated,prefixedwiththeapprovalname,ConWHSVehWF.ThesearesuffixedwithCancelMenuItem,andSubmitMenuItem.Foreachofthese,settheLabelandHelpTextpropertieswithasuitablelabel,forexample:

Menuitem Label HelpText

CancelMenuItem Cancel Cancelthevehicleworkflow

SubmitMenuItem Submit Submitvehicletoworkflow

Createthelabelsusingnamesandnotnumbers,aswewillreusetheselabelsinotherelements.

12. Weneedtohandlethestatechange,sofirstweneedtocreateaBaseEnumname,ConWHSVehApprStatus,withthefollowingelements:

Element Label Description

Draft @SYS75939 Draft--theworkflowhasnotyetbeensubmittedtoworkflow

Waiting Waiting Theworkflowhasbeensubmitted,buthasnotyetbeenallocatedanapprover

Inspection Inspection Thevehicleisbeinginspected

InReview Inreview Theworkflowhasbeenallocatedoneormoreapprovers

Approved Approved Theworkflowhasbeenapproved

Rejected Rejected Theworkflowwasrejectedbytheapprovers

Revise Revised Achangewasrequestedbyanapprover

Theselabelsshouldbecreatedusingnamedlabelidentifiers,aswedidforthemenuitemsaswewillreusethemonotherelements.

13. Usethe@SYS101302label(ApprovalStatus)intheBaseEnum'sLabelproperty.AddthenewBaseEnumtotheConWHSVehicleTabletableasStatus,andaddittotheOverviewfieldgroup.Makethefieldread-only.

14. OpentheConWHSVehWFworkflowtypeandcompletetheLabelandHelpTextfields.Thesearetoassisttheworkflowdesignerinselectingthecorrectworkflowtypewhencreatinganewworkflow.

15. Next,createaclasscalledConWHSVehicleStatusHandler,andwritethefollowingpieceofcode:

classConWHSVehicleStatusHandler

{

///<summary>

///Setsthevehicle'sapproval

///status

///</summary>

///<paramname="_vehicleRecId">

///Thevehiclerecordid

///</param>

///<paramname="_status">

///Thenewstatus

///</param>

publicstaticvoidSetStatus(RefRecId_vehicleRecId,

ConWHSVehApprStatus_status)

{

ConWHSVehicleTablevehicle;

ttsbegin;

selectforupdatevehicle

wherevehicle.RecId==_vehicleRecId;

if(vehicle.RecId!=0

&&vehicle.Status!=_status)

{

vehicle.Status=_status;

vehicle.update();

}

ttscommit;

}

}

Createcommonselectstatements,suchastheprecedingone,asastaticFindmethod,forexample,FindByRecId.Whenwritinganyselectstatement,checkthatthetablehasasuitableindex.

16. Saveandclosetheclass.17. CreateanewclassnamedConWHSVehWFBaseandaddthefollowingmethod:

publicbooleanValidateContext(WorkflowContext_context)

{

If(_context.parmTableId()!=

tableNum(ConWHSVehicleTable))

{

//Workflowmustbebasedonthevehicletable

throwerror("@ConWHS:ConWHS72");

}

ConWHSVehicleTablevehicle;

selectRecIdfromvehicle

wherevehicle.RecId==_context.parmRecId();

if(vehicle.RecId==0)

{

//Vehiclecannotbefoundfortheworkflowinstance

throwerror("@ConWHS:ConWHS73");

}

returntrue;

}

Wethrowanerrorincaseofavalidationerror,asweneedtheworkflowtostopwithanerrorshoulditfail;theworkflowcannotcontinuewithaninvalidcontext.

18. Next,addamethodsothattheworkflowtype'shandlerclassknowswhetherornotthesupportedelementsactuallyran.Sincethatfinalresultwillbeapprovedorrejected,wecan'tusethesamefieldtostatethattheworkflowiscompleted.Infact,iftheworkflowtypecompleted,butnothingwasdone,thedocumentshouldresetbacktoDraft(notsubmitted).Writethemethodasfollows:

publicbooleanCanCompleteWF(WorkflowContext_context)

{

ConWHSVehicleTablevehicle;

selectRecIdfromvehicle

wherevehicle.RecId==_context.parmRecId();

if(vehicle.RecId!=0)

{

//Codetocheckiftheworkflowcanbecompleted,

//i.e.nothinginprogress

returntrue;

}

returntrue;

}

19. Saveandclosethisclass,andopentheConWHSVehWFEventHandlerclassandaltertheclassdeclarationsothatitextendsConWHSVehWFBase.

20. Addthefollowingmethodsinordertohandletheworkflowtype'sevents:

publicvoidstarted(WorkflowEventArgs_workflowEventArgs)

{

WorkflowContextcontext;

context=_workflowEventArgs.parmWorkflowContext();

if(this.ValidateContext(context))

{

ConWHSVehicleStatusHandler::SetStatus(

context.parmRecId(),

ConWHSVehApprStatus::Waiting);

}

}

publicvoidcanceled(WorkflowEventArgs_workflowEventArgs)

{

WorkflowContextcontext;

context=_workflowEventArgs.parmWorkflowContext();

if(this.ValidateContext(context))

{

ConWHSVehicleStatusHandler::SetStatus(

context.parmRecId(),

ConWHSVehApprStatus::Draft);

}

}

publicvoidcompleted(WorkflowEventArgs_workflowEventArgs)

{

WorkflowContextcontext;

context=_workflowEventArgs.parmWorkflowContext();

if(this.ValidateContext(context))

{

If(!this.CanCompleteWF(context))

{

ConWHSVehicleStatusHandler::SetStatus(

context.parmRecId(),

ConWHSVehApprStatus::Draft);

}

}

}

ThestatuschangesherearethatwemovefromDrafttoWaitingwhentheworkflowenginestarts,andbacktoDraftifcanceled.Shouldtheworkflowcomplete,butfailtheCanCompleteWFcheck,resetitbacktoDraft.

21. Finally,opentheConWHSVehWFSubmitManagerclassandcompletethemainmethod,asshownhere:

publicstaticvoidmain(Args_args)

{

RefRecIdrecId;

CompanyIdcompanyId;

RefTableIdtableId;

WorkflowCommentcomment;

WorkflowSubmitDialogdialog;

WorkflowVersionTableversion;

recId=_args.record().RecId;

tableId=_args.record().TableId;

companyId=_args.record().DataAreaId;

//Themethodhasnotbeencalledcorrectly.

if(tableId!=tablenum(ConWHSVehicleTable)

||recId==0)

{

throwerror(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);

}

//SettheworkflowstatustoSubmitted.

ConWHSVehicleStatusHandler::SetStatus(

_args.record().RecId,

ConWHSVehApprStatus::Waiting);

if(FormDataUtil::isFormDataSource(_args.record()))

{

FormDataUtil::getFormDataSource(

_args.record()).research(true);

}

_args.caller().updateWorkflowControls();

}

22. OpentheConWHSVehSubmitMenuItemmenuitemandchangetheObjectpropertytoConWHSVehWFSubmitManager.23. Closeallcodeeditorsanddesignersandbuildtheproject.Thecompilerwillhighlightcodewe

forgottohandlebyshowingtheTODOcommentsaswarnings.

Howitworks...Theworkflowtyperequiredafewelementsbeforewecreatedtheactualworkflowtype.Thedocumentisdefinedbyaquery,whichhasamaintable.Thiscouldbeaqueryofsalesordersandsalesorderlines,wherethesalesorderisthemaintable,andletstheworkflowdesignerusefieldsfromthequerytodefinemessagestotheuser,andalsocontrolhowtheworkflowbehaves.Theworkflowhasspecialapplicationelementtypesforworkflow,whichpointtoclassesthatimplementspecificinterfaces.

Theworkflowtypeisahigherlevelthantheworkflowelements.Workflowelementsarethetasksassignedtotheuser,andtheyhandlestatessuchasReview,Reject,Approve,andsoon.Theworkflowtypeisatahigherlevel,andcontrolswhethertheworkflowisstarted,cancelled,orcompleted.

Itmayseemoddthatwedon'tmaptheworkfloweventtypesdirectlytotheBaseEnumelements.Theworkflowenginedoesn'treadthisfield;itknowswithinitselfthestatusoftheworkflow.Thestatusfieldistoallowustoeasilyreadthestatusoractonaparticularworkflowevent.Forthisreason,wedon'tactuallyneedtohandlealloftheeventsthattheworkflowprovides.

TheConWHSVehWFEventHandlerclasswastiedtotheworkflowtype,andisusedtopersisttheworkflow'sstateinthetargetdocumentrecord--thevehiclerecord,inourcase.

Theparmmethodontheworkflowdocumentclass,ConWHSVehWFDocument,addsacalculatedmemberthatcanbeusedbytheworkflowdesignertoeithermakedecisionsintheworkflowdesign,ordisplayedinmessagestotheusers.Theparmmethodshavetobewrittenwiththesameinputparametersasshownintheexamplemethod,andwearefreetowriteanycodewelike,andreturndataofanybasetypethatcanbeconvertedtoastring,suchasstrings,dates,andBaseEnum.Wecannot,therefore,returntypessuchasrecords,objects,orcontainers.Considerhowthemethodwillperform,asthiswillberunwheneveritneedstobeevaluatedbytheworkflowengine.

Seealso...Checkoutthefollowinglinksforhelpsettingupworkflowsandforfurtherreading:

Workflowsystemarchitecture(https://docs.microsoft.com/en-us/dynamics365/operations/organization-administration/workflow-system-architecture)Creatingaworkflow(https://docs.microsoft.com/en-us/dynamics365/operations/organization-administration/create-workflow)Overviewoftheworkflowsystem(https://docs.microsoft.com/en-us/dynamics365/operations/organization-administration/overview-workflow-systemandhttps://ax.help.dynamics.com/en/wiki/overview-of-the-workflow-system/)

Workflowelements(https://docs.microsoft.com/en-us/dynamics365/operations/organization-administration/workflow-elements)

CreatingaworkflowapprovalAworkflowapprovalisanelementthatallowsapprovaltaskstoberouted,whichcanbeapprovedorrejected.Thedesigncanthenusethisoutcomeinordertotriggertasks,orsimplyinformtheuser.Theworkflowapprovalstatusispersistedasafieldonthedocumentrecord(thatis,thevehiclerecordinourcase),inthesamewaythattheworkflowtypedoes.

Asaresultofthis,thereareoftentwofieldsontheworkflow'smaintable,oneforworkflowdocumentstate,andanotherforworkflowelementstate.Insomecases,suchashumanresourceworkflows,theBaseEnumiscombinedintoonefield.Thiscanseemconfusing,butwhentheworkflowstatusfieldisproperlydefined,itsimplifiestheprocess.

Wecannotcreateextensionsforworkflowelements,sowecannotuseworkflowtypescreatedbyotherpartieswithoutcustomization(over-layering).

GettingreadyWejustneedtohavecreatedaworkflowtype,orhaveasuitableworkflowtypetoaddtheapprovalto.

Howtodoit...Tocreateaworkflowapproval,followthesesteps:

1. AddanewitemtotheprojectbyselectingBusinessProcessandWorkflowfromtheleft-handlist,andthenWorkflowApprovalfromtheright.EnterConWHSVehApprWFastheNameandclickonAdd.

2. CompletetheWorkflowApprovaldialogasshownhere:

3. ClickonNext.4. Youwillbepresentedwithalloftheelementsthewizardwillcreateforus,remindingusagainwhy

thelimitis20charactersandalsowhythenamingisimportant.ClickonFinish.5. OpenthenewConWHSVehApprWFworkflowapproval,expandtheOutcomesnode,andnotethatthesystem

hasassociatedaworkfloweventhandlerclasswithApprove,Reject,andRequestChange.Tocompletethiselement,completetheLabelandHelpTextpropertiesontherootConWHSVehApprWFnodeelement.Theworkflowdesignerwillneedthistoidentifythecorrectworkflow.

6. TherewillbefivenewActionMenuItemscreated,prefixedwiththewithapprovalname,ConWHSVehApprWF.ThesearesuffixedwithApprove,DelegateMenuItem,Reject,RequestChange,andResubmitMenuItem.Foreachofthese,settheLabelandHelpTextpropertieswithasuitablelabel,forexample:

Menuitem Label Helptext

Approve Approve Approvethenewvehiclerequest

DelegateMenuItem Delegate Delegatethisapprovaltoacolleague

Reject Reject Rejectthenewvehiclerequest

RequestChange Revise Sendtherequestbackforrevision

ResubmitMenuItem Resubmit Resubmitthenewvehiclerequest

Createthelabelsusingnamesandnotnumbers,aswewillreusetheselabelsinotherareas.

Aswellasmenuitems,italsocreatedaneventhandlerclass,whichisnamedbasedontheworkflowapproval,suffixedwithEventHandler.Thisclasswillimplementseveninterfaces,whichenforcethatamethodisimplemented,onepereventtype.

7. Opentheworkeventhandlerclass,ConWHSVehApprWFEventHandler,andaltertheclassdeclarationsothatitextendsConWHSVehWFBase.

8. ThisclassimplementstheWorkflowElementDeniedEventHandlerinterface,eventhoughwechosenottointhecreationdialog;removethisfromthelist.

9. Then,locatethedeniedmethodanddeleteit.10. WenowneedtowritesomecodeforeachmethodthatwasgeneratedforuswithaTODO.Thesample

codetowriteforeachmethodisasfollows:

publicvoidstarted(WorkflowElementEventArgs_workflowElementEventArgs)

{

WorkflowContextcontext;

context=

_workflowElementEventArgs.parmWorkflowContext();

if(this.ValidateContext(context))

{

ConWHSVehicleStatusHandler::SetStatus(

context.parmRecId(),

ConWHSVehApprStatus::InReview);

}

}

11. Followthispatternforeachmethodusingthefollowingtabletodeterminewhichstatustoset:

Element Method

Waiting started

InReview created

Approved completed

Rejected returned

Revise changeRequested

Draft cancelled

12. Forthecreatedmethod,theinputparameterisadifferenttype;simplychangethemethodasfollows:

publicvoidcreated(WorkflowWorkItemsEventArgs_workflowWorkItemsEventArgs)

{

WorkflowContextcontext;

WorkflowElementEventArgsworkflowArgs;

workflowArgs=

_workflowWorkItemsEventArgs.parmWorkflowElementEventArgs();

context=workflowArgs.parmWorkflowContext();

if(this.ValidateContext(context))

{

ConWHSVehicleStatusHandler::SetStatus(

context.parmRecId(),

ConWHSVehApprStatus::InReview);

}

}

13. Inthepreviousrecipe,wewroteamethodtodetermineiftheworkflowdidanythingthatwasusedtoresettheworkflowshouldnothinghavebeendonewhentheworkflowtypecompleted.OpentheConWHSVehWFBaseclassandalterthemethodasfollows:

publicbooleanCanCompleteWF(WorkflowContext_context)

{

ConWHSVehicleTablevehicle;

selectRecIdfromvehicle

wherevehicle.RecId==_context.parmRecId();

booleancanComplete;

if(vehicle.RecId!=0)

{

switch(vehicle.Status)

{

caseConWHSVehApprStatus::Approved:

caseConWHSVehApprStatus::Rejected:

canComplete=true;

default:

canComplete=false;

}

}

returncanComplete;

}

14. Thefinalpieceofcodetowriteistheresubmissioncode.Atemplatewascreatedforus,soopentheConWHSVehAppWFResubmitActionMgrclass.

15. Inthemainmethod,removetheTODOcommentandwritethefollowingcodesnippet:

publicstaticvoidmain(Args_args)

{

//Themethodhasnotbeencalledcorrectly.

if(_args.record().TableId!=

tablenum(ConWHSVehicleTable)

||_args.record().RecId==0)

{

throwerror(strfmt("@SYS19306",funcname()));

}

//Resubmitthesameworkflow,Workflowhandles

//resubmitaction

WorkflowWorkItemActionManager::main(_args);

//SettheworkflowstatustoSubmitted.

ConWHSVehicleStatusHandler::SetStatus(

_args.record().RecId,

ConWHSVehApprStatus::Waiting);

_args.caller().updateWorkflowControls();

}

16. OpentheConWHSVehApprWFworkflowapproval,selecttheDenyoutcome,andchangetheEnabledpropertytoNo.

17. Finally,opentheworkflowtypeandthenright-clickonSupportedElementsnode.SelectNewWorkflowElementReferenceandsetthepropertiesasfollows:

Field EDT/Enum Description

ElementName ConWHSVehApprWF Thisistheelement'sname

Name ApprovalVehicle Thisisashortversionofthename,prefixedwiththetype

Type Approval Thisistheworkflowelement'stype

18. Saveandcloseallcodeeditorsanddesignersandbuildtheproject.Don'tforgettosynchronize,aswehaveaddedanewfield.

Howitworks...Theworkflowapprovalissetupwithoutcomes,whicharereferencedtoaneventhandlerclassthatimplementsaninterfaceforeachoutcomeithandles.Eachoutcomeistied,internally,tothatinterface.Whentheoutcomeoccurs,itwillconstructthereferencedeventhandlerclassusingtheinterfaceasthetype.Itthencallstheappropriatemethod.Thispatternofinstantiatingaclassusingtheinterfaceasthetypeiscommonpattern,andwehaveusedthisourselvesinChapter10,ExtensibilityThroughMetadataandDataDate-Effectiveness.

Therearesomeevents(StartedandCancelled,forexample)thataresetontheworkapproval'smainpropertysheet.Allthiswascreatedforuswhenwecreatedtheworkflowapprovalelement.

TheclassthatthecodegeneratedforusimplementsallrequiredinterfaceswithTODOstatementswhereweneedtowritecode.Thecodeisusuallysimple,and,inourcase,wearejustupdatingthevehicle'sstatusfield.Thegeneratedcodewillalwaysimplementallinterfacesthattheworkflowelementcansupport,soitiscommontoremovemethodsandinterfacesfromtheeventhandlerclass.

Creatingamanualworkflowtask

Amanualtaskisataskthatisassignedtoauserinordertoperformanaction.Theactioncanbeanytask,suchasInspectvehicle,andtheuserwillthenstatethatthetaskwascomplete.

Thisworkflowwillbeusedtoinstructthevehicletobeinspected,andrecordwhetheritwasinspectedinanewfieldonthevehicletable.

GettingreadyThisfollowsfromtheCreatingaWorkflowTyperecipe,asweneedaworkflowdocumentclass.

Howtodoit...Tocreatethemanualworkflowtask,followthesesteps:

1. WeneedanewBaseEnumfortheinspectionstatus,asthiswillbeusedbothtoseewhetheravehiclehasbeeninspectedandalsotocontrolthestateoftheworkflowtask;nameitConWHSVehInspStatusandcreatetheelementsasshowninthefollowingtable:

Element Label Description

NotInspected Notinspected Thisvehiclehasnotyetbeeninspected

Waiting Waiting Thisworkflowhasbeensubmitted,buthasnotyetbeenallocatedanapprover

InProgress InProgress Thisworkflowhasbeenallocatedtooneormoreworkerstoperformthetask

Completed Completed Thisworkflowhasbeencompleted

2. CreateanewDateEDTforConWHSVehInspDate,settingthepropertiesasfollows:

Field EDT/Enum Description

Extends TransDate ThisEDTshouldbeusedforalldates.

Label Dateinspected Createanamedlabelforthis,suchas@ConWHS:DateInspected.

HelpText

Thedatetheinspectionwascarriedout

ThisisleftgenericandnottiedtoitseventualimplementationinordertomaketheEDTreusable.Thehelptextdoesnotreferencethevehicleforthisreason.

3. AddthefollowingfieldstothevehicletableandsettheAllowEditandAllowEditOnCreatetoNo:

Field EDT/Enum Description

InspStatus ConWHSVehInspState ThisisthestatusBaseEnumcreatedinthepreviousstep

InspComment WorkflowComment Thiswillholdthelastnotewhenthetaskiscompleted

InspDate ConWHSVehInspDate Thisisthedateonwhichtheworkflowtaskwascompleted

4. CreateafieldgroupnamedInspectionandsettheLabelpropertytoalabelforInspection.AddthefieldstothisgroupandthenaddthefieldgrouptoasuitableplaceintheConWHSVehicleTableform.

5. Next,let'saddastatushandlerclass;createanewclassname,ConWHSVehicleInspStatusHandler.Createamethodtohandlethestatuschange,andsettheInspCommentandInspDatefieldsfromthemethod'sparameters.Thecodeiswrittenasfollows:

///<summary>

///Handletheinspectiondatechange

///</summary>

///<paramname="_vehicleRecId">

///Therecordidofthevehicle

///</param>

///<paramname="_status">

///Thenewstatus

///</param>

///<paramname="_comment">

///Commentissetwhenthestatus

///iscomplete

///</param>

///<paramname="_inspDate">

///InspDateissetwhenthe

///statusiscomplete

///</param>

publicstaticvoidSetStatus(RefRecId_vehicleRecId,

ConWHSVehInspStatus_status,

WorkflowComment_comment='',

ConWHSVehInspDate_inspDate=dateNull())

{

ConWHSVehicleTablevehicle;

ttsbegin;

selectforupdatevehicle

wherevehicle.RecId==_vehicleRecId;

if(vehicle.RecId!=0)

{

vehicle.InspStatus=_status;

//iftheinspectioniscompleteset

//thecommentandinspectiondatefields

//otherwiseclearthem,astheworkflow

//mayhavebeencancelled.

switch(_status)

{

caseConWHSVehInspStatus::Complete:

vehicle.InspComment=_comment;

vehicle.InspDate=_inspDate;

break;

default:

vehicle.InspComment='';

vehicle.InspDate=dateNull();

}

vehicle.update();

}

ttscommit;

}

6. Againsttheproject,addanewitemandchooseWorkflowTaskfromtheBusinessprocessandWorkflowlist.UsetheConWHSVehWFInspectnameandclickonAdd.

7. ConfiguretheWorkflowTaskdialogasshowninthefollowingscreenshot:

8. ClickonNext.9. Onthenextpage,chooseCompleteintheTypedrop-downlist,andenterCompleteinthefieldbefore

clickingonAdd.

Youcanaddfurtheroutcomes,whichwillfollowthesamepatternwhenimplemented.

10. ClickonNextandthenFinish.11. Foreachactionmenuitemcreatedbythewizard,completetheLabelandHelpTextproperties.

YoumayrecognizethatthecodegeneratedbythisprocessisverysimilartotheWorkflowapproval.WewillfollowthatpatternagainbyhandlingtherequiredmethodsintheConWHSVehWFInspectEventHandlerclass.

12. Sincewedon'thandleallofthepossibleoutcomes,weshouldonlyimplementtherequiredinterfaces.Also,inordertohaveaccesstotheValidateContextmethod,weshouldextendConWHSVehWFBase.Theclassdeclarationshouldreadasshownhere:

publicfinalclassConWHSVehWFInspectEventHandler

extendsConWHSVehWFBase

implementsWorkflowElementCanceledEventHandler,

WorkflowElementCompletedEventHandler,

WorkflowElementStartedEventHandler,

WorkflowWorkItemsCreatedEventHandler

13. Also,removethemethodslinkedtotheinterfacethatweremoved.Changethestartedmethodasshownhere.Itmaintainsthevehiclestatusandinspectionstatusfields:

publicvoidstarted(

WorkflowElementEventArgs_workflowElementEventArgs)

{

WorkflowContextcontext;

context=

_workflowElementEventArgs.parmWorkflowContext();

if(this.ValidateContext(context))

{

ConWHSVehicleInspStatusHandler::SetStatus(

context.parmRecId(),

ConWHSVehInspStatus::Waiting);

ConWHSVehicleStatusHandler::SetStatus(

context.parmRecId(),

ConWHSVehApprStatus::Inspection);

}

}

14. Thecanceledmethodshouldresetbothstatusfieldsbacktotheirinitialstates:

publicvoidcanceled(

WorkflowElementEventArgs_workflowElementEventArgs)

{

WorkflowContextcontext;

context=

_workflowElementEventArgs.parmWorkflowContext();

if(this.ValidateContext(context))

{

ConWHSVehicleInspStatusHandler::SetStatus(

context.parmRecId(),

ConWHSVehInspStatus::NotInspected);

ConWHSVehicleStatusHandler::SetStatus(

context.parmRecId(),

ConWHSVehApprStatus::Draft);

}

}

15. Thecompletedmethodneedstogetthecurrentsystemdate,andalsofetchthelastcommentfromtheworkflow.Thisisdoneinthefollowingcode:

publicvoidcompleted(WorkflowElementEventArgs_workflowElementEventArgs)

{

WorkflowContextcontext;

context=

_workflowElementEventArgs.parmWorkflowContext();

WorkflowCorrelationIdcorrelationId;

correlationId=context.parmWorkflowCorrelationId();

WorkflowTrackingTabletrackingTable;

trackingTable=

Workflow::findLastWorkflowTrackingRecord(

correlationId);

WorkflowTrackingCommentTablecommentTable;

commentTable=trackingTable.commentTable();

WorkflowCommentcomment=commentTable.Comment;

Timezonetimezone=

DateTimeUtil::getUserPreferredTimeZone();

if(this.ValidateContext(context))

{

ConWHSVehicleInspStatusHandler::SetStatus(

context.parmRecId(),

ConWHSVehInspStatus::Complete,

comment,

DateTimeUtil::getSystemDate(timezone));

}

}

16. Finally,writethecreatedmethod.Thisiswhenthetaskisassignedtooneormoreusers.Thecodeshouldbewrittenasfollows:

publicvoidcreated(

WorkflowWorkItemsEventArgs_workflowWorkItemsEventArgs)

{

WorkflowContextcontext;

WorkflowElementEventArgsworkflowArgs;

workflowArgs=_workflowWorkItemsEventArgs.

parmWorkflowElementEventArgs();

context=workflowArgs.parmWorkflowContext();

if(this.ValidateContext(context))

{

ConWHSVehicleInspStatusHandler::SetStatus(

context.parmRecId(),

ConWHSVehInspStatus::InProgress);

ConWHSVehicleStatusHandler::SetStatus(

context.parmRecId(),

ConWHSVehApprStatus::Inspection);

}

}

17. WeshouldalsoupdatetheCanCompletemethodontheConWHSVehWFBaseclass,butwhatwedohereisdependentonwhatwewanttocontrol.Weareindangerofhardcodingabusinessrule,whichisironicallywhatworkflowsaredesignedtoavoid.Asaresultofthis,wejustwanttoensurethatthedocument(vehiclerecord)isalwaysleftinaconsistentstatewhentheworkflowtypecompletes.Thefollowingpieceofcodewillonlyreturnfalseifeithertheapprovalortaskisinprogress:

publicbooleanCanCompleteWF(WorkflowContext_context)

{

ConWHSVehicleTablevehicle;

selectRecIdfromvehicle

wherevehicle.RecId==_context.parmRecId();

booleancanComplete=true;

if(vehicle.RecId!=0)

{

switch(vehicle.Status)

{

caseConWHSVehApprStatus::Revise:

caseConWHSVehApprStatus::Waiting:

caseConWHSVehApprStatus::InReview:

caseConWHSVehApprStatus::Inspection:

canComplete=false;

default:

canComplete=true;

}

switch(vehicle.InspStatus)

{

caseConWHSVehInspStatus::InProgress:

caseConWHSVehInspStatus::Waiting:

canComplete=false;

}

}

returncanComplete;

}

18. Next,completetheConWHSVehApprWFResubmitActionMgrclassasfollows:

publicstaticvoidmain(Args_args)

{

//Themethodhasnotbeencalledcorrectly.

if(_args.record().TableId!=

tablenum(ConWHSVehicleTable)

||_args.record().RecId==0)

{

throwerror(strfmt("@SYS19306",funcname()));

}

//Resubmitthesameworkflow,Workflowhandlesresubmitaction

WorkflowWorkItemActionManager::main(_args);

//SettheworkflowstatustoSubmitted.

ConWHSVehicleInspStatusHandler::SetStatus(

_args.record().RecId,

ConWHSVehInspStatus::Waiting);

_args.caller().updateWorkflowControls();

}

19. Finally,opentheworkflowtypeandthenright-clickontheSupportedElementsnode.SelectNewWorkflowElementReferenceandsetthepropertiesasfollows:

Field EDT/Enum Description

ElementName ConWHSVehWFInspect Thisistheelement'sname

Name TaskInspect Thisisashortversionofthename,prefixedwiththetype

Type Task Thisistheworkflowelement'stype

20. CopyandpastethetasknameintotheElementNameandNameproperties.21. Saveandclosealldesignersandcodeeditorsandbuildtheproject,followedbysynchronizingthe

databasewiththeproject.

Howitworks...

Theconceptisthesameasfortheworkflowapprovalinthepreviousrecipe.TheWorkflowtaskelementisadefinitionthatthedesignerwillusewhencreatingaworkflow.Thecodewewrotesimplyhandlestheeventsasweneedto.

Thecomplicatedparttounderstandisthestatushandling.Itseemsnaturaltohaveastatusfieldforeachworkflowelement(thetype,approval,andtask),andwiththisparadigm,wewouldbeleftthinkingwhythereisn'tastandardBaseEnumwecouldsimplyuse.Thestatusofthedocument,andthestatusesthedocumentcanbedefinedbyus--whatmakessensetothebusiness,andnotwhatmakessenseincode.Fortheinspectiontask,wewanttoknowifavehicleiswaitinginspection,isinprogress,orifitiscomplete.

HookingupaworkflowtotheuserinterfaceThisisthefinalstepindesigningourworkflow,andinvolvessettingapropertyontheformreferencedbythedocumentmenuitem,ConWHSVehicleTable,andaddingtheoptiontodesignworkflowstothemenu.

GettingreadyTheminimumweneedtohavecompletedforthisiscreateaworkflowtype.

Howtodoit...Inordertobeabletodesignandprocessworkflows,followthesesteps:

1. ExpandUserinterface,Menuitems,andthenDisplayfromtheApplicationExplorer.LocatedWorkflowConfgurationBasic,right-clickonitandchooseDuplicateinproject.

2. LocatethenewmenuitemintheSolutionexplorer,andrenameittoConWHSWorkflowConfiguration.

Dependingontheversionofthedevelopmenttools,youwillneedtoundothechangetoWorkflowConfigurationBasicCopyandthenaddtherenamedelementtosourcecontrol.

3. OpenthenewmenuiteminthedesignerandsettheEnumParameterpropertytoConWHS,andthencreatelabelsfortheLabelandHelpTextproperties.Thelabelshouldbethemodulename,followedbythewordworkflows,Vehiclemanagementsystemworkflows,forexample.

4. AddthismenuitemtotheSetupsubmenuofourmenu.Thisshouldusuallybethesecondoptioninthelist.

5. OpentheConWHSVehicleTableforminthedesigner.

6. SelecttheDesignnodeoftheformdesignandlocatethefollowingproperties:

Property Value

WorkflowDataSource ConWHSVehicleTable

WorkflowEnabled Yes

WorkflowType ConWHSVehWF

7. Right-clickontheMethodsnodeoftheformandselectOverride|canSubmitToWorkflow.Alterthecodesothatitreadsasfollows:

publicbooleancanSubmitToWorkflow()

{

If(ConWHSVehicleTable.Status==

ConWHSVehApprStatus::Draft)

{

returntrue;

}

returnfalse;

}

Wemayneedthistobemoreelaborateinsomecases,buttheminimumisthatwecan'tallowaworkflowtobesubmittedthatisalreadyinprogress.

8. Saveandclosealldesignersandbuildtheproject.

Howitworks...

TheworkflowconfigurationisagenericformthatbuildsbasedontheModuleAxaptaBaseEnum.WelinkedConWHStotheworkflowcategory,whichwasthenlinkedtotheworkflowtype.Thiswill,therefore,allowtheworkflowdesignertocreateandmodifyworkflowsforthismodule.

Theformchangesweresimplytolinktheworkflowtypetotheform,andwhichdatasourceisthedocumentdatasource.Thisisthenusedtoqueryifthereareanyactiveworkflowsforthattype,andwillshowtheoptiontosubmitthevehicleforapprovalifthereisanactivevehicleworkflowdesign.

Creatingasampleworkflowdesign

Let'stesttheelementswehavecreated.Thefollowingworkflowdesignisonlyintendedtotesttheworkflowwehavecreated,andomitsmanyofthefeaturesthatwewouldnormallyuse.Wewillalsousethesameuserforsubmissionandapproval,andyouwillseethatappeartobewaitingfortheworkflowengineaswetest.Thisseemsaproblematfirstglance,butinreal-lifescenarios,thisisfine.Inpractice,thetasksandapprovalsareperformedbydifferentusersandarenotdoneasaseriesoftasks.Theywillreceiveanotification,andtheycanthenperformthatactionandpasstheballbacktotheworkflowengine.

GettingreadyBeforewestart,ensurethattheprojectisbuiltandsynchronizedwiththedatabase.

Howtodoit...Tocreatetheworkflowdesign,followthesesteps:

1. OpenthefollowingURL,andfromthere,openVehiclemanagement|Setup|Vehiclemanagementworkflows:

https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=usmf

YoucannavigatedirectlytotheconfigurationformusingthefollowingURL:https://usnconeboxax1aos.cloud.onebox.dynamics.com/?cmp=usmf&mi=ConWHSWorkflowConfiguration

2. ClickonNEW,andyoushouldseetheworkflowtypeinthelist,andislistedusingtheLabelandHelpTextpropertiesthatweset.

3. Selecttheworkflowtypelink,asshowninthefollowingscreenshot:

4. Youwillthenbeaskedtologin,whichyoushoulddoasthesameuseryouusedtologintoOperations.Youwillthenbepresentedwithanewwindow,withthetwoworkflowelementsthatwewroteandsomeflowcontroloptionsintheleft-handpane.

5. DragtheNewvehicleapprovalworkflowandInspectvehicleelementsontothedesignsurface,asshowninthefollowingscreenshot:

6. AsyourmousehoversovertheStartelement,youwillseesmallhandlesappear--dragoneofthesehandlessothatitconnectstheStartelementtotheInspectvehicle1element.Connectallelements,asshowninthefollowingscreenshot:

7. SelecttheInspectvehicle1element,andclickonBasicSettingsfromtheactionpane.Configureitasshowninthefollowingscreenshot:

Thetextwithin%symbolswasaddedusingtheInsertplaceholderbutton;clickonthisandyouwillseethatourparmmethodwasaddedandtheparmprefixwasautomaticallyremoved.

8. ClickonAssignmentfromtheleft,andchooseUserfromtheAssignuserstothisworkflowelementlist.SelecttheUsertab,andmanuallyassignthistoAdmin.Thisisthedefaultadministrationuser.Sincewearesimplytestingourcodeworks,wewillassignalltasksandapprovalstoouruser.

9. ClickonClose.10. Double-clickontheNewvehicleapprovalworkflow1elementandclickonBasicSettingsfromthe

actionpane.ChangeNametoNewvehicleapprovalworkflowandclickonClose.

Wewouldnormallyconfigurethenotifications;forexample,wewouldusuallynotifyWorkflowOriginatoriftheapprovalwasrejected.

11. Then,selectStep1,pressBasicSettings,andconfigureasshownhere:

12. ClickonAssignmentandassignthistoouruseraswedidbefore.PressClose.Thereisabreadcrumbatthetopoftheworkflowdesigner'sdesignsurface,whichshowsWorkflow|Newvehicleapproval.ClickonthewordWorkflowfromthebreadcrumb.

13. Finally,wemustgivesubmissioninstructionstothepersonwhosubmittedthevehicleapproval.Right-clickonanemptyareaoftheworkflowdesigner'sdesignsurfaceandclickonProperties.EntersuitableinstructionsintheSubmissioninstructionsfieldandclickonClose.

14. ClickonSaveandclose,andthentheOKontheSaveworkflowdialog.SelectActivethenewversionontheActivateworkflowdialogandclickonOK.

Let'snowtestiftheworkflowworks:

1. Openthevehicleform,andyoushouldseetheWorkflowbuttonasshowninthefollowingscreenshot:

2. ClickonthebuttonandselectSubmit,whichisthelabelweassignedtotheConWHSVehWFSubmitMenuItemmenuitem.EnteracommentandclickonSubmit.

3. TheoptionsshouldchangetoCancelandViewhistory.YoucanchooseViewhistorytoseetheprogressoftheworkflowengine.Ifthetasksaren'tassignedwithinaminute,checkthattheMicrosoftDynamics365forOperations-BatchManagementServiceWindowsserviceisrunning.AlsocheckthattherearebatchjobsforWorkflowmessageprocessing,Workflowduedataprocessing,andWorkflowline-itemnotifications.Ifnot,openWorkflowinfrastructureconfigurationfromSystemadministration|Workflow,andclickonOK.

4. Afterapproximatelytwominutes,havingpressedtherefreshicon,youshouldseetheComplete,Delegate,andCanceloptions.Thesearetheoptionsfortheinspectiontask.SelectComplete.EnteraninspectioncommentandclickonComplete.Viewthevehicledetailsandwaitforaboutaminute.Clickontherefreshbutton.Youshouldseeinformationsimilartothefollowing:

5. Afterafurtherminute,refreshtheform,andtheoptionswillchangetotheapprovaloptions.ChooseApprovefromthelist.

Howitworks...

Theworkflowdesignhasmanyoptionsavailabletous,andisusedtotelltheworkflowenginehowtheapprovalsandtasksshouldbeprocessed.Whenwedevelopworkflows,wedosoasgenericallyaspossibleinordertoleavethebusinesslogictotheworkflowdesigner.Thismeansthatwereducetheneedforchangestothecodeasthebusinessevolves.

Thetestwastohelpdemonstratewhathappenstothestatusfieldsastheworkflowisprocessed.Thisshouldhelpusinourownworkflowdevelopmentinunderstandingthelinkbetweeneventsandstatuschanges.

StateMachinesInthischapter,wewillcoverthefollowingrecipes:

CreatingastatemachineCreatingastatemachinehandlerclassUsingmenuitemstocontrolastatemachineHookingupthestatemachinetoaworkflow

IntroductionStatemachinesareanewconceptinD365O,andaverywelcomefeature.Previously,thecontrolofstatusfieldswashandcraftedincode,whichcouldbeoftenhardtoreadastherewasnoobviouspatterntofollow;havingsaidthat,wewillalwayslookatasimilarstandardexampletoourcaseandusethatidea.Thisisnotplagiarism,itisgoodpractice.Itisagoodgeneralruletoseekexamplesinstandardcode,asthereisamuchhigherchancethatanotherdeveloperwillunderstandthecodewe'vewritten.

Statemachinesallowustodefineinmetadatahowthestatustransitionsfromaninitialstatetoitsfinalstate.Theserulesarethenenforcedbycodethatthestatemachinewillgenerate.

Thereisarestrictionthough.Theremustbeoneinitialstateandonefinalstate.Whenweareatthefinalstate,thereisnogoingback.Ifwetakethesalesorderstatus,wehavetwofinalstates:InvoicedandCancelled.Thereisanotherreasonwhywewouldn'tuseastatemachineonthistypeofstatus.Thesalesorderstatusisareflectionofactualorderstate;itissystemcontrolled.Statemachinesaredesignedtoenforcedstatuschangelogicwhenthestateisassertedbyauser.

CreatingastatemachineThisfirstrecipeistocreateastatemachineforvehicleinspection.InChapter14,WorkflowDevelopment,wecreatedaworkflowtaskandaninspectionstatusfield.Inthisrecipe,wewilluseastatemachinetohandletheinspectionstatuschangelogic.

GettingreadyWeneedtohaveatablewithastatusfieldwithaninitialandfinalstatus,suchastheInspStatusfieldweaddedtotheConWHSVehicleTabletableinChapter14,WorkflowDevelopment.

Howtodoit...Tocreateastatemachine,followthesesteps:

1. OpenConWHSVehicleTableinthedesigner.Right-clickontheStateMachinesnodeandchooseNewStatemachine.

2. RenamethenewstatemachineInspStateMachineandcompletethepropertiesasshowninthefollowingtable,creatinglabelsfortheDescriptionandLabelproperties:

Property Value

Description Usethistocontroltheinspectionstatus

Label Inspectionstatus

DataField InspStatus

3. Right-clickonthenewstatemachinedefinitionandselectNewState.4. Completethepropertiesofthisstateusingthefollowingtable:

Property Value

EnumValue NotInspected-changethistoWaiting,andthenbackagaintodefaulttheLabelproperty.

Description Thevehiclehasnotyetbeeninspected

Label Notinspected

StateKind Initial

WewillcreateastateforeachelementintheConWHSVehInspStatusBaseEnum,soitisagoodideatocreatedescriptionlabelsinadvanceandjustpastethemin.Usenamedlabelsforthis,notnumeric.IuseasuffixofHT,whichisshortforHelpText,forlabelsthatareusedforbothhelptextanddescriptionsofelements.

5. Createtheremainingstatesusingthefollowingtableasaguide:

EnumValue Name StateKind Description

Waiting Waiting Intermediate Thevehicleisawaitinginspection

InProgress InProgress Intermediate Thevehicleinspectionisinprogress

Complete Complete Final Thevehicleinspectioniscomplete

6. Theresultshouldlooklikethefollowingscreenshot:

7. Wewillnowneedtotellthestatemachinethetransitionrules.Wewilldefinetherulesasfollows:NotInspectedcanonlytransitiontoWaitingWaitingcanonlytransitiontoInProgressInProgresscantransitiontobothWaitingandCompleteCompleteisthefinalstateandcannottransitionbackwards

8. Again,createlabelsinadvance.Thefollowingtableexplainsthetypeofwordingweshoulduse:

LabelID Label

VehTransWaiting Addtowaitinglist

VehTransWaitingHT Addthevehicletothelistofvehiclesawaitinginspection

VehTransInProgress Startinspection

VehTransInProgressHT Startthevehicleinspectionprocess

VehTransBackWaiting Reverttowaiting

VehTransBackWaitingHT Placethevehiclebackontothewaitinglist

VehTransComplete Completeinspection

VehTransCompleteHT Complete,andfinalizethevehicleinspection

9. Todothis,right-clickontheNotInspectedstateandselectNewStatetransition.Thistime,theLabelandDescriptionpropertiesdefinetheaction,notthestate.SetthepropertiestodefinethetransitiontotheWaitingstateasfollows:

Property Value

Description @ConWHS:VehTransWaitingHT

Label @ConWHS:VehTransWaiting

Name TransitionToWaiting

TransitionToState Waiting

10. AddanewtransitionstatetotheWaitingStatestateusingthefollowingtable:

Property Value

Description @ConWHS:VehTransInProgressHT

Label @ConWHS:VehTransInProgress

Name TransitionToInProgress

TransitionToState InProgress

11. Next,addtwotransitionstatestotheInProgressstate.Thefirstistorevertbacktowaiting:

Property Value

Description @ConWHS:VehTransBackWaitingHT

Label @ConWHS:VehTransBackWaiting

Name TransitionToWaiting

TransitionToState Waiting

12. ThesecondstatetoaddtotheInProgressstatecompletesthestatemachine,andshouldbeconfiguredasfollows:

Property Value

Description @ConWHS:VehTransCompleteHT

Label @ConWHS:VehTransComplete

Name TransitionToComplete

TransitionToState Complete

13. Saveyourchanges,theresultshouldlooklikethefollowingscreenshot:

14. Thefinalstepistoright-clickontheInspStateMachinestatemachine,andclickonGenerate.Thisgeneratesthecodethatwillbeusedtocontroltheinspectionstatusprogression.

IfyougettheerrorGivenkeydoesnotexistinthedictionary,itisbecausethenameofthestatedidnotmatchtheEnumValueproperty.Thismaybechangedinfuturereleasessothatitcanbenameddifferently.

15. Thegeneratedclassesmaynotbeaddedtoyourproject;todoso,locatetheclassesthatstartwithConWHSVehicleTableInspStateMachineanddragthemontotheClassesnodeofyourproject.Donotmodifytheseclasses;theseareshowninthefollowingscreenshot:

Howitworks...Whatthisprocessactuallydoesisgeneratefourclasses.ThemainclassisnamedConWHSVehicleTableInspStateMachine,whichisaconcatenationofthetable'snameandthestatemachine'sname.Theotherthreeclassesareallprefixedwiththisclass,andallowtypeddatetobepassedtothedelegatesthatwerewrittenintothisclass.

Thefactwehaveastatemachinedoesnotpreventtheuserfrommanuallychangingthestatusfield'svalue.Italsodoesnotstopusfrommanuallychangingthestatusincode.Sotherestrictiononthefinalstatusbeingfinalisonlytruewhenusingthestatemachine.

Therearetwowaysinwhichwecanusethestatemachine:

AttachtoworkfloweventsUsewithmenuitemsaddedtoaform

Wewillexploretheseinthefollowingrecipes.

CreatingastatemachinehandlerclassThestatemachineprovidescontroloverthetransitionrules,but,sometimes,wewanttoensurethatothervalidationrulesareobeyedinordertovalidatewhetherthetransitioncanbedone.

ThisisdonebysubscribingtotheTransitiondelegateoftheConWHSVehicleTableInspStateMachineclassthatwasgeneratedbythestatemachine.

ThecodeinthisreciperefactorstheConWHSVehicleInspStatusHandlerclassthatwecreatedinChapter14,WorkflowDevelopment.Thecodewritteninthisrecipewilltieitprogrammaticallytothestatemachine.Shouldyouwishtoattachthestatementtotheworkflowdirectly(whichisagreatidea),thestatuswillbesetbythestatemachine.Therefore,theeventhandlersmustnotsetthestatus.Furthermore,shouldthevalidationwritteninthisrecipefail,wemustensurethattheworkflow'sinternalstatusmatchesthestatemachine'sstatus.Thiscouldbebycancelingtheworkflowbythrowinganerror.

GettingreadyWecreatedaclassnamedConWHSVehicleInspStatusHandler;wewillextendthisclasssothatwecanuseitwiththestatemachine.

Howtodoit...

Tocreateahandlerclasstoaddfurthervalidationtothestatemachine,followthesesteps:

1. OpentheConWHSVehicleInspStatusHandlerclassandaddthefollowingpieceofcode:

publicConWHSVehInspStatusfromStatus;

publicConWHSVehInspStatustoStatus;

publicConWHSVehicleTablevehicle;

publicbooleanValidate()

{

switch(toStatus)

{

caseConWHSVehInspStatus::Complete:

if(vehicle.InspComment=='')

{

DictFieldfield=newDictField(

tableNum(ConWHSVehicleTable),

fieldNum(ConWHSVehicleTable,

InspComment));

//Thefield%1mustbefilledin"

returncheckFailed(strFmt(

"@SYS110217",

field.label()));

}

break;

}

returntrue;

}

publicvoidrun()

{

if(toStatus==fromStatus)

{

return;

}

if(this.Validate())

{

switch(toStatus)

{

caseConWHSVehInspStatus::Complete:

Timezonetz=DateTimeUtil::

getClientMachineTimeZone();

ConWHSVehInspDateinspDate;

inspDate=DateTimeUtil::getSystemDate(tz);

vehicle.InspDate=inspDate;

break;

}

}

else

{

vehicle.InspStatus=fromStatus;

}

}

Thereisnothingnewabouttheprecedingcode,exceptthatwedon't(andmustnot)callupdateontherecord.Itisjustavalidationclassthatwillstopthetransitionifthecommentisblank.

2. Thecodetotieittothetransitiondelegateisasfollows:

[SubscribesTo(classStr(ConWHSVehicleTableInspStateMachine),

delegateStr(

ConWHSVehicleTableInspStateMachine,Transition))]

publicstaticvoidHandleTransition(

ConWHSVehicleTableInspStateMachineTransitionEventArgs

_eventArgs)

{

ConWHSVehicleInspStatusHandlerhandler;

handler=newConWHSVehicleInspStatusHandler();

handler.vehicle=_eventArgs.DataEntity();

handler.fromStatus=_eventArgs.ExitState();

handler.toStatus=_eventArgs.EnterState();

handler.Run();

}

Howitworks...

Whenthestatemachinegeneratedtheclasses,itaddedadelegatethatiscalledwheneverthestatechanges.Thisdelegateiscalledbeforethechangesarecommitted.Thetableispassedbyreference,whichmeansthatwecanrevertthestatusbackwithoutcallingupdate.Ifwedidcallupdate,wecouldcauseconcurrencyissueswithinthestandardcode.

There'smore...

Whenworkingwithahandlerclass,alsobecarefulwithtransactionstate.Wecouldupdatedatainatable,forinstance,amanuallycraftedstatushistorytable.Wecannicelyhandleanypotentialexceptionwithatry...catchstatementwithinourhandlerclass,butwecan'tcontrolwhathappenswhencontrolreturnsbacktothestatemachine.Forexample,ifweupdateahistorytable,butthecodefailslateron,wecouldendupwithanon-durabletransactionifthecodehandlestheexceptionandcontinuestocommitthetransaction.

Usingmenuitemstocontrolastatemachine

Inthissection,wewillactuallyaddthestatemachinetotheform,sowecanuseit.Usingmenuitemsforthisisaniceconcisewaytocontrolthestatemachine,andfollowstheUIpatternsfoundinotherareas,suchastheprojectsmodule.

GettingreadyTheprerequisiteforthisrecipeisthatwehaveatablewithastatemachinethathasbeengenerated.

Howtodoit...Tocreatethestatemachinemenuitems,followthesesteps:

1. AddanewactionmenuitemtotheprojectnamedConWHSVehInspStatusWaiting.Completethepropertysheetasfollows,intheorderstatedinthefollowingtable:

Property Value

StateMachineDataSource ConWHSVehicleTable

StateMachineTransitionTo Waiting

Label ThelabelyouuseintheWaitingstate'sLabelproperty

HelpText ThelabelyouuseintheWaitingstate'sDescriptionproperty

NeedsRecord Yes

2. Createthemenuitemsfortheremainingstates(InProgressandComplete)followingthesamepattern.3. OpentheConWHSVehicleTableforminthedesigner.4. Undertheform'sDesignnode,expandtheActionPaneHomecontrol,andthenActionPaneActionButtonGroup.

Right-clickonthiscontrolandchooseNew|MenuButton.5. RenamethenewcontrolInspStatusMenuButton.SettheTextandHelpTextpropertiestothesameaswe

usedonthestatemachine,forexample,@ConWHS:InspectionStatusand@ConWHS:InspectionStatusHTrespectively.

6. Then,dragthethreemenuitemsontothismenubutton.SettheDataSourcepropertytoConWHSVehicleTable--thetablethatthestatemachineoperateson.

7. Ifyoucan'taddthemdirectly,dragthemfirstontotheActionPaneActionButtonGroupbuttongroup,andthendragthemfromtheretothecorrectplace.

8. Saveandcloseallcodeeditorsanddesignwindowsandbuildtheproject.

Howitworks...Whenwecreatedthemenuitems,thesystemdefaultedmanypropertiesforus.Ifthetableonlyhasonestatemachine,allwehadtodowassetthelabelproperties.Youmaynoticethatitchangesthemenuitem'spropertiessothatitreferencedthestatemachineclassthatwasgeneratedbythetable'sstatemachine.

Whenwetestthebuttons,youcanseethatifwechooseatransitionthatisnotvalid,wegetthiserror:

Wecan'tchangethismessage,asitiscontrolledbyaprotectedmethod,andweshouldn'teditthegeneratedclasses,asthecodechangeswillbelostshouldthestatemachineberegenerated.Thisisalittleodd,asthegeneratedcodedoesgathertheuserfriendlylabelsweadded.

HookingupthestatemachinetoaworkflowInthisrecipe,wewillhookupourstatemachinetotheConWHSVehWFInspworkflowtask.

GettingreadyWeneedtohaveaworkflowtaskandhavecompletedtherecipesinthischapter.

Howtodoit...

Tohookupthestatemachinetoaworkflowtask,followthesesteps:

1. OpentheConWHSVehWFInspworkflowtask.2. SettheCanceledStateMachinepropertytoInspStateMachine.

Yes,thisisspelledCanceled,butitismorethancompensatedbythecleverwayithasdeterminedthelistofvalidstatemachines.

3. SettheCanceledStateMachineTargetStatepropertytoWaiting;wewon'tbeallowedtouseNotInspected.Duetothisbeingthestatemachine'sinitialstate,itwillletyousetthisvalue,butthestatemachinewillrejectthechange.

4. SettheStartedStateMachinepropertytoInspStateMachine,andtheStartedStateMachineTargetStatepropertytoWaiting.

YouwillalsoneedtoallowtheWaitingstatetotransitiondirectlytoComplete.Don'tforgettoclickonGeneratetoregeneratethestatemachineclass.

5. SelecttheCompletedoutcome,settheStateMachinepropertytoInspStateMachine,andthesetStateMachineTargetStatepropertytoComplete.

6. Sincewenowhavetwowaystosetthestatus,wewill(asashorttermfix)disablethestatusupdatecodethatsetthestatusintheSetStatusmethodoftheConWHSVehInspStatusHandlerclass.OpenthisclassandremovethelinethatsettheInspStatusfield.

WewillupdatethiscodeproperlyintheThere'smore...section.

7. Upontestingthis,wewillfindthatthetaskcompletedsetsthecommentcorrectly,butthestatusdoesn'tchangetocompleted.Thereasonisbecausetheworkfloweventsfirelast,sothestatemachinevalidationrejectedtheupdatebecausethecommentwasnotyetset.Weneedtoupdateourvalidationlogicsothatitonlyrunswhentriggeredfromtheform.AltertheValidatemethodoftheConWHSVehInspStatusHandlerclasssothatitreadsasfollows:

publicbooleanValidate()

{

switch(toStatus)

{

caseConWHSVehInspStatus::Complete:

if(vehicle.InspComment==''

&&FormDataUtil::isFormDataSource(vehicle))

{

//Thefield%1mustbefilledin"

DictFieldfield=newDictField(

tableNum(ConWHSVehicleTable),

fieldNum(ConWHSVehicleTable,

InspComment));

returncheckFailed(strFmt(

"@SYS110217",

field.label()));

}

break;

}

returntrue;

}

Thehighlightedcodechecksiftherecordbufferisaformdatasource;ifthecodewascalledfromwithinworkflow,thetablewillnotbelinkedtoaform'sdatasource.

8. Buildandtesttheworkflow;allshouldworkcorrectly.

Howitworks...

Thechangewehavemadewastosimplytietheeventsontheworkflowtasktoastateofthestatemachine.Thismeansthattheeventhandlermethodswenormallywriteshouldnotupdatethestatus,buttheycanperformactionsthatshouldhappenwhentheeventhappens.

Thestatemachineiscalledbytheworkflowengine,justbeforetheeventsarecalled.Thisiswhywehadtoremovethevalidationonthecomment--thestateischangedbeforethecompletedeventwascalled,whichmeansthatthecommentwasempty.Thereisn'tmuchwecandointhiscasebuttoallowtheworkflowtocontinue.Wecouldusetheworkflowdesignertocheckforthiseventandresubmitthetasktotheuser.

There'smore...Thisseemstogreatlysimplifytheworkflowdevelopment.Wedon'tneedalloftheeventhandlermethods,sincemostofthemonlyupdatetherecord'sstatus.Thereissomethoughtrequired,ifweconsiderthefollowingscenario.

Theworkflowiscancelledbytheuser,whichmeansthatthestatuswillgobacktoWaiting.WechoseWaiting,forwhentheworkflowtaskwascancelled,becausethestatemachinewillthrowanerrorifwetrytosetittotheinitialstate.Theproblemisthatwecan'tchangethestatustothesamestatus;wewillstillgetanerror.Theerrorisnotjustamessagetotheuser;itwillplacetheworkflowinafailedstate,whichwillrequireanadministratortocancel,resubmit,orresumeit.

Theproblemisthatweshouldnothaveascenariowhereauseractioncancauseafailurethatrequiresanadministratortorescuethem;weneedtohandlethiseventualityelegantlywithinourcode.

ThefirstthingwecoulddoisaddaninternalstatetothestatusBaseEnumandtothestatemachine,forexample,"Internalprocessing".Wewouldnotcreateamenuitemforthisasitisonlyforinternaluse.Onthestatemachine,wewouldallowanytransitionfromthisstate;itcantransitionfreelyfromandtothisstate.

ThisisthestateweusefortheCancelledevent.ThismeansthattheworkflowcansetthestatustoWaitingaftertheworkflowwascanceled.

ThenextpartofthechangeswewouldmakewouldbetoremoveallcallstoConWHSVehiInspStatusHandler::SetStatus(...).WewouldwriteanewmethodcalledSetComment,whichiscalledfromtheCompletedeventontheworkflowtaskeventhandlerclass.