Printing on iPad from WebView

GercoBrandwijkGercoBrandwijk USMember ✭✭
edited May 2015 in Xamarin.Forms

Hello guys,

I want to print a webview with the default printer functionalities which are available on both Android and iOS. I have a DependencyService with a Print method, which calls the platform specific code to access the printing functionalities. The problem is that it is not working on iPad, but it works on Android and iPhones.

The code for iOS is the following:

UIWebView platformWebView = (UIWebView)webView.PlatformControl;

UIPrintInteractionController printer = UIPrintInteractionController.SharedPrintController;

printer.ShowsPageRange = true;

printer.PrintInfo = UIPrintInfo.PrintInfo;
printer.PrintInfo.OutputType = UIPrintInfoOutputType.General;
printer.PrintInfo.JobName = name;

printer.PrintPageRenderer = new UIPrintPageRenderer()
{
    HeaderHeight = 40,
    FooterHeight = 40
};
printer.PrintPageRenderer.AddPrintFormatter(platformWebView.ViewPrintFormatter, 0);

printer.Present(true, (handler, completed, err) =>
{
});

I now that the printer.Present function is primary made for iPhones (http://iosapi.xamarin.com/?link=M:UIKit.UIPrintInteractionController.Present) but it should work when you use True als the first parameter (that's also conform the documentation, not in the doc at the link, but visible when you press F12 on the method in Visual Studio). The problem is that nothing is happening on the iPad when I call this method. No error, nothing.
I am aware of the method printer.PresentFromRectInView, but I don't know what I need to pass as the first two parameters (http://iosapi.xamarin.com/index.aspx?link=M:UIKit.UIPrintInteractionController.PresentFromRectInView).

Does anyone have a working printing solution for iPad?

Thanks in advance.

Regards,
Gerco

Tagged:

Answers

  • MitchMilamMitchMilam USMember ✭✭✭

    @GercoBrandwijk You might wish to repost this in the iOS forum to get better coverage (and since this is at the iOS layer).

  • GercoBrandwijkGercoBrandwijk USMember ✭✭

    @MitchMilam Thanks for the advice. I opened an question in the iOS section.

  • AleStrAleStr ITMember

    Hi @GercoBrandwijk
    I am experiencing the same problem.
    I can't find your new question in the iOS section. Can you give me the link, or some update about the issue?

    Thanks

  • JohnHardmanJohnHardman GBUniversity mod

    @GercoBrandwijk - could you share your Android equivalent please?

    Many thanks,

    John H.

  • MommMomm USMember ✭✭✭

    @JohnHardman said:
    @GercoBrandwijk - could you share your Android equivalent please?

    Many thanks,

    John H.

    For Android 19+, you can use:

                WebView webView = new WebView(Xamarin.Forms.Forms.Context);
                webView.LoadDataWithBaseURL("file:///android_asset/", html, "text/HTML", "UTF-8", null);
    
                PrintDocumentAdapter adapter = webView.CreatePrintDocumentAdapter();
    
                PrintManager printManager = (PrintManager)Xamarin.Forms.Forms.Context.GetSystemService(Context.PrintService);
                printManager.Print("PrintHtml", adapter, null);
    
  • GercoBrandwijkGercoBrandwijk USMember ✭✭
    edited February 2016

    @JohnHardman

    Code for Android:

    try
    {
        PrintManager printManager = (PrintManager)Forms.Context.GetSystemService(Context.PrintService);
    
        Android.Webkit.WebView platformWebView = (Android.Webkit.WebView)webView.PlatformControl;
    
        printManager.Print(name, platformWebView.CreatePrintDocumentAdapter(name), null);
    
        return true;
    }
    catch (Exception)
    {
        return false;
    }
    

    Code for iOS:

    try
    {
        UIWebView platformWebView = (UIWebView)webView.PlatformControl;
    
        UIPrintInteractionController printer = UIPrintInteractionController.SharedPrintController;
    
        printer.ShowsPageRange = true;
    
        printer.PrintInfo = UIPrintInfo.PrintInfo;
        printer.PrintInfo.OutputType = UIPrintInfoOutputType.General;
        printer.PrintInfo.JobName = name;
    
        printer.PrintPageRenderer = new UIPrintPageRenderer()
        {
            HeaderHeight = 40,
            FooterHeight = 40
        };
        printer.PrintPageRenderer.AddPrintFormatter(platformWebView.ViewPrintFormatter, 0);
    
        if (Device.Idiom == TargetIdiom.Phone)
        {
            printer.PresentAsync(true);
        }
        else if (Device.Idiom == TargetIdiom.Tablet)
        {
            printer.PresentFromRectInViewAsync(new CGRect(200, 200, 0, 0), platformWebView, true);
        }
    
        return true;
    }
    catch (Exception)
    {
        return false;
    }
    

    The used webView variable is an instance from the Xamarin.Forms.WebView class, which is passed to this method in a DependenyService.

  • JohnHardmanJohnHardman GBUniversity mod

    Many thanks @GercoBrandwijk and @Momm

    @GercoBrandwijk - I am confused by the PlatformControl member of Xamarin.Forms.WebView . I am not seeing that in the definition of WebView. Is this an extension you have added?

    Thanks again,

    John H.

  • GercoBrandwijkGercoBrandwijk USMember ✭✭
    edited February 2016

    @JohnHardman

    Yes, that is indeed the case. The time of the webView variable is actually CV_WebView on my side. This class contains the following:

    public class CV_WebView : WebView
    {
        public Object PlatformControl;
    }
    

    For this you need of course a custom renderer to fill this PlatformControl.
    On Android that looks like this:

    [assembly: ExportRenderer(typeof (CV_WebView), typeof (CV_WebViewRenderer))]
    
    namespace Droid.Renderers
    {
        public class CV_WebViewRenderer : WebViewRenderer
        {
            protected override void OnElementChanged(ElementChangedEventArgs<WebView> e)
            {
                base.OnElementChanged(e);
    
                ((CV_WebView) this.Element).PlatformControl = this.Control;
    
                this.Control.Settings.BuiltInZoomControls = true;
                this.Control.Settings.DisplayZoomControls = false;
            }
        }
    }
    

    And on iOS like this:

    [assembly: ExportRenderer(typeof (CV_WebView), typeof (CV_WebViewRenderer))]
    
    namespace iOS.Renderers
    {
        public class CV_WebViewRenderer : WebViewRenderer
        {
            protected override void OnElementChanged(VisualElementChangedEventArgs e)
            {
                base.OnElementChanged(e);
    
                ((CV_WebView) this.Element).PlatformControl = this.NativeView;
            }
        }
    }
    
  • JohnHardmanJohnHardman GBUniversity mod

    @GercoBrandwijk - Many thanks again. I'll give that a go in the morning, then investigate the Windows and WinPhone equivalents :-)

  • GercoBrandwijkGercoBrandwijk USMember ✭✭

    @JohnHardman

    Please share that too, because I don't have a WinPhone solution yet (not that high priority for me at this moment), but I would like to implement it in the WinPhone app too.

  • AndrewCotterell.6017AndrewCotterell.6017 USMember ✭✭

    This saved me hours of research!!!! works beautifully!

  • JohnHardmanJohnHardman GBUniversity mod
    edited June 2017

    @GercoBrandwijk - Apologies for the long delay. I no longer develop for Win 8.1, but here is a first go at a UWP version. It still needs some work, but it shows the basics of UWP printing. I've included a couple of URLs in the comments as to where more information can be found. Unfortunately, the forum won't let me post the whole thing in one go, so I'll split it in two...

    using System;
    using System.Collections.Generic;
    using System.Threading.Tasks;
    
    using Windows.Graphics.Printing;
    using Windows.UI.Xaml;
    using Windows.UI.Xaml.Controls;
    using Windows.UI.Xaml.Printing;
    
    using Xamarin.Forms;
    
    using CommonInfrastructure;
    
    namespace MyAppUniversalWindows
    {
    
        public interface IWebViewPrinterStrings
        {
            string AbandonedFormatString { get; }
            string CanceledFormatString { get; }
            string FailedFormatString { get; }
            string SubmittedFormatString { get; }
            string DefaultFormatString { get; }
            string ToastTitleString { get; }
            string ToastAcceptButtonString { get; }
            string NullDescriptionString { get; }
            string PrinterServiceBusy { get; }
            string AwaitingResponse { get; }
    
        } // public interface IWebViewPrinterStrings
    
        public class WebViewPrinterStrings : IWebViewPrinterStrings // TODO - move these strings to resources and localise
        {
            private static WebViewPrinterStrings _theInstance;
            public static WebViewPrinterStrings Instance // TODO - replace this when localise strings
            {
                get
                {
                    if (_theInstance == null)
                        _theInstance = new WebViewPrinterStrings();
                    return _theInstance;
                }
            }
    
            public string AbandonedFormatString {  get { return "Print job '{0}' abandoned"; } }
            public string CanceledFormatString {  get { return "Print job '{0}' canceled"; } }
            public string FailedFormatString {  get { return "Print job '{0}' failed"; } }
            public string SubmittedFormatString {  get { return "Print job '{0}' submitted"; } }
            public string DefaultFormatString {  get { return "Print job '{0}' completed with unknown status"; } }
            public string ToastTitleString {  get { return "MyApp"; } }
            public string ToastAcceptButtonString {  get { return "OK"; } }
            public string NullDescriptionString { get { return "{null}"; } }
            public string PrinterServiceBusy {  get { return "Print service busy. Please try again shortly"; } }
            public string AwaitingResponse {  get { return "Awaiting response from printer service"; } }
    
        } // public class WebViewPrinterStrings : IWebViewPrinterStrings
    
        // Based on code from 
        // https://forums.xamarin.com/discussion/91163/problem-with-printing-webview-in-uwp-phone#latest
        // and 
        // https://github.com/Microsoft/Windows-universal-samples
        public class WebViewPrinter
        {
            private string _jobName;
            private Windows.UI.Xaml.Controls.WebView _nativeWebView;
            private PrintDocument _printDocument;
            private PrintManager _printManager;
            private PrintTask _printTask;
    
            /// <summary>
            /// A list of UIElements used to store the print preview pages.  This gives easy access
            /// to any desired preview page.
            /// </summary>
            internal List<UIElement> printPreviewPages;
    
            public WebViewPrinter()
            {
                _nativeWebView = new Windows.UI.Xaml.Controls.WebView();
    
                printPreviewPages = new List<UIElement>();
            }
    
            /// <summary>
            /// Insert the specified HTML after the specified node in the HTML source.
            /// </summary>
            /// <param name="htmlSource">The HTML to insert into.</param>
            /// <param name="nodeToAddAfter">The node to insert after.</param>
            /// <param name="htmlToInsert">The HTML to insert.</param>
            /// <returns>Resulting HTML if successful. Original HTML if error.</returns>
            /// <remarks>Note that this implementation is hacky and simplistic. It may
            /// need re-visiting at some point.</remarks>
            private async Task<string> InsertAfterNodeAsync(
                string htmlSource,
                string nodeToAddAfter,
                string htmlToInsert)
            {
                try
                {
                    StringComparison stringComparison = StringComparison.Ordinal; // TODO - re-visit this when implementing localisation
    
                    int indexOfNode = htmlSource.IndexOf("<" + nodeToAddAfter, stringComparison);
                    if (indexOfNode < 0)
                        return htmlSource;
    
                    string afterNodeStart = htmlSource.Substring(indexOfNode + 1 + nodeToAddAfter.Length);
                    int indexOfTagClose = afterNodeStart.IndexOf(">", stringComparison);
                    if (indexOfTagClose < 0)
                        return htmlSource;
    
                    string remainder = afterNodeStart.Substring(indexOfTagClose + 2);
    
                    string result
                        = htmlSource.Substring(0, indexOfNode)
                          + "<"
                          + nodeToAddAfter
                          + afterNodeStart.Substring(0, indexOfTagClose + 1)
                          + htmlToInsert
                          + remainder;
    
                    return result;
                }
                catch (Exception ex)
                {
                    await InsightsWrapper.ReportExceptionAsync(ex);
                    return htmlSource;
                }
            }
    
            /// <summary>
            /// Unwire all events as part of cleanup.
            /// </summary>
            private void UnwireAllEvents()
            {
                Device.BeginInvokeOnMainThread(async () =>
                {
                    try
                    {
                        if (_printManager != null)
                        {
                            _printManager.PrintTaskRequested -= PrintTaskRequested;
                        }
    
                        if (_printTask != null)
                        {
                            _printTask.Completed -= PrintTask_Completed;
                            _printTask.Previewing -= PrintTask_Previewing;
                            _printTask.Progressing -= PrintTask_Progressing;
                            _printTask.Submitting -= PrintTask_Submitting;
                        }
    
                        if (_printDocument != null)
                        {
                            _printDocument.GetPreviewPage -= GetPrintPreviewPage;
                            _printDocument.Paginate -= CreatePrintPreviewPages;
                            _printDocument.AddPages -= AddPrintPages;
                        }
                    }
                    catch (Exception ex)
                    {
                        await InsightsWrapper.SilentlyReportExceptionAsync(ex);
                    }
                });
            }
    
            private void RegisterForPrinting()
            {
                _printDocument = new PrintDocument();
                _printDocument.Paginate += CreatePrintPreviewPages;
                _printDocument.GetPreviewPage += GetPrintPreviewPage;
                _printDocument.AddPages += AddPrintPages;
    
                _printManager = PrintManager.GetForCurrentView();
    
                try
                {
                    _printManager.PrintTaskRequested += PrintTaskRequested;
                }
                catch (Exception ex)
                {
                    _printManager.PrintTaskRequested -= PrintTaskRequested; // this is to allow recovery in case of error resulting in cleanup not happening
                }
            }
    
            /// <summary>
            /// Print the specified HTML.
            /// </summary>
            /// <param name="jobName">Name of the print job.</param>
            /// <param name="htmlSource">The HTML to print.</param>
            public async Task PrintHtmlAsync(string jobName, string htmlSource)
            {
                _jobName = jobName;
    
                // Tweak the HTML slightly so that print preview works better.
                // TODO - change this once the HTML being printed is made responsive
                string updatedHtmlSource = 
                    await InsertAfterNodeAsync(
                        htmlSource,
                        "head",
                        "<style>body {width: 100vw; height: 100vw; max-height: 100%; max-width: 100%; margin: auto; padding: auto; position: absolute; top:0; bottom:0; left:0; right:0;}</style>");
    
                _nativeWebView.NavigationCompleted += OnNavigatedTo;
                _nativeWebView.NavigateToString(updatedHtmlSource);
            }
    
  • JohnHardmanJohnHardman GBUniversity mod

    Here comes the second part...

            private async void OnNavigatedTo(
                Windows.UI.Xaml.Controls.WebView sender,
                WebViewNavigationCompletedEventArgs args)
            {
                // Note that args.Uri is null here as we are using an HTML string
                try
                {
                    RegisterForPrinting();
    
                    bool showprint = await PrintManager.ShowPrintUIAsync();
                }
                catch (System.IO.FileNotFoundException fnfex)
                {
                    IToastService toastService = DependencyService.Get<IToastService>();
                    if ((toastService != null) && (toastService.ToastsAllowed))
                    {
                        await toastService.DisplayToastInfoAsync(
                            WebViewPrinterStrings.Instance.ToastTitleString,
                            WebViewPrinterStrings.Instance.PrinterServiceBusy,
                            WebViewPrinterStrings.Instance.ToastAcceptButtonString);
                        return;
                    }
                    UnwireAllEvents();
                }
                catch (Exception ex)
                {
                    if ((ex.Message != null) && (ex.Message.Contains("0x80010115")))
                    {
                        // {"OLE has sent a request and is waiting for a reply. (Exception from HRESULT: 0x80010115)"}
    
                        IToastService toastService = DependencyService.Get<IToastService>();
                        if ((toastService != null) && (toastService.ToastsAllowed))
                        {
                            await toastService.DisplayToastInfoAsync(
                                WebViewPrinterStrings.Instance.ToastTitleString,
                                WebViewPrinterStrings.Instance.AwaitingResponse,
                                WebViewPrinterStrings.Instance.ToastAcceptButtonString);
                            return;
                        }
                    }
                    else
                    {
                        await InsightsWrapper.ReportExceptionAsync(ex);
                        UnwireAllEvents();
                        return;
                    }
                }
            }
    
            private async void PrintTaskRequested(
                PrintManager sender, PrintTaskRequestedEventArgs args)
            {
                try
                {
                    var deff = args?.Request?.GetDeferral();
                    if (deff != null)
                    {
                        _printTask = args.Request.CreatePrintTask(_jobName, OnPrintTaskSourceRequested);
    
                        _printTask.Completed += PrintTask_Completed;
                        _printTask.Previewing += PrintTask_Previewing;
                        _printTask.Progressing += PrintTask_Progressing;
                        _printTask.Submitting += PrintTask_Submitting;
    
                        deff.Complete();
                    }
                }
                catch (Exception ex)
                {
                    await InsightsWrapper.ReportExceptionAsync(ex);
                }
            }
    
            private void PrintTask_Submitting(PrintTask sender, object args)
            {
                // no-op
            }
    
            private void PrintTask_Progressing(PrintTask sender, PrintTaskProgressingEventArgs args)
            {
                // no-op
            }
    
            private void PrintTask_Previewing(PrintTask sender, object args)
            {
                // no-op
            }
    
            // Note that the Completed handler is called from a thread other than the UI thread
            private async void PrintTask_Completed(PrintTask sender, PrintTaskCompletedEventArgs args)
            {
                Device.BeginInvokeOnMainThread(async () =>
                {
                    try
                    {
                        IToastService toastService = DependencyService.Get<IToastService>();
                        if (toastService != null)
                        {
                            bool gotError = true;
                            string message;
    
                            switch (args.Completion)
                            {
                                case PrintTaskCompletion.Abandoned:
                                    message = string.Format(
                                        WebViewPrinterStrings.Instance.AbandonedFormatString, 
                                        (_jobName ?? WebViewPrinterStrings.Instance.NullDescriptionString));
                                    break;
                                case PrintTaskCompletion.Canceled:
                                    message = string.Format(
                                        WebViewPrinterStrings.Instance.CanceledFormatString, 
                                        (_jobName ?? WebViewPrinterStrings.Instance.NullDescriptionString));
                                    gotError = false;
                                    await toastService.DisplayToastInfoAsync(
                                        WebViewPrinterStrings.Instance.ToastTitleString, 
                                        message, 
                                        WebViewPrinterStrings.Instance.ToastAcceptButtonString);
                                    break;
                                case PrintTaskCompletion.Failed:
                                    message = string.Format(
                                        WebViewPrinterStrings.Instance.FailedFormatString, 
                                        (_jobName ?? WebViewPrinterStrings.Instance.NullDescriptionString));
                                    break;
                                case PrintTaskCompletion.Submitted:
                                    message = string.Format(
                                        WebViewPrinterStrings.Instance.SubmittedFormatString, 
                                        (_jobName ?? WebViewPrinterStrings.Instance.NullDescriptionString));
                                    gotError = false;
                                    await toastService.DisplayToastInfoAsync(
                                        WebViewPrinterStrings.Instance.ToastTitleString, 
                                        message, 
                                        WebViewPrinterStrings.Instance.ToastAcceptButtonString);
                                    break;
                                default:
                                    message = string.Format(
                                        WebViewPrinterStrings.Instance.DefaultFormatString, 
                                        (_jobName ?? WebViewPrinterStrings.Instance.NullDescriptionString));
                                    break;
                            }
    
                            if (gotError)
                            {
                                await toastService.DisplayToastErrorAsync(
                                    WebViewPrinterStrings.Instance.ToastTitleString, 
                                    message, 
                                    WebViewPrinterStrings.Instance.ToastAcceptButtonString);
                            }
                        }
    
                        UnwireAllEvents();
                    }
                    catch (Exception ex)
                    {
                        await InsightsWrapper.SilentlyReportExceptionAsync(ex);
                    }
                });
            }
    
            private async void OnPrintTaskSourceRequested(PrintTaskSourceRequestedArgs args)
            {
                try
                {
                    if (args != null)
                    {
                        if (_printDocument != null)
                        {
                            var def = args.GetDeferral();
                            if (def != null)
                            {
                                await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(
                                    Windows.UI.Core.CoreDispatcherPriority.Normal,
                                    async () =>
                                    {
                                        try
                                        {
                                            args.SetSource(_printDocument.DocumentSource);
                                        }
                                        catch (Exception ex)
                                        {
                                            await InsightsWrapper.ReportExceptionAsync(ex);
                                        }
                                    });
                                def.Complete();
                            }
                        }
                    }
                }
                catch (Exception ex)
                {
                    await InsightsWrapper.ReportExceptionAsync(ex);
                }
            }
    
            private async void AddPrintPages(object sender, AddPagesEventArgs e)
            {
                try
                {
                    // Loop over all of the preview pages and add each one to add each page to be printed
                    PrintDocument printDoc = sender as PrintDocument;
                    if ((printDoc != null) && (printPreviewPages != null))
                    {
                        foreach (UIElement previewPage in printPreviewPages)
                        {
                            printDoc.AddPage(previewPage);
                        }
                        printDoc.AddPagesComplete();
                    }
                }
                catch (Exception ex)
                {
                    await InsightsWrapper.ReportExceptionAsync(ex);
                    _printManager.PrintTaskRequested -= PrintTaskRequested;
                }
            }
    
            private async void CreatePrintPreviewPages(object sender, PaginateEventArgs e)
            {
                try
                {
                    // Get the page description to deterimine how big the page is (for future use)
                    PrintTaskOptions printTaskOptions = e.PrintTaskOptions;
                    IList<string> displayedOptions = printTaskOptions.DisplayedOptions;
                    displayedOptions.Clear();
                    displayedOptions.Add(StandardPrintTaskOptions.Copies);
                    displayedOptions.Add(StandardPrintTaskOptions.Orientation);
                    displayedOptions.Add(StandardPrintTaskOptions.MediaSize);
                    displayedOptions.Add(StandardPrintTaskOptions.Collation);
                    displayedOptions.Add(StandardPrintTaskOptions.Duplex);
    
                    //PrintPageDescription pageDescription = printTaskOptions.GetPageDescription(0);
    
                    // Clear the cache of preview pages
                    // and add the one preview page
                    printPreviewPages.Clear();
                    printPreviewPages.Add(_nativeWebView);
    
                    // Report the number of preview pages created
                    PrintDocument printDoc = sender as PrintDocument;
                    printDoc?.SetPreviewPageCount(printPreviewPages.Count, PreviewPageCountType.Final);
                }
                catch (Exception ex)
                {
                    await InsightsWrapper.ReportExceptionAsync(ex);
                }
            }
    
            private async void GetPrintPreviewPage(object sender, GetPreviewPageEventArgs e)
            {
                try
                {
                    if (e != null)
                    {
                        PrintDocument printDoc = sender as PrintDocument;
                        printDoc?.SetPreviewPage(e.PageNumber, _nativeWebView);
                    }
                }
                catch (ArgumentException aex)
                {
                    await InsightsWrapper.SilentlyReportExceptionAsync(aex);
                }
                catch (Exception ex)
                {
                    await InsightsWrapper.ReportExceptionAsync(ex);
                }
            }
    
        } // public class WebViewPrinter
    
    } // namespace MyAppUniversalWindows
    
    // eof
    
  • BrianConradBrianConrad USMember ✭✭✭

    Can anyone explain why on different screen size iPads the print output is different? Screen size should not affect printer output from a webview. I have no problem getting the print output required on Android and UWP but iOS seems a bit bizarre. What exactly is the CGRect supposed to be doing? Changing it doesn't seem to do much. Is there some trick I'm missing to getting the same output regardless of the device screen size.

  • BrianConradBrianConrad USMember ✭✭✭

    OK, the solution is to use WKWebView (WebKit WebView) instead of the deprecated UIWebView. That made my pages format properly regardless of the iPad screen size. Only problem is a page with two columns is not working though it did with UIWebView.

    A little trick when using the Simulator and Printer Simulators when getting a "Device Offline" message is to click on the Printer Simulator Preferences and click OK. That brings the Printer Simulators back online.

  • JonathanPeelJonathanPeel USMember ✭✭

    @GercoBrandwijk I am trying your example, but I am getting an error saying that 'WebView' does not contain a definition for 'PlatformControl'.
    Do I need to add anything to get this to work?

Sign In or Register to comment.