Kendo Grid Excel Export returning 405

1 Answer 15 Views
Grid
Ross
Top achievements
Rank 1
Ross asked on 04 Jun 2024, 09:44 PM

I had recently had an issue with Kendo Grid's export to excel feature not working and it was correctly resolved in this forum post:
https://www.telerik.com/forums/kendo-grid-excel-export-crashing-widget

I am now experiencing an issue that appears to be distinct and unrelated to the above problem on a different view within my project.

I have a Grid where I want to export the entire contents of the grid (both hidden and unhidden columns) to an excel spreadsheet.  When I click my "Export to Excel" button the following happens:

  • The "Exporting" animation shows up and hangs

  • I receive a console error indicating that the export request returned a status 405, I can also see this in the network tab

  • The breakpoint I set in my ExcelExportSave() method in my HomeController is not being hit.

What could be causing this? 

I have already implemented the fix suggested in the above linked post to import that I add JSZip directly to my layout:

<script src="https://unpkg.com/jszip/dist/jszip.min.js"></script>

 

Additional Context

The Export to Excel will fail even if I attempt to export the contents of the grid before any lines are added.

This is a read-only grid that populates 1 to N lines based on an Ajax call.  Due to the way I need this to function for my users I did not use the dataSource CRUD operations.  Rather, I use external JavaScript to directly edit the table contents.  This could be a possible source of the problem, but I do not know how I would fix.

There are two ways to add lines to the table, the first is to paste in rows and wait for the Ajax call.  The second is to add them using an input field above the grid. 

The Code

Here's the table and the relevant JavaScript that populates it:

Html.Kendo().Grid<UPH.ADCommander.Models.ExtendedUserDisplayObject>()
.Name("Grid")
.Columns(columns =>
{
    columns.Bound(c => c.SamAccountName).Title("SamAccount").Width(110).HeaderHtmlAttributes(new { style = "font-weight: bold" }).EditorTemplateName("SamAccountNameEditor");
    columns.Bound(c => c.GivenName).Title("First Name").HeaderHtmlAttributes(new { style = "font-weight: bold" });
    columns.Bound(c => c.MiddleInitial).Title("MI").Width(50).HeaderHtmlAttributes(new { style = "font-weight: bold" }).Hidden(true);
    columns.Bound(c => c.Surname).Title("Last Name").HeaderHtmlAttributes(new { style = "font-weight: bold" });
    columns.Bound(c => c.Title).Title("Title").HeaderHtmlAttributes(new { style = "font-weight: bold" });
    columns.Bound(c => c.Manager).Title("Manager").HeaderHtmlAttributes(new { style = "font-weight: bold" });
    columns.Bound(c => c.ExtendedAttribute2).Title("Ex. Attr 2").HeaderHtmlAttributes(new { style = "font-weight: bold" });
    columns.Bound(c => c.ExtendedAttribute3).Title("Ex. Attr 3").HeaderHtmlAttributes(new { style = "font-weight: bold" }).Hidden(true);
    columns.Bound(c => c.ExtendedAttribute4).Title("Ex. Attr 4").HeaderHtmlAttributes(new { style = "font-weight: bold" }).Hidden(true);
    columns.Bound(c => c.ExtendedAttribute5).Title("Ex. Attr 5").HeaderHtmlAttributes(new { style = "font-weight: bold" }).Hidden(true);
    columns.Bound(c => c.ExtendedAttribute6).Title("Ex. Attr 6").HeaderHtmlAttributes(new { style = "font-weight: bold" }).Hidden(true);
    columns.Bound(c => c.ExtendedAttribute7).Title("Ex. Attr 7").HeaderHtmlAttributes(new { style = "font-weight: bold" }).Hidden(true);
    columns.Bound(c => c.ExtendedAttribute9).Title("Ex. Attr 9").HeaderHtmlAttributes(new { style = "font-weight: bold" }).Hidden(true);
    columns.Bound(c => c.ExtendedAttribute12).Title("Ex. Attr 11").HeaderHtmlAttributes(new { style = "font-weight: bold" }).Hidden(true);
    columns.Bound(c => c.ExtendedAttribute13).Title("Ex. Attr 12").HeaderHtmlAttributes(new { style = "font-weight: bold" }).Hidden(true);
    // test columns, add more once PoC is getting the necessary data
})
.Sortable()
.Selectable(selectable => selectable
    .Mode(GridSelectionMode.Multiple)
    .Type(GridSelectionType.Cell)
)
.Navigatable()
.Scrollable()
.AllowCopy(true)
.ToolBar(tools =>
{
    tools.Custom().Text("Copy to Clipboard").HtmlAttributes(new { id = "copyButton" });
    tools.Excel();
    tools.Custom().Text("Clear").HtmlAttributes(new { id = "clearButton" });
    tools.Search();
})
.AutoBind(false)
.DataSource(dataSource => dataSource
    .Ajax() // no actual Ajax call is made with this, this is solely to define a primary key for the table so we can use the dataSource API
    .Update("GetUserDetail", "UserLookup")
    .Model(model =>
    {
        model.Id(p => p.SamAccountName); // Set SamAccountName as the ID field
    })
)
.Excel(excel => excel
    .FileName("UserDetails_" + DateTime.Now.Date.ToShortDateString() + ".xlsx")
    .Filterable(true)
    .ProxyURL(Url.Action("ExcelExportSave", "Home"))
)

                       

    function initializeGridPasteHandler(gridSelector) {
        $(gridSelector).on('contextmenu', function (e) {
            if ($(e.target).is(".k-link, .k-grid-toolbar, .k-grid-pager")) {
                return;
            }

            var offset = $(this).find("table").offset();
            var textarea = $("<textarea>");
            textarea.css({
                position: 'absolute',
                opacity: 0,
                top: offset.top,
                left: offset.left,
                border: 'none',
                width: $(this).find("table").width(),
                height: $(this).find(".k-grid-content").height()
            })
                .appendTo('body')
                .on("click", function (e) {
                    textarea.remove();
                    $(document.elementFromPoint(e.clientX, e.clientY)).click();
                })
                .on('paste', function () {
                    setTimeout(function () {
                        var value = $.trim(textarea.val());
                        var grid = $(gridSelector).data("kendoGrid");
                        var rows = value.split('\n');

                        var newRows = addRowsToGrid(rows);

                        sendUserDetailsToController(newRows);

                        textarea.remove();
                    });
                }).focus();

            return false;
        });
    }

 

function addRowsToGrid(rows) {
    console.log("addRowsToGrid fired");
    var grid = $("#Grid").data("kendoGrid");
    var existingData = grid.dataSource.data().toJSON();

    var newData = rows.map(function (row) {
        return { SamAccountName: row.trim() };
    });

    // Append new data to the existing data
    var combinedData = existingData.concat(newData);

    // Update the grid with the combined data
    grid.dataSource.data(combinedData);

    return newData;
}

 

// send the controller a list of samAccount names to get the user detail information
function sendUserDetailsToController(newRows) {
    console.log("sendUserDetailsToController fired");

    // Show the loading indicator
    $("#gridLoader").show();

    var samAccountNames = newRows.map(function (row) {
        return row.SamAccountName;
    });

    // Make the AJAX call with the new samAccountNames only
    $.ajax({
        type: "POST",
        url: "/UserLookup/GetUserDetails",
        contentType: "application/json",
        data: JSON.stringify(samAccountNames),
        success: function (response) {
            console.log("Data sent to the controller successfully.");
            updateGridWithUserDetails(response); // Update the grid with the response data
        },
        error: function (xhr, status, error) {
            console.log("An error occurred while sending data to the controller.");
        },
        complete: function () {
            $("#gridLoader").hide();; // close loading animation
        }
    });
}

 

function updateGridWithUserDetails(userDetails) {
    console.log("updateGridWithUserDetails fired");
    var grid = $("#Grid").data("kendoGrid");
    var dataSource = grid.dataSource;
    var data = dataSource.data();

    userDetails.forEach(function (userDetail) {
        var matchedRow = data.find(function (row) {
            console.log("user: " + userDetail.samAccountName);
            return row.SamAccountName.toLowerCase() === userDetail.samAccountName.toLowerCase();
        });

        console.log("matched row:" +matchedRow.SamAccountName)
        // Necessary in case of a case mismatch, e.g. SSS401 will match sss401 on the backed if you don't do this you'll get a fun bonus row added.
        matchedRow.SamAccountName = userDetail.samAccountName;

        if (matchedRow) {
            console.log("matched row:" + matchedRow.SamAccountName);

            // Update the SamAccountName to the correctly cased value
            matchedRow.set("SamAccountName", userDetail.samAccountName);

            // Update the rest of the row with the user details
            matchedRow.set("GivenName", userDetail.givenName);
            matchedRow.set("MiddleInitial", userDetail.middleInitial);
            matchedRow.set("Surname", userDetail.surname);
            matchedRow.set("Title", userDetail.title);
            matchedRow.set("Manager", userDetail.manager);
            matchedRow.set("ExtendedAttribute2", userDetail.extendedAttribute2);
            matchedRow.set("ExtendedAttribute3", userDetail.extendedAttribute3);
            matchedRow.set("ExtendedAttribute4", userDetail.extendedAttribute4);
            matchedRow.set("ExtendedAttribute5", userDetail.extendedAttribute5);
            matchedRow.set("ExtendedAttribute6", userDetail.extendedAttribute6);
            matchedRow.set("ExtendedAttribute7", userDetail.extendedAttribute7);
            matchedRow.set("ExtendedAttribute9", userDetail.extendedAttribute9);
            matchedRow.set("ExtendedAttribute12", userDetail.extendedAttribute12);
            matchedRow.set("ExtendedAttribute13", userDetail.extendedAttribute13);
        } 
    });

    // Refresh the grid to display the updated data
    grid.refresh();
}

 

function onAddRowClick(){
    var samAccountName = $("#searchInput").val().trim();

    // Check if the input is not empty
    if (samAccountName === "") {
        alert("Please enter a SamAccount name.");
        return;
    }
    var newRow = { SamAccountName: samAccountName };
    var newRows = addRowsToGrid([samAccountName]);

    // send to the Ajax call
    sendUserDetailsToController(newRows);
}

Note that # gridLoader is just my poor attempt at making a waiting animation.  It references this .css class.  This should be irrelevant to the problem, but for ease of running this content locally:

#gridLoader {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background: rgba(255, 255, 255, 0.8);
    z-index: 1000;
    display: none;
    align-items: center;
    justify-content: center;
}

Finally, here is my HomeController action:

        [HttpPost]
        public ActionResult ExcelExportSave(string contentType, string base64, string fileName)
        {
            var fileContents = Convert.FromBase64String(base64);
            return File(fileContents, contentType, fileName);
        }

 

 

 

 

My Environment and Project

This is an ASP.NET Core 8 MVC project being run in Visual Studio 2022 on a Windows laptop.  The project does use Microsoft Identity authentication and I am developing by running it on Localhost.  Notably, my HomeController currently has no authentication or authorization applied as I'm still just doing early stage development on localhost.  

Any assistance that could be provided or suggestions as to why this is failing would be appreciated.

 

 

1 Answer, 1 is accepted

Sort by
0
Mihaela
Telerik team
answered on 07 Jun 2024, 04:49 PM

Hello Ross,

By default, the Grid's DataSource performs the data operations on the server, so when exporting the Grid data to Excel, it triggers a Read request to the server. You can test it in the Excel Export online demo:

Having this in mind, if you disable the server operations, the Grid will not initiate a request to the server when exporting the data:

@(Html.Kendo().Grid<UPH.ADCommander.Models.ExtendedUserDisplayObject>()
  .Name("Grid")
  ...
  .DataSource(dataSource => dataSource
    .Ajax() // no actual Ajax call is made with this, this is solely to define a primary key for the table so we can use the dataSource API
    .ServerOperation(false)
    .Update("GetUserDetail", "UserLookup")
    ...
  )
)

In terms of the "ExcelExportSave" Action, the proxy will be used when the browser isn't capable of saving files locally, as stated in the server-side API:

https://docs.telerik.com/aspnet-core/api/kendo.mvc.ui.fluent/gridexcelsettingsbuilder#proxyurlsystemstring

If you set the option ForceProxy(true), the content will be forwarded to specified Action, even if the browser supports saving files locally:

.Excel(excel => excel
    .FileName("UserDetails_" + DateTime.Now.Date.ToShortDateString() + ".xlsx")
    .Filterable(true)
    .ForceProxy(true)
    .ProxyURL(Url.Action("ExcelExportSave", "Home"))
)

I hope these suggestions will help you to resolve the issue.

 

Regards,
Mihaela
Progress Telerik

Stay tuned by visiting our public roadmap and feedback portal pages. If you're new to the Telerik family, be sure to check out our getting started resources, as well as the only REPL playground for creating, saving, running, and sharing server-side code.
Tags
Grid
Asked by
Ross
Top achievements
Rank 1
Answers by
Mihaela
Telerik team
Share this question
or