asp.net-mvc-4knockout.jsevopdf

Creating a pdf from a Knockout JS view & viewModel


Scenario: In our application a user can create a invoice by filling in certain fields on a Knockout view. This invoice can be previewed, via another Knockout page. I want to use the preview url within our PDF creator (EVOPdf), so we can provide the user with a PDF from this invoice.

To preview the invoice we load the data (on document ready) via an ajax-request:

var InvoiceView = function(){
    function _start() {
        $.get("invoice/GetInitialData", function (response) {
            var viewModel = new ViewModel(response.Data);
            ko.applyBindings(viewModel, $("#contentData").get(0));
        });
    };
    return{
        Start: _start
    };
}();

My problem is within the data-binding when the PDF creator is requesting the url: the viewModel is empty. This makes sense because the GetInitialData action is not called when the PDF creator is doing the request. Calling this _start function from the preview page directly at the end of the page does not help either.

<script type="text/javascript">
    $(document).ready(function() {
        InvoiceView.Start();
    });
</script>

Looking at the documentation of EvoPdf, JavaScript should be executed, as the JavaScriptEnabled is true by default: http://www.evopdf.com/api/index.aspx

How could I solve this, or what is the best approach to create an pdf from a knockout view?

Controller action code:

public FileResult PdfDownload(string url)
{
    var pdfConverter = new PdfConverter();

    // add the Forms Authentication cookie to request
    if (Request.Cookies[FormsAuthentication.FormsCookieName] != null)
    {
        pdfConverter.HttpRequestCookies.Add(
            FormsAuthentication.FormsCookieName,
            Request.Cookies[FormsAuthentication.FormsCookieName].Value);
    }

    var pdfBytes = pdfConverter.GetPdfBytesFromUrl(url);

    return new FileContentResult(pdfBytes, "application/pdf");
}

Javascript:

var model = this;
model.invoiceToEdit = ko.observable(null);

model.downloadInvoice = function (invoice) {
    model.invoiceToEdit(invoice);
    var url = '/invoice/preview';
    window.location.href = '/invoice/pdfDownload?url=' + url;
};

Solution

  • The comment of xdumaine prompted me to think into another direction, thank you for that!

    It did take some time for the Ajax request to load, but I also discovered some JavaScript (e.g. knockout binding) errors along the way after I put a ConversionDelay on the pdf creator object

    pdfConverter.ConversionDelay = 5; //time in seconds
    

    So here is my solution for this moment, which works for me now:

    To start the process a bound click event:

    model.downloadInvoice = function (invoice) {
        var url = '/invoice/preview/' + invoice.Id() + '?isDownload=true';
        window.open('/invoice/pdfDownload?url=' + url);
    };
    

    which result in a GET resquest on the controller action

    public FileResult PdfDownload(string url)
    {
        var pdfConverter = new PdfConverter { JavaScriptEnabled = true };
    
        // add the Forms Authentication cookie to request
        if (Request.Cookies[FormsAuthentication.FormsCookieName] != null)
        {
            pdfConverter.HttpRequestCookies.Add(
                FormsAuthentication.FormsCookieName,
                Request.Cookies[FormsAuthentication.FormsCookieName].Value);
        }
    
            pdfConverter.ConversionDelay = 5; 
            var absolutUrl = ToAbsulte(url);
            var pdfBytes = pdfConverter.GetPdfBytesFromUrl(absolutUrl);
    
            return new FileContentResult(pdfBytes, "application/pdf");
    }
    

    The Pdf creator is requesting this action on the controller, with isDownload = true (see bound click event):

    public ActionResult Preview(string id, bool isDownload = false)
    {
        return PartialView("PdfInvoice", new InvoiceViewModel
        {
            IsDownload = isDownload,
            InvoiceId = id
        });
    }
    

    Which returns this partial view:

    PartialView:

    // the actual div with bindings etc.
    @if (Model.IsDownload)
    {
        //Include your javascript and css here if needed
        @Html.Hidden("invoiceId", Model.InvoiceId);
    
        <script>
            $(document).ready(function () {
                var invoiceId = $("#invoiceId").val();
                DownloadInvoiceView.Start(invoiceId);
            });
        </script>
    }
    

    JavaScript for getting the invoice and apply the knockout bindings:

    DownloadInvoiceView = function() {
       function _start(invoiceId) {
            $.get("invoice/GetInvoice/" + invoiceId, function(response) {
                var viewModel = new DownloadInvoiceView.ViewModel(response.Data);
                ko.applyBindings(viewModel, $("#invoiceDiv").get(0));
            });
        };
    
        return {
            Start: _start
        };
    
    }();
    
    DownloadInvoiceView.ViewModel = function (data) {
        var model = this;
        var invoice = new Invoice(data); //Invoice is a Knockout model
    
        return model;
    };