Showing posts with label .NET Business Connector. Show all posts
Showing posts with label .NET Business Connector. Show all posts

Monday, June 14, 2010

Working with Proxies and the Ambiguous “No .NET Business Connector session could be found” or “Cannot access a disposed object” Errors

Recently, I created an X++ class with a series of methods that I wanted to be able to access via a Web Part in Enterprise Portal. Microsoft’s documentation on Proxies provided a good basis for setting this up.

The problem however came in using the following using block to wrap the code calling the X++ method:

Everything worked great, that was until I used the same block in two different events (called during the same page load) to call another X++ method. It wasn’t immediately clear what was going on, because my page started throwing the ambiguous error “No .NET Business Connector session could be found”, which led me to believe that I either broke something in my code, or that Enterprise Portal was acting quirky.
using (IAxaptaAdapter a = this.AxSession.AxaptaAdapter)
{
…
}


As I would eventually find out, attempting to obtain a new session, for example, by using the helper function (below) more than once during a given page load, is where the trouble resides. The first time you get the session, everything is great; however the second time, the session is returned as null, which effectively causes the problem.
private ISession AxSession
{
get
{
AxBaseWebPart webpart = AxBaseWebPart.GetWebpart(this);
return webpart == null ? null : webpart.Session;
}
}

The simplest way to setup this error-producing scenario is to call this using block two times in a row, using similar code as seen below. Everything works great stepping through the first block, but the page blows up during the second.


protected void Page_Load(object sender, EventArgs e)
{


using (IAxaptaAdapter a = this.AxSession.AxaptaAdapter)
{
MyCustomClass test = new MyCustomClass(a);
string s = test.myCustomFunction();
}


using (IAxaptaAdapter a = this.AxSession.AxaptaAdapter)
{
MyCustomClass test = new MyCustomClass(a);
string s = test.myCustomFunction();
}


}
Even more ugly is the error that is thrown if you attempt to use the using code block on a web control that has an AxDataSource, this error actually reveals the root problem behind the previous error.

[ObjectDisposedException: Cannot access a disposed object.]

The solution to both errors is rather simple. Setup your IAxaptaAdapter as a global variable in your class, and establish the session only one time during your Page_Load event.

public partial class myClass : System.Web.UI.UserControl
{
private IAxaptaAdapter axapta;


private ISession AxSession
{
get
{
AxBaseWebPart webpart = AxBaseWebPart.GetWebpart(this);
return webpart == null ? null : webpart.Session;
}
}


protected void Page_Load(object sender, EventArgs e)
{
axapta = this.AxSession.AxaptaAdapter;
…

You will then be able to access the session multiple times, for example, you could call your X++ method in the page load event, and later in the Button1_Click event.


protected void Page_Load(object sender, EventArgs e)
{
axapta = this.AxSession.AxaptaAdapter;


MyCustomClass test = new MyCustomClass(axapta);
string s = test.myCustomFunction();


}


protected void Button1_Click(object sender, EventArgs e)
{
Tmt_CallReportClass test = new Tmt_CallReportClass(axapta);
string s = test.myCustomFunction();
}

This will also allow you to have a AxDataSource control on the page, without any issues.

Tuesday, March 16, 2010

Using the .NET Business Connector to Export Sales Orders with Multiple Sales Lines into AX from a .NET Application: Part 2

Expanding on the previous blog, let’s say that another requirement has been added, which is, to add the ability to track the date that a record was imported into the system, which will be used to prevent duplicate records for a given customer in a given month from being imported.

The first step would be to add an ImportDate field in the SalesTable via the AOT

The second step would then be to modify our class (ImportToAX) method (ImportSalesOrder) to accept an import date.

The third step would then be to modify our class (ImportToAX) method (ImportSalesOrder) to include logic to determine if a duplicate record (a given customer in a given month) already exists. Programming instinct coupled with my novice skills in AX development tell me that in order to determine if we already have a record for a given customer in a given month, I would add the following lines into our X++ class (ImportToAX) method (ImportSalesOrder):



Select count(RecId) From salesTable
where salesTable.CustAccount == CustAccount && mthofyr(salesTable.ImportDate) == mthofyr(ImportDate) && year(salesTable.ImportDate) == year(ImportDate);
ret = salesTable.RecId;

if (ret == 0)
{
………
}
else
{
throw Global::error("Only one record is allowed per customer in a given month!");
}


This however is invalid code, which throws two errors when compiled. The first being:

“Illegal use of WHERE expression”

This error is thrown, apparently because methods cannot be used in a where clause, and since I have attempted to use the mthofyr() and year() methods within my where clause, we have an issue. Therefore, one possible way of getting around this, is to take a different approach, such as the following valid code:

found = false;

while select salesTable where salesTable.CustAccount == CustAccount
{
if (mthofyr(salesTable.ImportDate) == mthofyr(ImportDate) && year(salesTable.ImportDate) == year(ImportDate)) found = true;
}

if (!found)
{
………
}
else
{
throw Global::error("Only one record is allowed per customer in a given month!");
}

The second error that we get:

“Container and unbounded string (text) fields are not allowed in a WHERE expression”

This is inherently related to our method definition, where the CustAccount parameter is defined as an str:

str ImportSalesOrder(str CustAccount, real Total, real Taxes, real Fees, date ImportDate)
{
……
}

The problem is that the salesTable.CustAccount field is actually defined as the extended data type, CustAccount, which extends the base type str. Because AX is a bit finicky, a standard str type cannot be compared to an extended data type (even though the extended data type extends the base type we are comparing to). Therefore, the simplest solution is to make our incoming CustAccount parameter of type CustAccount.

str ImportRoyaltyReport(CustAccount CustAccount, real Total, real Taxes, real Fees, date ImportDate)
{
……
}


This actually has no ill affect the .NET side of the project. In fact, the only parts of our .NET function needing change would be first in the function definition:

static public bool ImportOrder(string CustomerAccount, decimal Total, decimal Tax, decimal Fees, DateTime ImportDate)

And then the code to pass in the additional Import Date parameter to the AX method call:

object[] param = new object[5];
param[0] = CustomerAccount;
param[1] = Total;
param[2] = Tax;
param[3] = Fees;
param[4] = ImportDate;
Object retval = Import.Call("ImportSalesOrder", param);


Finally, to reduce code redundancy in our AX class (ImportToAX) method (ImportSalesOrder), the method could be compacted to avoid having the same block of code repeated 3 times (as in Part 1). Here we can use a couple additional variables, a loop, and a switch statement, which will transform our method as whole to look like the following:

str ImportSalesOrder(CustAccount CustAccount, real Total, real Taxes, real Fees, date ImportDate)
{

SalesTable salesTable;
SalesLine salesLine;
InventDim inventDim;
NumberSeq NumberSeq;

str retval;
int line;
str line_item;
real line_amt;
boolean found;
;

retval = "";

found = false;

while select salesTable where salesTable.CustAccount == CustAccount
{
if (mthofyr(salesTable.ImportDate) == mthofyr(ImportDate) && year(salesTable.ImportDate) == year(ImportDate)) found = true;
}

if (!found)
{

// Generate the next SalesId (which is a Number Sequence)...

NumberSeq = NumberSeq::newGetNumFromCode(SalesParameters::numRefSalesId().numberSequence);
salesTable.SalesId = NumberSeq.num();

// Create the Sales Order...

salesTable.initValue();
salesTable.CustAccount = CustAccount;
salesTable.deliveryDate = today();
salesTable.PurchOrderFormNum = "Imported Data";
salesTable.Tmt_RoyaltyImportDate = ImportDate;

salesTable.initFromCustTable();

salesTable.insert();

// Create the 3 Sales Lines...

for (line = 1; line <= 3; line++) { switch(line) { case 1: line_item = "Total"; line_amt = Total; break; case 2: line_item = "Taxes"; line_amt = Taxes; break; case 3: line_item = "Fees"; line_amt = Fees; } salesLine.clear(); salesLine.SalesId = salesTable.SalesId; inventDim = salesLine.inventDim(); inventDim.InventSiteId = "01"; salesLine.setInventDimIdFromInventDim(inventDim); salesLine.ItemId = line_item; salesLine.SalesQty = 1; salesLine.SalesUnit = 'EA'; salesLine.SalesPrice = line_amt; salesLine.LineAmount = line_amt; salesLine.createLine(NoYes::No, NoYes::Yes, NoYes::No, NoYes::No, NoYes::No, NoYes::No); } retval = salesTable.SalesId; } else { throw Global::error("Only one record is allowed per customer in a given month!"); } return retval; }

Friday, March 12, 2010

Using the .NET Business Connector to Export Sales Orders with Multiple Sales Lines into AX from a .NET Application: Part 1

One requirement that had been given to me was the ability for an existing .NET application to be enhanced to interface with AX. The existing application’s sole purpose was to decode and parse messages being sent to a specified email account, and to export data from these messages into an database. Since the old database and its parent application was going to be slowly phased out with the implementation of AX, the exporting application would need to be enhanced to be able to export key fields of these messages into AX as a Sales Order with multiple Sales Lines. After some thought, I concluded that the solution to this requirement was rather simple in concept, and would need to include an AX Class with one method (used to do the import into the SalesTable and SalesLine tables), as well as an upgrade to the .NET application, utilizing the .NET Business Connector (used to call and execute the AX method).

The basis for my AX method was inspired by a German Dynamics AX Blog, with a title that roughly translates to “Microsoft Dynamics AX API - Part 1: Creating Orders” , by Mathias Füßler. This gave me a good starting point for how to programmatically insert Sales Orders with a related Sales Line. As expected, I needed to make some modifications to the example code for it to work in Dynamics AX 2009, as well as to make it to do exactly what I needed it to do.

The first part of my derived solution for example, was to create a Class in AX, which we will call ImportToAX. This class would contain one key method that we will call ImportSalesOrder. Each Sales Order required a Customer Account number, and three Sales Lines including Total, Taxes, Fees; therefore, the ImportSalesOrder method should accept these required pieces of information as base type parameters.

// This is the only method in the ImportToAX Class
str ImportSalesOrder (str CustAccount, real Total, real Taxes, real Fees)
{

SalesTable salesTable;
SalesLine salesLine;
InventDim inventDim;
NumberSeq NumberSeq;
str retval;
;

retval = "";

try
{

// Obtain the next SalesId (Next Number Sequence)...

NumberSeq = NumberSeq::newGetNumFromCode(SalesParameters::numRefSalesId().numberSequence);
salesTable.SalesId = NumberSeq.num();

// Create the Sales Order...

salesTable.initValue();
salesTable.CustAccount = CustAccount;
salesTable.deliveryDate = today();
salesTable.PurchOrderFormNum = "Imported Data";

salesTable.initFromCustTable();

salesTable.insert();

// Create the first Sales Line...

salesLine.clear();

salesLine.SalesId = salesTable.SalesId;

// The following three lines were added, because AX was requiring a defined site for inventory dimensions…
inventDim = salesLine.inventDim();
inventDim.InventSiteId = "01";
salesLine.setInventDimIdFromInventDim(inventDim);

salesLine.ItemId = ' Total ';
salesLine.SalesQty = 1;
salesLine.SalesUnit = 'EA';
salesLine.SalesPrice = Total;
salesLine.LineAmount = Total;

salesLine.createLine(NoYes::No, NoYes::Yes, NoYes::No, NoYes::No, NoYes::No, NoYes::No);

// Create the second Sales Line...

salesLine.clear();

salesLine.SalesId = salesTable.SalesId;
inventDim = salesLine.inventDim();
inventDim.InventSiteId = "01";
salesLine.setInventDimIdFromInventDim(inventDim);
salesLine.ItemId = 'Taxes';
salesLine.SalesQty = 1;
salesLine.SalesUnit = 'EA';
salesLine.SalesPrice = Taxes;
salesLine.LineAmount = Taxes;

salesLine.createLine(NoYes::No, NoYes::Yes, NoYes::No, NoYes::No, NoYes::No, NoYes::No);

// Create the third Sales Line...

salesLine.clear();

salesLine.SalesId = salesTable.SalesId;
inventDim = salesLine.inventDim();
inventDim.InventSiteId = "01";
salesLine.setInventDimIdFromInventDim(inventDim);
salesLine.ItemId = 'Fees';
salesLine.SalesQty = 1;
salesLine.SalesUnit = 'EA';
salesLine.SalesPrice = Fees;
salesLine.LineAmount = Fees;

salesLine.createLine(NoYes::No, NoYes::Yes, NoYes::No, NoYes::No, NoYes::No, NoYes::No);

retval = salesTable.SalesId;
}
catch
{
retval = "";
}

return retval;
}


The second part of the solution was to create a .NET Class utilizing the .NET Business Connector (which requires a reference to C:\Program Files\Microsoft Dynamics Ax\50\Client\Bin\Microsoft.Dynamics.BusinessConnectorNet.dll). The following .NET Class (C#) contains one core function which is used simply to connect to Dynamics AX, and then to execute the AX method with its required parameters:

using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Dynamics.BusinessConnectorNet;

namespace ImportToAXExample
{
public class ImportToAXClass
{

static public bool ImportOrder(string CustomerAccount, decimal Total, decimal Tax, decimal Fees)
{
Axapta ax;

bool Success = true;

try
{
ax = new Axapta();
ax.Logon(null, null, null, null);

AxaptaObject Import;
Import = ax.CreateAxaptaObject("ImportToAX");

object[] param = new object[4];
param[0] = CustomerAccount;
param[1] = Total;
param[2] = Tax;
param[3] = Fees;
Object retval = Import.Call("ImportSalesOrder", param);

if (retval.ToString() == "")
{
Success = false;
}

ax.Logoff();
}
catch()
{
Success = false;
}

return Success;
}

}
}

To tie it all together, this .NET Class could then be added to the existing .NET Application, and its only function, ImportOrder, could then be called from the application for each of the records that the application processes. Each time this function is called, required records within the SalesTable and SalesLine tables are created in AX.