viernes, 31 de mayo de 2013

[Dynamic CRM] Integrar Google Maps – PopUp Mapa Web (2/2)

 

Introducción


Se continua el artículo

[Dynamic CRM] Integrar Google Maps – PopUp Mapa Web (1/2)

 

Publicar Web Resources


El primer paso ser crear una nueva solución.

SNAGHTML39670615

En el cuadro de creación de la solución se define solo algunos campos

SNAGHTML3968906a

Editando la solución se irán agregando los js y html que requiere el mapa

SNAGHTML396a861d

Creamos un ítem por cada archivo

SNAGHTML397059c0

El listado final de archivos que conforman la solución seria:

SNAGHTML397ba532

Como paso final se publica

 

Edición de la Ribbon toolbar


Para agregar un nuevo botón en la toolbar del menú de CRM se hará uso de una utilidad de Codeplex

CRM 2011 Visual Ribbon Editor

El primer paso será configurar la conexión al CRM desde la herramienta

SNAGHTML398171b4

Una vez conectado usamos el botón “Open” para que se desplegué la selección de la entidad a la cual se quiere editar el ribbon

SNAGHTML3983c2df

Seleccionamos “account” y aceptamos. En el combo del tipo de ribbon disponibles para la entidad seleccionamos Homepage, la herramienta nos mostrara la representación visual de la toobar actual

SNAGHTML398551a6

Usaremos el botón “New Button” para crear nuestro botón que lanzara el mapa.

La definición del botón tiene asociada una imagen que previamente publicamos como recurso

SNAGHTML398a0e12

La solapa “Action” nos permitirá definir que función javascript será invocada al presionar el boton

Aquí se hará uso un archivo Mapa.js el cual define las funciones responsables de lanzar el popup de la ventana que contiene el mapa

 

function openMapStateProvince() 
{
    window.open('WebResources/new_Mapahtml?Data=grupo%3Dstateorprovince');
}

function openMapTerritory() 
{
    window.open('WebResources/new_Mapahtml?Data=grupo%3Dterritory');
}

Estas funciones javascript definen la propiedad por la cual se agruparan las cuentas y definen los colores de las marcas

SNAGHTML399220b7

 

El ultimo paso será grabar las modificaciones usando el botón “Save”, esto demorara un rato en aplicar los cambio.

 

Conclusión


Una vez concluida la publicación de los recursos y agregado el botón en la ribbon toolbar solo queda lanzar el mapa y disfrutar del resultado.

Recursos utilizados:

Versión 3 del API de JavaScript de Google Maps

Consejos prácticos para el InfoWindow en Google Maps

Superposiciones

[Dynamic CRM] Integrar Google Maps – PopUp Mapa Web (1/2)

 

Introducción


Al trabajar con contactos un requerimiento bastante buscado se relaciona con la localización o distribución de estos en un mapa, poder desplegar los cliente en una distribución geográfica da una idea de área de cobertura de ventas.

El objetico del articulo será justamente poder despegar en un grafico dinámico los cliente listados en Dynamic CRM, se contara con un botón en la toobox, el cual abrirá una ventana popup del browser con el mapa y los puntos representando cada cliente.

SNAGHTML33391976

 

image

Durante la confección del articulo no solo se explicara como generar el código que genere el grafico, sino que también incluirá su publicación e integración dentro de Dynamic CRM.

Comenzaremos analizando el código que permite hace ruso de las API de Google Maps, para continuar luego con la integración de este a CRM.

 

Estructura del Código


Lo primero que haremos será integrar código de la API de Google Maps con javascript, pero será necesario consultar los servicio de Dynamic CRM para recuperar los datos del fetchxml que se define en la lista de clientes.

En un proyecto web crearemos varios js y un html que contendrá el mapa

SNAGHTML333bd352

 

Invocar Servicios CRM desde javascript

En el archivo CRMHelper.js se encontrara toda al funcionalidad requerida para invocar el servicio de CRM.

Se necesitara de una query xml conocida en CRM como fetchxml para conocer que cuentas estaban listadas cuando el usuario presiono en el icono del mapa, estas cuentas serán posicionadas como puntos.

El primer paso es conocer como consultar un servicio CRM mediante javascript.

 

//toma el root de la url del sitio CRM
var context = window.parent.opener.Xrm.Page.context;
var serverUrl = context.getServerUrl();
if (serverUrl.match(/\/$/)) {
    serverUrl = serverUrl.substring(0, serverUrl.length - 1);
}

//ejecuta la consulta fetchxml al servicio de CRM
function FetchResultsXml(fetchXml) {


    var request = '<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">' +
    '<s:Body>'+
    '<Execute xmlns="http://schemas.microsoft.com/xrm/2011/Contracts/Services">' +
    '<request i:type="b:RetrieveMultipleRequest" ' +
    ' xmlns:b="http://schemas.microsoft.com/xrm/2011/Contracts" ' +
    ' xmlns:i="http://www.w3.org/2001/XMLSchema-instance">' +
    '<b:Parameters xmlns:c="http://schemas.datacontract.org/2004/07/System.Collections.Generic">' +
    '<b:KeyValuePairOfstringanyType>' +
    '<c:key>Query</c:key>' +
    '<c:value i:type="b:FetchExpression">' +
    '<b:Query>'+ CrmEncodeDecode.CrmXmlEncode(fetchXml) +
    '</b:Query>' +
    '</c:value>' +
    '</b:KeyValuePairOfstringanyType>' +
    '</b:Parameters>' +
    '<b:RequestId i:nil="true"/>' +
    '<b:RequestName>RetrieveMultiple</b:RequestName>' +
    '</request>' +
    '</Execute>' +
    '</s:Body></s:Envelope>';
    
    var xmlhttp = new XMLHttpRequest();
    xmlhttp.open("POST", serverUrl + "/XRMServices/2011/Organization.svc/web", false);
    xmlhttp.setRequestHeader("Accept", "application/xml, text/xml, */*");
    xmlhttp.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
    xmlhttp.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/Execute");

    xmlhttp.send(request);
 
    return xmlhttp.responseXML;
}

 

Aquí se define el xml que define el mensaje SOAP para invocación al servicio de CRM, dentro se ubica el fetchxml.

La url que utilizada para invocar el servicio se descubre por medio de window.parent.opener.Xrm.Page.context, usando la función getServerUrl()

El resultado que obtendremos es un xml con los datos de las cuentas, los cuales serán parseados para formar objetos javascript que represente los datos de cada cuenta. 

El las siguientes funciones se parsean el resultado y vuelcan la info. a un array definido en javascript. Resultara mas  simple si se tiene a mano el xml ha procesar, para esto lo que hice fue poner el resultad en un div usando la líneas:

var resultCRM = FetchResultsXml(fetchxml);

$('#xmlresult').html(resultCRM.xml);

Se invoca a la función que ejecuta la invocación al servicio, mostrando el resultado en el div, con el Developer Tools del IE (accedemos mediante F12) analizaremos la estructura de tag que deberemos parsear.

 

function GetFieldValue(entity, fieldName) {
    var vals = entity.getElementsByTagName("a:KeyValuePairOfstringanyType");
    
    for (var j = 0; j < vals.length; j++) {

        if (vals[j].getElementsByTagName("b:key")[0].firstChild.nodeValue == fieldName) {

            var valueNode = vals[j].getElementsByTagName("b:value");
            
            if (valueNode.length > 0)

                if (valueNode[0].childNodes.length == 1)
                    return valueNode[0].firstChild.nodeValue;
                else
                    return valueNode[0].childNodes[2].firstChild.nodeValue; //se toma el valor del nodo "name"
                    
            else
                return "";
        }
    }
}

function GetEntityName(fetchResults) {

    var entityElems = fetchResults.getElementsByTagName("a:EntityName");
    
    if (entityElems.length > 0) {
        return entityElems[0].firstChild.nodeValue;
    }
   
}


function GetArrayFromFetchResults(fetchResults) {

    var entityResults = new Array();

    var entityname = GetEntityName(fetchResults);

    var entityElems = fetchResults.getElementsByTagName("a:Entity");


    for (var i = 0; i < entityElems.length; i++) {

        entityResults[i] = new Object();

        if (entityname == 'account') {
            entityResults[i].id = GetFieldValue(entityElems[i], "accountid");
            entityResults[i].name = GetFieldValue(entityElems[i], "name");
            entityResults[i].addressname = GetFieldValue(entityElems[i], "address1_line1") == null ? "" : GetFieldValue(entityElems[i], "address1_line1");

            entityResults[i].latitude = GetFieldValue(entityElems[i], "address1_latitude");
            entityResults[i].longitude = GetFieldValue(entityElems[i], "address1_longitude");

            entityResults[i].stateorprovince = GetFieldValue(entityElems[i], "address1_stateorprovince") == null ? "" : GetFieldValue(entityElems[i], "address1_stateorprovince");
            entityResults[i].territory = GetFieldValue(entityElems[i], "territoryid") == null ? "" : GetFieldValue(entityElems[i], "territoryid");
        }

    }
   
    
    return entityResults;
}

La funcionalidad inicia en el método GetArrayFromFetchResults() el cual invoca a GetEntityName() para conocer que entidad se esta trabajando, en al imagen se puede observar el tag que se recupera:

SNAGHTML343cfd92

si bien en este ejemplo solo se usa la entidad account podría adaptarse el código para visualidad otro tipo de entidades por eso es bueno conocer de cual se trata.

El siguiente paso será recuperar cada uno de los atributos por cada account. Tomamos el tag “entity” usando la línea:

var entityElems = fetchResults.getElementsByTagName("a:Entity");

como se observan son 4, una por cada account de la lista que muestra CRM.

SNAGHTML3440082c

Dentro de la función GetFieldValue() accedemos directo al tag KeyValuePairOfstringanyType, la colección de estos tag contiene el valor de cada atributo, en este punto hay que diferenciar entre datos simple del CRM como ser, por ejemplo, el name del account.

SNAGHTML34459eb1

en donde el value es un valor simple, esto se recupera mediante la líneas:

if (valueNode[0].childNodes.length == 1) 
      return valueNode[0].firstChild.nodeValue;

ya que el tag “value” no tendrá ningún nodo hijo.Pero hay otros atributos complejos como ser el territorio, al ser una entidad relacionada el valor a recuperar tiene otra estructura de nodos.

SNAGHTML3449d338

es por eso que pasara por al línea:

return valueNode[0].childNodes[2].firstChild.nodeValue; //se toma el valor del nodo "name"

una vez entendido como parsear el xml solo queda armar el array con las propiedades de la entidad, esto es justamente lo que retorna GetArrayFromFetchResults()

 

Uso de las Google Map API


Luego de obtener los datos se procede a configurar el mapa usando las API de Google MAP, esto código se define en el archivo Mapa.html

Lo primero será declarar las librerías necesarias

 

<script type="text/javascript" src="http://maps.googleapis.com/maps/api/js?sensor=false"></script>

<script src="ClientGlobalContext.js.aspx" type="text/javascript"></script>

<script src="new_jquery1.9" type="text/javascript"></script>
<script src="new_CRMHelper" type="text/javascript"></script>
<script src="new_linqjs" type="text/javascript"></script>

 

Jquery es una librería necesaria. También vemos el link a las API de Google Map.

Pero seguro notaran una de nombre linqjs, esta será usada para poder agrupar los datos desde javascript, se trata de:

linq.js - LINQ for JavaScript

Nota: los script que llevan el prefijo new_ es porque fueron subidos como resource al propio CRM, este paso lo veremos mas adelante al publicar los js y html

El código que define el mapa es el siguiente:

 

        <script type="text/javascript">

            $(function() {
                try {

                    var data = decodeURIComponent($.getUrlVar('Data'));
                    var grupo = data.split('=')[1];

                    var fetchxml = window.opener.parent.document.getElementById('contentIFrame').contentDocument.getElementById('effectiveFetchXml').attributes['value'].nodeValue;

                    var resultCRM = FetchResultsXml(fetchxml);

                    $('#xmlresult').html(resultCRM.xml);
                    var resultArray = GetArrayFromFetchResults(resultCRM);

                    //se define la lista de colores que puede tomar los marker
                    var listcolor = new Array('FE7569', '0404B4', 'FFFF00', '088A08', '01DFD7', '8A0886', '1C1C1C');


                    var mapOptions = {
                        zoom: 4,
                        mapTypeId: google.maps.MapTypeId.ROADMAP
                    }

                    var map = new google.maps.Map(document.getElementById("map_canvas"), mapOptions);

                    var bounds = new google.maps.LatLngBounds();

                    var infowindow = new google.maps.InfoWindow();

                    var index = 0;
                    Enumerable.From(resultArray)
                                .GroupBy('$.' + grupo, '', function(key, group) { return { sucu: key, group: group} })
                                .ForEach(function(x) {

                                    var pinImage = new google.maps.MarkerImage("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|" + listcolor[index],
                                                                new google.maps.Size(21, 34),
                                                                new google.maps.Point(0, 0),
                                                                new google.maps.Point(10, 34));

                                    x.group.ForEach(function(y) {

                                        var marker = new google.maps.Marker({
                                            position: new google.maps.LatLng(y.latitude, y.longitude),
                                            icon: pinImage,
                                            map: map,
                                            html: "<b>" + y.name + "</b>
Direccion: " + y.addressname + "
Estado: " + y.stateorprovince + "
Territorio: " + y.territory
                                        });

                                        google.maps.event.addListener(marker, "click", function() {

                                            infowindow.setContent(this.html);
                                            infowindow.open(map, this);

                                        });

                                        bounds.extend(marker.position);
                                    });

                                    index++;

                                    if (index == listcolor.length)
                                        index = 0;

                                });


                    map.fitBounds(bounds);

                }
                catch (err) {
                    var txt = "There was an error on this page.\n\n";
                    txt += "Error description: " + err.message + "\n\n";
                    txt += "Click OK to continue.\n\n";
                    alert(txt);
                }
            });

        </script>

 

Ahora vamos a ir analizando las diferentes línea que permite crear el mapa. Empezamos tomando de la url el valor de la querystring, el cual define como agruparemos para generar en el mapa los puntos de diferentes colores

var data = decodeURIComponent($.getUrlVar('Data'));
var grupo = data.split('=')[1];

Luego tomaremos el fetchxml

var fetchxml = window.opener.parent.document.getElementById('contentIFrame').contentDocument.getElementById('effectiveFetchXml').attributes['value'].nodeValue;

la línea es bastante larga pues el dato se encuentra bastante escondido, si analizamos el html del CRM con el Developer Tools y buscamos “effectiveFetchXml” veremos que el tag se encuentra dentro de un iframe.

De esta forma obtenemos valor que conforma el xml que utiliza CRM para definir las cuentas que lista en pantalla.

SNAGHTML3473800b

 

A continuación lanzamos la consulta al servicio con el código analizado en la sección previa.

var resultCRM = FetchResultsXml(fetchxml);

//$('#xmlresult').html(resultCRM.xml);
var resultArray = GetArrayFromFetchResults(resultCRM);

 

Se declaran las variables que definen el mapa:

  • mapOptions, define información global del mapa, en este caso zoom y tipo
  • map, contiene la instancia del div que contendrá el mapa
  • bounds, se utiliza para general la región que permita centrar la vista, de por si el centrado no es automático, es necesario indicar las posiciones que conforman la región en que se quiere centrar el foco visual
  • infowindow, define el popup con al información contextual para cada marca

 

var mapOptions = {
    zoom: 4,
    mapTypeId: google.maps.MapTypeId.ROADMAP
}

var map = new google.maps.Map(document.getElementById("map_canvas"), mapOptions);

var bounds = new google.maps.LatLngBounds();

var infowindow = new google.maps.InfoWindow();

La siguiente líneas serán las encargadas de agrupar e iterar los datos de las cuentas, es aquí donde se hace uso de la librería linq.js para poder agrupar la lista de cuenta por la propiedad definida en la url.

 

Enumerable.From(resultArray)
          .GroupBy('$.' + grupo, '', function(key, group) { return { sucu: key, group: group} })
          .ForEach(function(x) {

Las líneas de código que se encuentran dentro del ForEach crearan las marcas en el mapa.

Se define la forma del icono, en el link Google Maps Pins se podrá encontrar información sobre la generacion de estos iconos. 

var pinImage = new google.maps.MarkerImage("http://chart.apis.google.com/chart?chst=d_map_pin_letter&chld=%E2%80%A2|" + listcolor[index],
                            new google.maps.Size(21, 34),
                            new google.maps.Point(0, 0),
                            new google.maps.Point(10, 34));

Se itera por lo ítem que forman cada grupo, recordemos que cada grupo determina el color de sus ítems

x.group.ForEach(function(y) { ....

Se crea el market, en este se define la posición concreta del punto en el mapa, también se define un contenido asociado cuando se pulse sobre la marca asignando el html que representa el contenido

var marker = new google.maps.Marker({
	position: new google.maps.LatLng(y.latitude, y.longitude),
	icon: pinImage,
	map: map,
	html: "<b>" + y.name + "</b><br/>Direccion: " + y.addressname + "<br/>Estado: " + y.stateorprovince + "<br/>Territorio: " + y.territory
});

Se asocia el market con un evento, el cual desplegara el popup con información de la localización. Además se agrega la posición a la lista de bounds para definir el área en que se centrara la grafica.

 

google.maps.event.addListener(marker, "click", function() {

    infowindow.setContent(this.html);
    infowindow.open(map, this);

});

bounds.extend(marker.position);

Luego de terminado el ciclo por cada grupo y cuenta, se asigna los bounds al map

map.fitBounds(bounds);

 

Hasta aquí seria todo lo necesario con código para generar el mapa.

En la siguiente parte del artículo veremos como publicar en CRM y asociar a un botón que se encuentra en la ribbon

 

Código


domingo, 19 de mayo de 2013

[Reporting Service] [Dynamic CRM] - Integrar con google maps (2/2)

 

Introducción


Continuamos con el articulo anterior

[Reporting Service] [Dynamic CRM] - Integrar con google maps (1/2)

 

Creación del componente


Antes de arrancar hay resaltar que no podremos usar .net 4 pues el rdl que estamos editando con VS2008 solo soporta hasta .net 3.5

Este punto no es menor, ya que esto nos imposibilita hacer uso de las librerías de CRM SDK, por lo tanto deberemos hacer uso del servicio de WCF para poder consultar al CRM y obtener la info de las cuentas

Entonces el primer paso será crear un proyecto del tipo Class Library, remarco la definición de .net 3.5

SNAGHTML2eb3ce99

En este punto se puede usar tanto VS 2008 o 2010, en este caso decidí usar 2010 pero remarcando la opción del framework utilizado.

En el proyecto veremos una clase que representa el proxy del servicio, esta se creo mediante el uso de la utilidad

Herramienta Lenguaje de descripción de servicios Web (Wsdl.exe)

utilizando la url: http://<sitio>:<puerto>/ContosoHQ/XRMServices/2011/Organization.svc

en este caso la organización de ejemplo que estamos usando es ContosoHQ, pero esto se debe reemplazar por el que estén utilizando. La clase resultante es la OrganizationService.cs

SNAGHTML3922419d 

Se creo un helper el cual no brindara información de CRM utilizando el servicio, se trata del CRMHelper.cs

Se utiliza el fetchxml proveniente del reporte como filtro para conocer que cuentas se están mostrando en el reporte.

 

public static List<AccountCRM> GetAccounts(string query)
{
    try
    {
        List<AccountCRM> accounts = new List<AccountCRM>();

        using (OrganizationServiceClient crmService = new OrganizationServiceClient())
        {
            
            EntityCollection myAccounts = crmService.RetrieveMultiple(new FetchExpression() { Query = query });

            foreach (Entity entity in myAccounts.Entities)
            {
               
                AccountCRM account = new AccountCRM()
                {
                    id = ((XmlText)((XmlNode[])entity.Attributes.First(x => x.key == "accountid").value)[2]).Value,
                    razonsocial = entity.Attributes.First(x => x.key == "name").value.ToString(),
                    address = entity.Attributes.First(x => x.key == "address1_line1").value.ToString(),
                    stateorprovince = entity.Attributes.First(x => x.key == "address1_stateorprovince").value.ToString(),
                    country = entity.Attributes.First(x => x.key == "address1_country").value.ToString(),
                    territory = ((EntityReference)entity.Attributes.First(x => x.key == "territoryid").value).Name,
                };
                
                //se valida si la key puede no retornarse 
                //si la entidad tiene un valor nulo o vacio el servicio no la retorna como respuesta 
                //a pesar que este incluida en el fetchxml
                var latitud = entity.Attributes.FirstOrDefault(x => x.key == "address1_latitude");
                var longitude = entity.Attributes.FirstOrDefault(x => x.key == "address1_longitude");

                if (!(latitud == null && longitude == null))
                {
                    account.Position = new GeoPosition()
                    {
                        Latitude = Convert.ToDouble(latitud.value),
                        Longitude = Convert.ToDouble(longitude.value),
                    };
                }

                accounts.Add(account);

            }
        }

        return accounts;

    }
    catch (Exception ex)
    {
        Trace.WriteLine(string.Format("[{0:dd-MM-yyyy HH:mm}] Message:{1}, StackTrace: {2}", DateTime.Now, ex.Message, ex.StackTrace));
        throw;
    }

}

El código que sigue hará uso de las cuenta para generar la url el cual permitirá a la api de google maps generar la imagen

public static byte[] GetMap(string fetchxml)
{
    //asignamos la cultura en en-US para que la puntuacion de la localizacion resuelva correctamente
    Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");

    try
    {
        //obtenemos las cuentas
        List<AccountCRM> accounts = CRMHelper.GetAccounts(fetchxml);

        if (accounts.Count > 0)
            return GoogleStaticMap(accounts);
        else
            return null;

    }
    catch (Exception ex)
    {
        Trace.WriteLine(string.Format("[{0:dd-MM-yyyy HH:mm}] Message:{1}, StackTrace: {2}", DateTime.Now, ex.Message, ex.StackTrace));
        return null;
    }

}

 

Una vez ejecutado el fetchexml y recuperada las cuenta se procede armar la url que generara la imagen del mapa

private static byte[] GoogleStaticMap(List<AccountCRM> accounts)
{
    List<string> colors = new List<string>() { "red", "blue", "yellow", "green", "orange" };
    try
    {
        //en la url se define el tamaño de la imagen, formato y tipo de mapa
        string url = "http://maps.googleapis.com/maps/api/staticmap?size=900x450&maptype=roadmap&format=jpg{0}&sensor=false";


        //solo se procesan las cuentas que tengan geo-posicionamiento
        accounts = accounts.Where(x => x.Position != null).ToList();

        if (accounts.Count() == 0)
            return null;

        //se agrupa las cuentas por su territorio
        //para definir los colores que se aplicara a cada marca en el mapa
        var groupAccounts = accounts.GroupBy(x=> x.territory);

        //se recorre cada grupo armando la marca
        List<string> markersList = new List<string>();
        int colorindex = 0;
        foreach (var item in groupAccounts)
        {
            markersList.Add(string.Format("&markers=color:{0}|{1}", colors[colorindex], string.Join("|", item
                                                                                            .Select(x => string.Format("{0},{1}", x.Position.Latitude, x.Position.Longitude))
                                                                                            .ToArray())
                         ));

            colorindex++;

            if (colorindex == colors.Count)
                colorindex = 0;
        }
        string markers = string.Join("", markersList.ToArray());


        url = string.Format(url, markers);

        //se invoca a la url para obtener la imagen del mapa
        Uri uri = new Uri(url);
        WebClient client = new WebClient();
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);

        Stream stream = request.GetResponse().GetResponseStream();

        //se convierte el stream en byte[]
        return ReadFullStream(stream, request.GetResponse().ContentLength);


    }
    catch (Exception ex)
    {
        Trace.WriteLine(string.Format("[{0:dd-MM-yyyy HH:mm}] Message:{1}, StackTrace: {2}", DateTime.Now, ex.Message, ex.StackTrace));
        throw;
    }

}

 

Como paso final copiaremos la dll generada en la carpeta del proyecto donde estamos editando el rdl

 

Vincular RDL con librería .net


Estando en al edición del rdl en el Visual Studio 2008, seleccionamos la opción

image

veremos un dialogo del cual no interesa la opción “Referencias”

image

Usaremos la opción de “Agregar” para buscar la dll que creamos en el paso anterior

image

 

Arrastramos el control imagen al diseñador del reporte

image

el control el diseñador del reporte nos despliega el dialogo donde podremos definir la formula que invocara al método de nuestra libreria

SNAGHTML2fdec868

La formula seria la siguiente:

=ContosoGoogleMap.ReportHelper.GetMap(Parameters!CRM_FilteredAccount.Value)

como se observa define el namespace + clase + método

al hacer uso del parámetro:  Parameters!CRM_FilteredAccount.Value obtendremos el fetchxml que le llega al reporte, por ejemplo, podría ser algo como:

<fetch version="1.0" output-format="xml-platform" mapping="logical" distinct="false">
	<entity name="account">
		<all-attributes />
		<filter type="and">
			<condition attribute="address1_city" operator="eq" value="Buenos Aires" />
		</filter>
	</entity>
</fetch>

El propio CRM envía de forma automática el xml del fetchxml al reporte.

 

Configuración Reporting Service


1- Se copiara la dll a la carpeta

%ProgramFiles%\Microsoft SQL Server\MSRSXX.<Instance Name>\Reporting Services\ReportServer\bin

 

2- Modificar el archivo rssrvpolicy.config de la carpeta

%ProgramFiles%\Microsoft SQL Server\MSRSXX.<Instance Name>\Reporting Services\ReportServer

colocando justo debajo del <CodeGroup> que lleva el $CodeGen$ la definición:

<CodeGroup
	class="FirstMatchCodeGroup"
	version="1"
	PermissionSetName="FullTrust"
	Name="ContosoGoogleMapGroup"
	Description="">
	<IMembershipCondition
		class="UrlMembershipCondition"
		version="1"
		Url="C:\Program Files\Microsoft SQL Server\MSRS10_50.MSSQLSERVER\Reporting Services\ReportServer\bin\ContosoGoogleMap.dll"/>
</CodeGroup>

3- Modificar en el rssrvpolicy.config la línea que lleva el Report_Expressions_Default_Permissions validando que el PermissionSetName este en “FullTrust”.

SNAGHTML2feed0c1

Esto será necesario para poder escribir a disco, además de poder realizar las invocaciones a las url de CRM y Google, sino se define se obtendrán errores como ser:

Error de solicitud de permiso de tipo 'System.Security.Permissions.SecurityPermission, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'.

o

Error de solicitud de permiso de tipo 'System.Web.AspNetHostingPermission, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089'

 

4 – Modificar el web.config de la carpeta

%ProgramFiles%\Microsoft SQL Server\MSRSXX.<Instance Name>\Reporting Services\ReportServer

en este se debe definir la configuración del <system.serviceModel> que se encuentra en el app.config del archivo de test, este debería estar a nivel del <configuration>

SNAGHTML2ff3687e

también se puede definir el <system.diagnostics>

 

5 - Como paso final seria recomendable reiniciar el servicio de reporting para asegurar que todo lo modificado tome efecto

SNAGHTML2fa75ce8

 

Publicar del reporte en CRM


Una vez que se tenga el rdl modificado con la imagen cuya formula invoca a la librería y el servidor de reporte configurado, se procede a publicar el reporte. Para esta opción volvemos a Dynamic CRM donde editaremos el reporte y usaremos la opción desplegable que nos permite seleccionar un archivo para subirlo

SNAGHTML2ffd86d7

Se localiza el rdl, guardar y lanzar el reporte

SNAGHTML2ffee2db

El reporte resultante nos mostrara un listado de cliente y debajo el mapa con su localización, la cual pintara un color por el territorio al que pertenecen

image

 

Recursos


Guía para desarrolladores de la versión 2 del API de Google Static Maps

ver la sección de titulo “Marcadores”

Reporting Service - .net dll integration - problem security access with web service and file

 

Código


[Reporting Service] [Dynamic CRM] - Integrar con google maps (1/2)

 

Introducción


La utilización de mapas para aportar valor en las aplicaciones es un aspecto cada vez mas requerido, las aplicaciones CRM están especializadas en trabar con cliente por lo que conocer su distribución aporta valor al usuario.

En este caso uniremos tres tecnologías Dynamic CRM el cual nos aportara los datos de las cuentas, Reporting Service para el listado de información, y Google Maps para obtener la imagen del mapas que será incrustado en el reporte

Por lo extenso del articulo se realizaran dos partes, esta primera donde se verán los pasos necesarios para la creación del reporte y su edición en el Visual Studio

Una segunda parte se encargara de presentar como integrar la librería dll con el reporte y su posterior publicación en CRM.

 

Creación del reporte desde Dynamic CRM


El primer paso será definir la estructura básica del reporte, para esta tarea nos ayudaremos con el el wizard que provee CRM.

1- Creamos el nuevo reporte

image

 

2- Se define como entidad primaria al cliente

image

 

3- Se definen los filtros si hace falta, en este caso solo listaremos los clientes de Buenos Aires

image

 

4- Se definen las columnas del reporte

image

 

5- Se aceptan los cambios validando que la entidad asociada sea la cuenta

image

 

Estos pasos nos permitirán contar con una estructura básica del reporte que mas tarde usaremos para vincular con la librería encargada de generar la imagen del mapa.

Ejecutamos el reporte para validar que este correcto

SNAGHTML20981b19

image

 

Edición Reporte desde Visual Studio 2008


El siguiente paso requiere de Visual Studio 2008 con las tools de Business Intelligence las cuales se instalan al agregar el Sql Server 2008 Express Advanced Services, este incluye el servicio de Reporting Service.

En el Visual Studio deberíamos poder crear un proyecto como el siguiente:

SNAGHTML2abc8dae

 

En CRM ubicamos el reporte y lo editamos

image

Usamos la opción para descargar el reporte como archivo rdl

image

El archivo descargado lo deberíamos ubicar en la carpeta del proyecto que creamos con el Visual Studio

image

Para luego agregarlo al proyecto

SNAGHTML2acbd8d2

 

Si editamos el reporte veremos que se conserva el diseño, así como la información de conexión y campos disponibles

SNAGHTML2acf227e

 

Es aquí donde vamos a definir la invocación a la librería que devolverá la imagen con el mapa de google representando la ubicación de los cuentas.

En el siguiente articulo veremos como crear el componente y vincularlo al reporte.

jueves, 9 de mayo de 2013

[Dynamic CRM] Actualizar Geo Localización (Latitud/Longitud) con Google Maps

 

Introducción


Si se quiere trabajar con mapas es imprescindible contar con la correcta resolución del posicionamiento de las entidades, la idea es actualizar los campos latitud y longitud de la entidad cuenta

image

Nota:Esta vista que estas observando la cree para poder tener la info de la latitud y longitud listado a simple vista

En este caso la actualización será masiva a un grupo de cuentas, la ejecución se realizara desde un test haciendo uso de una consulta fetchxml para resolver las cuentas que se quieren actualizar

Esta misma técnica se podría aplicar para crear un plug-in que se adjunte a los campos de dirección de la cuenta, ante el cambio de este campo se lanzaría la operación de actualización del posicionamiento global.

 

Obtener información de las cuentas


Haremos uso de las librerías de CRM SDK para poder recuperar las cuentas según la query definida en el fetchxml proporcionado

El primer paso será agregar la referencia a las librerías del SDK

image

En el archivo de configuración se debe definir la url al sitio del CRM que se este utilizando

 

<?xml version="1.0" encoding="utf-8" ?>
<configuration>

  <connectionStrings>
    <add name="CRMServer" connectionString="Url=http://localhost:5555/ContosoHQ;"/>
  </connectionStrings>


  <system.diagnostics>
    <trace autoflush="true" indentsize="4">
      <listeners>
        <remove name="Default" />
        <add name="myListener"  type="System.Diagnostics.TextWriterTraceListener" initializeData="TraceLog.log" />
      </listeners>
    </trace>
  </system.diagnostics>
  
</configuration>

Aquí se define tanto la conexión como la línea que permite definir el trace

En la siguiente imagen se puede observar la utilización la definición de la conexión en la clase que proporciona el SDK

 

SNAGHTML6597704

 

Nota: en este caso desarrolle dentro del propio equipo donde tenia instalado el servidor de CRM por eso utilice localhost, pero es lógico que esto deba cambiarse si se accede de forma remota

El siguiente código permite recuperar la información de las cuentas:

 

public static List<AccountCRM> GetEntityMap(string fetchxml)
{

    List<AccountCRM> entityList = new List<AccountCRM>();

    try
    {
        using (var service = new OrganizationService("CRMServer"))
        {

            EntityCollection entityCol = service.RetrieveMultiple(new FetchExpression(fetchxml));

            foreach (Entity entity in entityCol.Entities)
            {

                AccountCRM account = new AccountCRM()
                {
                    id = (Guid)entity.Attributes["accountid"],
                    razonsocial = entity.Attributes["name"].ToString(),
                    address = entity.Attributes["address1_line1"].ToString(),
                    stateorprovince = entity.Attributes["address1_stateorprovince"].ToString(),
                    country = entity.Attributes["address1_country"].ToString(),
                };

                if (entity.Attributes.ContainsKey("address1_latitude") && entity.Attributes.ContainsKey("address1_longitude"))
                {
                    account.Position = new GeoPosition()
                    {
                        Latitude = Convert.ToDouble(entity.Attributes["address1_latitude"]),
                        Longitude = Convert.ToDouble(entity.Attributes["address1_longitude"]),
                    };
                }


                entityList.Add(account);

            }

            return entityList;
        }
    }
    catch (Exception ex)
    {
        Trace.WriteLine(string.Format("[{0:dd-MM-yyyy HH:mm}] Message:{1}, StackTrace: {2}", DateTime.Now, ex.Message, ex.StackTrace));
        throw;
    }

}

Se utilizo el siguiente fetchxml para recuperar las cuentas que nos interesa actualizar

 

string fetch = @"<fetch version='1.0' count='50' output-format='xml-platform' mapping='logical' distinct='false'>
                  <entity name='account'>
                    <attribute name='accountid' />
                    <attribute name='name' />
                    <attribute name='address1_city' />
                    <attribute name='address1_stateorprovince' />
                    <attribute name='address1_line1' />
                    <attribute name='address1_country' />
                    <attribute name='address1_longitude' />
                    <attribute name='address1_latitude' />
                    <filter type='and'>
                        <condition attribute='statecode' operator='eq' value='0' />
                        <condition attribute='address1_city' value='Buenos Aires' operator='eq'/>
                    </filter>
                  </entity>
                </fetch>";

 

Por supuesto esto es completamente re-definible solo se usa la opción:

SNAGHTML67b89e9

La búsqueda avanzada abre el dialogo que permite definir los filtros que crean la consulta fetchxml

 

Resolver Geo Localización de las cuentas


Para obtener el posicionamiento global de las cuentas según su dirección haremos uso de google maps

 

public static GeoPosition GetGeoPosition(AccountCRM account)
{
    const string _googleUri = "http://maps.googleapis.com/maps/api/geocode/xml?";

    try
    {
        if (string.IsNullOrEmpty(account.address) ||
            string.IsNullOrEmpty(account.stateorprovince) ||
            string.IsNullOrEmpty(account.country))
            return null;

        string url = string.Format("{0}address={1}&components=locality:{2}|country:{3}&sensor=false"
                               , _googleUri
                               , HttpUtility.UrlEncode(account.address)
                               , account.stateorprovince
                               , account.country);

        WebClient client = new WebClient();
        Uri uri = new Uri(url);
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);

        XmlDocument doc = new XmlDocument();
        doc.Load(request.GetResponse().GetResponseStream());
        XmlNode root = doc.DocumentElement;
        if (root.SelectSingleNode("/GeocodeResponse/status").InnerText == "OK")
        {
            return new GeoPosition()
            {
                Latitude = Double.Parse(root.SelectSingleNode("/GeocodeResponse/result/geometry/location/lat").InnerText),
                Longitude = Double.Parse(root.SelectSingleNode("/GeocodeResponse/result/geometry/location/lng").InnerText)
            };
        }
        else
            return null;

    }
    catch (Exception ex)
    {
        Trace.WriteLine(string.Format("[{0:dd-MM-yyyy HH:mm}] Message:{1}, StackTrace: {2}", DateTime.Now, ex.Message, ex.StackTrace));
        throw;
    }

}

Solo es cuestión de definir la url de google maps con los valores de dirección, localidad y país, lanzar la ejecución mediante WebClient y procesar la respuesta.

El ultimo paso consiste en actualizar la entidad en CRM

 

public static void UpdatePosition(string fetchxml)
{
    System.Threading.Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("en-US");

    //se obtiene la lista de cuentas
    List<AccountCRM> accountList = GetEntityMap(fetchxml);

    using (var service = new OrganizationService("CRMServer"))
    {
        //se recorre cada cuenta para actualizar el posicionamiento
        foreach (var item in accountList)
        {
            //se recupera las coordenadas de posicionamiento
            GeoPosition position = MapHelper.GetGeoPosition(item);

            //se crea la entidad account 
            //a la cual se le actualizara la info de posicionamiento
            var entity = new Entity("account");
            entity["accountid"] = item.id;

            if (position == null)
            {
                entity["address1_latitude"] = null;
                entity["address1_longitude"] = null;
            }
            else
            {
                entity["address1_latitude"] = position.Latitude;
                entity["address1_longitude"] = position.Longitude;
            }

            service.Update(entity);
        }
    }

}

En la primer línea se define la cultura en en-US para evitar problemas de conversión de tipos cuando se recupera las posiciones de geo localización

Una vez que se recuperan las cuentas se recorren para resolver la posición según al dirección que tengan asignada, según la respuesta de google maps se asignara las coordenadas de localización o en caso de no poder resolver se asigna null.

 

Código