Академический Документы
Профессиональный Документы
Культура Документы
Page
495
Automation
The main purpose of Automation is to provide a seamless interface to outside applications without user intervention. An example would be to automatically create a letter in Microsoft Word whenever certain conditions occurred, such as a welcome letter when the customer record is created. A second reason for using Automation is to save development time, which also reduces the cost of development. In order to use automation, the program that you want to automate must be an automation server. Examples of automation servers include Microsoft Word, Microsoft Excel, and Microsoft Outlook. The following description outlines the procedures for using an automation server from C/SIDE. There are very few steps required that are specific to C/SIDE (C/AL). Using an automation server consists of five steps: 1. Declare the top-level interface (class) of the automation server as a variable of type Automation. 2. Declare all the other interfaces (classes) as variables of type Automation. 3. Use the C/AL function CREATE on the variable declared in step 1. Do not use CREATE on any other variables. 4. Use the methods and properties of the automation server in your C/AL code. You can CLEAR (destroy) the top-level object if you want. Otherwise, it is destroyed automatically when the variable goes out of scope. Write most of your code during step 4 using the methods and the properties of the automation server. The syntax and the semantics of these methods and properties are documented in the documentation for each automation server. Using these methods and properties in C/AL does not involve any new or changed syntax. There are two major concerns when deciding where to place code that uses automation. The first is the fact that an object that uses automation can be compiled only if the automation server is installed on the machine where the compilation takes place. This means that if an object is to be recompiled and modified on a machine where the automation server is not installed, you have to modify the code drastically in order to compile it again. Therefore, it is recommended that you isolate code that uses automation in separate code units.
Page
496
Page
497
Give the variable a name (Excel), and select Automation as the DataType. In the Subtype field, click the AssistButton to open the following window:
Page
498
After selecting the Automation Server for the Microsoft Excel Object Library, you would then select a class (choose Application) as shown below:
Once you have declared the variable, you can access the Methods and Properties that exist for the control from C/AL. You can explore these methods and properties in the C/AL Symbol Menu.
Page
499
2. When you click OK or Apply in the C/AL Symbol Menu, a "template" statement with the method or property is pasted into the C/AL Editor (provided that the editor is open) at the current insertion point:
As you can see from the picture above, checking the Paste Arguments does make a difference in what's returned to the editor. When the Paste Arguments is checked, you get the method or property returned along with the arguments that are expected. Otherwise, only the property or method is returned.
Page
500
Declaring Variables
The next series of steps involve defining the necessary variables: 4. On the menu bar, click VIEWC/AL GLOBALS. 5. Define a Record variable that has the Customer Amount table as Subtype. Here, we have called it CustAmount. On the properties set the Temporary property to Yes. 6. Define an Automation variable with Microsoft Excel 11.0 Object Library as Automation Server and Application as Automation Object. Call it Excel. 7. Define an Automation variable with Microsoft Excel 11.0 Object Library as Automation Server and Workbook as Automation Object. Call it Book 8. Define an Automation variable with Microsoft Excel 11.0 Object Library as Automation Server and Range as Automation Object. Call it Range. 9. Define an Automation variable with Microsoft Excel 11.0 Object Library as Automation Server and Worksheet as Automation Object. Call it Sheet.
Page
501
Page
502
Changing Properties
In Object Designer, Select Report 123456755 and Design it. Open the Properties window and change ProcessingOnly to "Yes" as shown on the screen below.
Page
503
We now create Microsoft Excel. Note that CREATE has an optional argument, NewServer, which by default is FALSE. This means that an already running instance of the automation is reused. If we had set NewServer to TRUE, as in CREATE(wdApp, TRUE), we would have requested a new instance of Microsoft Excel. Note that ultimately the automation server itself can control whether it can be reused or not (see the documentation for the server in question if this aspect is important for your application.)
Add this code to the OnPreDataItem trigger: CREATE(Excel);
Next, make Microsoft Excel visible. Add this to the OnPreDataItem trigger:
Excel.Visible(TRUE);
Microsoft Excel produces a General Protection Fault error when you close a new Excel worksheet created while Microsoft Excel is invisible. To solve the problem, you can make Microsoft Excel visible immediately after you create a new worksheet. Alternatively, you can make Microsoft Excel visible just before you create a new Excel worksheet and then make it invisible again immediately after creating the new Excel worksheet. Next, we add a new workbook to Microsoft Excel. Add the following to the OnPreDataItem trigger:
Book:=Excel.Workbooks.Add(-4167); Sheet:=Excel.ActiveSheet; Sheet.Name := 'TOP 10';
Page
504
In the "Book :=" line, we use the Add method of the Workbooks collection to return a new workbook. Then we use the ActiveSheet property of the Application class to make sure that what we do next affects the active sheet of the new workbook. In the third line, we give the sheet a name. Note that we are passing the number -4167. We now need to add code to the OnAfterGetRecord trigger. Again, select DataItem "Customer", and press F9. On the OnAfterGetRecord trigger we need to first call the CALCFIELDS function to get the values for "Sales (LCY)" and "Balance (LCY)". Add the following to the OnAfterGetRecord trigger:
CALCFIELDS("Sales (LCY)","Balance (LCY)");
If both fields equal zero then skip this record. Otherwise, initialize the CustAmount record, fill the record with the Customer No. and then assign the amounts based on what ShowType the user has selected. Please note that we take the negative value of what is stored so that our graph displays upright.
The following should be added to the OnAfterGetRecord trigger: IF ("Sales (LCY)" = 0) AND ("Balance (LCY)" = 0) THEN CurrReport.SKIP; CustAmount.INIT; CustAmount."Customer No." := "No."; IF ShowType = ShowType::"Sales (LCY)" THEN BEGIN CustAmount."Amount (LCY)" := -"Sales (LCY)"; CustAmount."Amount 2 (LCY)" := -"Balance (LCY)"; END ELSE BEGIN CustAmount."Amount (LCY)" := -"Balance (LCY)"; CustAmount."Amount 2 (LCY)" := -"Sales (LCY)"; END;
Page
505
Page
506
Next, determine if there are any records in the CustAmount table. If so, then set the variable j to '3'. You then loop through all the records in the CustAmount table. As you loop through, you try and find that record in the Customer table. First, SETRANGE on the "No." equal to the CustAmount."Customer No.". If you aren't able to find the record, an error message appears and this report ends. However, since you had just added the records, you shouldn't encounter the error. Finally, add the values (Customer No., Customer Name, and Amount (LCY) to the Sheet that we have created. This also goes in the OnPostDataItem trigger.
IF CustAmount.FIND('-') THEN BEGIN j:='3'; REPEAT Customer.SETRANGE("No.",CustAmount."Customer No."); Customer.FIND('-'); Sheet.Range('A'+j).Value := CustAmount."Customer No."; Sheet.Range('B'+j).Value := Customer.Name; Sheet.Range('C'+j).Value := -CustAmount."Amount (LCY)"; j:=INCSTR(j); UNTIL CustAmount.NEXT = 0; END;
Now you are ready to chart the information inserted into Excel. Set a range based on the columns and rows that we utilized. Next, add a new chart and call the ChartWizard to plot the information for us. Using the ChartWizard provides a fast and simple way of accomplishing our goal. You can more tightly control the design of the graph by setting it up using the methods and properties of the various Chart objects (ChartArea, Legend, and so on). First, define a range for the data for the graph. The following should be placed in the OnPostDataItem trigger of the Customer DataItem:
Range:=Sheet.Range('B3:C12');
Then, add a new chart sheet and give it a name (also in the OnPostDataItem trigger):
Chart:=Book.Charts.Add; Chart.Name := 'Top 10 Customers - Graph';
Page
507
Use the first eight of the optional arguments of the ChartWizard method: Argument Source Description The range that contains the source data for the new chart The chart type Value xlRange the object returned by xlSheet.Range('A2:C3'). -4100 the enumerator for the Stacked Column with 3D effect. See "Finding an enumerator value" in the chapter "Using COM Technologies in C/SIDE" of the application designers' guide. We found this one by trial and error, because the Microsoft Excel documentation does not say much about it. 2 the enumerator for the xlRows XlRowCol enumerator.
Gallery
Format
PlotBy
Page
508
Argument CategoryLabels
Description An integer specifying the number of rows or columns within the source range that contain category labels. An integer specifying the number of rows or columns within the source range that contain series labels TRUE to include a legend
SeriesLabels
HasLegend
0 for some reason this value works well. There is a possible error in either Microsoft Excel or the documentation here. We pass a string, 'Top 10 Customers' This works well and the run-time conversion will succeed. 'Customer' 'Sales (LCY)'
Title
VARIANT Value (X) Axis Title VARIANT Category (Z) Axis Title
Close and save the report. Locate the report in the Object Designer and press the Run button.
Page
509
Page
510
Responding to Events
C/SIDE can receive events from the components (automation servers and OCXs) that it controls. When you declare a global variable of the type Automation, you can specify whether you want to receive events. You do so by setting the WithEvents property for the variable to Yes. This automatically generates AL triggers for the events that the component provides. A trigger name consists of the name of the automation variable followed by "::<Event name>." For example, if you declare an automation variable with the name MyEventVar, and the component provides the event MessageReceived(...), the name of the trigger is MyEventVar::MessageReceived(...). For more information about responding to events, please refer to the chapter Receiving Events in C/SIDE in the Application Designer's Guide.
NOTE: If you delete a global variable to which event triggers are associated, the event triggers and their code is also deleted. Furthermore, if you change the DataType or SubType for a global variable from an Automation type to another DataType, then all the event triggers and their code is deleted.
Limitations
A few limitations of using automation are: Exception Handling. C/SIDE does not allow the retrieval of information about exceptions from a control or automation server through the Invoke method of the IDispatch interface and the EXCEPINFO structure. There are ways that you can work around this limitation. C/SIDE only supports the default outgoing, that is, source interfaces, which are defined by the automation variable. If more than one outgoing interface is defined by the automation server for a coclass, only event triggers for the default outgoing interface are generated in the AL code.
Page
511
Page
512
Automation Exercise
Create a codeunit that pushes out G/L Account information to Microsoft Excel and creates a simple chart. The customer has specified that they only wish to see a summary of the income accounts. 1. Create a codeunit. 2. Create the following variables as shown in the picture below:
The rest of the code is on the OnRun trigger. 3. First we need a filter on the income accounts. Next we need to look only at the Account Types with the option of End-Total because they contain the balances.
WITH GLAccount DO BEGIN // We only want to look at look at the Income Accounts SETRANGE("No.", '6000', '6995'); //We want to only look at the totals for each category SETRANGE("Account Type", GLAccount."Account Type"::"EndTotal");
Page
513
5. We want to assign our column names and prepare to copy the account information to the proper row. Since we do not know exactly how many rows that we end up with, we need to initialize our counter. Next, as we are writing out our account information, we count the number of rows that we write out so that we can determine the range of cells to pass to the chart wizard. Note that we want to switch the sign on our balance since it is stored as a negative number within Microsoft Navision.
Sheet.Range('A1').Value := 'No.'; Sheet.Range('B1').Value := 'Name'; Sheet.Range('C1').Value := 'Balance'; RowNo :='3'; TotalRows := 0; REPEAT CALCFIELDS(Balance); Sheet.Range('A'+RowNo).Value := GLAccount."No."; Sheet.Range('B'+RowNo).Value := GLAccount.Name; Sheet.Range('C'+RowNo).Value := -GLAccount.Balance; RowNo :=INCSTR(RowNo); TotalRows := TotalRows + 1; UNTIL NEXT = 0; END; END;
Page
514
8. Run the codeunit. The Excel file and graph should appear as follows:
Page
515
Page
516
3. True or False. You must first use the CREATE command before you can use an Automation object?
4. True or False. You would use the FREE command to release the automation object from memory.
Page
517
2.
3.
Page
518