viernes, 31 de mayo de 2013

[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


viernes, 15 de marzo de 2013

n-Layer - SchoolManager - Herencia y navegación de entidades relacionadas (2/2)

 

Introducción


Se continua con la explicación que comenzó en:

 n-Layer - SchoolManager - Herencia y navegación de entidades relacionadas (1/2)

En esta ocasión nos centraremos en como recuperar entidades relacionadas o asociadas, usando ado.net

Vamos a enfocarnos en el vinculo que tiene un un instructores y sus cursos.

image

En el articulo anterior se pudo comprender como se persisten las relaciones en conjunto con los concepto de herencia, pero en este caso vamos a recuperar la información de una entidad y sus relaciones todo en la misma operación.

Seguramente seria una buena idea crear dos funcionalidades, una que permita recuperar la entidad sin sus relaciones GetByKey(), o sea la entidad pura, y otro método en donde una única query recupere y arme la estructura jerárquica GetByKeyWithRelations().

 

Recuperar entidad y relaciones


Para poder llevar a cabo esta tarea se va a necesitar de la ayuda de linq, este permitirá trabajar una entidad simple para darle estructura.

Comenzaremos definiendo una entidad plana que contenga las propiedades tanto del Instructor como del curso.

public class InstructorComposed
{
    public int PersonID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }

    //Fecha de contratación
    public DateTime? HireDate { get; set; }
    public string Location { get; set; }

    public int CourseID { get; set; }
    public string Title { get; set; }
}

Pero además se deberá tener la entidad que nos interesa devolver como respuesta, esta si tiene estructura, o sea una lista de cursos

public abstract class PersonEntity
{
    public int PersonID {get; set;}

    public string LastName {get; set;}
    public string FirstName {get; set;}
}

public class InstructorEntity : PersonEntity
{
    public InstructorEntity()
    {
        this.Courses = new List<CourseEntity>();
    }

    //Fecha de contratación
    public DateTime? HireDate { get; set; }

    public string Location { get; set; }

    public List<CourseEntity> Courses { get; set; }

}


public class CourseEntity
{
    public int CourseID { get; set; }
    public string Title { get; set; }
}

 

El reto será lograr convertir una entidad de propiedades simples a una de propiedades complejas. Seguramente se preguntaran porque no se recupera esto directo con una query, el tema es que una consulta no devuelve como resultado registros con estructuras anidadas, sino que solo nos proporciona dos dimensiones, filas y columnas.

Se ha creado el método GetByKeyWithRelations() para obtener la entidad Instructor con sus cursos relacionados:

/// <summary>
/// Devuelve el instructor incluyendo las relaciones con las demas entidades
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public static InstructorEntity GetByKeyWithRelations(int id)
{
    InstructorEntity item = null;

    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        string query = @"SELECT P.PersonID, 
                                P.LastName, 
                                P.FirstName, 
                                P.HireDate, 
                                P.EnrollmentDate, 
                                OA.Location,
                                C.CourseID,
                                C.Title
                        FROM Person P 
                            INNER JOIN OfficeAssignment OA 
                            ON P.PersonID = OA.InstructorID
                                INNER JOIN CourseInstructor CI 
                                ON CI.PersonID = P.PersonID
                                    LEFT JOIN Course C
                                    ON C.CourseID = CI.CourseID
                        WHERE P.PersonID = @id
                        ORDER BY P.PersonID";

        SqlCommand cmd = new SqlCommand(query, conn);
        cmd.Parameters.Add("@id", SqlDbType.Int).Value = id;

        SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);

        item = ConvertInstructorWithRelations(reader);
        
    }

    return item;
}

Es importante destacar como en la consulta sql hace uso de un LEFT JOIN para recuperar todos los datos del instructor tengan o no cursos asociados, el SELECT incluirá los campos de la entidad Instructor y también los del Curso, serán coincidentes con la definición de la clase InstructorComposed.

El siguiente paso será procesar los datos y darles formato:

private static InstructorEntity ConvertInstructorWithRelations(IDataReader reader)
{

    List<InstructorComposed> list = new List<InstructorComposed>();

    while(reader.Read())
    {
        InstructorComposed item = new InstructorComposed(){
            PersonID = Convert.ToInt32(reader["PersonID"]),
            LastName = Convert.ToString(reader["LastName"]),
            FirstName = Convert.ToString(reader["FirstName"]),
            HireDate = reader["HireDate"] == DBNull.Value ? (DateTime?)null : Convert.ToDateTime(reader["HireDate"]),
            Location = Convert.ToString(reader["Location"]),

            CourseID = Convert.ToInt32(reader["CourseID"]),
            Title = Convert.ToString(reader["Title"])
        };

        list.Add(item);
    }

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

    InstructorEntity instructor = (from item in list
                                   group item by item.PersonID into g
                                   select new InstructorEntity()
                                   {
                                       PersonID = g.Key,
                                       LastName = g.First().LastName,
                                       FirstName = g.First().FirstName,
                                       HireDate = g.First().HireDate,
                                       Location = g.First().Location,
                                       Courses = g.Select(x=> new CourseEntity()
                                                            {
                                                                CourseID = x.CourseID,
                                                                Title = x.Title
                                                            }).ToList()
                                   }).First();

    return instructor;

}

 

La primer parte es bien conocida, se convierte el reader volcando los datos de los campos a la instancia de la entidad. Pero la segunda parte es la mas interesante, porque es allí donde mediante la utilización de linq que damos estructura al objeto plano que se recupera de la query.

En este caso se hace uso de la capacidad de agrupar que brinda linq para poder juntar todos los cursos que pertenecen al instructor. En el ejemplo solo  se necesito recupera un único instructor pero podría haberse utilizado la misma técnica para trabajar una colección de estos.

 

Código


El código se encuentra en el articulo anterior.

jueves, 14 de marzo de 2013

n-Layer - SchoolManager - Herencia y navegación de entidades relacionadas (1/2)

 

Introducción


El diseño de una estructura en capas mucha veces requiere que se trabajen con objetos complejos, por lo general una entidad no tienes solo propiedades simples, algunas puedes representar la relación con otras entidades, es mas puede que la entidad en si sea solo una parte de una mayor

En esta oportunidad nos centraremos justamente en dos aspectos:

  • representar una herencia, aquí abordaremos no solo como recuperar una entidad definida con un padre, sino también como persistirla
  • cargar entidades relacionadas,

Para esto contaremos con la entidad Instructor en el modelo de administración de la base de datos de una escuela.

SNAGHTML248b611

El modelo de datos que usaremos define la tabla Persona, pero en la misma tabla se pueden abstraer otras dos entidades Instructor y Alumno.

image

La implementación de la herencia en este caso lleva el nombre de “tabla por subclase”, en donde la relación uno a uno con la tabla OfficeAssigment determina si es un instructor o un Alumno, en este caso no se usa ningún campo discriminador para el tipo, la relación actúa como medio para determinarlo. Si hay una relación con la tabla OfficeAssigment  será un instructor, sino lo hay será un alumno.

 

Herencia de entidades (Recuperar entidad) 


Las entidades intervinientes en este modelo se representan en la siguiente imagen:

image

 

A simple vista se puede observar que la entidad Instructor hereda de persona, la pregunta que trataremos de responder es como definir un modelo de persistencia para esta entidad.

Empezaremos analizando la clase InstructorRepository la cual cuenta con el método:

public static InstructorEntity GetByKey(int id)
{
    InstructorEntity item = null;

    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        string query = @"SELECT P.PersonID, 
                                P.LastName, 
                                P.FirstName, 
                                P.HireDate, 
                                P.EnrollmentDate, 
                                OA.Location
                        FROM Person P 
                            INNER JOIN OfficeAssignment OA 
                            ON P.PersonID = OA.InstructorID
                        WHERE P.PersonID = @id
                        ORDER BY P.PersonID";

        SqlCommand cmd = new SqlCommand(query, conn);
        cmd.Parameters.Add("@id", SqlDbType.Int).Value = id;

        SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.CloseConnection);

        if (reader.Read())
        {
            item = ConvertInstructor(reader);
        }

    }

    return item;
}
private static InstructorEntity ConvertInstructor(IDataReader reader)
{
    InstructorEntity item = new InstructorEntity();

    item.PersonID = Convert.ToInt32(reader["PersonID"]);
    item.LastName = Convert.ToString(reader["LastName"]);
    item.FirstName = Convert.ToString(reader["FirstName"]);

    item.HireDate = reader["HireDate"] == DBNull.Value ? (DateTime?)null : Convert.ToDateTime(reader["HireDate"]);
    
    item.Location = Convert.ToString(reader["Location"]);

    return item;


}

Recuperar una entidad, o una lista de Instructores no parece diferir mucho a como se haría con una entidad simple, en este caso la query involucra tanto la tabla del Instructores como la de Personas, lo que implica usar el INNER JOIN para unir los registros.

Herencia de entidades (Crear entidad)


Donde si veremos mayor cambios es al momento de actualizar la entidad, pues requiere impactar las actualizaciones en dos tablas diferentes

Empecemos por crear un nuevo instructor, la clase InstructorRepository contiene el método Save()

Definir un instructor implica varios pasos:

  1. crear el registro en la tabla base, en este caso insertar el registro en Persona
  2. crear el registro en la tabla OfficeAssignment
  3. si la entidad tenia cursos asignados se crea la relación con esto

 

1- Grabar la entidad Persona, esta operación es bien simple, solo implica un INSERT en la tabla y recuperar el id generado.

public static void Save(PersonEntity person)
{
    string sql = @"INSERT INTO Person (
                    LastName,
                    FirstName)
              VALUES (@LastName, 
                    @FirstName);
              SELECT SCOPE_IDENTITY";


    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        SqlCommand cmd = new SqlCommand(sql, conn);
        cmd.Parameters.AddWithValue("@LastName", person.LastName);
        cmd.Parameters.AddWithValue("@FirstName", person.FirstName);

        person.PersonID = Convert.ToInt32(cmd.ExecuteScalar());

    }
}

2- Aquí no solo se actualiza los datos concretos del instructor en la tabla padre, sino que además se inserta en la tabla concreta que define el tipo, en esta operación se hace uso del mismo id que se recupero al crear la entidad padre.

public static void Save(InstructorEntity instructor)
{

    PersonRepository.Save((PersonEntity)instructor);

    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        //
        // Actualiza los campos de la tabla Persona 
        // con el campo que define solo el instructor
        //
        string sqlUpdateP = @"UPDATE Person 
                            SET HireDate = @HireDate 
                            WHERE PersonID = @PersonID";

        using (SqlCommand cmd = new SqlCommand(sqlUpdateP, conn))
        {
            cmd.Parameters.AddWithValue("@HireDate", instructor.HireDate.HasValue ? instructor.HireDate.Value : (object)DBNull.Value);
            cmd.Parameters.AddWithValue("@PersonID", instructor.PersonID);

            cmd.ExecuteNonQuery();
        }

        //
        // Inserta el registro que define al instructor concretamente
        //
        string spInsertOA = @"INSERT OfficeAssignment (InstructorID, Location) 
                                   VALUES (@InstructorID, @Location)";

        using (SqlCommand cmd = new SqlCommand(spInsertOA, conn))
        {
            cmd.Parameters.AddWithValue("@InstructorID", instructor.PersonID);
            cmd.Parameters.AddWithValue("@Location", instructor.Location);

            cmd.ExecuteNonQuery();
        }

    }

    //
    // Se procesa los cursos asignados 
    //
    CourseRepository.RelateWithPerson((PersonEntity)instructor, instructor.Courses);

}

3- En caso de existir entidades relacionadas se realiza la operación de merge entre los datos provenientes de la selección del usuario y los datos existentes en la tabla

Para realizar la tarea de forma simple se elimina toda relación y se procede a crearlas nuevamente, pero si se anima se podría haber utilizado la instrucción MERGE de T-SQL.

En la línea:

//
// Se procesa los cursos asignados 
//
CourseRepository.RelateWithPerson((PersonEntity)instructor, instructor.Courses);

Se invoca la clase responsable de crear la relación entre las entidad persona y cursos.

 

/// <summary>
/// Dada una persona y una lista de cursos se crea la relacion entre las entidades.
/// 
/// Para implementar un merge simple que permita registrar los cursos agregados o eliminados por el usuario, 
/// se realiza se elimina toda la relacion y volverla a insertar 
/// </summary>
/// <param name="person"></param>
/// <param name="courses"></param>
public static void RelateWithPerson(PersonEntity person, List<CourseEntity> courses)
{

    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        //se elimina la relacion existentes
        string sqlDelete = @"DELETE CourseInstructor WHERE PersonID = @PersonID";

        using (SqlCommand cmd = new SqlCommand(sqlDelete, conn))
        {
            cmd.Parameters.AddWithValue("@PersonID", person.PersonID);

            cmd.ExecuteNonQuery();
        }


        //se relaciona los cursos asociados a la entidad 
        string sqlCourseInstructor = @"INSERT CourseInstructor (CourseID, PersonID) 
                                            VALUES (@CourseID, @PersonID)";

        using (SqlCommand cmd = new SqlCommand(sqlCourseInstructor, conn))
        {

            foreach (CourseEntity course in courses)
            {
                cmd.Parameters.Clear();
                cmd.Parameters.AddWithValue("@CourseID", course.CourseID);
                cmd.Parameters.AddWithValue("@PersonID", person.PersonID);

                cmd.ExecuteNonQuery();
            }
        }

    }


}

Herencia de entidades (Actualizar entidad)


La actualización de une entidad que implementar una herencia es muy similar a la creación.

  1. actualizar el registro en la tabla base
  2. actualizar el registro en la tabla OfficeAssignment
  3. si la entidad tenia cursos asignados se crea la relación con esto

 

1- Se invoca al metodo Update() de PersonRepository

public static void Update(PersonEntity person)
{
    string sql = @"UPDATE Person SET
                        LastName = @LastName,
                        FirstName = @FirstName
                    WHERE PersonID = @PersonID";

    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        SqlCommand cmd = new SqlCommand(sql, conn);
        cmd.Parameters.AddWithValue("@LastName", person.LastName);
        cmd.Parameters.AddWithValue("@FirstName", person.FirstName);
        cmd.Parameters.AddWithValue("@PersonID", person.PersonID);

        cmd.ExecuteNonQuery();

    }
}

 

2 – Se actualiza la tabla que define al Instructor, este se define en el método Update() de la clase InstructorRepository.

public static void Update(InstructorEntity instructor)
{
    //
    //Se actualiza el registro de la Persona
    //
    PersonRepository.Update((PersonEntity)instructor);


    using (SqlConnection conn = new SqlConnection(ConfigurationManager.ConnectionStrings["default"].ToString()))
    {
        conn.Open();

        //
        // Actualiza los campos de la tabla Persona 
        // con el campo que define solo el instructor
        //
        string sqlUpdateP = @"UPDATE Person 
                                SET HireDate = @HireDate 
                            WHERE PersonID = @PersonID";

        using (SqlCommand cmd = new SqlCommand(sqlUpdateP, conn))
        {
            cmd.Parameters.AddWithValue("@HireDate", instructor.HireDate.HasValue ? instructor.HireDate.Value : (object)DBNull.Value);
            cmd.Parameters.AddWithValue("@PersonID", instructor.PersonID);

            cmd.ExecuteNonQuery();
        }

        //
        // Actualiza el registro que define al instructor concretamente
        //
        string sqlUpdateOA = @"UPDATE OfficeAssignment 
                                        SET Location = @Location 
                                   WHERE InstructorID = @InstructorID";

        using (SqlCommand cmd = new SqlCommand(sqlUpdateOA, conn))
        {
            cmd.Parameters.AddWithValue("@InstructorID", instructor.PersonID);
            cmd.Parameters.AddWithValue("@Location", instructor.Location);

            cmd.ExecuteNonQuery();
        }

    }

    //
    // Se procesa los cursos asignados 
    //
    CourseRepository.RelateWithPerson((PersonEntity)instructor, instructor.Courses);

}

3 – Al igual que al crear la entidad se actualizan las relaciones con las demás entidades, en este caso se aplica el mismo código para reflejar la relación con los cursos.

 

Código


 

[c#]
 

domingo, 17 de febrero de 2013

[Winform] Instanciar Form conociendo su nombre

 

Introducción


Conociendo solo el nombre del formulario se abra una instancia del mismo.

Para poder realizar esta acción es necesario conocer el nombre del form, pero también bajo que namespace se ubica.

image

por ejemplo para abrir el form de alta de cliente se identifica

SNAGHTML239b6cc

pero si otro form se encuentra dentro de una carpeta es necesario definir el namespace donde se encuentra

SNAGHTML23d2650

Uso de Reflection


Para poder abrirlo vamos a necesitara Reflection el cual no permitirá descubrir el Type del form basado en la unión del namespace + nombre

es por eso que se define:

using System.Reflection;

 

private void btnAbrirForm_Click(object sender, EventArgs e)
{
    Assembly asm = Assembly.GetEntryAssembly();
    Type formtype = asm.GetType(string.Format("{0}.{1}", txtNamespace.Text, txtNombreForm.Text));

    Form f = (Form)Activator.CreateInstance(formtype);
    f.Show();
}

Se toma el assembly donde se encuentran los formularios y se obtiene el tipo, con el cual se creara la instancia dinámicamente

Código


[C#]
 

domingo, 10 de febrero de 2013

[WinForms] Verificar si el form esta abierto (instancia única)

 

Introducción


Determinar si un form ha sido abierto, evitando que una nueva instancia sea creada.

 

Validar instancia entorno SDI


Para validar la existencia de la instancia de un formulario se hará uso del Application.OpenForms, con este y la ayuda de linq podremos buscar la existencia del form abierto.

image

La selección de botón abrirá una instancia, pero las siguientes pulsaciones solo harán que el mismo form pase al frente.

 

private void btnAbrirFormclientes_Click(object sender, EventArgs e)
{
    //se localiza el formulario buscandolo entre los forms abiertos 
    Form frm = Application.OpenForms.Cast<Form>().FirstOrDefault(x => x is frmEdicionCliente);

    if (frm != null)
    {
        //si la instancia existe la pongo en primer plano
        frm.BringToFront();
        return;
    }
    
    //sino existe la instancia se crea una nueva
    frm = new frmEdicionCliente();
    frm.Show();
}

El truco esta en poder determinar la lista de forma abiertos

image

 

Validar instancia entorno MDI


En un entorno MDI la validación es similar solo cambia la propiedad usada para determinar la lista de forms abiertos

image

 

private void abrirFormClienteToolStripMenuItem_Click(object sender, EventArgs e)
{
    //se localiza el formulario buscandolo entre los forms abiertos 
    Form frm = this.MdiChildren.FirstOrDefault(x => x is frmEdicionCliente);

    if (frm != null)
    {
        //si la instancia existe la pongo en primer plano
        frm.BringToFront();
        return;
    }
    
    //se abre el form de clientes
    frm = new frmEdicionCliente();
    frm.MdiParent = this;
    frm.Show();

}

 

Código


domingo, 3 de febrero de 2013

[WinForms] Listar formulario del proyecto

 

Introducción


Listar los forms del proyecto puede parecer una tarea a simple vista fácil, pero tiene sus particularidades.

En el ejemplo se listara los nombre de los formularios, pudiendo abrirlo mediante la selección en una lista.

SNAGHTML2eee2d7c

 

Listar Formularios


Para listar los formularios podremos ayudarnos de linq, este nos permitirá filtrar por el tipo de clase del cual heredan los forms.

Para obtener las propiedades (como ser el Text) del formulario se requiere una instancia del mismo. Recordemos que en .net todos son objetos, por lo que un form requiere de una instancia para poder acceder a sus propiedades.

 

private void frmPrincipal_Load(object sender, EventArgs e)
{
    
    Assembly asm = Assembly.GetExecutingAssembly();
    
    List<Form> formList = asm.GetTypes()
                                .Where(x => x.IsSubclassOf(typeof(Form)))
                                .Select(x => (Form)Activator.CreateInstance(x))
                                .ToList();


    lstForms.DisplayMember = "Text";
    lstForms.DataSource = formList;
    
}

También se puede realizar la misma tarea sin hacer uso de linq

 

private void frmPrincipal_Load(object sender, EventArgs e)
{

    Assembly asm = Assembly.GetExecutingAssembly();

    List<Form> formList = new List<Form>();

    foreach (Type item in asm.GetTypes())
    {
        if (item.IsSubclassOf(typeof(Form)))
        {
            formList.Add((Form)Activator.CreateInstance(item));
        }
    }

    lstForms.DisplayMember = "Text";
    lstForms.DataSource = formList;

}

Código


martes, 15 de enero de 2013

[GridView] - Ocultar dinámicamente botones, imágenes y link de la fila

 

Introducción


Al desplegar una tabla de  información al usuario es muy común proporcionar acciones sobre los registros, en este caso no centraremos en el uso del GridView el cual aplicara ciertas condiciones sobre estas acciones.

Se trabajar con una lista de Productos a los cuales se podrá realizar acciones.

Las reglas que se aplican son:

  • Si cantidad de stock esta en cero mostrara el link de “Order” para poder solicitar la compra.
  • Si el producto esta discontinuo se mostrara el icono en la columna “Discontinued”
  • Si el producto esta discontinuo no se permitirá editar el producto

image

 

Ocultar las acciones dinámicamente


Para lograr el objetivo usaremos un único evento, el RowDataBound, el cual se lanzara por cada row que sea generada en el grid.

El código completo seria:

protected void gvProducts_RowDataBound(object sender, GridViewRowEventArgs e)
{
    if (e.Row.RowType != DataControlRowType.DataRow)
        return;

    //se recupera la entidad que genera la row
    Product prod = e.Row.DataItem as Product;

    //se valida que el stock esta en cero 
    //para remover el link de la primer columna
    if (prod.UnitsInStock != 0)
        e.Row.Cells[0].Controls.Clear();

    //se verifica si el producto esta discontinuo
    //removiendo el icono de la columna Discontinued
    if (!prod.Discontinued)
        e.Row.Cells[1].Controls.Clear();

    //se verifica si el producto esta discontinuo
    //para quitar el boton de edicion
    if (prod.Discontinued)
    {
        ImageButton img = e.Row.FindControl("imgEdit") as ImageButton;
        e.Row.Cells[7].Controls.Remove(img);
    }

    //esta es equivalente a la opcion anterior 
    //que remueve la imagen de edicion
    //if (prod.Discontinued)
    //{
    //    e.Row.Cells[7].Controls.RemoveAt(1);
    //}

}

Ahora vamos a analizarlos por partes

Línea 7 : El uso del e.Row.DataItem permite recuperar la entidad concreta del origen de datos para poder usarla en la evaluación, con esta se puede acceder a propiedades de la entidad sin necesidad de haber definido una columna en el grid. En este caso se utiliza una entidad creada por uno, pero si se utilizara un DataTable se debe castear a un DataRowView.

Líneas 11-12: verifica el stock y remueve el control Link de la celda

Líneas 16-17: valida la propiedad Discontinued y remueve la imagen cuando el producto esta activo.

Líneas 21-25: hace uso de la propiedad Discontinued para conocer si debe evitar que el producto pueda ser editado.

Líneas  29-32: implementan la misma funcionalidad que las líneas 21-25, solo es otra forma de lograr lo mismo, pero sin tener que buscar el ImageButton

 

Código


Se requiere VS 2010 y Sql Server 2008 R2

[C#]