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, 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


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#]
 

jueves, 8 de noviembre de 2012

[Visual Studio] Base de Datos integradas al proyecto

 

Vamos a analizar una situación particular que se produce al integrar las bases de datos, ya sean Ms Access, Sql Compact, Sql Server, o cualquier otra que pueda integrarse al Visual Studio

Cuando se desarrollan aplicaciones de escritorio, WinForm o WPF y a estos proyectos se le agrega una db, puede que las actualizaciones de datos no impacten sobre el archivo que se visualiza en el “Solution Explorer”.

Nota: si bien el ejemplo del articulo estará basado en Sql Compact (.sdf) la solución planteada aplicaría a otras base de datos como ser Ms Access (.mdb, .accdb), Sql Server (.mdf), etc

 

Problema


Cuando un conectionstring se define usando el nombre de la db o |DataDirectory| para especificar la localización del archivo de base de datos, en ambos casos se hace referencia a la carpeta donde se encuentra ejecutándose el .exe

string connstring = "Data Source=|DataDirectory|Contactos.sdf;Persist Security Info=False;";

Al usar este tipo de connection string es bastante común ver desarrollos en donde la db se encuentra integrada al Visual Studio.

clip_image001

Si se está ejecutando desde el Visual Studio la carpeta, por defecto, donde compila y deja los archivos resultantes será el \bin\Debug del proyecto, es allí donde la aplicación espera que se encuentre la base de datos.

No hay que engañarse, los cambios que realicen actualización a la db no serán efectuadas sobre el archivo que se visualiza integrado al VS (el que se visualiza en la imagen anterior), sino que las modificaciones se realizaran sobre la copia que crea el Visual Studio en la carpeta \bin\Debug

clip_image003

Como se observa, el Visual Studio creo una copia de la db en la carpeta donde compila, es por eso que .exe y .mdb están juntos, es más cuando se detenga la ejecución y se inicie nuevamente una nueva copia será efectuada pisando los datos previos.

Por lo tanto, si luego de ejecutar la aplicación desde el VS, la aplicación realizo actualizaciones en los datos, al detenerla y realizar doble click en el archivo de base de datos que está integrado en el “Solution Explorer” para poder trabajarlo desde el “Server Explorer”, no se visualizara ningún cambio.

clip_image004

Al usar el “Show Table Data” no habrá cambios que visualizar

clip_image005

Sera necesario usar la opción

clip_image006

Para poder acceder a al db que se copió en el \bin\Debug y al hacer bloque click en esta y verla en el Server Explorer allí si estarán las actualizaciones realizaras en la ultima ejecución.

clip_image007

Nota: recuerden que al volver a ejecutar desde el Visual Studio la db que se encuentra en la carpeta \bin\Debug será reemplazada por una nueva copia

Esta situación solo se manifiesta si se ejecuta desde el Visual Studio, cuando se lleve el .exe a la pc del usuario esta situación no se presentara, la aplicación se ejecuta directamente sin intermediarios que realice la copia de la db a una carpeta de compilación (apunto a que no esta el Visual Studio en medio creando una carpeta donde ubicar los archivos resultantes necesario para la ejecución).

 

Solución


Si se quiere evitar esta situación y hacer uso de la misma db que esta integrada al proyecto, el primer paso será deshabilitar la copia que realiza el VS, para ello se debería ir a las propiedades del archivo

clip_image008

Y allí cambiar la opción a “Do not copy”

clip_image009

Y luego se deberá modificar el string de conexión indicando la ruta completa el archivo de la db, usar solo el nombre o |DataDirectory| ya no será valido (porque no se copiara el archivo a la carpeta de compilacion)

string connstring = "Data Source=C:\...\Contactos.sdf;Persist Security Info=False;";

 

Conclusión


Si bien esta característica que aporta el Visual Studio al realizar una copia automática del archivo de base de datos a la carpeta de compilación podría ser útil en algunas situaciones, (como ser aquellas donde se espera datos inicializados para realizar pruebas), pero hay otras donde no es útil, por eso existen opciones que se pueden cambiar para deshabilitar esta acción.

Al implementar la solución mencionada logramos referenciar la db que se encuentra integrada al proyecto.

[ADO.NET] Ms Access y arquitectura 64bit

 

Cuando desarrollamos bajo una plataforma con arquitectura de 64 bits se pueden presentar problema si la db usada se trata de Ms Access

Este problema se presenta porque el motor que usa para establecer la conexión desde código no provee compatibilidad con esta arquitectura

Para efectuar el artículo se hizo uso una PC con arquitectura 64bits

 

Base de datos .mdb


Para demostrar el problema se confecciono un ejemplo simple

clip_image001

Una base de datos Access 2000 integrada en proyecto la cual lista contactos en un grid

string connstring = "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=Contactos.mdb;User Id=admin;Password=;";

try
{
	using (OleDbConnection conn = new OleDbConnection(connstring))
	{
		conn.Open();

		string query = "SELECT * FROM Contactos";
		OleDbCommand cmd = new OleDbCommand(query, conn);
		OleDbDataAdapter da = new OleDbDataAdapter(cmd);
		DataTable dt = new DataTable();
		da.Fill(dt);

		dataGridView1.DataSource = dt;
	}
}
catch (Exception ex)
{
	MessageBox.Show(ex.Message);
}

El proveedor utilizado

Provider=Microsoft.Jet.OLEDB.4.0

Si se ejecuta sin realizar ningún otro cambio se obtendrá

The 'Microsoft.Jet.OLEDB.4.0' provider is not registered on the local machine.

clip_image003

Para que esto no suceda la solución es cambiar el Platform Target del proyecto, para esto solo se debe ir a la propiedades del proyecto

clip_image004

Y allí cambiar la opción mencionada a x86, con esto haremos que el .exe compile con compatibilidad a 32bits

clip_image006

Ahora si al ejecutar la aplicación funcionara sin problemas

clip_image007

 

Base de datos .accdb


Otra alternativa que podría evaluarse es la utilización de ACE.OLEDB como proveedor para trabajar con la db Ms Access, para ello seguramente se necesite instalar

Componente redistribuible del motor de base de datos de Microsoft Access 2010

Nota: si se tiene el Office 2010 (o superior) en la pc quizás no se requiera la instalación

Al cambiar de proveedor, ya no se usara Jet por lo que se define

Provider=Microsoft.ACE.OLEDB.12.0

clip_image009

Al ejecutar la aplicación funciona correctamente más allá de estar definida para compilar a 64bits

Nota: también se probó ACE.OLEDB con una base de datos .mdb (generada con Ms Access 2010 pero grabada con compatibilidad con Access 2000) pudiendo establecer la conexión correctamente sin que afectara la arquitectura 64bits

 

Conclusión


Si se quiere hacer uso de Jet como proveedor para establecer la conexión a los datos será necesario cambiar en el proyecto la plataforma a la cual se compila

Para evitar el problema se hará uso del proveedor ACE.OLEDB el cual proporciona compatibilidad con 64bit