We're sorry GroupDocs doesn't work properply without JavaScript enabled.

Free Support Forum - groupdocs.com

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 - https://github.com/groupdocs-viewer/GroupDocs.Viewer-for-.NET-UI.

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 - https://docs.groupdocs.com/viewer/net/save-output-to-stream/