File viewer for MAUI

341 Views Asked by At

When my app was in Xamarin, I used a dependency service to open pdfs and office files with PresentViewController:

previewController.DataSource = new(directory, filename, filename);
controller.PresentViewController(previewController, true, null);

I thought that Launcher is the Maui replacement

Launcher.Default.OpenAsync(new OpenFileRequest(*title*, new ReadOnlyFile(*filePath*))

Office is installed on my Mac. With Maui, I get strange results on MacOS. Instead of opening the docx, this pops up:

enter image description here

Clicking on that pop up does nothing. Edit Extensions... doesn't give me much to do enter image description here

The Launcher documentation indicates that is should work.

It says it "automatically detects the file type (MIME), and opens the default app for that file type" which is what Xamarin did. I never had to muck with MIMEs though.

Curiously, the answer in this s.o. post uses the App Store to open files. Huh??

My Xamarin app on an iPad launches pdfs and office docs into the same preview screen even if the corresponding apps aren't installed. MAUI's call to Launcher just fails.

The same Maui Launcher code on Windows works. It opens a corresponding app and feeds the file into it. (e.g. docx opens Word and then Word pulls the docx into it.)

What is the deal with Apple products? How do I get Launcher to work or some other preview mechanism? Are we to use the UIKit code in the replacement for DependencyService using this way?

1

There are 1 best solutions below

0
On

This solution is only for readonly access to the files that I was trying open with Launcher. Though readonly, it is a solution that can open a wide variety of filetypes. If you need read/write, I still don't know how to use Launcher for either MACATALYST or iOS.

The MAUI replacement for Xamarin's Dependency Service is called Platform Specific Conditional Code. Combining the old with the new, gives me what I need. For example, I have a Command-tied OpenSelectedFile method in my ViewModel which contains this fragment:

#if (IOS || MACCATALYST)
  AppleFileHandler.OpenFile(fileAndPath);
#elif ANDROID
  // unkown - in Xamarin we didn't support Android. We'd like to for MAUI.
#else
  var roFile = new ReadOnlyFile(fileAndPath);
  ret = await Launcher.Default.OpenAsync(new OpenFileRequest(roFile.FileName, roFile));
#endif

AppleFileHandler is what I wrote to limit the complexity of the above statements. Much of the code I took from my Xamarin dependency service, which was written about 8 years ago and still works. (Note: I still do not know what the Android equivalent is, but Launcher works fine in Windows.)

#if (IOS || MACCATALYST)
using QuickLook;
using UIKit;

public class AppleFileHandler
{
    public static void OpenFile(string fileName)
    {
        var fileInfo = new FileInfo(fileName);
        using (QLPreviewController previewController = new())
        {
            previewController.DataSource = new PreviewControllerDataSource(Path.Combine(fileInfo.DirectoryName, fileInfo.Name), fileInfo.Name);
            UINavigationController controller = FindNavigationController();
            controller?.PresentViewController(previewController, true, null);
            controller = null;
        }
    }

    private static UINavigationController FindNavigationController()
    {
        foreach(UIWindow window in UIApplication.SharedApplication.Windows)
        {
            if (window.RootViewController.NavigationController != null)
                return window.RootViewController.NavigationController;
            else
            {
                UINavigationController value = CheckSubs(window.RootViewController.ChildViewControllers);
                if (value != null)
                    return value;
            }
        }

        return new UINavigationController();
    }

    private static UINavigationController CheckSubs(UIViewController[] controllers)
    {
        foreach(UIViewController controller in controllers)
        {
            if (controller.NavigationController != null)
                return controller.NavigationController;
            else
            {
                UINavigationController value = CheckSubs(controller.ChildViewControllers);
                if (value != null)
                    return value;
            }
            return null;
        }
        return new UINavigationController();
    }
}
#endif

The above code depends on the next two classes. As I understand it, this code could be in the the relevant platform folder or outside, like I have, with preprocessor statements.

#if (IOS || MACCATALYST)
using Foundation;
using QuickLook;

public class DocumentItem : QLPreviewItem
{
    private string title;
    private string url;

    public DocumentItem(string title, string url)
    {
        this.title = title;
        this.url = url;
    }

    public override string PreviewItemTitle => title;
    public override NSUrl PreviewItemUrl => NSUrl.FromFilename(url);
}
#endif

and

using System;
#if (IOS || MACCATALYST)
using QuickLook;

public class PreviewControllerDataSource : QLPreviewControllerDataSource
{
    private string url;
    private string fileName;

    public PreviewControllerDataSource(string url, string fileName)
    {
        this.url = url;
        this.fileName = fileName;
    }

    public override IQLPreviewItem GetPreviewItem(QLPreviewController controller, nint index)
    {
        return new DocumentItem(fileName, url);
    }

    public override nint PreviewItemCount(QLPreviewController controller)
    {
        return 1;
    }
}
#endif