1

I have a dynamic filtered table of elements, and each one have a group of buttons to operate with them. One of them shows a partial HTML generated with Razor with the element's data.

Table entry:

foreach (var item in listado.Where(x => x.Raw != null)) {
<tr>
    <td>@item.Id</td>
    <td>@item.Usuario</td>
    <td>@item.NIF_CIF</td>
    <td>
        @if (!(String.IsNullOrEmpty(item.Telefono)) && !(String.IsNullOrWhiteSpace(item.Telefono))) {
            @item.Telefono
        } else {
            @Html.Raw("No disponible")}
    </td>
    <td>@Math.Round(item.Raw.PrecioTotal).ToString("0.##")&nbsp;&euro;</td>
    <td>@item.FechaCreacion.ToShortDateString()</td>
    <td class="btn-group-sm" role="group">
        <button class="eliminar btn btn-secondary btn-danger" data-itemid="@item.Id">Eliminar</button>
        <button class="convertirPDF btn btn-secondary btn-info" data-itemid="@item.Id">PDF</button>
        <button class="detalles btn brn-secondary btn-info" data-itemid="@item.Id">Detalles</button></td>
</tr>
<tr id="vistaDetalles@(item.Id)" hidden>
    <td colspan="7">
        @Html.Partial("_PresupuestoFinal", item)
    </td>
</tr> }

The element I want to work with is generated in the line

@Html.Partial("_PresupuestoFinal", item)

So then, with jQuery, I provide function to those buttons. The PDF button code is:

$(".convertirPDF").on("click", function (id) {
    var itemId = $(this).data('itemid');
    var presupuesto = $('#content').html($(this).find('#vistaDetalles' + itemId).html());
    Pdf(presupuesto);
});

function Pdf(presupuesto) {
    presupuestoHTML = presupuesto;

    $.ajax({
        method: "GET",
        url: 'DescargarPDF',
        data: { presupuestoHTML: presupuesto },
        cache: false,
        async: true,
    });
};

But every tim I press the button, I get on console:

TypeError: can't convert undefined to object

Referring to the line when I invoke the function Pdf. What I want to do is to pick that particular html (the one corresponding to the element) and convert it to a PDF file. Since I can't get enough info to know where the error is, what am I doing wrong?

BTW that url: 'DescargarPDF line is pointing to the method in my controller.

EDIT

Here's my controller method as requested:

public void DescargarPDF (string presupuestoHTML) {

        Response.Clear();
        Response.ContentType = "application/pdf";
        Response.AddHeader("content-disposition", "attachment;filename=" + "PDF.pdf");
        Response.Cache.SetCacheability(HttpCacheability.NoCache);

        MemoryStream ms = new MemoryStream();
        TextReader txtReader = new StringReader(presupuestoHTML);

        // Creamos un objeto de clase itextsharp document
        Document doc = new Document(PageSize.A4, 25, 25, 30, 30);

        // Creamos un pdfwriter de itextsharp que lee el documento y dirige un stream XML a un archivo
        PdfWriter oPdfWriter = PdfWriter.GetInstance(doc, ms);

        // Creamos un trabajador para parsear el documento
        HTMLWorker htmlWorker = new HTMLWorker(doc);

        // Abrimos el documento y se lo pasamos al trabajador
        doc.Open();
        htmlWorker.StartDocument();

        // Parseamos el presupuesto en html al documento
        htmlWorker.Parse(txtReader);

        // Cerramos el documento y el trabajador
        htmlWorker.EndDocument();
        htmlWorker.Close();
        doc.Close();

        var bPDF = ms.ToArray();

        Response.BinaryWrite(bPDF);
        Response.End();
    }

The script doesn't hit it.

2nd EDIT

So I got more info on the error:

presupuestoHTML[toArray]

Is what's causing the error. Don't know where, or how.

3rd EDIT

Ok so I know now the issue is on the script retrieving the html I want. I changed that line:

$(".convertirPDF").on("click", function (id) {
    itemId = $(this).data('itemid');
    presupuesto = document.documentElement.getElementsByTagName('vistaDetalles' + itemId);
    Pdf(presupuesto);
});

And now the error in console is:

TypeError: 'item' called on an object that does not implement interface HTMLCollection.

I'll keep looking into it, hope this gets the issue clearer.

2nd UPDATE

I've been working on this, I got everything working except for the file download. I'll explain:

Here's the script, it hits the controller method with the proper data.

$(".convertirPDF").on("click", function (id) {
    var itemId = $(this).data('itemid');
    // var presupuesto = $('#vistaDetalles' + itemId).html();
    Pdf(itemId);
});

function Pdf(itemid) {
    var id = itemid;

    $.ajax({
        method: "POST",
        url: 'DescargarPDF',
        data: { itemId: id },
        cache: false,
        async: true,
    });
};

So that works. Here's my controller method, where I suspect the trouble is caused:

 public FileResult DescargarPDF (int itemId) {
        var presupuesto = ReglasNegocio.Fachada.Consultas.ObtenerPresupuesto(itemId);             
        var archivo = new Rotativa.PartialViewAsPdf("_PresupuestoFinal", presupuesto) { FileName = "Presupuesto_" + itemId + ".pdf" };

        return File(archivo.FileName, "application/pdf");
    }

That's my last try so far. I've tried Rotativa's ActionAsPdf as well and I got on the browser console a stream of data (which I have to guess it's the pdf file) not the downloadable file. I've also tried other options like converting the file to a byte array and stream it but since I don't want to save the file, that option is discarded (functions require a file path, which I'm not able to provide because the file is not saved, still on memory). Still working on it.

dCarMal
  • 151
  • 7
  • That all looks reasonable to me. Can you set breakpoints in your anonymous click handler function and step through? It might also be worth `console.log()` -ging everything, to see if you can spot what's undefined, e.g. the Pdf function, presupuesto, and putting console logs inside the Pdf function too to see if you're actually making it into the function after all. – Rup Aug 30 '18 at 11:23
  • Does the request hits the controller? Can you show the method's code? – matramos Aug 30 '18 at 11:25
  • Thanks for your answers. No, I don't think the request hits the controller, the error message comes from the browser (firefox) debug console. – dCarMal Aug 30 '18 at 11:28
  • Are you familiar with debugging? I suggest debugging both client side (js files on firefox) and back end (controller) to check what exactly is the issue. [debug js link](https://raygun.com/blog/debug-javascript-firefox/) [debug C# link](https://learn.microsoft.com/en-us/visualstudio/debugger/getting-started-with-the-debugger?view=vs-2017) – matramos Aug 30 '18 at 11:32
  • @matramos Yes, that's how I got the error I mention on the question. However, I can't get further information through the developer tools integrated on the browser (or maybe I'm not using them right), but I'll keep looking. Thanks. – dCarMal Aug 30 '18 at 11:36
  • Rotativa is pretty easy to use for creating a PDF view of what you want. It can be designed as a view on its on which you can access through any other view by creating an @Html.ActionLink. It also has a controller action which will aid you in learning some C#. – Scanner Aug 30 '18 at 11:53
  • @Scanner Thank you very much for your answer. I'll give a good look into it. The only reason I was using iTextSharp it's because is open source. – dCarMal Aug 30 '18 at 11:56
  • @dCarMal You're welcome. Checkout this link for gudiance http://github.com/webgio/Rotativa if you need any help just let me know – Scanner Aug 30 '18 at 12:00
  • @Scanner Well, I've working on it, and the issue is in the script, not the controller/method/library. I think it needs me to specify the type of the object where I save the html, according to what I could dig into the error. – dCarMal Aug 30 '18 at 12:33
  • @Rup I'm working with Visual Studio (community 2017) and the f*****g thing won't let me use braking points on the script. Tried to do it with firefox integrated debug tools, but I can't follow the execution as expected. Am I doing something wrong or not doing it at all? Thanks. – dCarMal Aug 30 '18 at 12:38
  • Congrats on solving it! If there's anything here that might be useful to other people in the future then you could write an answer and explain the problem. Debug stepping: I don't know sorry - I normally use Chrome directly not through Visual Studio, and it doesn't always do what you expect. In that case it often does come down to adding lots of logs to show flow and state. – Rup Aug 30 '18 at 13:09
  • @Rup Thank you. Now I hit the controller, but with a null object. Going to update the update and keep working on it. I'm taking your suggestion and use chrome too for debugging. Thanks again. – dCarMal Aug 30 '18 at 13:19
  • Oh, sorry, I thought that last update was you've solved it :-/ If the problem now is just extracting the HTML, I think you need [`.find`](http://api.jquery.com/find/) (.get uses an integer index) and [`.html`](http://api.jquery.com/html/), e.g. `$('#document').find('#vistaDetalles' + itemId).html()` Or you can merge the .find into the #document selector instead, `$('#document #vistaDetalles' + itemId).html()`. – Rup Aug 30 '18 at 13:22
  • @Rup Yeah, I also thought that was it. I've tried both lines you suggested, both still send a null object to the controller. Maybe it has something to do with Razor? Still working on it, thank you very much for your help. – dCarMal Aug 30 '18 at 13:32
  • try this `var presupuesto = $('#document').get('#vistaDetalles' + itemId).html();` – er-sho Aug 30 '18 at 13:37
  • @ershoaib Thanks for your suggestion. I've just tried it, it throws an incomprehensible error: ` TypeError: $(...).get(...) is undefined ` and I really dont get why is this error thrown. Thank you anyway. – dCarMal Aug 30 '18 at 13:45
  • try this `var presupuesto = $('#vistaDetalles' + itemId).html();` dont include document in jquery selector, directly pass your `tr` instead – er-sho Aug 30 '18 at 13:48
  • @ershoaib Thank you very much, that worked! However, now I got a different kind of error :-/ It's in spanish, so it basically says that the string of data passed to the controller is too big and considers it an attack. Is there any other way of passing the html, am I doing something wrong or lack some kind of configuration on my project? Again, thank you very much, I'm going to update the question. – dCarMal Aug 30 '18 at 13:57
  • this is your action method `public void DescargarPDF (string presupuestoHTML)` so mark with attribute as `[ValidateInput(false)]` – er-sho Aug 30 '18 at 14:04
  • means it look like `[ValidateInput(false)] public void DescargarPDF (string presupuestoHTML) { //your rest of code here }` and send it again your html content – er-sho Aug 30 '18 at 14:05
  • also try to set your `maxQueryStringLength=2097151` in web.config like `` – er-sho Aug 30 '18 at 14:10
  • @ershoaib I'm looking where to put that line in my project, but I've been thinking about it since yesterday and I'm concerned about two things: security (it's a project for a local use program, but still) and server overload (it's going to run on a server that right now is relatively busy and this kind of operation would be one of it's most used features). Maybe there's an easier way to move that amount of information, so I'll be looking into that too. In any case, thank you very much. – dCarMal Aug 31 '18 at 06:44

2 Answers2

1

I asked another question to try to solve the not downloading file issue, and I got it.

Before anything, I was doing the request through ajax because I thought to be a good way to do so, but as it turns out, there's a much simpler way: not using ajax.

So, I removed the script for the button and the button itself, so now looks like this:

<a href="DescargarPDF/?itemId=@item.Id" target="_blank" class="btn btn-secondary btn-info">PDF</a>

It has the same appearance than before, but it's actually a link to my controller's method, which right now looks like this:

public FileResult DescargarPDF (int itemId) {
        var presupuesto = ReglasNegocio.Fachada.Consultas.ObtenerPresupuesto(itemId);             
        var archivo = new Rotativa.PartialViewAsPdf("_PresupuestoFinal", presupuesto) { FileName = "Presupuesto_" + itemId + ".pdf", PageSize = Rotativa.Options.Size.A4 };
        var binario = archivo.BuildFile(this.ControllerContext);        

        return File(binario, "application/pdf", archivo.FileName);           
    }

I know in most cases this wouldn't be a valid solution since I just left ajax behind, but there are many other questions where the answer worked for them and they still use ajax to manage the request.

Still, I hope this helps. Thanks to everyone. Happy coding.

UPDATE

I just found out why my PDF file was dropped into console, check the other question, I left a little explanation for that particular issue. Thanks everyone.

dCarMal
  • 151
  • 7
0

You could use only javascript:

Kerlos Bekhit
  • 128
  • 1
  • 7
  • Thank you very much for your answer, I'll keep that under my utilities, but sadly this is a project I'm developing to learn C#, so I have to use it. But again, thank you. – dCarMal Aug 30 '18 at 11:49