Integrating Viewer with ASP.Net Core 5+ MVC Hybrid Application with Azure Blob

Hi,

New to the viewer component here. My use case does not appear to blend with how the implementation of the viewer works based on your examples.

We have a multi tenant system where local disk storage is not available. Users click on a link with an ID of an attachment (www.appUrl.com/Files/DownloadFile/123).

Our controller fetches the file stream of this attachment (finds the blob data in SQL, then goes to blob) from the blob storage and serves it up as a stream to the front end so the users receive the “Save As” prompt.

Problem: We want to modify this experience so the user is redirected to a “viewer” page (instead of a download prompt) and the viewer renders the file stream in browser instead of a download. Then we want to offer an additional link on the viewer page for users to download the file as it works today. We also want to modify some of the controls on the viewer so its just a simple Read-Only document viewer with nothing else (I did see a post somewhere how to customize this but it did not seem simple).

I am unclear how to achieve this, as I expected there to be a viewer “View” implementation that takes a Model with some sort of blob reference, stream reference, or raw HTML set on the backend. Or, a view page that has client side JS that can call a server side endpoint to fetch whatever the content is to render. As I understand it, the viewer component can save the File Stream as HTML to another stream, but how to pass that to the front end is missing.

Can you point me in a proper direction? I am a GroupDocs.Total license member.

I should note: We are using .Net Core 5 with MVC and some hybrid approaches (small API controller for various front end things). This is hosted as an App Service inside azure.

Thank you,

AB

@abcmk

I’m sorry for the delayed response. Take a look at this sample app that is built using GroupDocs.Viewer.UI package. I require a couple of lines of code to configure the API and UI. Here is a part of Startup.cs file contents from the app

namespace sample_viewer_app
{
    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddTransient<IFileStorage, MyFileStorage>();
            services.AddTransient<IFileTypeResolver, MyFileTypeResolver>();

            services
                .AddGroupDocsViewerUI(config =>
                {
                    config
                        .DisableFileUpload()
                        .DisableFileBrowsing()
                        .DisableFileDownload()
                        .DisableRightClick();
                });

            services
                .AddControllersWithViews()
                .AddGroupDocsViewerSelfHostApi(config =>
                {
                    //config.SetLicensePath("c:\\licenses\\GroupDocs.Viewer.lic"); 
                    // or set environment variable 'GROUPDOCS_LIC_PATH'
                })
                .AddLocalCache("./Cache");
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            app
                .UseRouting()
                .UseEndpoints(endpoints =>
                {
                    endpoints.MapControllerRoute(name: "default",
                        pattern: "{controller=Home}/{action=Index}/{id?}");

                    endpoints.MapGroupDocsViewerUI(options =>
                    {
                        options.UIPath = "/viewer";
                        options.APIEndpoint = "/viewer-api";
                    });

                    endpoints.MapGroupDocsViewerApi(options =>
                    {
                        options.ApiPath = "/viewer-api";
                    });
                });
        }
    }

    class MyFileStorage : IFileStorage
    {
        public Task<IEnumerable<FileSystemEntry>> ListDirsAndFilesAsync(string dirPath)
        {
            throw new NotImplementedException();   
        }

        public Task<byte[]> ReadFileAsync(string filePath)
        {
            return File.ReadAllBytesAsync("./Storage/sample.docx");
        }

        public Task<string> WriteFileAsync(string fileName, byte[] bytes, bool rewrite)
        {
            throw new NotImplementedException();
        }
    }

    class MyFileTypeResolver : IFileTypeResolver
    {
        public Task<FileType> ResolveFileTypeAsync(string filePath)
        {
            // resolve file type by file path
            return Task.FromResult<FileType>(FileType.DOCX);
        }
    }
}

Using a custom implementation of IFileStorage to retrieve a file and IFileTypeResolver to resolve a file type you can open files by passing ID as a query string parameter file e.g. file=123.

Demo:

You can also run UI and API as separate applications. The source code of GroupDocs.Viewer.UI can be found on GitHub - GitHub - groupdocs-viewer/GroupDocs.Viewer-for-.NET-UI: UI - User Interface for GroupDocs.Viewer for .NET document viewer and automation API..

Let us know if it works for you.

Hi Vladimir,

Thank you for your example. Suppose the final missing pieces I am not understanding are what/where are the API calls being made from? Where is the “Viewer.View()” call and any ability to set config for output types? How do I specify what to do with the files generated after the stream is converted? How do I show/hide specific controls on the viewer in this example?

Suppose I was expecting an IActionResult that returns an instance of the viewer and or a stream of HTML generated from the filestream.

Is there documentation on what API calls the viewer supports? I have looked through many source examples and was unable to find it. What JS calls are made from the viewer to the self hosted API? Can those be overriden?

I should also note we need to be able to intercept any api calls from the Viewer to manage the current user making the request, as our storage is gated behind currentUser.Tenant identifiers.

Appreciate you taking the time to assist.

Thanks,
AB

@abcmk

There are a number of questions you’ve raised, let me try to answer them one by one.

All the logic is hidden in corresponding packages / assemblies.

The API calls are being made from the Angular UI which is part of GroupDocs.Viewer.UI package. The source code can be found on GitHub - link.

API calls are handled by ViewerController from GroupDocs.Viewer.UI.Api package.

This call is encapsulated in GroupDocs.Viewer.UI.SelfHost.Api package. You can specify the output type in the Startup.cs

ViewerType viewerType = ViewerType.HtmlWithEmbeddedResources;
 
 services
     .AddGroupDocsViewerUI(config =>
     {
         config.SetViewerType(viewerType);
     });
 
 services
     .AddControllersWithViews()
     .AddGroupDocsViewerSelfHostApi(config =>
     {
         config.SetViewerType(viewerType);
     })

There are four options

public enum ViewerType
{
    HtmlWithEmbeddedResources,
    HtmlWithExternalResources,
    Png,
    Jpg
}

You have two options here. First - you can do nothing and forget the output. Second - cache the results by using file cache or in-memory cache. Since you have access to the source code you can decide what to do with the output.

You can control the UI appearance from the Strartup.cs. By default, all the controls are enabled, you can disable and hide things by calling the corresponding methods on the config object.

services
    .AddGroupDocsViewerUI(config =>
    {
        config
            .DisableFileUpload() // users won't be able to upload a files
            .DisableFileBrowsing() // users won't be able to browse files
            .DisableFileDownload() // download button will be hidden
            .DisableRightClick()
            .HideSearchControl();
     });

If a feature is disabled API will forbid calls to the method by responding with an error.

Then, I believe you don’t need to reference any of the packages and the simplest solution would be the following:

    public IActionResult Viewer(string file)
    {
        string html = string.Empty;

        using(Viewer viewer = new Viewer(file))
        {
            HtmlViewOptions viewOptions = HtmlViewOptions.ForEmbeddedResources(
                (_) => new MemoryStream(),
                (int pageNumber, Stream pageStream) => {
                    var bytes = ((MemoryStream)pageStream).ToArray();
                    html  = Encoding.UTF8.GetString(bytes);
                });

            viewer.View(viewOptions, 1);
        }

        return Content(html, "text/html");
    }

The source code of the sample app that I’ve taken the code from viewer-net-sample-web.zip (1.6 MB)

Unfortunately, we do not have documentation for GroupDocs.Viewer.UI and related packages. You can check ViewerController file for all the contracts between UI and API. Yes, this can be overridden since all our client solutions are open source.

Sure, you can add a custom middleware to the pipeline and authorize all of the requests before they hit ViewerController.

Hi Vladimir,

I think we are getting closer to a working solution. However, I’ve now encountered a new snag that I cannot seem to make go away using your sample files provided or any word documents. I receive an error saying ‘Failed to detect file type’ on viewer.View(viewOptions, 1);

I have mocked obtaining a byte array from my file storage service that grabs data from Azure Blob shown in this snippet below. What am I doing incorrectly?

 public IActionResult Viewer()
    {
        string html = string.Empty;
        Stream content = null;

        string path = @"c:\temp\wordbefore.docx";
        if (!System.IO.File.Exists(path))
        {
            throw new Exception("File not found, create sample word file on disk: " + path)
            {

            };
        }
        var fileStream = System.IO.File.ReadAllBytes(path);  //fileService.DownloadAttachmentFromBlob(attachmentId);
        content = new MemoryStream(fileStream);
        using (Viewer viewer = new Viewer(content))
        {
            HtmlViewOptions viewOptions = HtmlViewOptions.ForEmbeddedResources(
                (_) => new MemoryStream(),
                (int pageNumber, Stream pageStream) => {
                    var bytes = ((MemoryStream)pageStream).ToArray();
                    html = Encoding.UTF8.GetString(bytes);
                });

            viewer.View(viewOptions, 1);
        }

        return Content(html, "text/html");
    }

@abcmk

Can you attach this file so we could take a look? The workaround would be passing LoadOptions as a second constructor parameter of Viewer class e.g.

...
LoadOptions loadOptions = new LoadOptions(FileType.DOCX);
using (Viewer viewer = new Viewer(content, loadOptions)) 
...

There are a couple of methods on FileType that may help you determine the file type e.g. FileType.FromExtension, and FileType.FromMediaType.

sample.docx (90.0 KB)

I have attached the same word document included in some of your source code examples. I will take a look at the load options and see what I can come up with, thank you.

Hi Vladimir,

I was able to successfully get the first page of the document to render. The next question is how to render the entire document, regardless of pages, at once without using the page stream page by page.

I would ideally like to restrict our viewer to stop rendering after the first 10 pages, but I am unclear how to do that if a document only has 1 or 2 pages, respectfully. How can I configure this to render the entire document without pagination?

I have tried viewer.View(viewOptions, new int[] {1, 2, 3 }) but changing this to an array only ever renders the last page in the document.

@abcmk

There are a couple of options here.

  1. Join all the strings into a single string with html += Encoding.UTF8.GetString(bytes);, here you have control over a number of pages you want to rendered viewer.View(viewOptions, new[] { 1, 2, 3, 4, 5 });
  2. Set viewOptions.RenderToSinglePage = true which will render complete document into a single page. This option is supported for Word documents and Archive files. In further versions, we’re going to extend support for this option.

Viewer will render the pages that exist. In case document has only one page and you pass the following viewer.View(viewOptions, new[] { 1, 2, 3 }); only the first page will be rendered.

It happens because string html = string.Empty; variable is set three times, you can use a list to store each of the pages. Here is an example - Save output to a stream | Documentation

Hi Vladimir,

Thanks for the great support getting us up and running. I am now experiencing some struggles with excel documents - sheets are not rendering, only the first page shows up and its raw. I found some sample code to extract the amount of sheets and their names, but I’m curious how to instruct the viewer to draw the sheets as one would expect, in a tabular layout. Is any of this possible or do we need to come up with some paging solution?

I have shown our implementation code for context sake:

    /// <summary>
    /// Ajax/Iframe call from our "Viewer Popup Window"
    /// </summary>
    /// <param name="id">The Attachment Id</param>
    /// <returns></returns>
    public async Task<IActionResult> View(int id)
    {
        var attachment = FileService.GetAttachmentFromSQL(id); //Reduced for brevity
            var fileStream = await FileService.DownloadAttachmentFromBlob(attachment); //Fetch blob from Azure Storage
            if (fileStream == null)
            {
                return NotFound();
            }

            LoadOptions loadOptions = new LoadOptions(FileType.FromExtension(Path.GetExtension(attachment.FilePath)));

            using (Viewer viewer = new Viewer(new MemoryStream(fileStream), loadOptions))
            {

                HtmlViewOptions viewOptions = HtmlViewOptions.ForEmbeddedResources(
                    (_) => new MemoryStream(),
                    (int pageNumber, Stream pageStream) => {
                        var bytes = ((MemoryStream)pageStream).ToArray();
                        html += Encoding.UTF8.GetString(bytes);
                    });

                viewOptions.Minify = true;
                viewOptions.RenderToSinglePage = true;
                viewOptions.SpreadsheetOptions.RenderGridLines = true;
                viewOptions.SpreadsheetOptions.RenderHeadings = true;

                viewer.View(viewOptions, 1);
            }

            return Content(html, "text/html");
        }
        return NotFound();
    }

@abcmk

Rendering to a single page is not supported for spreadsheet documents. By default, spreadsheets are rendered by page breaks page-break-view.png (83.9 KB). You can render a complete worksheet to a single HTML page by setting viewOptions.SpreadsheetOptions = SpreadsheetOptions.ForOnePagePerSheet();. When rendering a worksheet to a page it may be reasonable to skip rendering empty rows and columns

viewOptions.SpreadsheetOptions.SkipEmptyColumns = true;
viewOptions.SpreadsheetOptions.SkipEmptyRows = true;

It’s not supported out of the box right now. I’ve planned to implement this feature in the future release. The issue ID for reference is VIEWERNET-4124.

There does not seem to be a way to render all of the sheets in a spreadsheet, only ever the first sheet. This seems like an oversight?

@abcmk

To render all the sheets call View method without the second parameter

viewOptions.SpreadsheetOptions = SpreadsheetOptions.ForOnePagePerSheet();.
viewer.View(viewOptions); // render all the worksheets

The second parameter accepts an array of page numbers to render.

Of course, thanks vlad. Well done on hand holding!

@abcmk

You’re welcome!

@abcmk

GroupDocs.Viewer 22.11 supports the conversion of Excel spreadsheets to one HTML file. See the following section in release notes for more information: Added support for converting all Excel worksheets to one HTML file.

Viewer is not rendering all pages on Azure deployment. It works fine when running local. Rendering pages also takes a significant amount of time using embedded resources.

Please provide accurate sample code to render an entire document, regardless of type to the viewer. We do not have the option to use pagination or cache as these are classified documents, and the URLs cannot be exposed publicly so all data must be sent back via the API.

It appears as though sometimes the logic to returnValue += Encoding.UTF8.GetString(bytes) only gets called once, and does not iterate through the streams pages. This is inconsistent as other times it does. What am I doing wrong?

using (MemoryStream fileStream = new MemoryStream())
{
    await cloudBlockBlob.DownloadToStreamAsync(fileStream, accessCondition, blobRequestOptions, operationContext);
    fileStream.Position = 0;
    fileStream.Flush();
    cloudBlobContainer = null;
    cloudBlockBlob = null;
    
    fileStream.Position = 0;
    fileStream.Flush();
    LoadOptions loadOptions = new LoadOptions(FileType.FromExtension(Path.GetExtension(attachment.FilePath)));
    using (Viewer viewer = new Viewer(fileStream, loadOptions))
    {
            HtmlViewOptions viewOptions = HtmlViewOptions.ForEmbeddedResources(
            (_) => new MemoryStream(),
            (int pageNumber, Stream pageStream) =>
            {
                var bytes = ((MemoryStream)pageStream).ToArray();
                returnValue += Encoding.UTF8.GetString(bytes);
            });

        viewOptions.Minify = true;
        viewOptions.RenderToSinglePage = false;
        viewOptions.SpreadsheetOptions = SpreadsheetOptions.ForOnePagePerSheet();
        viewOptions.SpreadsheetOptions.SkipEmptyColumns = true;
        viewOptions.SpreadsheetOptions.SkipEmptyRows = true;
        viewOptions.SpreadsheetOptions.RenderGridLines = true;
        viewOptions.SpreadsheetOptions.RenderHeadings = true;
        viewOptions.SpreadsheetOptions.TextOverflowMode = TextOverflowMode.AutoFitColumn;
        viewOptions.RenderResponsive = true;

        viewer.View(viewOptions);
    }                                
}

@abcmk

Your post was moved to a new thread - Viewer is not rendering all pages on Azure deployment and local inconsistently.

1 Like