Google Android vs Microsoft Dynamics AX (II)

Hace ya algún tiempo hice un pequeño experimento de conexión a un Dynamics AX 2009 de pruebas desde un terminal Android (emulado). Pues bien, con aquel experimento obtuve algunos progresos más aunque no había tenido tiempo todavía para publicarlos.

En el anterior post sobre este tema explicaba el entorno de desarrollo y un sencillo esquema de lo que pretendía conseguir:

Diseño básico de la Arquitectura en las pruebas

Es un entorno con tres capas muy bien diferenciadas, una instalación normal de Dynamics AX 2009 con el .NET Business Connector por un lado, un servicio web hecho en C#.NET en medio y un terminal Android al otro lado.

Sobre la instalación AX no hay nada que mencionar, es una instalación normal sin ninguna personalización y con los datos de prueba que facilita Microsoft, explicaré un poco las otras dos capas que son más interesantes:

 

Aplicación nativa para Android que lee datos de AX

Creo que no es necesario explicar que no soy ningún experto en programación Android y lo aquí expuesto esta obtenido con mayor o menor acierto mediante la antigua metodología de ensayo-error mirando ejemplos publicados en internet. El próximo mes voy a un curso de Android y puede que mejore este ejemplo hasta llegar a hacer algo útil, o puede que no 🙂

“Lo que importa no es llegar, es el camino”

Dicho esto, utilizando las librerías para android ksoap2 que ya comenté en el otro capítulo he conseguido hacer y recibir peticiones SOAP desde android a mi servicio web con un código único y reutilizable. En primer lugar he creado una clase propia que haga y reciba esas peticiones desde mi proyecto.

El siguiente código contiene dos funciones que reciben una tabla desde el servicio web, la primera recibe la tabla entera, a la segunda función se le puede pasar un filtro. En la aplicación utilizo la primera para obtener toda la lista de clientes, y la segunda para obtener los pedidos abiertos por ese cliente:

public class AxWebService {
   
    // VM Test AX
    private static final String URL = "http://192.168.1.16/jaee.ax.ws/Service1.asmx";
   
    // Opciones del ws definidas en .NET
    private static final String NAMESPACE = "http://tempuri.org/";
    private static final String SOAP_ACTION = "http://tempuri.org/"; // + METHOD_NAME

    // Método para obtener una lista desde AX
    public static String[] getTable(String _metodName) {
        List<String> l = new ArrayList<String>();

        try {
            SoapObject request = new SoapObject(NAMESPACE, _metodName);

            SoapSerializationEnvelope envelope = new SoapSerializationEnvelope(SoapEnvelope.VER11);
            envelope.dotNet = true;
            envelope.setOutputSoapObject(request);

            AndroidHttpTransport androidHttpTransport = new AndroidHttpTransport(URL);
            androidHttpTransport.call(SOAP_ACTION + _metodName, envelope);

            KvmSerializable ks = (KvmSerializable)envelope.bodyIn;
            SoapObject ret = (SoapObject) ks.getProperty(0);
            for (int i = 0; i < ret.getPropertyCount(); i++) {
                String s = ret.getProperty(i).toString();
                l.add(s);
            }

        } catch (Exception e) {
            l.add("ERROR: " + e.getMessage() + " (" + _metodName + ")");
        }

        return l.toArray(new String[0]);
    }

    // Método para obtener una lista desde AX con algún filtro
    public static String[] getTableFiltered(String _metodName, String _fieldName, String _fieldValue) {
        List<String> l = new ArrayList<String>();

        try {
            SoapObject request = new SoapObject(NAMESPACE, _metodName);
            request.addProperty(_fieldName, _fieldValue);

            SoapSerializationEnvelope envelope = new SoapSerializationEnvelope(SoapEnvelope.VER11);
            envelope.dotNet = true;
            envelope.setOutputSoapObject(request);

            AndroidHttpTransport androidHttpTransport = new AndroidHttpTransport(URL);
            androidHttpTransport.call(SOAP_ACTION + _metodName, envelope);

            KvmSerializable ks = (KvmSerializable)envelope.bodyIn;
            SoapObject ret = (SoapObject) ks.getProperty(0);
            for (int i = 0; i < ret.getPropertyCount(); i++) {
                String s = ret.getProperty(i).toString();
                l.add(s);
            }

        } catch (Exception e) {
            l.add("ERROR: " + e.getMessage() + " (" + _metodName + ")");
        }

        return l.toArray(new String[0]);
    }
}

Una vez tenemos esta clase genérica de comunicación con el servicio web, creamos la primera Actividad en android, que será la que se ejecute al lanzar la aplicación. Esta actividad muestra una Vista con la lista de clientes de AX que nos permitirá buscar con el teclado simplemente escribiendo encima, y pulsar una para lanzar la segunda actividad que veremos ahora.

public class CustTableList extends ListActivity  {

    // xml de diseño de la vista sacada de
    // http://stackoverflow.com/questions/2432951/how-to-display-a-two-column-listview-in-android
   
    @Override
    public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);

      // Obtengo lista de clientes en un array de pares [codigo, nombre]
      String[] custTableArray = AxWebService.getTable("getCustTable");
     
      setListAdapter(new SimpleAdapter(
              this,
              Util.toArrayListMap(custTableArray),  // convierte el array en List<Map<S,S>>
              R.layout.row,                         // vista
              new String[] { "text1", "text2" },    // campos en el mapa
              new int[] { R.id.text1, R.id.text2 }) // campos en la vista
      );

      final ListView lv = getListView();
      lv.setTextFilterEnabled(true);
      lv.setClickable(true);

      // como hacer el onclick sacado de
      // http://www.androidpeople.com/android-listview-onclick
      lv.setOnItemClickListener(new AdapterView.OnItemClickListener() {
        @SuppressWarnings("unchecked")
        public void onItemClick(AdapterView<?> view, View arg1, int position, long arg3) {

              // Obtener el primer elemento de la lista (el código de cliente)
              Map<String, String> m = (HashMap<String, String>) lv.getItemAtPosition(position);
              String custAccount = m.get("text1");
             
              // Paso el código de cliente como un parámetro a la siguiente actividad
              Bundle b = new Bundle();       
              b.putString("custAccount", custAccount);
             
              // Llamada a la siguiente actividad (lista de pedidos)
              Intent i = new Intent(view.getContext(), SalesTableList.class);
              i.putExtras(b);
              startActivityForResult(i, 0);                  
          }
      });
 
    }
}

Quizás lo más interesante sea lo sencilla que ha quedado la petición de datos a AX para obtener la lista de clientes. Se podría seguir desarrollando de esta forma y construir una especie de mini-framework para trabajo con android en caso de querer utilizar ésta integración para un proyecto real.

// Obtengo lista de clientes en un array de pares [código, nombre]
String[] custTableArray = AxWebService.getTable("getCustTable");

La segunda actividad es como la primera, muestra en otra vista los pedidos abiertos asociados al cliente seleccionado en la primera:

public class SalesTableList extends ListActivity  {
   
    @Override
    public void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);

      // Parámetro recibido de la anterior actividad
      Bundle bundle = this.getIntent().getExtras();
      String custAccount = bundle.getString("custAccount");
     
      // Obtengo lista de pedidos en un array de pares [codigo, nombre]
      // filtrados por el código de cliente que me ha llegado
      String[] custTableArray = AxWebService.getTableFiltered(
              "getSalesTableByCustAcc",
              "_custAccount",
              custAccount);
     
      setListAdapter(new SimpleAdapter(
              this,
              Util.toArrayListMap(custTableArray),  // convierte el array en List<Map<S,S>>
              R.layout.row,                         // vista
              new String[] { "text1", "text2" },    // campos en el mapa
              new int[] { R.id.text1, R.id.text2 }) // campos en la vista
      );

      ListView lv = getListView();
      lv.setTextFilterEnabled(true);
    }
}

De nuevo la petición a AX es realmente sencilla:

// Parámetro recibido de la anterior actividad
Bundle bundle = this.getIntent().getExtras();
String custAccount = bundle.getString("custAccount");

// Obtengo lista de pedidos en un array de pares [código, nombre]
// filtrados por el código de cliente que me ha llegado
String[] custTableArray = AxWebService.getTableFiltered(
                            "getSalesTableByCustAcc",
                            "_custAccount",
                            custAccount);

Y nada más por esta parte, el resto son los detalles para configurar un proyecto en android y mostrar las vistas pero eso lo veo excesivo para lo que quiero demostrar aquí. Se puede conseguir mucha y mejor información en los links que incluyo al final.

Comentar que este código está hecho con un poco de “trampa” ya que no conseguí recibir un DataSet tipado en android así que le envío una lista de strings separados por un separador y en android lo rompo para obtener las dos columnas. Seguro que se puede hacer mejor pero no quise entrar mucho en los temas propios de android ya que lo que a mí me interesa es la integración en sí y como conozco mejor la parte .NET hice más trabajo allí para que android lo tuviera mas fácil.

 

Web Service .NET con librería .NET Business Connector

Esta es la parte que resultará más sencilla a cualquier desarrollador de Dynamics AX, el servicio web es muy sencillo, hace peticiones a Dynamics AX mediante el .NET Business Connector y devuelve el resultado a Android en forma de Lista de Strings:

namespace jaee.ax.ws
{
    /// <summary>
    /// Servicio Web de conexión AX
    /// </summary>
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [System.ComponentModel.ToolboxItem(false)]
    public class Service1 : System.Web.Services.WebService
    {
        String Separator { get { return "##"; } }

        [WebMethod]
        public String[] getCustTable()
        {
            List<String> l = new List<String>();

            String tableName = "CustTable";
            String field1 = "AccountNum";
            String field2 = "Name";

            try
            {
                Axapta ax = this.Axapta; // Conexión activa con AX
                AxaptaRecord axRecord;
                using (axRecord = ax.CreateAxaptaRecord(tableName))
                {
                    axRecord.ExecuteStmt("select * from %1");
                    while (axRecord.Found)
                    {
                        String f1 = axRecord.get_Field(field1).ToString();
                        String f2 = axRecord.get_Field(field2).ToString();

                        l.Add(String.Format("{0}{1}{2}", f1, this.Separator, f2));

                        axRecord.Next();
                    }
                }
            }
            catch (Exception e)
            {
                l.Add(String.Format("{0}{1}{2}", "ERROR", this.Separator, e.Message));
            }

            return l.ToArray();
        }

        [WebMethod]
        public String[] getSalesTableByCustAcc(String _custAccount)
        {
            List<String> l = new List<String>();

            String tableName = "SalesTable";
            String field1 = "SalesId";
            String field2 = "ShippingDateConfirmed";

            try
            {
                Axapta ax = this.Axapta; // Conexión activa con AX
                AxaptaRecord axRecord;
                using (axRecord = ax.CreateAxaptaRecord(tableName))
                {
                    axRecord.ExecuteStmt("select * from %1 where %1.CustAccount == '" + _custAccount + "' && %1.SalesStatus < 3"); // < Facturado
                    while (axRecord.Found)
                    {
                        String f1 = axRecord.get_Field(field1).ToString();
                        DateTime f2 = DateTime.Parse(axRecord.get_Field(field2).ToString());

                        l.Add(String.Format("{0}{1}{2}", f1, this.Separator, f2.Date.ToShortDateString()));

                        axRecord.Next();
                    }
                }
            }
            catch (Exception e)
            {
                l.Add(String.Format("{0}{1}{2}", "ERROR", this.Separator, e.Message));
            }

            return l.ToArray();
        }
    }
}

 
He obviado las funciones que utilizo para conectar a AX porque se pueden obtener fácilmente en la red, en otro post dedicado al .NET Business Connector publicaré diferentes funciones para realizar las tareas más comunes. Comentar que el conector .NET no es la metodología recomendada para conectar al próximo Dynamics AX 2012 por lo que habrá que ir cambiando el chip hacia el mundo de los web-services con la próxima versión.

Y finalmente, este es el resultado desde el emulador:

Android vs AX - 1Android vs AX - 2Android vs AX - 3

 
y desde mi HTC Desire 🙂 :

Android vs AX - Dynamics AX 2009 en HTC Desire

 
LINKS y fuentes utilizadas (espero que no se me olvide ninguna)

Descarga