community.dynamics.com · About the Author Simon Buxton has worked with Dynamics 365 for Operations...
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,[email protected],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,[email protected],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. [email protected]. 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/[email protected],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.