jqgrid + EF + MVC: ¿Cómo exportar en excel? ¿Qué método sugieres?

Resuelto Larry asked hace 12 años • 2 respuestas

Estoy usando jqgrid (estándar) con EF 4 + MVC3 . Me gustaría implementar la exportación de Excel. ¿Qué método me sugerirías?

Para generar Excel, me gustaría usar esta biblioteca del Dr. Stephen Walther, que tiene tres tipos de resultados y también permite definir encabezados. Por favor dígame si lo encuentra válido para mi propósito.

Hago esta pregunta porque todavía me estoy acercando a implementar la exportación a Excel y encontré varias técnicas. Algunos sugieren realizar una exportación csv, otros indican que debería devolver una salida JSON y no me queda claro si esta capacidad está presente en la versión gratuita de jqgrid. En cualquier caso, me gustaría pasar los datos al objeto de Walther.

Sobre el código jqgrid, encontré esta interesante respuesta de Oleg, pero no entiendo si podría aplicarse a mis necesidades.

Desafortunadamente, hasta ahora solo encontré partes de soluciones para exportar Excel con EF MVC, pero ninguna solución o ejemplos completos...

Acerca de la lógica MVC, voy a implementar y desarrollar este código como amablemente me sugirió @Tommy.

Lo siento si la pregunta puede ser tonta, solo soy un principiante (entusiasta).

¡Gracias por tu valiosa ayuda! Atentamente

Larry avatar Feb 18 '12 15:02 Larry
Aceptado

Como escribí antes (ver aquí y aquí , por ejemplo), la mejor manera de exportar datos de cuadrícula a XML es utilizar Open XML SDK 2.0 .

La publicación del Dr. Stephen Walther muestra cómo crear un archivo HTML que pueda leerse en Excel. No es un archivo de Excel y aún debe convertirse al formato de Excel. El uso de CSV tiene aún más problemas. Dependiendo del contenido de la tabla de origen, la conversión automática a tipos de datos de Excel puede ser absolutamente incorrecta. En un proyecto que desarrollé para un cliente, la cuadrícula contenía información sobre productos de software: nombre del producto, versión, etc. La versión del software a veces aparece como la fecha (1.3.1963, por ejemplo) y dichas celdas se convertirán incorrectamente (en alemán se usa '.' como separador en la fecha). Como resultado, tuvimos problemas realmente difíciles. El uso de CSV con textos que contienen comas también suele importarse incorrectamente. Incluso cuando uno cita las celdas que tienen comas ( ,) y escapa los textos que tienen cuotas, la importación sigue siendo incorrecta, especialmente en la primera columna. No quiero explicar aquí toda la historia de todos los intentos y errores, pero después de todo decido dejar el uso de CSV y HTML y comencé a usar Open XML SDK 2.0, que permite crear archivos Excel reales. con extensión XLSX. . La forma me parece perfecta porque no es necesario ningún componente de Office instalado en el servidor, ni licencias adicionales.

La única restricción es que uno debería poder utilizarDocumentFormat.OpenXml.dll , por lo que su programa de servidor debe ejecutarse en cualquier sistema operativo Windows. Como es bien sabido, el archivo XLSX es un archivo ZIP que contiene algunos archivos XML en su interior. Si aún no lo sabes, te recomiendo cambiar el nombre del archivo XLSX a archivo ZIP y extraerlo. Open XML SDK 2.0 es la biblioteca que funciona con archivos XLSX como con archivos XML. Por lo tanto, no se requieren componentes de Office adicionales.

Se puede encontrar mucha información sobre cómo utilizar Open XML SDK 2.0 (consulte aquí , aquí y aquí ). Muchos ejemplos de códigos útiles se pueden encontrar directamente en MSDN (consulte aquí ). Sin embargo, el uso práctico de Open XML SDK 2.0 no es tan fácil, al menos al principio. Entonces creé una demostración a partir de las partes del código que usé yo mismo.

Puede descargar el proyecto de demostración desde aquí . La demostración es una extensión de las demostraciones de la respuesta y esta. .

Para exportar datos utilizo la DataForExcelclase auxiliar. Tiene constructor en la forma

DataForExcel(string[] headers, DataType[] colunmTypes, List<string[]> data,
             string sheetName)

o en una forma un poco simplificada

DataForExcel(string[] headers, List<string[]> data, string sheetName)

y el único método público

CreateXlsxAndFillData(Stream stream)

El uso de la clase para crear un archivo Excel puede ser como el siguiente

var excelData = new DataForExcel (
    // column Header
    new[]{"Col1", "Col2", "Col3"},
    new[]{DataForExcel.DataType.String, DataForExcel.DataType.Integer,
          DataForExcel.DataType.String},
    new List<string[]> {
        new[] {"a", "1", "c1"},
        new[] {"a", "2", "c2"}
    },
    "Test Grid");
Stream stream = new FileStream ("Test.xlsx", FileMode.Create);
excelData.CreateXlsxAndFillData (stream);
stream.Close();

El uso en la demostración de ASP.NET MVC es el siguiente

static readonly string[] HeadersQuestions = {
    "Id", "Votes", "Title"
};
static readonly DataForExcel.DataType[] ColunmTypesQuestions = {
    DataForExcel.DataType.Integer,
    DataForExcel.DataType.Integer,
    DataForExcel.DataType.String
};

public ActionResult ExportAllQuestionsToExcel () {
    var context = new HaackOverflowEntities ();
    var questions = context.Questions;
    questions.MergeOption = MergeOption.NoTracking; // we don't want to update the data

    // to be able to use ToString() below which is NOT exist in the LINQ to Entity
    // we should include in query only the properies which we will use below
    var query = questions.ToList ();
    if (query.Count == 0)
        return new EmptyResult ();
    var data = new List<string[]> (query.Count);
    data.AddRange (query.Select (item => new[] {
        item.Id.ToString(CultureInfo.InvariantCulture),
        item.Votes.ToString(CultureInfo.InvariantCulture),
        item.Title
    }));

    return new ExcelResult (HeadersQuestions, ColunmTypesQuestions, data,
                            "Questions.xlsx", "Questions");
}

donde ExcelResultse definen como

public class ExcelResult : ActionResult {
    private readonly DataForExcel _data;
    private readonly string _fileName;

    public ExcelResult (string[] headers, List<string[]> data, string fileName, string sheetName) {
        _data = new DataForExcel (headers, data, sheetName);
        _fileName = fileName;
    }

    public ExcelResult (string[] headers, DataForExcel.DataType[] colunmTypes, List<string[]> data, string fileName, string sheetName) {
        _data = new DataForExcel (headers, colunmTypes, data, sheetName);
        _fileName = fileName;
    }

    public override void ExecuteResult (ControllerContext context) {
        var response = context.HttpContext.Response;
        response.ClearContent();
        response.ClearHeaders();
        response.Cache.SetMaxAge (new TimeSpan (0));

        using (var stream = new MemoryStream()) {
            _data.CreateXlsxAndFillData (stream);

            //Return it to the client - strFile has been updated, so return it. 
            response.AddHeader ("content-disposition", "attachment; filename=" + _fileName);

            // see http://filext.com/faq/office_mime_types.php
            response.ContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
            response.ContentEncoding = Encoding.UTF8;
            stream.WriteTo (response.OutputStream);
        }
        response.Flush();
        response.Close();
    }
}

Para completar el código tengo que incluir el código de la clase DataForExcel:

public class DataForExcel {
    public enum DataType {
        String,
        Integer
    }
    private readonly string[] _headers;
    private readonly DataType[] _colunmTypes;
    private readonly List<string[]> _data;
    private readonly string _sheetName = "Grid1";
    private readonly SortedSet<string> _os = new SortedSet<string> ();
    private string[] _sharedStrings;

    private static string ConvertIntToColumnHeader(int index) {
        var sb = new StringBuilder ();
        while (index > 0) {
            if (index <= 'Z' - 'A') // index=0 -> 'A', 25 -> 'Z'
                break;
            sb.Append (ConvertIntToColumnHeader (index / ('Z' - 'A' + 1) - 1));
            index = index % ('Z' - 'A' + 1);
        }
        sb.Append ((char)('A' + index));
        return sb.ToString ();
    }

    private static Row CreateRow(UInt32 index, IList<string> data) {
        var r = new Row { RowIndex = index };
        for (var i = 0; i < data.Count; i++)
            r.Append (new OpenXmlElement[] { CreateTextCell (ConvertIntToColumnHeader (i), index, data[i]) });

        return r;
    }

    private Row CreateRowWithSharedStrings(UInt32 index, IList<string> data) {
        var r = new Row { RowIndex = index };
        for (var i = 0; i < data.Count; i++)
            r.Append (new OpenXmlElement[] { CreateSharedTextCell (ConvertIntToColumnHeader (i), index, data[i]) });

        return r;
    }

    private Row CreateRowWithSharedStrings(UInt32 index, IList<string> data, IList<DataType> colunmTypes) {
        var r = new Row { RowIndex = index };
        for (var i = 0; i < data.Count; i++)
            if (colunmTypes != null && i < colunmTypes.Count && colunmTypes[i] == DataType.Integer)
                r.Append (new OpenXmlElement[] { CreateNumberCell (ConvertIntToColumnHeader (i), index, data[i]) });
            else
                r.Append (new OpenXmlElement[] { CreateSharedTextCell (ConvertIntToColumnHeader (i), index, data[i]) });

        return r;
    }

    private static Cell CreateTextCell(string header, UInt32 index, string text) {
        // create Cell with InlineString as a child, which has Text as a child
        return new Cell (new InlineString (new Text { Text = text })) {
            // Cell properties
            DataType = CellValues.InlineString,
            CellReference = header + index
        };
    }

    private Cell CreateSharedTextCell(string header, UInt32 index, string text) {
        for (var i=0; i<_sharedStrings.Length; i++) {
            if (String.Compare (_sharedStrings[i], text, StringComparison.Ordinal) == 0) {
                return new Cell (new CellValue { Text = i.ToString (CultureInfo.InvariantCulture) }) {
                    // Cell properties
                    DataType = CellValues.SharedString,
                    CellReference = header + index
                };
            }
        }
        // create Cell with InlineString as a child, which has Text as a child
        throw new InstanceNotFoundException();
    }

    private static Cell CreateNumberCell(string header, UInt32 index, string numberAsString) {
        // create Cell with CellValue as a child, which has Text as a child
        return new Cell (new CellValue { Text = numberAsString }) {
            // Cell properties
            CellReference = header + index
        };
    }

    private void FillSharedStringTable(IEnumerable<string> data) {
        foreach (var item in data)
            _os.Add (item);
    }

    private void FillSharedStringTable(IList<string> data, IList<DataType> colunmTypes) {
        for (var i = 0; i < data.Count; i++)
            if (colunmTypes == null || i >= colunmTypes.Count || colunmTypes[i] == DataType.String)
                _os.Add (data[i]);
    }

    public DataForExcel(string[] headers, List<string[]> data, string sheetName) {
        _headers = headers;
        _data = data;
        _sheetName = sheetName;
    }

    public DataForExcel(string[] headers, DataType[] colunmTypes, List<string[]> data, string sheetName) {
        _headers = headers;
        _colunmTypes = colunmTypes;
        _data = data;
        _sheetName = sheetName;
    }

    private void FillSpreadsheetDocument(SpreadsheetDocument spreadsheetDocument) {
        // create and fill SheetData
        var sheetData = new SheetData ();

        // first row is the header
        sheetData.AppendChild (CreateRow (1, _headers));

        //const UInt32 iAutoFilter = 2;
        // skip next row (number 2) for the AutoFilter
        //var i = iAutoFilter + 1;
        UInt32 i = 2;

        // first of all collect all different strings in OrderedSet<string> _os
        foreach (var dataRow in _data)
            if (_colunmTypes != null)
                FillSharedStringTable (dataRow, _colunmTypes);
            else
                FillSharedStringTable (dataRow);
        _sharedStrings = _os.ToArray ();

        foreach (var dataRow in _data)
            sheetData.AppendChild (_colunmTypes != null
                                      ? CreateRowWithSharedStrings (i++, dataRow, _colunmTypes)
                                      : CreateRowWithSharedStrings (i++, dataRow));

        var sst = new SharedStringTable ();
        foreach (var text in _os)
            sst.AppendChild (new SharedStringItem (new Text (text)));

        // add empty workbook and worksheet to the SpreadsheetDocument
        var workbookPart = spreadsheetDocument.AddWorkbookPart ();
        var worksheetPart = workbookPart.AddNewPart<WorksheetPart> ();

        var shareStringPart = workbookPart.AddNewPart<SharedStringTablePart> ();
        shareStringPart.SharedStringTable = sst;

        shareStringPart.SharedStringTable.Save ();

        // add sheet data to Worksheet
        worksheetPart.Worksheet = new Worksheet (sheetData);
        worksheetPart.Worksheet.Save ();

        // fill workbook with the Worksheet
        spreadsheetDocument.WorkbookPart.Workbook = new Workbook (
                new FileVersion { ApplicationName = "Microsoft Office Excel" },
                new Sheets (
                    new Sheet {
                        Name = _sheetName,
                        SheetId = (UInt32Value)1U,

                        // generate the id for sheet
                        Id = workbookPart.GetIdOfPart (worksheetPart)
                    }
                )
            );
        spreadsheetDocument.WorkbookPart.Workbook.Save ();
        spreadsheetDocument.Close ();
    }

    public void CreateXlsxAndFillData(Stream stream) {
        // Create workbook document
        using (var spreadsheetDocument = SpreadsheetDocument.Create (stream, SpreadsheetDocumentType.Workbook)) {
            FillSpreadsheetDocument (spreadsheetDocument);
        }
    }
}

El código anterior crea un nuevo archivo XLSX directamente. Puede ampliar el código para admitir más tipos de datos a medida StringqueInteger que utilicé en el código.

En una versión más profesional de tu aplicación, puedes crear algunas plantillas XLSX para exportar diferentes tablas. En el código, puedes colocar los datos en las celdas, así que modifica la hoja de cálculo en lugar de crearla. De esta manera puedes crear archivos XLSX con formato perfecto. Los ejemplos de MSDN (ver aquí ) le ayudarán a implementar la forma cuando sea necesario.

ACTUALIZADO : La respuesta contiene código actualizado que permite generar Excel documentado con más formato de celda.

Oleg avatar Feb 19 '2012 13:02 Oleg

Miré la publicación de Stephen y es muy antigua, lo que por cierto no significa que esté mal. Si no necesita formatos, encabezados y estilos personalizados, creo que use CSV, ya que es muy simple.
Más importante aún, no crea que la exportación de Excel desde un sitio MVC que usa EF internamente para acceder a datos es más difícil que, por ejemplo, un sitio Ruby on Rails que usa ActiveRecord. Para mí son preocupaciones independientes, la exportación no debería ser nueva sobre las tecnologías subyacentes (al menos no directamente), solo la estructura de sus datos, eso es todo.
Busque bibliotecas Codeplex que permitan leer/escribir y exportar Excel; hay muchas en estos días, muchas soluciones realmente buenas que miles de desarrolladores en todo el mundo mantienen y prueban regularmente. Si yo fuera usted, no usaría la solución de Stephen porque parece que ocasionalmente la escribió en un bloc de notas y luego la pegó en la publicación: sin pruebas unitarias, sin puntos de extensibilidad + está en VB, por lo que es aún más difícil de entender, pero puede ser así soy yo. Espero que esta ayuda y buena suerte.

chester89 avatar Feb 18 '2012 13:02 chester89