La librearía AngularJS nos proporciona un potente framework de herramientas y procedimientos con los que implementar páginas web basadas en MVC. Toda esta funcionalidad se puede también utilizar dentro de nuestros recursos web –web resources- en CRM. En este post vamos a ver cómo implementar llamadas Soap con AngularJS y encadenar respuestas con los objetos promises.
Como sabemos, cuando se hace una query a CRM desde el frontal web utilizando la API debemos trabajar con funciones Callback –ya sea para peticiones utilizando la librería SDK.Rest o peticiones Soap a través del Organization Service- y en determinadas ocasiones, cuando tenemos que encadenar varias queries una detrás de otra el modo común de realizarlo es anidando Callbacks:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
function RequestData() { var inputParameters1 = GetDataParameters(); RetrieveRecords(inputParameters1, function (data1) { var inputParameters2 = GetDataParameters(data1); RetrieveRecords(inputParameters2, function (data2) { var inputParameters3 = GetDataParameters(data2); RetrieveRecords(inputParameters3, function (data3) { var inputParameters4 = GetDataParameters(data3); RetrieveRecords(inputParameters4, function (data4) { var inputParameters5 = GetDataParameters(data4); //... to infinity and beyond }); }); }); }); } function RetrieveRecords(inputParameters, callback) { //Code for asynchronous request with SDK.Rest or SOAP. //... if (success) { callback(data); } } |
Con la librería AngularJS, los objetos promises que veremos a continuación y los servicios “$http” y «$q», este ejemplo anterior se transformaría en algo del estilo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
function RequestData() { var inputParameters = GetDataParameters(); var promiseRequest1 = RetrieveRecords(inputParameters); promiseRequest1.then(function (data1) { var inputParameters1 = GetDataParameters(data1); var promiseRequest2 = RetrieveRecords(inputParameters1); return promiseRequest2; }).then(function (data2) { var inputParameters2 = GetDataParameters(data2); var promiseRequest3 = RetrieveRecords(inputParameters2); return promiseRequest3; }).then(function (data3) { var inputParameters3 = GetDataParameters(data3); var promiseRequest4 = RetrieveRecords(inputParameters3); return promiseRequest4; }).then(function (data4) { var inputParameters4 = GetDataParameters(data4); var promiseRequest5 = RetrieveRecords(inputParameters4); return promiseRequest5; }); // to infinity and beyond } function RetrieveRecords(inputParameters) { //Code for asyncronous request with SDK.Rest or SOAP . //... var promise = getPromise(data); if (success) { return promise; } } |
Como vemos, el código queda más limpio en el segundo caso. Además de dejar el código más bonito la librería angular nos ofrece una mayor libertad para controlar procesos asíncronos permitiéndonos acceder al proceso de carga o ejecutando varias peticiones en paralelo.
Recordemos que para realizar una llamada Soap necesitamos tres cosas: la URL del endpoint, la acción que vamos a utilizar, y el cuerpo del mensaje. Con estas tres cosas podemos realizar la petición Soap con AngularJS.
Para construir la llamada utilizaremos el servicio “$http” de Angular como se muestra en el ejemplo.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 |
var injector = angular.injector(["ng"]); var $http = injector.get("$http"); var $q = injector.get("$q"); function RequestData() { var url = GetUrl(); var action = "RetrieveMultiple"; var data = GetDataRetrieveAccounts(); var promise = GetPromise(url, action, data); promise.then(function (response) { var ids = GetIdsAccounts(ParseXml(response.data)); console.log(ids); }); } function GetIdsAccounts(deserializedResponse){ var nodes = deserializedResponse["s:Envelope"]["s:Body"]["RetrieveMultipleResponse"]["RetrieveMultipleResult"]["a:Entities"]["a:Entity"]; var Ids = Array(); for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; var Id = node["a:Id"]["#text"]; Ids.push(Id); } return Ids; } function GetPromise(url, action, data) { var config = GetConfig(action); var promise = $http.post(url, data, config); return promise; } function GetConfig(action) { var config = { headers: { 'Accept': 'application/xml, text/xml, */*', 'Content-Type': 'text/xml; charset=utf-8', 'SOAPAction': 'http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/' + action } } return config; } function GetUrl() { var url = GetContext().getClientUrl(); return url + "/XRMServices/2011/Organization.svc/web"; } function GetContext() { if (typeof (GetGlobalContext) != "undefined") { return GetGlobalContext(); } else { if (typeof Xrm != "undefined") { return Xrm.Page.context; } else { console.error("Error accessing context"); } } } function GetDataRetrieveAccounts() { var request = ["<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">", "<s:Body>", "<RetrieveMultiple xmlns=\"http://schemas.microsoft.com/xrm/2011/Contracts/Services\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">", "<query i:type=\"a:QueryExpression\" xmlns:a=\"http://schemas.microsoft.com/xrm/2011/Contracts\">", "<a:ColumnSet>", "<a:AllColumns>false</a:AllColumns>", "<a:Columns xmlns:b=\"http://schemas.microsoft.com/2003/10/Serialization/Arrays\" />", "</a:ColumnSet>", "<a:Criteria>", "<a:Conditions />", "<a:FilterOperator>And</a:FilterOperator>", "<a:Filters>", "<a:FilterExpression>", "<a:Conditions>", "<a:ConditionExpression>", "<a:AttributeName>name</a:AttributeName>", "<a:Operator>BeginsWith</a:Operator>", "<a:Values xmlns:b=\"http://schemas.microsoft.com/2003/10/Serialization/Arrays\">", "<b:anyType i:type=\"c:string\" xmlns:c=\"http://www.w3.org/2001/XMLSchema\">a</b:anyType>", "</a:Values>", "<a:EntityName i:nil=\"true\" />", "</a:ConditionExpression>", "</a:Conditions>", "<a:FilterOperator>And</a:FilterOperator>", "<a:Filters />", "</a:FilterExpression>", "</a:Filters>", "</a:Criteria>", "<a:Distinct>false</a:Distinct>", "<a:EntityName>account</a:EntityName>", "<a:LinkEntities />", "<a:Orders />", "<a:PageInfo>", "<a:Count>0</a:Count>", "<a:PageNumber>0</a:PageNumber>", "<a:PagingCookie i:nil=\"true\" />", "<a:ReturnTotalRecordCount>false</a:ReturnTotalRecordCount>", "</a:PageInfo>", "<a:NoLock>false</a:NoLock>", "</query>", "</RetrieveMultiple>", "</s:Body>", "</s:Envelope>"].join(""); return request; } function ParseXml(xml, arrayTags) { var dom = null; if (window.DOMParser) { dom = (new DOMParser()).parseFromString(xml, "text/xml"); } else if (window.ActiveXObject) { dom = new ActiveXObject('Microsoft.XMLDOM'); dom.async = false; if (!dom.loadXML(xml)) { throw dom.parseError.reason + " " + dom.parseError.srcText; } } else { throw "cannot parse xml string!"; } function isArray(o) { return Object.prototype.toString.apply(o) === '[object Array]'; } function parseNode(xmlNode, result) { if(xmlNode.nodeName == "#text" && xmlNode.nodeValue.trim() == "") { return; } var jsonNode = {}; var existing = result[xmlNode.nodeName]; if(existing) { if(!isArray(existing)) { result[xmlNode.nodeName] = [existing, jsonNode]; } else { result[xmlNode.nodeName].push(jsonNode); } } else { if(arrayTags && arrayTags.indexOf(xmlNode.nodeName) != -1) { result[xmlNode.nodeName] = [jsonNode]; } else { //Modified by Daniel Díaz 31/01/2017 if (xmlNode.nodeName == "#text") { result[xmlNode.nodeName] = xmlNode.nodeValue; } else { result[xmlNode.nodeName] = jsonNode; } } } if(xmlNode.attributes) { var length = xmlNode.attributes.length; for(var i = 0; i < length; i++) { var attribute = xmlNode.attributes[i]; jsonNode[attribute.nodeName] = attribute.nodeValue; } } var length = xmlNode.childNodes.length; for(var i = 0; i < length; i++) { parseNode(xmlNode.childNodes[i], jsonNode); } } var result = {}; if(dom.childNodes.length) { parseNode(dom.childNodes[0], result); } return result; } |
La propiedad data del objeto response en el primer then del método RequestData contendrá el resultado de la petición. En este caso el cuerpo del mensaje es una petición de RetrieveMultiple con un filtro y nos devolverá las cuentas cuyos nombres empiecen por “A”. Recordar que esta es una respuesta http y por defecto el endpoint de CRM que estamos atacando nos devolverá mensajes serializados en XML -no es posible cambiar la respuesta a JSON como sí permite la API REST OData-. Para deserializar el XML podemos recurrir a herramientas externas o implementar nuestro propio deserializador utilizando la versión reducida de jQuery que trae angular, analizando los nodos. Yo he utilizado como base este ejemplo con una pequeña modificación.
El resultado de la ejecución nos mostraría un array de todos los Ids de las cuentas que empiezan por «A» en nuestro CRM.
Pero donde realmente se puede sacar partido a esta librería es en el uso del servicio “$q”. Este servicio nos permite añadirle N objetos promesa y ejecutarlo. El código que esté dentro del then sucesivo se ejecutará cuando todas las peticiones hayan terminado y nos devolverá N respuestas, una para cada una de las peticiones ejecutadas. Esto es una gran noticia puesto que nos avisará cuando todas las peticiones tengan respuesta, y no se ejecutará cada vez que termine una de ellas. Así permitirá controlar el flujo de información de nuestro código de una manera mucho más limpia y clara.
Siguiendo con el ejemplo anterior, imaginemos que después de obtener todas las cuentas que empiecen por “A”, para cada una de ellas queremos obtener el nombre de todos los contactos asociados a esas cuentas.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 |
var injector = angular.injector(["ng"]); var $http = injector.get("$http"); var $q = injector.get("$q"); function RequestData() { var url = GetUrl(); var action = "RetrieveMultiple"; var data = GetDataRetrieveAccounts(); var promise = GetPromise(url, action, data); promise.then(function (response) { var ids = GetIdsAccounts(ParseXml(response.data)); return GetMultiplesPromises(ids); }).then(function (response) { var names = Array(); for (id in response) { var deserializedResponse = ParseXml(response[id].data); names = names.concat(GetNamesContacts(deserializedResponse)) } console.log(names); }); } function GetNamesContacts(deserializedResponse) { var nodes = deserializedResponse["s:Envelope"]["s:Body"]["RetrieveMultipleResponse"]["RetrieveMultipleResult"]["a:Entities"]["a:Entity"]; var Names = Array(); if (typeof (nodes) != "undefined") { for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; var Name = node["a:Attributes"]["a:KeyValuePairOfstringanyType"][0]["b:value"]["#text"]; Names.push(Name); } } return Names; } function GetIdsAccounts(deserializedResponse){ var nodes = deserializedResponse["s:Envelope"]["s:Body"]["RetrieveMultipleResponse"]["RetrieveMultipleResult"]["a:Entities"]["a:Entity"]; var Ids = Array(); for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; var Id = node["a:Id"]["#text"]; Ids.push(Id); } return Ids; } function GetMultiplesPromises(ids) { var objPromises = Object(); angular.forEach(ids, function (id) { var url = GetUrl(); var action = "RetrieveMultiple"; var data = GetDataRetrieveContact(id); objPromises[id] = GetPromise(url, action, data); }); return $q.all(objPromises);; } function GetPromise(url, action, data) { var config = GetConfig(action); var promise = $http.post(url, data, config); return promise; } function GetConfig(action) { var config = { headers: { 'Accept': 'application/xml, text/xml, */*', 'Content-Type': 'text/xml; charset=utf-8', 'SOAPAction': 'http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/' + action } } return config; } function GetUrl() { var url = GetContext().getClientUrl(); return url + "/XRMServices/2011/Organization.svc/web"; } function GetContext() { if (typeof (GetGlobalContext) != "undefined") { return GetGlobalContext(); } else { if (typeof Xrm != "undefined") { return Xrm.Page.context; } else { console.error("Error accessing context"); } } } function GetDataRetrieveContact(id) { var request = ["<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">", "<s:Body>", "<RetrieveMultiple xmlns=\"http://schemas.microsoft.com/xrm/2011/Contracts/Services\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">", "<query i:type=\"a:QueryExpression\" xmlns:a=\"http://schemas.microsoft.com/xrm/2011/Contracts\">", "<a:ColumnSet>", "<a:AllColumns>false</a:AllColumns>", "<a:Columns xmlns:b=\"http://schemas.microsoft.com/2003/10/Serialization/Arrays\">", "<b:string>fullname</b:string>", "</a:Columns>", "</a:ColumnSet>", "<a:Criteria>", "<a:Conditions />", "<a:FilterOperator>And</a:FilterOperator>", "<a:Filters>", "<a:FilterExpression>", "<a:Conditions>", "<a:ConditionExpression>", "<a:AttributeName>parentcustomerid</a:AttributeName>", "<a:Operator>Equal</a:Operator>", "<a:Values xmlns:b=\"http://schemas.microsoft.com/2003/10/Serialization/Arrays\">", "<b:anyType i:type=\"c:guid\" xmlns:c=\"http://schemas.microsoft.com/2003/10/Serialization/\">" + id + "</b:anyType>", "</a:Values>", "<a:EntityName i:nil=\"true\" />", "</a:ConditionExpression>", "</a:Conditions>", "<a:FilterOperator>And</a:FilterOperator>", "<a:Filters />", "</a:FilterExpression>", "</a:Filters>", "</a:Criteria>", "<a:Distinct>false</a:Distinct>", "<a:EntityName>contact</a:EntityName>", "<a:LinkEntities />", "<a:Orders />", "<a:PageInfo>", "<a:Count>0</a:Count>", "<a:PageNumber>0</a:PageNumber>", "<a:PagingCookie i:nil=\"true\" />", "<a:ReturnTotalRecordCount>false</a:ReturnTotalRecordCount>", "</a:PageInfo>", "<a:NoLock>false</a:NoLock>", "</query>", "</RetrieveMultiple>", "</s:Body>", "</s:Envelope>"].join(""); return request; } function GetDataRetrieveAccounts() { var request = ["<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">", "<s:Body>", "<RetrieveMultiple xmlns=\"http://schemas.microsoft.com/xrm/2011/Contracts/Services\" xmlns:i=\"http://www.w3.org/2001/XMLSchema-instance\">", "<query i:type=\"a:QueryExpression\" xmlns:a=\"http://schemas.microsoft.com/xrm/2011/Contracts\">", "<a:ColumnSet>", "<a:AllColumns>false</a:AllColumns>", "<a:Columns xmlns:b=\"http://schemas.microsoft.com/2003/10/Serialization/Arrays\" />", "</a:ColumnSet>", "<a:Criteria>", "<a:Conditions />", "<a:FilterOperator>And</a:FilterOperator>", "<a:Filters>", "<a:FilterExpression>", "<a:Conditions>", "<a:ConditionExpression>", "<a:AttributeName>name</a:AttributeName>", "<a:Operator>BeginsWith</a:Operator>", "<a:Values xmlns:b=\"http://schemas.microsoft.com/2003/10/Serialization/Arrays\">", "<b:anyType i:type=\"c:string\" xmlns:c=\"http://www.w3.org/2001/XMLSchema\">a</b:anyType>", "</a:Values>", "<a:EntityName i:nil=\"true\" />", "</a:ConditionExpression>", "</a:Conditions>", "<a:FilterOperator>And</a:FilterOperator>", "<a:Filters />", "</a:FilterExpression>", "</a:Filters>", "</a:Criteria>", "<a:Distinct>false</a:Distinct>", "<a:EntityName>account</a:EntityName>", "<a:LinkEntities />", "<a:Orders />", "<a:PageInfo>", "<a:Count>0</a:Count>", "<a:PageNumber>0</a:PageNumber>", "<a:PagingCookie i:nil=\"true\" />", "<a:ReturnTotalRecordCount>false</a:ReturnTotalRecordCount>", "</a:PageInfo>", "<a:NoLock>false</a:NoLock>", "</query>", "</RetrieveMultiple>", "</s:Body>", "</s:Envelope>"].join(""); return request; } function ParseXml(xml, arrayTags) { var dom = null; if (window.DOMParser) { dom = (new DOMParser()).parseFromString(xml, "text/xml"); } else if (window.ActiveXObject) { dom = new ActiveXObject('Microsoft.XMLDOM'); dom.async = false; if (!dom.loadXML(xml)) { throw dom.parseError.reason + " " + dom.parseError.srcText; } } else { throw "cannot parse xml string!"; } function isArray(o) { return Object.prototype.toString.apply(o) === '[object Array]'; } function parseNode(xmlNode, result) { if(xmlNode.nodeName == "#text" && xmlNode.nodeValue.trim() == "") { return; } var jsonNode = {}; var existing = result[xmlNode.nodeName]; if(existing) { if(!isArray(existing)) { result[xmlNode.nodeName] = [existing, jsonNode]; } else { result[xmlNode.nodeName].push(jsonNode); } } else { if(arrayTags && arrayTags.indexOf(xmlNode.nodeName) != -1) { result[xmlNode.nodeName] = [jsonNode]; } else { //Modified by Daniel Díaz 31/01/2017 if (xmlNode.nodeName == "#text") { result[xmlNode.nodeName] = xmlNode.nodeValue; } else { result[xmlNode.nodeName] = jsonNode; } } } if(xmlNode.attributes) { var length = xmlNode.attributes.length; for(var i = 0; i < length; i++) { var attribute = xmlNode.attributes[i]; jsonNode[attribute.nodeName] = attribute.nodeValue; } } var length = xmlNode.childNodes.length; for(var i = 0; i < length; i++) { parseNode(xmlNode.childNodes[i], jsonNode); } } var result = {}; if(dom.childNodes.length) { parseNode(dom.childNodes[0], result); } return result; } |
El resultado de esta ejecución nos proporcionaría el siguiente array de nombres:
Estos son sólo dos ejemplos de lo que esta librería puede ofrecernos en CRM Dynamics pero realmente las posibilidades son infinitas cuando juntamos peticiones Soap con AngularJS.
Mmmmm, me gusta mucho la parte de que se puede controlar que se ejecute una función cuando se acaben todas las peticiones, voy a tener que echarle un vistazo 😉
View CommentHola Olalla!! La verdad que sí que es muy útil. Me alegro mucho de que te haya servido!
View CommentPodrías hacer un ejemplo más sencillo? jaja buen post
View Comment