I was recently working with a client application (WPF) that used WCF Web Services to query and post data to Dynamics Ax (2012). One the requirements needed a new web service to be deployed. I once again reached one of those situations where the service worked in development and it passed testing with no issues. However, in 2 of roughly 10 sites, it started failing. One of my associates started debugging and found that the client application was sending the data, however the web service was receiving a null parameter. I should mention that the parameter was being sent as content data in a post request in json format.
Getting copy of the data being sent was not much of problem. I then emulated the call using Postman. Irritatingly, it worked. So, it works in Postman and does not work in the client application and we were almost stumped. As a last ditch attempt, I rewrote the sending routine to iterate through the records and send 5 records to be processed only. The routine failed on the second iteration. Debugging the call and extracting the data, I found that there was a French character in the stream. Rechecking the settings on Postman, I found that the Postman was using UTF-8. Honestly, I thought this would be default these days, but I guess I was mistaken. I modified the call to force it to use UTF-8 and it worked.
For reference:
using (WebClient client = new WebClient())
{
client.Headers[HttpRequestHeader.ContentType] = "application/json";
client.Encoding = Encoding.UTF8;
…<do other stuff>
var dataString = client.UploadString(apiUrl, jsonObj);
ResponseClass webresult = JsonConvert.DeserializeObject<ResponseClass>(dataString);
}
Friday, November 8, 2019
Tuesday, June 25, 2019
WPF: Validation and Dependency Objects
I recently had to do some support work for a WPF application. I had not used WPF or MVVM in several years, so it was a nice change. However, one of the requirements was to create a range validator and attach it to textbox. The architecture of this part tied the View to the base class of a View Model that had several derived classes. Each derived class had different minimum and maximum boundaries that needed to be reflected when the view was opened.
There were a couple of very good treatments that I found. The process is as follows:
There were a couple of very good treatments that I found. The process is as follows:
- Create the Min and Max Properties in the View Model.
- Create a class derived from ValdationRule.
- Create a wrapper class derived from DependencyObject. The Dependency Properties will represent the min and max that you need.
- Add a property of the wrapper class in the Validation Rule.
- Create a class derived from Freezable that will enable the View to access the DataContext and pass values to the wrapper.
- Add the elements to the View. If done properly, the Min and Max needed by the wrapper can be set to values defined in the ViewModel.
Tuesday, March 19, 2019
Ledger Dimension
I finally learned...A basic dimension is a default dimension. This is a value that concatenates a number of dimension values and is located in the Dimension Attribute Value Combination Table. If you add a Main Account, then you have a Ledger Dimension. The combination that you may try to retrieve or create MUST be part of the Main Account's Account Structure.
There are 2 approaches to getting a Ledger Dimension
1) Query it with AxdDimensionUtil::getLedgerAccountId
2) Create or retrieve it with DimensionDefaultingService::serviceCreateLedgerDimension
The code to retrieve a Ledger Dimension is:
private DimensionDefault getLedgerDimension(MainAccountNum _mainAccount, Num, str _bu, str _costcenter, str _dept)
{
DimensionDefault result = 0;
int position = 0;
int dimensionCount = 0;
container account;
account += [_mainAccountNum, _mainAccountNum, 0];
if(_bu)
{
account += ['BusinessUnit', _bu];
dimensionCount++;
}
if(_dept)
{
account += ['Department', _dept];
dimensionCount++;
}
if(_costcenter)
{
account += ['CostCenter', _costcenter];
dimensionCount++;
}
account = conPoke(account, 3, dimensionCount);
try
{
result = AxdDimensionUtil::getLedgerAccountId(account);
}
catch
{
return 0;
}
return result;
}
The code to called serviceCreateLedgerDimension requires a Main Account Number and Default Dimension:
private DimensionDefault getLedgerDimensionFromDefaultDimension(MainAccountNum _mainAccountNum, DimensionDefault _defaultDimension)
{
DimensionDefault result = 0;
DimensionDefault ledgerDimension;
container account = [_mainAccountNum,_mainAccountNum, 0];
ledgerDimension = AxdDimensionUtil::getLedgerAccountId(account);
result = DimensionDefaultingService::serviceCreateLedgerDimension(ledgerDimension, _defaultDimension);
return result;
}
There are 2 approaches to getting a Ledger Dimension
1) Query it with AxdDimensionUtil::getLedgerAccountId
2) Create or retrieve it with DimensionDefaultingService::serviceCreateLedgerDimension
The code to retrieve a Ledger Dimension is:
private DimensionDefault getLedgerDimension(MainAccountNum _mainAccount, Num, str _bu, str _costcenter, str _dept)
{
DimensionDefault result = 0;
int position = 0;
int dimensionCount = 0;
container account;
account += [_mainAccountNum, _mainAccountNum, 0];
if(_bu)
{
account += ['BusinessUnit', _bu];
dimensionCount++;
}
if(_dept)
{
account += ['Department', _dept];
dimensionCount++;
}
if(_costcenter)
{
account += ['CostCenter', _costcenter];
dimensionCount++;
}
account = conPoke(account, 3, dimensionCount);
try
{
result = AxdDimensionUtil::getLedgerAccountId(account);
}
catch
{
return 0;
}
return result;
}
The code to called serviceCreateLedgerDimension requires a Main Account Number and Default Dimension:
private DimensionDefault getLedgerDimensionFromDefaultDimension(MainAccountNum _mainAccountNum, DimensionDefault _defaultDimension)
{
DimensionDefault result = 0;
DimensionDefault ledgerDimension;
container account = [_mainAccountNum,_mainAccountNum, 0];
ledgerDimension = AxdDimensionUtil::getLedgerAccountId(account);
result = DimensionDefaultingService::serviceCreateLedgerDimension(ledgerDimension, _defaultDimension);
return result;
}
Tuesday, March 12, 2019
D365 FO and External DLLs
We recently had to add some external dlls to our D365 for Finance and Operations model. We found out that when the references are added to a project, an XML descriptor of the reference is placed in $\LCS Dynamics Ax\Trunk\<your branch>\Metadata\<your model>\<your model>\AxReference. However, once you ready to deploy, the actual dll must be manually added to $\LCS Dynamics Ax\Trunk\<your branch>\Metadata\Bin. Otherwise, the LCS build will fail with a class not found error.
Friday, February 8, 2019
Positive Pay in D365 For Finance and Operations for 8.1
One of my clients recently upgrade to D365 version 8.1. This was a Microsoft mandated change as this is the one where overlays will no longer be allowed. After the dust settled, I was asked to make some changes to their Positive Pay files. The process worked in their previous environment, however under 8.1 the documents were being produced without data.
After debugging the process, I was able to isolate the issue to the MS side of code. Specifically, the DMF process was not able to find the required records. I raised a ticket with MS and they resolved it in short order. There appears to be a breaking change in 8.1 there the entity name must be capitalized in the XSLT file.
So instead of the old:
Document/BankPositivePayExportEntity
You need to put:
Document/BANKPOSITIVEPAYEXPORTENTITY
The documentation from MS has not been updated yet.
After debugging the process, I was able to isolate the issue to the MS side of code. Specifically, the DMF process was not able to find the required records. I raised a ticket with MS and they resolved it in short order. There appears to be a breaking change in 8.1 there the entity name must be capitalized in the XSLT file.
So instead of the old:
Document/BankPositivePayExportEntity
You need to put:
Document/BANKPOSITIVEPAYEXPORTENTITY
The documentation from MS has not been updated yet.
Wednesday, November 28, 2018
File Uploads in Dynamics 365 For Operations
I was recently reviewing some file import methods and I came across a new (for me) method of uploading files from a client. It turns out this is quite simple.
This calls a file finding dialog:
FileUploadTemporaryStorageResult uploadResult = Dynamics.AX.Application.File::GetFileFromUser();
This returns a System.IO.Stream:
System.IO.Stream stream = uploadResult.openResult();
These snippets were part of class that imported a 2 column spreadsheet in Excel. Please note that X++ now supports using statements. Here is the full code for reference:
using System.IO;
using OfficeOpenXml;
using OfficeOpenXml.Style;
using OfficeOpenXml.Table;
public class PayrollCodeImport
{
public static void main(Args _args)
{
System.String value = null;
OfficeOpenXml.ExcelRange cell = null;
PayrollCode payrollCode;
//upload
FileUploadTemporaryStorageResult uploadResult = Dynamics.AX.Application.File::GetFileFromUser();
PayrollCodeId payrollCodeId;
Description description;
if(!uploadResult)
{
return;
}
//construct a new spreadsheet document with the upload stream
OfficeOpenXml.ExcelPackage package = new ExcelPackage(uploadResult.openResult());
//OXMLSpreadsheetDocument_RU spreadsheet = OXMLSpreadsheetDocument_RU::constructFromStream(uploadResult.openResult());
//get the worksheets from the workbook
OfficeOpenXml.ExcelWorksheets worksheets = package.get_Workbook().get_Worksheets();
//get the worksheet by name
OfficeOpenXml.ExcelWorksheet payrollcodeWorksheet = worksheets.get_Item("Import") as OfficeOpenXml.ExcelWorksheet;
//skip the header line
for (int i = payrollcodeWorksheet.Dimension.Start.Row + 1; i <= payrollcodeWorksheet.Dimension.End.Row; i++)
{
value = null;
cell = payrollcodeWorksheet.Cells.get_Item(i, 1);
value = cell.Value;
payrollCodeId = value;
value = null;
cell = payrollcodeWorksheet.Cells.get_Item(i, 2);
value = cell.Value;
description = value;
if(payrollCodeId && description)
{
payrollCode.Clear();
select forupdate payrollCode where payrollCode.PayrollCodeId == payrollCodeId;
ttsbegin;
if(!payrollCode.RecId)
{
payrollCode.PayrollCodeId = payrollCodeId;
payrollCode.Description = Description;
payrollCode.insert();
}
ttscommit;
}
}
}
}
This calls a file finding dialog:
FileUploadTemporaryStorageResult uploadResult = Dynamics.AX.Application.File::GetFileFromUser();
This returns a System.IO.Stream:
System.IO.Stream stream = uploadResult.openResult();
These snippets were part of class that imported a 2 column spreadsheet in Excel. Please note that X++ now supports using statements. Here is the full code for reference:
using System.IO;
using OfficeOpenXml;
using OfficeOpenXml.Style;
using OfficeOpenXml.Table;
public class PayrollCodeImport
{
public static void main(Args _args)
{
System.String value = null;
OfficeOpenXml.ExcelRange cell = null;
PayrollCode payrollCode;
//upload
FileUploadTemporaryStorageResult uploadResult = Dynamics.AX.Application.File::GetFileFromUser();
PayrollCodeId payrollCodeId;
Description description;
if(!uploadResult)
{
return;
}
//construct a new spreadsheet document with the upload stream
OfficeOpenXml.ExcelPackage package = new ExcelPackage(uploadResult.openResult());
//OXMLSpreadsheetDocument_RU spreadsheet = OXMLSpreadsheetDocument_RU::constructFromStream(uploadResult.openResult());
//get the worksheets from the workbook
OfficeOpenXml.ExcelWorksheets worksheets = package.get_Workbook().get_Worksheets();
//get the worksheet by name
OfficeOpenXml.ExcelWorksheet payrollcodeWorksheet = worksheets.get_Item("Import") as OfficeOpenXml.ExcelWorksheet;
//skip the header line
for (int i = payrollcodeWorksheet.Dimension.Start.Row + 1; i <= payrollcodeWorksheet.Dimension.End.Row; i++)
{
value = null;
cell = payrollcodeWorksheet.Cells.get_Item(i, 1);
value = cell.Value;
payrollCodeId = value;
value = null;
cell = payrollcodeWorksheet.Cells.get_Item(i, 2);
value = cell.Value;
description = value;
if(payrollCodeId && description)
{
payrollCode.Clear();
select forupdate payrollCode where payrollCode.PayrollCodeId == payrollCodeId;
ttsbegin;
if(!payrollCode.RecId)
{
payrollCode.PayrollCodeId = payrollCodeId;
payrollCode.Description = Description;
payrollCode.insert();
}
ttscommit;
}
}
}
}
Friday, August 24, 2018
Dynamics 365 FO Connection to ServiceBus
I recently received a request to connect D365 FO to an Azure ServiceBus in order to download messages that were being sent by another application. During the course of development, I found that there were 2 sets of projects that were developing connecting pieces: Microsoft.Azure.ServiceBus and Microsoft.ServiceBus. The first component, however, was tied to .Net 4.6.1 and D365 FO is tied to .Net 4.5.2. So, using the Azure component was out of the question. However there was no documentation on how to use the object model in the straight ServiceBus. Fortunately, both projects use a similar approach and a similar object model that I was able to make adaptations. There were 2 caveats.
Once downloaded, open it as a zip file (you may need to change the extension to zip) and extract out the dll. Then add the dll as a reference in D365. Once there, the following class can be used to connect and download messages. Sending messages would use a similar logic. Please note that my requirements were to use Topics and Subscriptions rather than Queues. Also, please note that I save all of the connection parameters in a separate table. That way the system may access different Topic/Subscription combinations as required. Also, if the connection information is changed, then this can be reflected in data.
public class ServiceBusConnect
{
public void new()
{
}
public void receiveMessages(str _topic, str _subscription)
{
str strBody;
System.IO.Stream stream;
System.IO.StreamReader reader;
Microsoft.ServiceBus.Messaging.SubscriptionClient subscriptionClient;
Microsoft.ServiceBus.Messaging.MessagingFactory messagingFactory;
Microsoft.ServiceBus.Messaging.BrokeredMessage message;
ServiceBusConnectionParameters parameters;
select firstonly parameters where parameters.Topic == _topic && parameters.Subscription == _subscription;
ServiceBusConnector.BrokeredMessageProcessor connector = new ServiceBusConnector.BrokeredMessageProcessor();
messagingFactory = Microsoft.ServiceBus.Messaging.MessagingFactory::Create(parameters.ConnectionString,
Microsoft.ServiceBus.TokenProvider::CreateSharedAccessSignatureTokenProvider(parameters.SharedAccessKeyName, parameters.SharedAccessKey));
subscriptionClient = messagingFactory.CreateSubscriptionClient(parameters.Topic, parameters.Subscription);
message = subscriptionClient.Receive(System.TimeSpan::FromSeconds(5));
if(message != null)
setPrefix("Messages");
while(message != null)
{
strBody = connector.GetBrokeredMessageContents(message);
info(strBody);
subscriptionClient.Complete(message.LockToken);
message = subscriptionClient.Receive(System.TimeSpan::FromSeconds(5));
}
subscriptionClient.Close();
}
}
The table ServiceBusConnectionParameters contains all the data for connection to Service Bus. The Topic and Subscription should be based on how your project's service is configured. The connection string will be sb://<your project>.servicebus.windows.net. The SharedAccessKeyName will be what ever is configured in your service bus. The default is RootManageSharedAccessKey. The shared access key is the primary key as displayed on your Service Bus Explorer.
As usual for using .net assemblies in D365, all class references must be fully qualified with their name spaces. The parameter in the subscriptionClient.Receive call is a timeout. If no messages are found within 5 seconds, the call is aborted and the message is set to null. Without it, the system just hangs indefinitely waiting for messages. The call to GetBrokeredMessageContents uses a call to the GetBody method on the message itselft. This method requires a generic reference to type the message. I could not, however, find a way to do that within D365. The wrapper to deserialize the message was pretty straightforward.
using System.IO;
using Microsoft.ServiceBus.Messaging;
namespace ServiceBusConnector
{
public class BrokeredMessageProcessor
{
public string GetBrokeredMessageContents(BrokeredMessage message)
{
string strResult;
using (var stream = message.GetBody<Stream>())
{
using (var reader = new StreamReader(stream))
{
strResult = reader.ReadToEnd();
}
}
return strResult;
}
}
}
- D365 does not appear to work well with .Net async approaches.
- D365 does not appear to work well with generics.
Once downloaded, open it as a zip file (you may need to change the extension to zip) and extract out the dll. Then add the dll as a reference in D365. Once there, the following class can be used to connect and download messages. Sending messages would use a similar logic. Please note that my requirements were to use Topics and Subscriptions rather than Queues. Also, please note that I save all of the connection parameters in a separate table. That way the system may access different Topic/Subscription combinations as required. Also, if the connection information is changed, then this can be reflected in data.
public class ServiceBusConnect
{
public void new()
{
}
public void receiveMessages(str _topic, str _subscription)
{
str strBody;
System.IO.Stream stream;
System.IO.StreamReader reader;
Microsoft.ServiceBus.Messaging.SubscriptionClient subscriptionClient;
Microsoft.ServiceBus.Messaging.MessagingFactory messagingFactory;
Microsoft.ServiceBus.Messaging.BrokeredMessage message;
ServiceBusConnectionParameters parameters;
select firstonly parameters where parameters.Topic == _topic && parameters.Subscription == _subscription;
ServiceBusConnector.BrokeredMessageProcessor connector = new ServiceBusConnector.BrokeredMessageProcessor();
messagingFactory = Microsoft.ServiceBus.Messaging.MessagingFactory::Create(parameters.ConnectionString,
Microsoft.ServiceBus.TokenProvider::CreateSharedAccessSignatureTokenProvider(parameters.SharedAccessKeyName, parameters.SharedAccessKey));
subscriptionClient = messagingFactory.CreateSubscriptionClient(parameters.Topic, parameters.Subscription);
message = subscriptionClient.Receive(System.TimeSpan::FromSeconds(5));
if(message != null)
setPrefix("Messages");
while(message != null)
{
strBody = connector.GetBrokeredMessageContents(message);
info(strBody);
subscriptionClient.Complete(message.LockToken);
message = subscriptionClient.Receive(System.TimeSpan::FromSeconds(5));
}
subscriptionClient.Close();
}
}
The table ServiceBusConnectionParameters contains all the data for connection to Service Bus. The Topic and Subscription should be based on how your project's service is configured. The connection string will be sb://<your project>.servicebus.windows.net. The SharedAccessKeyName will be what ever is configured in your service bus. The default is RootManageSharedAccessKey. The shared access key is the primary key as displayed on your Service Bus Explorer.
As usual for using .net assemblies in D365, all class references must be fully qualified with their name spaces. The parameter in the subscriptionClient.Receive call is a timeout. If no messages are found within 5 seconds, the call is aborted and the message is set to null. Without it, the system just hangs indefinitely waiting for messages. The call to GetBrokeredMessageContents uses a call to the GetBody method on the message itselft. This method requires a generic reference to type the message. I could not, however, find a way to do that within D365. The wrapper to deserialize the message was pretty straightforward.
using System.IO;
using Microsoft.ServiceBus.Messaging;
namespace ServiceBusConnector
{
public class BrokeredMessageProcessor
{
public string GetBrokeredMessageContents(BrokeredMessage message)
{
string strResult;
using (var stream = message.GetBody<Stream>())
{
using (var reader = new StreamReader(stream))
{
strResult = reader.ReadToEnd();
}
}
return strResult;
}
}
}
Subscribe to:
Posts (Atom)