martes, 4 de mayo de 2010

[Linq] DataSet – Agrupar y totalizar

 

Introducción


En este articulo analizaremos como trabajar con los datos procedentes de un datatable, realizando operaciones por medio de la ayuda de linq, y luego confeccionado un datatable distinto, cuya estructura de campos difiere de la original.

Para esta operación se contara con un datatable con documento de clientes, y un campo total que informa cuantos de estos se tienen registrados.

La idea es obtener en otro datatable la cantidad total agrupando todos los clientes.

 

Agrupar con Linq


Para esta operación se realizaría una agrupación mediante la funcionalidad que provee linq.

private void button1_Click(object sender, EventArgs e)
{
    IEnumerable<IGrouping<string, DataRow>> query = from item in Datos().AsEnumerable()
                                                    group item by item["Documento"].ToString() into g
                                                    select g;


    DataTable resultado = Transformar(query);

    dataGridView1.AutoGenerateColumns = true;
    dataGridView1.DataSource = resultado;

}

Como se observa en la query de linq se agrupa por medio del campo “Documento”, y el resultado de esta operación se alojo en “g”, valor que es devuelto en la consulta.

Si bien podría haberse igualado toda la consulta a una variable definida como “var”, esto hubiera imposibilitado el pasaje de la información a un método de transformación, para la creación del nuevo datatable, es por eso que se crea el IEnumerable<>, que contiene el IGrouping<> también genérico.

 

Creación del nuevo DataTable


En esta operación en una primera implementación es algo manual, ya que toma el resultado de la consulta linq e itera para cargar el nuevo datatable, y en su camino realiza las operaciones de calculo.

private DataTable Transformar(IEnumerable<IGrouping<string, DataRow>> datos)
{
    //
    // Se define la estructura del DataTable
    //
    DataTable dt = new DataTable();
    dt.Columns.Add("Documento");
    dt.Columns.Add("CantRegistros");
    dt.Columns.Add("Total");

    //
    // Se recorre cada valor agruparo por linq y se vuelca el resultado 
    // en un nuevo registro del datatable
    //
    foreach (IGrouping<string, DataRow> item in datos)
    {
        DataRow row2 = dt.NewRow();
        row2["Documento"] = item.Key;
        row2["CantRegistros"] = item.Count();
        row2["Total"] = item.Sum<DataRow>(x => Convert.ToInt32(x["Total"]));

        dt.Rows.Add(row2);
    }

    return dt;
}

Debe observarse como se trabaja con los métodos extendidos, y en ellos se aplica la funcionalidad Lambda, puntualmente al indica que campo debe sumarse.

 

[C#]
 

 

Creación del DataTable con CopyToDataTable()


Una alternativa algo mejor a la transformación de datos podrías ser por medio del uso del método de extensión CopyToDataTable()

DataTableExtensions.CopyToDataTable<(Of <(T>)>)

Hay que remarcar un punto con este método, ya que de forma estándar solo permite convertir a DataTable aquellas consulta linq que devuelvan como resultado una DataRow.

Pero en nuestro ejemplo planteado esto es diferente ya que el datatable a devolver requiere de una transformación de los registro con respecto a los datos originales.

Pero esta situación esta contemplada, ya que es posible implementar un método CopyToDataTable<> genérico, en donde a base de un tipo de dato se arme un datatable.

Para esta implementación, se hará uso de la explicación proporcionada por siguiente articulo:

Cómo implementar CopyToDataTable<T> donde el tipo genérico T no es un objeto DataRow

El código del link de msdn, se ha volcado al archivo de nombre “CopytoDataTable.cs”, se podrá observar en el código de ejemplo adjunto en el articulo.

La implementación de la funcionalidad quedo reducida al siguiente código:

private void button1_Click(object sender, EventArgs e)
{

    var query = from item in Datos().AsEnumerable()
                group item by item["Documento"].ToString() into g
                select new DocumentoResult
                {
                    Documento = g.Key,
                    CantRegistros = g.Count(),
                    Total = g.Sum(x => Convert.ToInt32(x["Total"]))
                };


    DataTable resultado = query.CopyToDataTable<DocumentoResult>();

    dataGridView1.AutoGenerateColumns = true;
    dataGridView1.DataSource = resultado;

}

Esta nueva alternativa ya no necesita iterar por cada registro que dio como resultado la consulta linq, es mas se puede hacer uso del “var”, puesto que no es necesario pasar por parámetro la consulta para trabajarla.

Pero si se necesito de un pequeño cambio, en donde se ha definido una clase, de nombre “DocumentoResult”, la cual define los campos del nuevo DataTable.

public class DocumentoResult
{
    public string Documento { get; set; }
    public int CantRegistros { get; set; }
    public int Total { get; set; }

}

Siendo esta la clase usada en la definición del método genérico CopyToDataTable<>, cuando se usa en la línea:

DataTable resultado = query.CopyToDataTable<DocumentoResult>();

 

[C#]
 

20 comentarios:

  1. Hola Leandro

    estoy creando un sistema en VB 2008, y necesito crear un boton que guarde los registros me podrias ayudar por favor. No tengo idea de como empezar con el código o si es mas facil con BindingNavigator.

    ResponderEliminar
  2. hola Jeymie

    Te aconsejaria que estas pregunta la realices en el foro, yo particupo alli, o sino alguien mas podra contestarte.

    Explicar este tema puede ser algo extenso, y no creo que en el mensaje del blog quede comodo hacerlo.

    VB.NET Foro

    saludos

    ResponderEliminar
  3. Hola Leandro, al intentar cargar el datatable en un checkedlistbox que no se encuentra en el form me da un error en tiempo de ejecucion que dice lo siguiente "Al menos un objeto debe implementar IComparable.", el error se encuentra localizado en la clase CopytoDataTable, que puedo hacer??? gracias por tu tiempo.

    ResponderEliminar
  4. hola Antonio José

    Respondi la pregunta en el mail


    saludos

    ResponderEliminar
  5. Hola Leandro, no me ha llegado el correo, un saludo.

    ResponderEliminar
  6. hola Antonio José

    Analice linq que estas usando.

    Intenta quitando el order by del al instruccion linq, sino entendi mal es este el que causa el problema del orden, porque estas definiendo una clase anonima que no sabe como ordenar

    Ese mensaje se genera en el metodo CopytoDataTable porque es alli cuando se ejecuta la instruccion linq, recuerda que puede definir el linq pero este no se ejecutara hasta la primera vez que accedas al mismo, mientras tanto no pasara nada

    saludos

    ResponderEliminar
  7. hola leandro estoy realizan una consulta con liq agrupando registro por nombre de cliente, me gustaria saver como agrego otro campo comun como es el rfc al datagridview

    ResponderEliminar
  8. hola TONY

    que seria un campo comun o un rfc en el DataGridView?
    la verdad no conozco estos campos a los cuales haces referencia

    saludos

    ResponderEliminar
  9. el RFC es otro campo de la tabla, solo que nesecito agrupar por nombre de clientey agragr otra columna con su respectivo rfc o clave

    ResponderEliminar
  10. hola TONY

    algo que no me ha quedado claro es porque mencioans un datagridview, cuando el articulo solo trabaja con dataset

    bien en el caso de campos compuesto para agrupar podrias hacer

    group item by new { documento = item["Documento"].ToString(), rfc = Convert.TOInt32(item["rfc"])} into g


    como veras se usa el "new" para definir un tipo anonimo que cree lo compuesto en el agrupado

    veras que use ademas nombre redefinidos para este grupo anonimo
    ahora si haces dentro del linq

    g.Key.documento
    g.Key.rfc


    saludos

    ResponderEliminar
  11. Hola Leandro, al hacer para agrupar

    group item by new { documento = item["Documento"].ToString(), rfc = Convert.TOInt32(item["rfc"])} into g

    da un error de conversion implicita en IEnumerable> ya que no devuelvo un string, como lo puedo solucionar??, un saludo y gracias.

    ResponderEliminar
  12. hola ajlz

    recuerda que estas creando una entidad anonima para agrupar

    deberias usar:
    g.Key.documento
    g.Key.rfc

    ya que es una entidad compleja la que asignas en "g", porque como bien comentas no es un string, es una entidad con dos propiedades

    saludos

    ResponderEliminar
  13. Leandro tengo una pregunta ..de como retornar una lista de Tipo anónimo ..en una consulta de linq c#... este es el error que me sale....Cannot implicitly convert type 'System.Collections.Generic.List' to 'System.Collections.Generic.List'

    ResponderEliminar
  14. hola JosaelECR

    no puedes retornar como resultado de una funcion un objeto anonimo

    podrias si crear una clase concreta y asignar la info para retornarla como respuesta pero algo anonimo no se puede retornar

    saludos

    ResponderEliminar
  15. Hola Leandro, gracias por tu aportación. Fijate que no he podido echar andar el codigo en VS 2005. Ya agregué las referencias de System.Linq, System.XML.Ling, y DataSetExtension... que más me hace falta?.

    Gracias!

    ResponderEliminar
  16. hola Lorenzo

    linq no existe en .net 2.0, no se como has hecho para agregar las referencias, pero dudo que compile

    vas a tener que migrar la solucion a .net 3.5 (VS2008) o superior para poder usar linq

    saludos

    ResponderEliminar
  17. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  18. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  19. Lo que estoy necesitando en realidad es parecido a lo que haces en el código anterior, estoy llenado un DataGridView con un dt, pero necesito antes de llenarlo realizar cálculos sobre la consulta que llena el data dt, como lo podría hacer? dejo el código que uso para que veas como lo estoy haciendo ahora

    Dim conexion As MySqlConnection = New MySqlConnection
    Dim comando As MySqlCommand = New MySqlCommand
    comando.Connection = conexion
    Try
    conexion.ConnectionString = cadenaConexion '"Server=localhost; Database=xxx; Uid=root; Password=xxx; Port=3306;"
    'MessageBox.Show("conectado exitosamente")
    Catch ex As Exception
    MessageBox.Show(ex.Message)
    End Try
    comando.CommandText = "SELECT ........"
    Dim dt As DataTable = New DataTable
    Dim da As MySqlDataAdapter = New MySqlDataAdapter(comando)
    da.Fill(dt)
    dgvArticulos.DataSource = dt

    ResponderEliminar