Working with Images (BLOBS) between Dynamics Ax and external applications (UPDATED: 06/01/2010)

(updated the 05/01/2010 and 06/01/2010)

The idea

Whenever you need to transfer data in a platform specific format (like Images), the easiest way of doing this is to create an envelope (in Xml) and put the data in an encoded format in this envelope.

Here’s a schema that shows such a scenario:

Dessin1

 

The Ax layer

On the Ax layer (the 2nd row) the image is transformed in a base64 string (the text file in the schema) and then put into an Xml-document (the envelope in the schema).

Just have a look at the table CompanyImage, that is used to persist the picture of an employee:

image

The type of the column in which the image is saved is “Container”. There is no explicit type for Images, but you will notice that the table in the SQL-Server database uses the type “Image” to persist the container information. The image-type of the SQL-Server 2008 is defined as:

Variable-length binary data from 0 through 2^31-1 (2,147,483,647) bytes.

In contrast to its name (“image”) this type is used to persist any binary data from Ax (container). Working in X++ with the binary data in the container requires the conversion to the type BinData which provides all necessary information to load, save, encode or decode binary data. This is what I will do in the following example.

Here’s a simple code that reads out the image (CompanyImage) and puts this in a XmlDocument:

    1:  public static XmlDocument  getImageXml(dataAreaId refDataAreaId, tableId refTableId, recId refRecId)
    2:  {
    3:      XmlDocument xmlEnvelope;
    4:      XmlElement childNode;
    5:      str content;
    6:   
    7:      CompanyImage ci;
    8:   
    9:      BinData bin = new BinData();
   10:  ;
   11:   
   12:  //getting image from CompanyImage
   13:  ci = CompanyImage::find(refDataAreaId, refTableId, refRecId);
   14:   
   15:  //creating BinData object from Container in the CompanyImage object
   16:  bin.setData(ci.Image);
   17:  //encoding the image to base64
   18:  content = bin.base64Encode();
   19:   
   20:  //creating the envelope and inserting a XmlElement that contains the image
   21:  //in a base64 string
   22:  xmlEnvelope = new XmlDocument();
   23:  childNode =  xmlEnvelope.CreateElement("image");
   24:  childNode.innerXml(content);
   25:   
   26:  xmlEnvelope.AppendChild(childNode);
   27:   
   28:  return xmlEnvelope;
   29:  }

Here’s the code for testing this function

    1:  static void Job11(Args _args)
    2:  {
    3:      str xmlContent;
    4:      XmlDocument xmlEnvelope;
    5:   
    6:      recId refRecId;
    7:      tableId refTableId;
    8:      dataAreaId refDataAreaId;
    9:   
   10:  int i;
   11:  ;
   12:  //Sample Data...
   13:  refDataAreaId = "ceu";
   14:  refTableId = 103;
   15:  refRecId = 5637144576;
   16:  xmlEnvelope = new XmlDocument();
   17:  //getting image from CompanyImage
   18:  xmlEnvelope = A1::getImageXml(refDataAreaId, refTableId, refRecId);
   19:   
   20:  xmlContent = xmlEnvelope.xml();
   21:  }

Just an useful hint: If you need to encode large images, you might run into an issue with the base64Encode method. Unfortunately this is not documented on msdn, but this is related to a limitation of the buffer size. This can be fixed as described by Paul Steynberg on his blog.

 

Whenever you communicate with an external application that can’t convert the Ax type XmlDocument, you need to wrap the method getImageXml with a method that returns a string:

    1:  public static str  getImage(dataAreaId refDataAreaId, tableId refTableId, recId refRecId)
    2:  {
    3:      return A1::getImageXml(refDataAreaId, refTableId, refRecId).xml();
    4:  }

 

The client (using BC.Net)

Now that we have a method that return a string instead of a XmlDocument, we can directly use CLR objects instead of using wrapper classes. In .Net we can handle the XmlDocument-content from the Ax 2009 in a string. But, since Xml is much more flexible than a simple encoded string, a method that does the conversion to a System.Xml.XmlDocument is recommended:

    1:          /// <summary>
    2:          /// Getting the Xml-envelope with the image
    3:          /// </summary>
    4:          /// <param name="refDataAreaId">ceu</param>
    5:          /// <param name="refTableId">103</param>
    6:          /// <param name="refRecId">5637144576</param>
    7:          /// <returns>The Xml-envelope with the image as content</returns>
    8:          private static XmlDocument GetXmlDocument(string refDataAreaId, 
    9:              int refTableId, 
   10:              long refRecId)
   11:          {
   12:              XmlDocument xml = new XmlDocument();
   13:   
   14:              using (Axapta ax = new Axapta())
   15:              {
   16:                  ax.Logon(refDataAreaId, string.Empty, string.Empty, string.Empty);
   17:   
   18:                  string xmlImageValue = (string)ax.CallStaticClassMethod("A1", "getImage", refDataAreaId, 103, 5637144576);
   19:   
   20:                  xml = new System.Xml.XmlDocument();
   21:                  xml.InnerXml = xmlImageValue;
   22:                  ax.Logoff();
   23:              }
   24:   
   25:              return xml;
   26:          }

Please be aware that this is just an example and that in a real world application you must implement an exception handling which I do not implement here to have code that is easier to read.

Now that we do have the xmlEnvelope with the encoded image as content, we need a method that does read out the content and returns the image:

    1:          private static Image UnwrapImage(XmlDocument xmlEnvelope)
    2:          {
    3:              XmlElement imageContent = (XmlElement) xmlEnvelope.FirstChild;
    4:              string base64Image = imageContent.InnerXml;
    5:   
    6:              byte[] imageBytes = Convert.FromBase64String(base64Image);
    7:   
    8:              MemoryStream stream = new MemoryStream(imageBytes);
    9:              Image content = Image.FromStream(stream);
   10:   
   11:              return content;
   12:          }

All that we need is an event that dos call this method and that assigns the image-content for example to a visual control like a picture-box:

    1:          private void button6_Click(object sender, EventArgs e)
    2:          {
    3:              string refDataAreaId = "ceu";
    4:              int refTableId = 103;
    5:              long refRecId = 5637144576;
    6:   
    7:              XmlDocument xmlEnvelope =
    8:                  GetXmlDocument(refDataAreaId,
    9:                      refTableId,
   10:                      refRecId);
   11:   
   12:              Image content = UnwrapImage(xmlEnvelope);
   13:   
   14:              pictureBox1.Image = content;
   15:          }

 

The Ax layer (using the AIF)

The first schema was based on the communication with Xml data. I was not explicitly showing the possibility to use the BC.Net because the real advantage of the xmlEnvelope data is given when all the communication is based on Xml. This is the case with WebServices. Creating an AIF-service that exposes a method returning the XmlEnvelope is quite easy. First you need a class that is derived from the AifDocumentService and that implements the following methods:

    1:  class AifSampleImageService extends AifDocumentService
    2:  {
    3:  }
    1:  public static str  getImage(dataAreaId refDataAreaId, tableId refTableId, recId refRecId)
    2:  {
    3:      return A1::getImageXml(refDataAreaId, refTableId, refRecId).xml();
    4:  }
    1:  protected void new()
    2:  {
    3:      ;
    4:      super();
    5:  }
    1:  public static AifSampleImageService construct()
    2:  {
    3:      return new AifSampleImageService();
    4:  }

After that a new Service and the operator needs to be configured in the AOT:

image

Be careful that the security key is configured. Otherwise this will result in an authorization error.

Once the Service is configured this new service is available in the AIF-configuration by refreshing the available AIF-services and enabling the new AIF-service the AIF-services in the AIF-administration:

image

Then the services need to be regenerated (“Generate button). The new service is now available and accessible with the Internet Explorer:

image

The Xml-schema (Xsd) that represents the AIF-operator is the following:

    1:  <?xml version="1.0" encoding="utf-8"?>
    2:  <xs:schema elementFormDefault="qualified" targetNamespace="https://tempuri.org" xmlns:xs="https://www.w3.org/2001/XMLSchema" xmlns:tns="https://tempuri.org">
    3:    <xs:element name="AifSampleImageServiceGetImageRequest">
    4:      <xs:complexType>
    5:        <xs:sequence>
    6:          <xs:element minOccurs="0" name="refDataAreaId" nillable="true" type="xs:string"/>
    7:          <xs:element minOccurs="0" name="refRecId" type="xs:long"/>
    8:          <xs:element minOccurs="0" name="refTableId" type="xs:int"/>
    9:        </xs:sequence>
   10:      </xs:complexType>
   11:    </xs:element>
   12:    <xs:element name="AifSampleImageServiceGetImageResponse">
   13:      <xs:complexType>
   14:        <xs:sequence>
   15:          <xs:element minOccurs="0" name="response" nillable="true" type="xs:string"/>
   16:        </xs:sequence>
   17:      </xs:complexType>
   18:    </xs:element>
   19:  </xs:schema>

 

As you can see in line 15, the return type of the operator “GetImage” has the type “string”. This is certainly not the best solution in a SOA-context, since you loose the type information of your Xml-document. In a real world application it would be much better to define a special type within Ax that describes the format.  It seems to be clear, that the following schema explains perfectly, that image-element contains a encoded base64 value:

    1:  <?xml version="1.0" encoding="utf-8"?>
    2:  <xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="https://www.w3.org/2001/XMLSchema">
    3:    <xs:element name="image" type="xs:base64Binary" />
    4:  </xs:schema>

The type “base64Binary” is part of the Xml-schema specification of the W3C and defined as:

base64Binary represents Base64-encoded arbitrary binary data. The ·value space· of base64Binary is the set of finite-length sequences of binary octets. For base64Binary data the entire binary stream is encoded using the Base64 Alphabet in [RFC 2045].

This is exactly what the XmlDocument contains:

    1:  <?xml version="1.0" encoding="UTF-8"?>
    2:  <envelope>
    3:  <image> [the image encoded in a base64 value] </image>
    4:  </envelope>

How to create data types is explained on the Aif blog in a good article about creating custom Ax services. There you could implement the method “getSchema” of the interface “AifXmlSerializable”. The article explains you in detail how to do this and this would go too far for this article. Just keep in mind that communicating Xml-documents in a string is a ‘no do’ in a SOA environment and that there are better ways with Dynamics Ax.

The .Net client (Consuming the AIF-service)

The Wsdl, that is exposed by this new service, can now be referenced in a Visual Studio 2008 project as a Service Reference:

image image

 The GetXmlDocument method from the first example (using the BC.Net) is changed to consume a WebService:
    1:          /// <returns>The Xml-envelope with the image as content</returns>
    2:          private static XmlDocument GetXmlDocumentFromAif(string refDataAreaId,
    3:              int refTableId,
    4:              long refRecId)
    5:          {
    6:              XmlDocument xml = new XmlDocument();
    7:   
    8:              AifSampleImageServiceClient client = new AifSampleImageServiceClient();
    9:              string xmlImageValue = client.getImage(refDataAreaId, refRecId, refTableId);
   10:              xml = new System.Xml.XmlDocument();
   11:              xml.InnerXml = xmlImageValue;
   12:   
   13:              return xml;
   14:          }
  

Now the an event lets us consume get and unwrap the image and display in a PictureBox as in the first example. The method UnwrapImage is taken from the first example.

    1:          private void button8_Click(object sender, EventArgs e)
    2:          {
    3:              string refDataAreaId = "ceu";
    4:              int refTableId = 103;
    5:              long refRecId = 5637144576;
    6:   
    7:              XmlDocument xmlEnvelope =
    8:                  GetXmlDocument(refDataAreaId,
    9:                      refTableId,
   10:                      refRecId);
   11:   
   12:              Image content = UnwrapImage(xmlEnvelope);
   13:   
   14:              pictureBox1.Image = content;
   15:          }