Welcome to MSDN Blogs Sign in | Join | Help

Issues concerning X++

This blog deals with issues about X++
Step-by-Step Checklist for Debugging Batch Jobs in Dynamics Ax

This post applies to Microsoft Dynamics Ax 2009 (with hot fix).

 

A. Enable Global Breakpoints

 

            When debugging in the client, you want to set breakpoints that can be caught by the debugger.  The following steps show how.

 

1.      Launch the Microsoft Dynamics AX Configuration tool from the Start > Administrative Tools menu on your client computer. The first time this tool opens, you will be on the Original configuration. Create a new configuration named Debug. Click Manage and choose the Create Configuration menu option.

 

2.      Enter a new name such as Debug, and choose copy from Original configuration and click Ok.

 

3.      A new configuration is created and shown in the configuration tool.  Click the Developer tab.

 

4.      Choose the following options:

·         “Enable global breakpoints to debug code running in the Business Connector or client” (required for this scenario)

·         “Enable user breakpoints to debug code in the Business Connector” (optional for this scenario)

 

Note: Take special care that the Configuration Target remains set to Local Client since this is the only client you can set global breakpoints from.

 

 

B. Configure Your AOS to Run Under Your User Credentials

 

1.      Launch the Services utility on your AOS server machine, and choose the AOS instance that you are configuring for debugging.

 

2.      Right-click on your chosen AOS instance name and choose Properties. On the properties window, choose the Logon tab. Select “This account” for the Logon As option.  Enter your full Domain account {e.g. contoso\YourName } and password credentials.  This account must be the same user account as the account used for debugging batch code.

 

3.      Click Apply once you have modified the user account.

 

4.      You will be prompted to restart the service. Choose yes to restart the service (note: ensure other users are not working on this service before restarting it).

 

 

C. Configure the Dynamics AX Server to Enable Debugging

 

1.      From your Start > Administrative Tools menu, open the Microsoft Dynamics AX Server Configuration tool. This tool has the same option to Manage configurations that you saw earlier in the client configuration tool.

 

2.      Choose the Manage and Create Configuration option to create a new server configuration. As before, copy from the original configuration and choose Debug as the name.

 

3.      Verify the AOS instance matches the instance you want to use for debugging.

 

4.      Choose the following options:

·         “Enable breakpoints to debug X++ code running on this server” (required for this scenario)

·         “Enable global breakpoints to debug X++ code running in batch jobs” (required for this scenario)

 

Note: When you click Apply, you will be prompted to restart the service, and you should choose Yes.

 

 

D. Set breakpoints and start debugging.

 

1.      Launch the Dynamics AX Client from the Start menu, and open the developer workspace.

 

2.      Open the class you would like to debug from the AOT and set breakpoints at desired locations.

 

3.      Launch the Dynamics Ax Debugger from the Start menu. The debugger must be opened directly in order to find and catch global breakpoints.

 

4.      Schedule the batch job to run. 

 

Example: To schedule work calendar delete to run in batch; launch the WorkCalendarDelete class in the AOT, click Batch tab, check Batch Processing check box and click Ok.

 

5.      Wait for the debugger to catch the breakpoints.

 

 

E. Version information.

Batch debugging on Windows Server 2008 using Dynamics Ax 2009 requires a hot fix.  This issue has been fixed in Dynamics Ax 2011.

Serializing Axapta Foundation Class instances as XML streams

The Axapta foundation  classes are a set of generally applicable classes that contain values of either simple types or other classes. They are:

  • Structs, structures of named fields,
  • Arrays, arrays of any type, not just simole type
  • Lists, lists allowing traversal with enumerators
  • Maps, dictionaries allowing lookup of values, yielding a result value
  • Sets, ordered groups where duplicates cannot occur.

The classes are generally well understood, so this is not going to be a tutorial on how to use them. This blog will deal with the infrastructure provided to allow serializing and deserializing values to XML streams, that can then be processed (searched with XPath queries, persisted in files or databases etc) as any other XML document.

 

In the example below we will be describing how this works for structs, but the same principles apply to the other foundation classes as well.

The struct class has a method that yields an XML representation of its content. Not suprisingly, this method is called Xml(). As you can imagine, this method can easily deal with instances of simple types, but structs are not limited to containing simple types: They can contain instances of any type defined by the user. Ideally we would like these classes to be able to participate in the serialization and subsequent deserialization. As it happens this is exactly what the infrastructure provides, with a little help from the developer.

 

When the xml method is called on the struct, the implementation will call xml (int indent) on any objects in the struct. It is then up to this implementation to serialize the state of the object into XML. Let's consider an example where a struct contains a user defined Point class, with the obvious set of x,y values. The class, listed below contains an XML method that returns a string containing the XML format of the state of the object. The indent parameter indicates the indentation level; padding with spaces as done in the example below is not really needed: There are no semantics for white space in XML, but it makes the XML document look good in the eyes of carbon based lifeforms.

 

class Point
{
    int x;
    int y;

    public void new(int _x, int _y) 
    {
        x = _x; y = _y;
    }

    public str ToString()
    {
        return strfmt("Point(%1,%2)", x,y);
    }

    public str xml(int indent=0)
    {
        return strrep(" ", indent) + strfmt("<Point x='%1' y='%2' />", x,y);
    }
}

 

If you use string operations like above, you should be careful to not include the XML metacharacters <,>,&,'," in the values, You should use the &lt;, &gt; &amp; &apos; and &quot; placeholders instead. You can use .NET to build the XML stream for you (it will escape the characters for you) or place content inside [CDATA[]] tags. For the example above this would clearly be overkill.

 

With this in place, you can serialize the content of a struct into an XML stream and work with it as an XML document:

 

static void structserializetest(Args _args)
{
    struct s = new Struct ("str name; int age; Point p");
    XmlDocument d = new XmlDocument();
    str xml;

    s.value("Name", "John Doe");
    s.value("Age", 42);
    s.value("Point", new Point(43, 77));
 

    xml = s.xml();

    // Load the XML we just serialized into an XML DOM

    d.loadXml(xml);

}

 

But that's not all. If you add a

 

static public CreateFromXml(XmlNode n)
{
     return new Point(
        str2int(n.attributes().getNamedItem("x").value()),
        str2int(n.attributes().getNamedItem("y").value()));
}

 

method to the class you will be able to create the point instance from deserializing the XML you just created:

 

// … continued from example above…

// Load the XML wejust serialized into an XML DOM

d.loadXml(xml);

 

copy = struct::createFromXML(d.documentElement()); // Serialize

 

// Show the results. Should match the original value.

print copy.definitionString();

print copy.toString();

 

 

 

 

Is the X++ Compiler Too Flexible?

In Microsoft Dynamics AX 2009, the X++ compiler is sometimes too flexible in its rules for code. It is likely at some of these flexibilities will be eliminated in future releases.

This blog entry describes some flexibilities of the X++ compiler that we recommend you not utilize.

 

 

[1] Sequence of Default Parameters

 

Recommendation: In a method declaration, declare parameters that have default values after the last parameter that has no default.

 

The X++ compiler and runtime currently allows you to disregard this recommendation, as the following code example demonstrates.

 

 

class MyClass

{

  static public int AddTwoNumbers

      (int _firstNum = 4

      ,int _secondNum)

  {

    return (_firstNum + _secondNum);

  }

}

 

 

  The following job calls the above AddTwoNumbers method. Notice there is no way to accept the default value of the _firstNum parameter.

 

 

static void Job1(Args _args)

{

  int iAnswer;

  iAnswer = MyClass::AddTwoNumbers(8 ,16);

  print( IAnswer );

  pause;

}

 

 

 

[2] Access Modifier on Methods

 

Recommendation: Each time you create a new method on a class, explicitly add an access modifier to the method declaration, meaning public, protected, or private.

If no access modifier is given, the default behavior is public.

 

 

 

[3] No Access Modifier on Classes

 

Recommendation: Do not add an access modifier to any class declaration.

The X++ compiler ignores the keywords public and protected and private on classes, so adding an access modifier can only confuse other programmers. In effect, all X++ classes are public.

 

 

 

[4] Put TODO First

 

Recommendation: The TODO should be the first non-whitespace after the start of the comment.

 

When the X++ compiler detects the string TODO in a comment, it lists a task on the Tasks tab of the compiler output window.  The compiler goes a little too far in trying to detect a TODO.  For instance, the TODO is detected in each of these two examples:

 

 

// Remember TODO Remove the diagnostic prints.

 

 

/* Important TODO:

Rewrite this SQL as a more efficient set operation.

*/

 

 

The two above examples would be fine if the first words were removed: it would be better to remove the first words Remember and Important.

Using the Cross company feature from the Business Connector.

In Ax 2009 the new cross company feature was introduced. It allows the programmer to specify a container containing strings denoting company names to the crosscompany hint:

container c = ['dat', 'dmo'];
select crosscompany: c * from custtable where custtable.Name == "Jones";

That is all very well in X++, of course, but how do you handle it when accessing data through the business connector?

The first approach would be to do something like the following:

AxaptaRecord r = axapta.CreateAxaptaRecord("CustTable"); 
ax.ExecuteStmt("select crosscompany: ['dmo', 'dat'] * from %1 where %1.Name == 'Jones'", r);

However, that will not work: The argument to the crosscompany hint is not an expression of type container, it is a variable of type container. Hence you cannot provide arbitrary expressions, only variable references. This was done to make the implementation of the feature simpler.

The solution lies in the realization that the string that enters the ExecuteStmt is actually entered into a function that is compiled and executed at runtime. There are no limits to what can be expressed in this string, as long as it is legal X++ statements.

So, consider doing:

AxaptaRecord r = axapta.CreateAxaptaRecord("CustTable"); 
ax.ExecuteStmt("container c = ['dmo', 'dat']; select crosscompany: c * from %1 where %1.Name == 'Jones'", r);

which would actually work.

 

 

 

The Enterprise Portal team introduces new blog...

The EP team has just created a blog about their stuff. These guys know their stuff, and are heavy users of managed code interoperability with X++. Check it out at http://blogs.msdn.com/epblog/.

 

Using graphics in forms and legacy reports.

Let's face it - The forms and reports in Dynamics Ax are pretty bland. They're very good at presenting information grouped in logical ways, but the information presented is invariably in the form of numbers and strings. The further you get up into the decision hierarchy the less the decision makers are concerned about details: Instead they are concerned about trends and ratios and key performance indicators that are easily consumed, even on the golf course.

 

So, if the old saying about a picture saying a thousand words is true, maybe it’s about time we put some pizzazz into forms and reports. The following is some work that I did for the recent Convergence 2008 conference in Orlando.

 

Basically the effort was about showing yet another area where managed code can be leveraged from X++. I chose to present rich information in charts both in forms and reports. In the forms case I chose a web service to provide the data to be shown in the form because it was consistent with the message that Steve Ballmer was presenting in his keynote at Convergence. The report graphics however were shown in a modified existing legacy report. Note, that the new reporting feature that we have added in Ax 2009 can do much more than what we are doing here, completely without any of the messy custom code needed in this approach. The approach does have merit though, because it allows an ISV to create some value for customers running on older versions of the product. If the target installation is really old (and does not support managed code from X++), then COM controls must be used instead. I’ll not go into detail about this in this blog about that approach though.

 

The examples here are implemented using a managed graphics library called Xceed (http://xceed.com/Chart_WinForms_Intro.html). This was the library at hand at the time: Its use does not imply an endorsement of this particular (or any other product). There are other high quality graphics rendering libraries in existence, some of them free, some of them not.

 

I will start by presenting the approach chosen for reports. The gist of it is: Collect the information to present in a pie chart while the report is being built. Then, use the graphics API to create a bitmap file. Bind the bitmap file to a bitmap control on the form. That’s all it takes.

 

On my machine, the Customer turnover report now looks like this:

The main changes are in the fetch method. You will find that two additional data structures are maintained: One (called Names) is simply a list of the account numbers to include in the pie chart; the other (called revenues) is a list of their revenues. That is all the information needed to actually generate the bitmaps (which is done in the calculateBitmap method on the report). Please refer to the xpo file below for further details.

 

I wanted to add some graphics to forms as well, but I did know of any place where it would make sense in the current application. I’m afraid that that is a testament of how little I know the application more than anything else. To drive home the point made by Steve Ballmer (about computing as services etc) I decided to use a web service to fetch information and display that in the form. The web service I chose is a demographics service from www.cdyne.com. Basically the service allows you to enter an address and get lots of interesting information about the neighborhood: The mean income and house value, the racial mix, mean age of inhabitants etc. I added the pie charts etc to the customer form to show all this information. You can see a screenshot below:

 

 

Note: The data shown in here is for the purpose of technology demonstration only. It may be illegal to use information about gender or race to target potential customers. Also, the data in the Dynamics Ax demo data was designed to look like real addresses, but for legal reasons they are not quite accurate. I put in some sample known addresses for the demo. The web service only covers the US.

 

You will find the code that draws the bitmaps in the file below:

The Xceed library is a commercial product that requires a license key. It can be obtained by buying their product, or by getting a 45 day demo license. Go to the xceed site (http://xceed.com/US_Suite_Intro.html) for details. The demographics service is also a pay service, but they have a demo license that can be used for this sort of thing.

You will need to add references to the xceed libraries in the references node in Dynamics Ax for the compiler to know about them. The libraries to add are:

xceed.chart
xceed.chart.core
xceed.chart.Datamanipulation
xceed.chart.Graphics2D
xceed.chart.GraphicsCore
xceed.chart.Standard
xceed.chart.UIControls
xceed.chart.Utilities

Please find the source code for the updated customer turnover report here:

The Custtable form with the demographics tab is found here:

 

Caveat: Using the += and -= operators for dates.

As you may know, it is possible in X++ to add integers to dates. The semantics are that the integer value is considered a number of days to add or subtract to the date.

{
   Date d;
   // ...
   d = d + 7; // Seven days later
   // ...
   d = d - 7; // Seven days prior.
}

The example above works well. However, the programmer may see the code above and decide that it can be expressed more succinctly by using the composite assignment operators +=  and -=. In the case above there is no gain by using these, but in cases where the lefthand side is very complex (involving lots of method calls with side effects) a case can be made for the argument that it should be replaced with += and -=.

The obvious thing to do then is:

{
   Date d;
   // ...
   d += 7; // Seven days later
   // ...
   d -= 7; // Seven days prior.
}

This too will work as expected. The problem occurs if you try to use the += operator to subtract days or the -= operator to add days; that is, supplying a negative number of days to add or subtract.

{
   Date d;
   // ...
   d += 7; // Seven days later
   // ...
   d += -7; // ERROR Seven days prior.
}

The problem is based in the fact that the compiler will generate code to do a type conversion from the integer argument to a date, which will succeed for positive values, but not for negative ones. It is not easy to fix, but in due course we will, either by disallowing the composite assignment operators on dates or by throwing an exception when the argument is negative.

To make matters even worse, the kernel will simply show a dialog box but continue execution with wrong data afterwards.

The learning from all this is that if you use these operators for dates, you must make sure the expressions on the right hand side always evaluate to non-negative values. If you cannot guarantee this, use the d = d + expr syntax instead.

 

 

 

Missing values: getting the System.Missing.Value value

When dealing with office integration scenarios and when calling APIs that are based on old COM interfaces, there  is a need to specify that a value is not provided. The notmal way to do this (say, in C#) is to use

System.Reflection.Missing.Value

Now, as it happens, Value is not a static property, as one might expect, but instead a public static field. It is currently not possible in X++ to reference the value of a field.

There are at least two ways to solve the problem:

  1. You can use a managed method to get the value. This has the disadvantage that you need to put this glue code in an assembly that is deployed to your Ax installation. This may or may not be a problem, depending on your situation, but it is not exactly elegant.
  2. Or, you can use reflection from X++ to get the value, as shown here:
   System.Type type = System.Type::GetType("System.Reflection.Missing");
   System.Reflection.FieldInfo info = type.GetField("Value");
   System.Object value = info.GetValue(null);

This has a performance penalty involved, but that can rpobably be ignored in most cases.

Macros - Definitions and Pitfalls

The X++ language features a macro expansion facility. With it, you can define macros, use macro values, do conditional compilation etc. In this blog I'll describe the semantics of the constructs and provide some guidance to resolve some of the problems beginners and experts alike are having with this language feature.

Macros are unstructured in that they are not defined by the grammar of the language. The handling of macros takes place before the text reaches the compiler.

Macros may appear inside methods and class declarations anywhere that white space is permitted, and may also appear after the ending } in class definitions.

The semantics of each of the macro keywords are described below:

Macro constructs

#define

The syntax is

#define.MyName(SomeValue)

This defines a macro called MyName with the value SomeValue. When this definition is in effect, any references to #MyName will be replaced with the character sequence SomeValue. The definition has no other semantics aside from defining the symbol MyName: The text does not reach the compiler itself. When the compilation of the current method is over, the symbol (MyName in this case) is no longer remembered. If the symbol is already defined, the old value is discarded and replaced by the new value.

#globaldefine

The syntax is

#globaldefine.MyName(SomeValue)

This has the same semantics as #define, described above.

#definc

The syntax is

#definc.MyName

This macro construct is used mainly when the value is a integer value. The preprocessor will increment the value of the symbol by one. If the value was not defined before the #definc occurs, an error is issued by the compiler. If the value before the #definc is not an integer, the old value will be overwritten with the value 0 and then incremented, yielding the value 1.

#defdec

The syntax is

#defdec.MyName

This macro construct is used mainly when the value is a integer value. The preprocessor will decrement the value of the symbol by one. If the value was not defined before the #defdec occurs an error is issued by the compiler. If the value before the #defdec is not an integer, the old value will be overwritten with the value 0 and then decremented, yielding the value -1.


#undef

The syntax is

#undef.MyName

The effect of this is to remove the symbol MyName from the list of current macro definitions. It is not considered an error to remove a symbol that has not previously been #defined.

#if ... #endif

The syntax is

#if.MySymbol
    …
#endif

Or

#if.MySymbol(SomeValue)
    …
#endif

In the first case the textual content marked with … in the examples above is inserted into the source stream if MySymbol has previously been defined. In the second case, the content is inserted into the source stream if and only if the symbol is defined and has the indicated value.

The #if constructs may be nested to any level but there is no #else construct.


#ifnot ... #endif

The syntax is

#ifnot.MySymbol
    …
#endif

Or

#ifnot.MySymbol(SomeValue)
    …
#endif

In the first case the textual content marked with … in the examples above is inserted into the source stream if MySymbol has not been defined. In the second case, the content is inserted into the source stream if the symbol is not defined or is defined but does not have the indicated value.

The #if constructs may be nested to any level. There is no #else construct.

#macrolib

The syntax is

#macrolib.MyName

The name must denote a node in the macros branch of the AOT. The text in that node is processed by the preprocessor. The net effect is to insert the content of the named macro in the source text where the directive appears. It is an error if the node is not found in the macros branch in the AOT.

#macro / #localmacro

The keywords #macro and #localmacro are interchangeable; there is no difference in the semantics of the two. This construct is used to define a symbol to denote textual content possibly spanning several lines.

The syntax is

#localmacro.MySymbol
    ….
#endmacro

Example:

class MyBaseClass extends Runbase
{
     int v1;
     #define.myMacro(“Hello world”)
     #localmacro.currentlist
        v1
     #endmacro

     public container pack()
     {
         return [#currentlist];  // #currentlist expands to v1
     }

     public void run()
     {
         print #myMacro;   // #myMacro expands to “Hello world”
     }
}

class MyDerivedClass extends myBaseClass
{
    int v2;
    #localmacro.currentlist
       v2
    #endmacro


    public container pack()
    {
        return [super(), #currentlist];  // #currentlist expands to v2
    }

    public void run()
    {
        print #myMacro;  // #myMacro expands to “Hello world”
    }
}

#MySymbol

The syntax is

#MySymbol

This inserts the value of the symbol into the source stream. It is an error to refer to a symbol that has not been defined.
If the name denotes a node in the macros branch of the AOT, the text in that node is processed by the preprocessor (in this case #MySymbol is a shorthand for #macrolib.MySymbol).

Common Problems

In this part of the blog post, I'd like to describe some of the problems programmers have when dealing with macros.

Macro parameters

It seems to be a little known fact and a source of some confusion that macros can be parametrized: Values can be given to the %<n> placeholders at the macro expansion site. Simple textual substitution of the positional parameter with the given, actual parameter then takes place. If no parameter is supplied, the empty string is used. So

#define.MyString("Hello World from %1")

will define a named macro (MyString) with one parameter. If that macro is expanded as shown below:

#MyString(X++)

the resulting string will be

"Hello World from X++"

Note that the place of expansion did not supply the letters X++ inside quotes. That would have generated a compiletime error:

#MyString("X++")

would have generated

"Hello World from "X++""

That is what is meant by simple string substitution.

The confusion can also occur because the % notation is also used for parameter substitution in the strFmt string formatting function. As you know, this function has a variable length parameter list, and each reference of %n in the first argument (a string) will be expanded to contain a textual representation of the n'th argument, as shown below:

print strfmt("The value is %1", theValue);

Now, some programmers have been known to want to specify the first argument, i.e. the string containing the substitutions, with a macro symbol:

#define.TheText("The value is %1")
print strfmt(#TheText, theValue);

But, the macro substition kicks in before the compiler sees the source code. The macro substition engive will not find a parameter to place where the %1 is, so the compiler will see:

print strfmt("The value is ", theValue);

which is probably not what the programmer intended.

Macros in class declarations

Some confusion stems from the situation where macros are defined in class declarations. In order to understand how this is handled it is useful to review what the compiler does when it compiles a method. It starts by calculating the sequence of class derivations that the class is part of. It then parses each of the class declarations with the least derived one first, filling its internal symbol table with the macros as it goes along. After compiling the most local class declaration (the most derived one) the compiler compiles the method itself. Any symbol defined in any of the class declarations will subsequently be available for use in the methods. Symbols defined in a class declaration may be replaced by values defined in more derived class declarations.

Parenthesis in macro strings

The scanner dealing with macro strings is quite simple minded. It will not handle the situation where closing parenthesis characters are included in the string. So,

#define.Another("(This is text in parenthesis)")

will generate a compiler lexical error. If you need to do this, you should use the #localmacro directive instead:

#localmacro.Another
    "(This is text in parenthesis)"
#endmacro

In this context the end of the macro is signalled by the #endmacro string, not a right parenthesis.

Entering sales orders over the phone...

At the convergence conference in Copenhagen I wanted a demo that could demonstrate another way of integrating with Ax: I went ahead and wrote a demo that leverages the Speech Server (2007) to accept spoken commands via the telephone to enter sales orders. As it happens this has some customer value apart from the purely technical demonstration: nearly everyone has access to a telephone, but not everyone has a smartphone or a phone that is capable of interfacing with the internet. Those that do often have user interfaces that are not rich enough for entering sales orders. It would be useful to allow the use of a phone, portable or otherwise, with a simple keypad and the spoken word to enter orders.

 

It turns out that there is a toolkit from MS that is very suitable for implementing the kind of application I was contemplating: the Microsoft Office Communications Server 2007 Speech Server SDK. In fact this framework is a large superset of what is actually needed for this application. In a production environment you would have a Office Communications Server 2007 that is hooked up to the incoming phone line by a piece of dedicated hardware (a Voice-over-IP/VoIP gateway). The speech application is then run in the Windows Workflow Foundation (WF) running on the Speech Server. The Speech Server (2007) Developer Edition (download from http://www.microsoft.com/downloads/details.aspx?FamilyId=BB183640-4B8F-4828-80C9-E83C3B2E7A2C&displaylang=en)  leverages the Windows Workflow Foundation in that it takes the form of a series of activities that are linked together in a dialog workflow that describe the phone conversation. The system features a rich drawing surface where these activities can be wired together and their properties set.

 

You may find more about the Speech Server by going into the MSDN Forums at http://forums.microsoft.com/unifiedcommunications/default.aspx?siteid=57. You will find people there who can address questions you may have, both technical and from a product perspective. Also, there is a lot of really solid information to be found on http://gotspeech.net/.

 

The workflow that was created for this demo is discussed below. Please bear in mind that the scenario may not be very realistic; it exists to present the underlying technology, not to solve a customer problem. The flow is as shown in workflow. The code for the demo can be found in http://cid-b2f15fddcf82cd4e.skydrive.live.com/self.aspx/Public/VoiceResponseOrdering2.zip.

 

As you can see, the system will start up by saying hello and welcome to the user phoning in (after setting up the Ax connection in the code activity). Then an activity will solicit the user’s company by uttering:

 

“Enter your customer I.D, either by using the phone keypad or by saying your customer number or by saying your company name.”

 

The user has some choices here: Either the user can use the phone’s keypad to press the customer number (like 4-0-0-1), the user can say the digits (“four”, “zero”, “zero”, “one”) or the number (“four thousand and one”) or the user can say the name of the company (“The Glass Bulb”). The functionality of the Question/Answer activity is such that the question soliciting the company will be repeated until an answer is provided. When a number is entered by any of the means discussed above, there is a validation that the number denotes a company in Ax. For convenience, the initialization phase of the workflow gets all the customers into an XML document (through the business connector); The validation described above consists of an XPath query over this document. In this way the chattiness between the workflow and Ax is reduced.  If the validation fails, the user is informed that the input did not designate a valid customer and he is asked for the ID again through the execution of the Goto activity.

 

At this time, the user has supplied a valid company. We now need to get the items that the user is ordering. This is done in a two phase process: The first step is to enter an item by uttering:

 

“Enter an order item. You may say either the item number or the item name.”

 

This is very similar to what was done above to get the customer id. In this case, however, the items ids do not lend themselves easily to entry through the keypad. The user may enter an item id, like “CN-01” or an item name like “Chrome Architect Lamp”. Once the item has been entered and validated (again by consulting an XML document that was fetched from Ax up front using the business connector), the user us asked for the quantity that he wants to order:

 

“How many items do you wish to order? Use the keypad or say the number.”

 

The user can say “Three” or use the 3 key on the phone to achieve the same result. At this point the user will be notified of the item and how many were ordered:

 

“You added three CN-01 to your cart”

 

The information about the items ordered are stored in a list ready for later use. At this point the process of identifying an item and how many restarts. To be able to exit this loop, there is the option of pressing the hash key (#) on the keypad or saying “stop” where an item is expected. When this is done, execution picks up at the code activity that adds the sales order to Ax, again using the business connector. The user is they told that the order will be shipped to the address that the system has on file for the customer.

 

The demo as presented to you here was written in a hurry, mainly in a hotel room with jetlag, as is the norm before conferences. There is one thing that I would have liked to invest more in that would have made the solution more suitable for production, but wasn’t a concern In a tightly controlled demo situation. This has to do with how the  grammars to recognize the user’s utterances are created. Currently the grammars are built from the contents of the Ax database (through the XML files that I alluded to above). While this does work in a demo, it is an inflexible solution because it only allows the user to say exactly what is expected as in:

 

“chrome architect lamp”.

 

What is needed is a conversational grammar so the user would be able to say:

 

“I’d like a chrome architect lamp”,

                                         

or

 

“Gimme a chrome architect lamp please”

 

The Speech Server SDK does provide for such grammars, but I did not have the time to create the conversational grammars that are needed for this, most notably time to figure out how to use the editor used for creating these grammars. In that tool you can define all sentences that a caller can use. I hope to learn that in a future project, so Speech Server can understand flexible spoken input based on content from a database with natural language sentences. For that tool seems really powerful!

 

 

Pen based form demo

This demo was written for Convergence 2007 to showcase how Dynamics Ax can leverage managed code. In this example it uses a managed API to allow form navigation using tablet gestures, that is small figures drawn with the pen on the tablet screen.

 

The technology shown here will work in 4.0 as well as in 5.0 when that version hits the streets.

 

The theory of operation of this is quite simple, because of the beautiful API that is provided for capturing gestures. This application is merely scratching the surface of what this API can do. The only thing needed for this to work is to set up an ink analyser to do the heavy lifting for you. For this example we choose to handle the following gestures:

 

Stroke Action
Left Previous record
Right Next record
< First record
> Next record
Check Close form
Scratch out Delete this record
Circle  Print this record

The complete list of available gestures can be found at http://msdn2.microsoft.com/en-us/library/ms827547.aspx

The trick for this is how to communicate the event that happens in the managed world to the unsuspecting X++ code. There is no direct way of subscribing to an event in the managed world in X++. The way I have done this may be useful in other scenarios as well, so I'll describe it in some detail:

The control on which the gestures are drawn is a Window control, which has no other semantics than publishing its window handle. As anyone who has done any windows programming back in the old unmanaged days will know, the windows handle is an identifier that is used to identify the window for all operations (drawing etc) that takes place in the window. The trick here is to pass this windows handle to the constructor of the class that recognizes the gestures. When a gesture is recognized, the proper event handler is called, and this event handler will simply post a custom message to the windows handle. When Ax is done with its own messages, this message will be handled. There is no default handler for this message (it is a custom message after all, not a predefined one), but because Uffe had a remarkable foresight 10 years ago, he added a way of subscribing to these custom events.This is done by calling the installMessageProc method on the form with the name of a method to call as a result of the given message being posted to the given handle. The init method on the form then does the following:

void init()
{
   int h;
   // ... 
   super();
   
   h = PenArea.hWnd(); // Get the windows handle from control.

   // Pass it on the the stroke handler so it can react by  
   // posting messages to this windows handle:
   strokeHandler = new Villadsen.StrokeHandler(h);

   // Make sure the method called CallbackMethod is called  
   // when the custom message with the id 0x7ffe is posted 
   // to the handle
   this.installMessageProc(0x7ffe, h, "Callbackmethod");

   // ...
}

The CallbackMethod then simply branches on the gesture that was recognized and does the appropriate thing. For legibility I have created a method for each action.

void CallbackMethod(int hwnd, int message, int wParam, int lParam, int px, int py)
{
   #InkGestures

//    print "Called back! Value is ", wparam, ",", lparam;

   switch (lparam)
   {
       case #Left:
           Element.DoPrevious();
           break;
       case #Right:
           Element.DoNext();
           break;
       case #ChevronLeft:
           Element.DoFirst();
           break;
       case #ChevronRight:
           Element.DoLast();
           break;
       case #Check:
           Element.close();
           break;
       case #ScratchOut:
           Element.DoDelete();
           break;
       case #Circle:
           // Circle has to be drawn pretty fast.
           Element.printPreview();
           break;
   }
}

 

The managed code (i.e the StrokeHandler) is shown below. Note that the managed libraries do not have an implementation of PostMessage, so one is created by using the DllImport facility.

 

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using Microsoft.Ink;
using System.Runtime.InteropServices;

namespace Villadsen
{
   public class StrokeHandler
   {
       private Microsoft.Ink.InkOverlay inkOverlay;
       private Microsoft.Ink.InkAnalyzer inkAnalyzer;
       private int handle;

       [DllImport("user32", EntryPoint = "PostMessage")]
       public static extern int PostMessageA(int hwnd, int wMsg, int wParam, int lParam);

       public StrokeHandler(int windowsHandle)
       {
           this.handle = windowsHandle;

           this.inkOverlay = new InkOverlay((IntPtr)this.handle);
           this.inkAnalyzer = new InkAnalyzer(inkOverlay.Ink, null);

           // Only collect gestures
           inkOverlay.CollectionMode = CollectionMode.GestureOnly;
           // But do collect all of them.
           inkOverlay.SetGestureStatus(ApplicationGesture.AllGestures, true);
           // Call this event handler to handle an incoming gesture
           inkOverlay.Gesture += new InkCollectorGestureEventHandler(inkOverlay_Gesture);
           // The drawing surface should be in front of the control.
           inkOverlay.AttachMode = InkOverlayAttachMode.InFront;
           // Draw in red
           inkOverlay.DefaultDrawingAttributes.Color = Color.Red;

           inkOverlay.Enabled = true;
       }

       // Event handler for the incoming events. This will post the incoming gensture
       // back to Ax through the windows handle of the form that created the StrokeHandler.
       void inkOverlay_Gesture(object sender, InkCollectorGestureEventArgs e)
       {
           // System.Console.WriteLine("Got a gesture {0}", e.Gestures[0].Id);
           PostMessageA(this.handle, 0x7ffe, 1, (int)e.Gestures[0].Id);
           e.Strokes.Clear();
       }
   }
}

That's it! That's really all there is to it.You can find the source code on ftp://ftp.villadsen.dk/GestureDemo.zip

 

 

Difference between " and ' ?

We have been asked whether there is any difference between the two types of quotes that can be used by Ax. The answer is very simple: X++ makes absolutely no distinction between the two. The reason that there are two quote characters is to facilitate constructs themselves containing quote characters, like

str s = ‘<MyTag attr="hello">’;

Writing this with only one quote character would be difficult to read because escape characters would have to be used to distinguish the quotes defining the extent of the string from those that occur in the string, as

str s = "<MyTag attr=\"hello\">";

That said, the application community has best practices that mandate that programmers use '(single quotes) for text that cannot appear on UI, like

System.Int32 myVar = CLRInterop::CLRNull('System.Int32');

Whilst text that does should use double quotes for text like

myButton.Label = "@SYS12345";

 

SQL improvements in the next version of Dynamics AX

I'd like to share with you a description of some of the features that we've finished for the next release. The features described in this blog post are improvements and additions to the X++ data access statements. One deals with ordering and grouping data, another expands the possibilities of the update statement by allowing join clauses; The third feature involves allowing values (or more precisely, constants that are not field related) into the selection that is inserted into a table in an insert_recordset statement.

Ordering and grouping Data

Previously, the order by and group by clauses acted on the table in the current join or select clause. So, the following

1: CustTable ct; 
2: CustTrans transactions; 
3:  
4:  select  *  from ct  order by AccountNum  
5:  join transactions  order by AmountCur  
6:  where ct.AccountNum == transactions.AccountNum;

would order by the account number, then on the amount. Note that the order by syntax doesn't include the table to which the field belongs, because it is implicit in the order by clauses position. This way of doing things works fine so long as you do not have requirements to order in a different order than what is mandated by the join structure. For instance, if you wanted to order by amount and then by accountnum you'd have to reverse the select/join hierarchy. To get rid of these problems, we have introduced a way of specifying an order by (and a group by) list that is not tied to a particular place in the join hierarchy. This is done by specifying a list of qualified names of fields in a single order by or group by clause:

1:  select *  from ct  
2:  join transactions  
3:  order by transactions.AmountCur, ct.AccountNum  
4:  where ct.AccountNum == transactions.AccountNum;

There can be only one such augmented order by clause in a selection.

Improvements to the update statement

The update statement may now have a join clause attached to it, as shown in the code snippet below:

1: // Three Table Join 
2: update_recordset salestable 
3: setting currencycode = 'USD' 
4: join custtable 
5: where  custtable.accountnum = salesTable.custaccount 
6: join custgroup 
7: where custgroup.name == 'Retail' && custgroup.custgroup == custTable.custgroup;

We have also added some diagnostics that will cause the compiler to complain if the user tries to use the value of fields coming from tables that are given in exists and non exists joins (The database backend is not required to fetch values for these, merely to find out if there are matching records or not).

The Insert statement.

The insert statement is generally used to insert data from one table into another. The programmer typically writes a selection of fields matching the fields to insert into the target table and delimits the data by using suitable where and join clauses:

1: insert_recordset MyTargetTable(targetField1, targetField2) 
2:     select sourceField1, sourceField2 from MySourceTable where ... 
3:  

This is indeed suitable for the case where all the data to be inserted into the target table is already stored in the source table, but that is not always the case. The new feature allows the user to insert values that are given by variable references, not field references.

Exception handling and transactions

The runtime semantics of exceptions is peculiar in X++. The difference from the semantics from other languages is that exception handling considers any catch blocks that are in a scope that contains a running transaction as ineligible to handle the exception. Consequently the behavior of exceptions is quite different in the situation where a transaction is running and in the situation where no transactions are running. This makes the exception semantics different from exception semantics of other languages, even though the syntax is very much the same. This is a perfect setup for confusion even for seasoned X++ programmers.

Please consider the example below for the following discussion:

 1: try 
 2: { 
 3:     MyTableType t; 
 4:     ttsBegin;    // Start a transaction 
 5:     try  
 6:     { 
 7:         throw error(“Something bad happened”); 
 8:     } 
 9:     catch 
10:     { 
11:         // Innermost catch block 
12:     } 
13:     update_recordset t … // Must run within a transaction 
14:     ttsCommit;    // Commit the transaction 
15: } 
16: catch 
17: { 
18:     Info (“In outermost exception handler”); 
19: } 
20:  
20: 

When the exception is thrown in the code above, the control is not transferred to the innermost catch block, as would have been the case if there had been no transaction active; Instead the execution resumes at the outermost exception handler, i.e. the first exception handler outside the try block where where the transaction started (This is the catch all exception handler in line 19).

This behavior is by design. Whenever an exception is thrown all transactions are rolled back (except for two special exceptions namely Exception::UpdateConflict and Exception::UpdateConflictNotRecovered, that do not roll back the active transactions). This is an inseparable part of the semantics of throwing exceptions in X++. If the exception is not one of the two exceptions mentioned above, the flow of control will be transferred to the handler of the outermost block that contains a TTSBegin. The reasoning behind this is that the state of the database queries would not be consistent when the transactions are rolled back, so all code potentially containing this code is skipped.

Another issue is balancing of the transaction-level.

Please consider an amended example:

 1: try 
 2: { 
 3:     MyTableType t; 
 4:     ttsBegin;      // Start a transaction – Increment TTS-level to 1 
 5:     try  
 6:     { 
 7:         ttsBegin;  // Start a nested transaction – Increment TTS-level to 2 
 8:         throw new SysException(“Something bad happened”); 
 9:         ttsCommit;     // Decrease TTS-level to 1 
10:     } 
11:     catch 
12:     { 
13:         // Innermost catch block 
14:         // What is the tts level here? 
15:         info (appl.ttslevel()); 
16:         // Something bad happened -> abort the transaction 
17:         ttsAbort; 
18:     } 
19:     update_recordset t … // Must run within a transaction 
20:     ttsCommit;       // Decrease TTS-level to 0, and commit 
21: } 
22: catch 
23: { 
24:     Info (“In outermost exception handler”); 
25: } 

Let’s examine the case where we allowed the inner catch block to run as the result of throwing the exception. The question is: What should the TTS-Level be in the innermost catch block?

TTSlevel is 1: This won’t work, as the innermost catch block must be able to compensate (and commit) or abort the level-2 transaction. TTS Level 1 would mandate that the system has taken the decision to either abort or commit the level-2 transaction.

TTSlevel is 2: This won’t work either, as the innermost catch block should be able to abort the level-2 transaction. A ttsabort inside the innermost catch block will also abort the level-1 transaction, and render the code following the catch block void. This in turn requires that the innermost catch block throws a new exception, and then we end up in the outermost catch block anyway.

I hope this provides a little clarity in these murky waters.
 

Correction guide - Compiler warnings

The following is based on material kindly provided by Michael Fruergaard Pontoppidan. I have edited it substantially and added some new material to make it fit within the context of this blog.

Introduction

This guide will show you how to resolve compiler warnings in Axapta.

Compiler warnings are a strong indication that the code contains flaws or questionable constructs. The compiler groups the warnings in several levels according to the warning's severity: Level 1 is for very questionable issues, downto level 4 which are smaller issues. All compiler warnings must all be eliminated from production code to improve the overall code quality.

Note that the Dynamics Ax development environment also features a best practise framework that diagnoses the code's adherence to the established best practises. The diagnostics issued by this tool are not covered here.

Contents

Introduction.

Compiler Warnings Level 1.

Break statement found outside legal context.

The new method of a derived class does not call super().

The new method of a derived class may not call super().

Function never returns a value.

Not all paths return a value.

Assignment/Comparison loses precision.

Compiler Warnings Level 2.

Unreachable code.

Compiler Warnings Level 3.

Empty compound statement.

Updating Views is not supported.

Compiler Warnings Level 4.

Class names should start with an uppercase letter.

Member names should start with a lowercase letter.

Compiler Warnings Level 1

Break statement found outside legal context

Break statements are used to skip to the end of for, while, do-while and switch statements. If a break statement is encountered outside the body of one of these statements it is ignored at runtime and should be removed from the code to avoid confusion and improve legibility.

Example

 1: {
 2:     int i;
 3: 
 4:     for (i = 1; i <= 100; i++)
 5:     {
 6:         foo(i);
 7:         if (i==8)
 8:             break;
 9:     }
10:     if (i==8)
11:         break;
12: }

Fix:

Move or remove the break statement to properly reflect the intention.

 1: {
 2:     int i;
 3: 
 4:     for (i = 1; i <= 100; i++)
 5:     {
 6:         foo(i);
 7:         if (i==8)
 8:             break;
 9:     }
10: }

The new method of a derived class does not call super()

Always call super() in the constructor (i.e. the new method) of a class: The parent class could have some initialization of its own to do. You should do this even if the parent class implementation does not currently set up any state, because the implementation may subsequently be changed. Failure to call the parent's initialization code will typically break the base class.

Example

This class is derived from the Runbase class. If you don't call super() in this new method the code in the inherited new method will never be executed, depriving the Runbase framework of being properly initialized.

 1: class MyClass extends Runbase
 2: {
 3:     void new()
 4:     {
 5:         ;
 6:         emplId = EmplTable::userId2EmplId(curUserId());
 7:     }
 8: }

Fix:

 1: class MyClass extends Runbase
 2: {
 3:     void new()
 4:     {
 5:         ;
 6:         super(); // Initialize base class. 
 7:         emplId = EmplTable::userId2EmplId(curUserId());
 8:     }
 9: } 

The new method of a derived class may not call super()

Always call super() in the constructor (i.e. the new method) of a class. The parent class could have some initialisation of its own to do. You should do this even if the parent class implementation does not currently set up any state, because the implementation may subsequently be changed.

Note that this message is different from the previous one: In this case the compiler can prove that super will not always be called (i.e. there are paths in the code that cause the method to end without calling super). In the previous case the compiler can prove that no paths cause super() to be executed.

Example

This class is inherited from the Runbase class. If you don't always call super() in this new method the code in the inherited new method will never be executed.

 1: class MyClass extends Runbase
 2: {
 3:     void new()
 4:     {
 5:         ;
 6:         if (this.init())
 7:             super(); // Not called if init() fails. 
 8:         emplId = EmplTable::userId2EmplId(curUserId());
 9:     }
10: }

Fix:

Always call super as shown below:

 1: class MyClass extends Runbase
 2: {
 3:     void new()
 4:     {
 5:         ;
 6:         this.init();
 7:         super(); // Always called. 
 8:         emplId = EmplTable::userId2EmplId(curUserId());
 9:     }
10: }

Function never returns a value

Any non abstract method with a return type (i.e. a method that does not return void) must explicitly return a value, irrespective of which path the execution takes through the code. If this is not the case, the warning above is issued.

Sometimes programmers write methods that have no implementation (and therefore no return statements) because they expect that derived classes should provide an implementation. In this case, an abstract class or an interface should be used, because this causes the compiler to check that all abstract methods (or interface methods) are actually overridden by a derived class.

Example

The user has written the following code because he expects the programmer to define a more meaningful implementation in derived classes. If applicable, the method should be made abstract forcing derived classes to implement this method.

1: abstract class MyClass
2: {
3:     InventQty onHandQty(InventOnHand inventOnHand)
4:     {
5:     }
6: }

Fix:

1: abstract class MyClass 
2: {
3:    abstract InventQty onHandQty(InventOnHand inventOnHand)
4:    {
5:        // No implementation. Implementations must be provided 
6:        // by non abstract derived classes. 
7:    }
8: }

Not all paths return a value

The method has a return value (i.e. it is not declared as void) and it is possible for the method to end without executing a return statement. This is never a problem for methods that do not return values (i.e. are declared as void) because they always return when the last statement is executed. This is a warning that invariably indicates a flaw in the code.

This warning is different from the one described above: In this case the compiler can prove that there are paths that do not return a value. The warning message above is issued if the compiler can prove that there are no paths that return a value.

Example

The switch statement below contains case entries for some of the enumeration literals in the PurchaseType enumeration type. However, if the purchaseType parameter does not contain one of the values provided for in the cases, no case entry would be selected and the method would end without explicitly returning a value. Even if all the values provided in the enumeration were dealt with (by providing a case entry for each literal) the compiler would still issue the warning message.

 1: static PurchCreateOrderForm construct(PurchaseType purchaseType, NoYesId project)
 2: {
 3:     switch (purchaseType)
 4:     {
 5:         case PurchaseType::Journal:
 6:             return PurchCreateOrderForm_Journal::construct(purchaseType, project);
 7:         case PurchaseType::Quotation:
 8:             return PurchCreateOrderForm_Quotation::construct(purchaseType, project);
 9:         case PurchaseType::Purch:
10:             return PurchCreateOrderForm_Purch::construct(purchaseType, project);
11:         case PurchaseType::Subscription:
12:             return PurchCreateOrderForm_Subscription::construct(purchaseType, project);
13:     }
14: }

Fix:

In the case below, the problem could be resolved by always returning after the switch statement.
 1: static PurchCreateOrderForm construct(PurchaseType purchaseType, NoYesId project)
 2: {
 3:     switch (purchaseType)
 4:     {
 5:         case PurchaseType::Journal:
 6:             return PurchCreateOrderForm_Journal::construct(purchaseType, project);
 7:         case PurchaseType::Quotation:
 8:             return PurchCreateOrderForm_Quotation::construct(purchaseType, project);
 9:         case PurchaseType::Purch:
10:             return PurchCreateOrderForm_Purch::construct(purchaseType, project);
11:         case PurchaseType::Subscription:
12:             return PurchCreateOrderForm_Subscription::construct(purchaseType, project);
13:     }
14:     return new PurchCreateOrderForm();
15: }
Another approach is to provide a default handler to catch the items not explicitly dealt with.
 1: static PurchCreateOrderForm construct(PurchaseType purchaseType, NoYesId project)
 2: {
 3:     switch (purchaseType)
 4:     {
 5:         case PurchaseType::Journal:
 6:             return PurchCreateOrderForm_Journal::construct(purchaseType, project);
 7:         case PurchaseType::Quotation:
 8:             return PurchCreateOrderForm_Quotation::construct(purchaseType, project);
 9:         case PurchaseType::Purch:
10:             return PurchCreateOrderForm_Purch::construct(purchaseType, project);
11:         case PurchaseType::Subscription:
12:             return PurchCreateOrderForm_Subscription::construct(purchaseType, project);
13:         default:
14:             return new PurchCreateOrderForm();
15:     }
16: }

Assignment/Comparison loses precision

This warning is issued when an implicit conversion is performed whereby the value is modified. This happens in these scenarios:

  • Real to int
  • Int64 to int

Real to int

Thye problem occurs when a real is being assigned to or compared to a value fo an integer type. This may be correct, but it should be made explicit that the value is truncated, not rounded. Also, unexpected results may happen when the real (floating point) value is greater than what is representable in a value of the given integer type (i.e. outside the range of (-2.147.483.678; 2.147.483.677) for 32 bit integers and (-9223372036854775808; 9223372036854775807) for 64 bit integers).

The global method real2int(real) can be used in most cases, providing truncation.

Example

The variable scrapConst is declared as int in the example below. When executing the assignment statement scrapConst = BOMMap.scrapConst() a type conversion from real to int is performed, because the resulting type from the method call is real.

 1: container consumptionTypeItem (BOMMap BOM)
 2: {
 3:     BOMMap BOMMap;
 4:     int scrapConst;
 5:     ;
 6:     BOMMap     = BOM.data();
 7:     scrapConst = BOMMap.scrapConst();
 8: 
 9:     // ... 
10: }

Fix:

The fix depends on what you want to achieve; there are more solutions to this case. If you want to rounding to take place (instead of truncation) you should use the round function before assigning the value to the integer. Typically the proper fix is to declare scrapConst as a real.

Int64 to int

Axapta 4.0 natively supports 64 bit integers. The problem occurs when trying to assign a value of type int64 to a variable of type int, or comparing int64 values with int values.

Example

In the example below, the value assigned to j is maxInt() and can just fit into a signed 32-bit integer.

The value assigned to i equals maxInt()+1 and can not fit into a signed 32-bit integer, so Axapta will choose a 64 bit integer. Converting an int64 to an int gives the warning because a 64 bit value is not generally representable in 32 bits.

1: {
2:     int i = 0x7fffffff;
3:     int j = 0x80000000;
4:     int k = 0x80000000u;
5: }

Fix:

Depending on what you want you can either choose to use a int64, which can safely hold the value, or place the value into an int, losing the most significant 32 bits. Note that X++ features syntax where a U can be appended to the integer literal to indicate that the value should be interpreted as an unsigned value. The third assignment in the example above will not generate a warning message because 0x80000000u is interpreted as an unsigned int. However, there are no unsigned types in X++; all arithmetic is signed.

You can use the any2int function if you want to go ahead and disregard the most significant bits.

1: {
2:     int i = any2int(0x80000000);
3:     int64 j = 0x80000000;
4: }

Compiler Warnings Level 2

Unreachable code

Unreachable code is code fragments that will never be executed, as the code flow prevents the code from being used. This warning is issued for code that is placed immediately after statements that unconditionally change the flow of control, i.e. the return, break, continue and throw statements.

Example

The break statement will never be executed as the return will exit the method.

 1: static BOMRouteApprove construct(Args args)
 2: {
 3:     BOMRouteApprove approve;
 4: 
 5:     switch (args.dataset())
 6:     {
 7:         case TableNum(routeTable):
 8:             return RouteApprove::newRouteTable(args.record());
 9:             break; // Execution never reaches this. 
10:         default:
11:             throw error(strfmt("@SYS29104", classId2Name(classIdGet(approve))));
12:     }
13: }

Fix:

Simply remove the statement that is never executed:
 1: static BOMRouteApprove construct(Args args)
 2: {
 3:     BOMRouteApprove approve;
 4: 
 5:     switch (args.dataset())
 6:     {
 7:         case TableNum(routeTable):
 8:             return RouteApprove::newRouteTable(args.record());
10:         default:
11:             throw error(strfmt("@SYS29104", classId2Name(classIdGet(approve))));
12:     }
13: }

Compiler Warnings Level 3

Empty compound statement

A compound statement is a statement that contains any number of other statements so that they are precieved by the compiler as a being single statement. A compound statement is constructed by prefixing the statement list with a { character and suffixing the statement list with }. An empty compound statement, then, is simply a compound statement containg no statements. This is either a sign of sloppy code and should be removed, or (worse) a sign of missing functionality that the programmer was intending to put in later. In that case, the missing functionality should be implemented or a //ToDo comment should be inserted. The //ToDo will not remove the compiler warning, as it is just a comment, but it will help the programmer remember to fill in the code.

One situation exists, as shown below, where it makes sence to have an empty compond statement. This happens when an exception must be caught without any explict functionality to be performed as a result. The exception handler must be provided to make sure it is not thrown in the calling context.

Example

The empty catch statement doesn't clean up the infolog, so the exception text falls through.

 1: void run()
 2: {
 3:     BOMParmReportFinish    BOMParmReportFinish;
 4:     BOMReportFinish        BOMReportFinish;
 5: 
 6:     if (!this.validate())
 7:         throw error("@SYS18447");
 8: 
 9:     select BOMParmReportFinish
10:         index ParmLineNumIdx
11:         where BomParmReportFinish.parmId == parmId;
12: 
13:     while (BOMParmReportFinish)
14:     {
15:        try
16:        {
17:            BOMReportFinish = BOMReportFinish::newParmBuffer(BOMparmReportFinish, this.postJournalPerBOMParm());
18:            BOMReportFinish.run();
19:            if (this.isInBatch())
20:                BOMReportFinish.journalTableData().updateBlock(JournalBlockLevel::System, JournalBlockLevel::None);
21: 
22:            this.findCallerRecord();
23:        }
24:        catch (Exception::Deadlock)
25:        {
26:            retry;
27:        }
28:        catch (Exception::Error)
29:        {
30:            // Empty catch statement 
31:        }
32:     }
33: }

Fix:

Make it explicit that you want this behavoiur by adding a call to exceptionTextFallThorugh() in the catch statement. This method (in the Globals class, so it is available in the global scope) doesn't do anything.

 1: void run()
 2: {
 3:     BOMParmReportFinish    BOMParmReportFinish;
 4:     BOMReportFinish        BOMReportFinish;
 5: 
 6:     if (!this.validate())
 7:         throw error("@SYS18447");
 8: 
 9:     select BOMParmReportFinish
10:         index ParmLineNumIdx
11:         where BomParmReportFinish.parmId == parmId;
12: 
13:     while (BOMParmReportFinish)
14:     {
15:        try
16:        {
17:            BOMReportFinish = BOMReportFinish::newParmBuffer(BOMparmReportFinish, this.postJournalPerBOMParm());
18:            BOMReportFinish.run();
19:            if (this.isInBatch())
20:                BOMReportFinish.journalTableData().updateBlock(JournalBlockLevel::System, JournalBlockLevel::None);
21: 
22:            this.findCallerRecord();
23:        }
24:        catch (Exception::Deadlock)
25:        {
26:            retry;
27:        }
28:        catch (Exception::Error)
29:        {
30:            exceptionTextFallThrough();
31:        }
32:     }
33: }

Updating Views is not supported.

Views provide a way of seeing several tables as set up by a query as one table. These pseudotables are readonly. As such it does not make sense to try to modify the data. If an attempt is made to update the data, by adding the forupdate keyword to the select statement using a view in the from clause as illustrated below, a warning occurs.

 

1: static void MyJob(Args a)
2: {
3:     CustOpenInvoices openInvoices;
4:     while select forupdate * from openInvoices
5:     {
6:         print openInvoices.AmountCur;
7:     }
8:     pause;
9: }

Fix:

Use the tables that the views "look into" to actually access the data. You may do this by using a query object to access the data, or provide a select statement over the tables sampled by the view query.

Compiler Warnings Level 4

Class names should start with an uppercase letter.

According to best practice conventions all class names must start with a capital letter.

Example

1: public class smmRepairDocuRef extends RunBase
2: {
3:     container tableCollectionsInVirtualCompany;
4: }

Fix:

1: public class SmmRepairDocuRef extends RunBase
2: {
3:     container tableCollectionsInVirtualCompany;
4: }

Note:

You can run the add-ins tool title case update to solve these issues.

Member names should start with a lowercase letter.

According to best practice conventions member names must start with a lowercase letter.

Example

1: public class MyClass
2: {
3:     Runbase Runbase;
4: }

Fix:

1: public class MyClass
2: {
3:     Runbase runbase;
4: }

Note:

You can run the add-ins tool title case update to solve these issues.

More Posts Next page »
Page view tracker