Page 2 of 2

Even Header

2 | Page

 

First page

Developing a GroupDocs.Viewer plugin for Kentico CMS

 

This article describes a process of creation, integration and using a plugin for Kentico CMS, which includes GroupDocs.Viewer functionality into Kentico.

Introduction

GroupDocs.Viewer is a web-oriented middleware designed to allow end-users to view different types of documents, such as PDF, DOC/DOCX, XLS/XLSX and so on right from their web-browsers, without need of installing any other software or browser plugins (such as Adobe Flash, Silverlight or Java applets). GroupDocs.Viewer, which is located and runs on hhghg gthe server-side, just transforms an input document to a set of images, HTML markup, CSS, JS and SVG, and then sends all these data to the client-side.

Kentico is a pretty mature and advanced CMS, and is deeply focused on the document management. So it will be very interesting and useful to integrate the ability of viewing different documents into Kentico.

Right now GroupDocs.Viewer exists in different forms: it is available as a .NET class library (DLL), Java library (JAR) and as a Cloud API (SaaS). Because Kentico is an ASP.NET project in its core, we will use GroupDocs.Viewer for .NET library.

Must say that GroupDocs.Viewer plugin for Kentico already exists (http://groupdocs.com/marketplace/plugins/viewer/kentico) and is available along with its source code. But when you take a look on it, you will see that it uses GroupDocs.Viewer as SaaS — it simply creates an iframe on the web-page and redirects all input documents to the Cloud API. This requires you to have a GroupDocs account and use a third party web resource (GroupDocs Cloud API) in your site. But what if you want that all work must be done on your own server, without any external requests? What if all of your documents are confidential/classified and you definitely don’t want to send them via Internet? What if you have a GroupDocs.Viewer as a library file on your server and do not want use a remote API? Because your website is located on the intranet (or something like that) and has no access to the Internet? Or you do not want rely on the internet connection? This article describes su [1]ch situation.

Because Kentico CMS basically is an ASP.NET WebForms project, we will use a GroupDocs.Viewer for .NET library. The big part of all features of the Kentico is located in the web parts. In terms of Kentico, web part is a component of the page with its own functionality. This conception is very similar to the ASP.NET web user controls (ASCX).

In the basic redistributable package of Kentico there is a “Document library” ( http://devnet.kentico.com/docs/devguide/index.html?document_library_overview.htm) web part, which manages documents. It allows to upload documents, download, and update, view the list of all available documents, set a permissions for them, rename them and so on. When you click on the document name, it downloads to your PC and your web-browser stores it on your local machine with or without asking to set a location for storing. I want to create a new plugin in a form of new web part, which will have all the functionality of the “Document library”, but with one main distinction: when you click on the document, it must be viewed right into the browser using GroupDocs.Viewer, but not downloading as a file.

 

Page 2 of 2

Even Header

2 | Page

 

First page

 

In this article I will show you how to integrate GroupDocs.Viewer library into your Kentico project, how to develop a new web part with all features of the original web part, and how to use GroupDocs.Viewer from this new web part.

Required knowledge and platform

I must say that in order not to write a whole book I must rely on the some defined level of your knowledge and experience.

I expect that you know .NET and C# in general, ASP.NET and ASP.NET Web Forms, and und

 

 

 

 

 

 

 

 

 

 

 

 

 

erstand how client/server communication works. You must understand the differences between Web Site project and Web Application project. You also must know the very basics of SQL Server: how to connect to it using SQL Server Management Studio, MS Visual Studio or some other DB-browser, how to view content of the table and so on. Also I expect that you know the basics of IIS: how to open IIS manager, how to manage sites and application pools. [2]

You must be familiar with Kentico CMS itself, of course. At least you must be able to install it successfully.

In other words, if you are a .NET developer and are familiar with Kentico, you know all that I need from you.

Now about my software platform and versions, and what you can use.

OS: Windows 7 SP1 x64

Web server: IIS 7.5

RDBMS: SQL Server 2012 RTM Enterprise

Kentico CMS: 7.0

I had installed Kentico as a Web Application project. By default it installs into the “wwwroot” folder inside “inetpub”, but you can select any other folder you want. When installing Kentico I had checked in “Intranet Portal” sample site because it widely uses “Document library” web part.

I use a 30-day trial version of Kentico. In order to have all functionality available I use “localhost” domain name for the site.

For a SQL Server, you are welcome to use any other and older version, including freeware Express Edition.

So, now, when all is clear, let’s go.

Part 1. Adding a GroupDocs.Viewer to the Kentico project

Our first step is to add GroupDocs.Viewer library to the project and make sure, that it works well.

Here I need to make a short description about how GroupDocs.Viewer works. In fact it is mainly based on the HTTP-handlers (implementations of the System.Web.IHttpHandler interface). When you download HTML-page where GroupDocs.Viewer is placed, initially only basic JavaScript- and CSS-files are presented on the page. When the page is fully downloaded, these scripts run and invoke the main functionality

Page 2 of 2

Even Header

2 | Page

 

First page

through HTTP-handlers. And, of course, because GroupDocs.Viewer is a server-side application, the target document must be located on the server. But GroupDocs.Viewer can handle documents not only as the files in the local file system; it also supports byte streams.

GroupDocs.Viewer also has a caching mechanism. It caches every document that you had viewed in the local folder of the web-site. So you must allow a write-access to the Kentico project folder. You can do this from IIS manager. Open it, find a Kentico website -> mouse right click on it -> Edit permissions -> “Security” tab -> add write access for Users.

I suggest you to place GroupDocs.Viewer.dll file into a “Lib” folder of the Kentico project – this is a place where all libraries are located. Then add a reference to it.

Now let’s create a new ASPX web page where GroupDocs.Viewer will be placed. At this moment there will not be any documents to view — we just want to make sure that [3]GroupDocs.Viewer works. In order to keep purity in the root folder of the Kentico project I had created a simple folder with “GDViewer” name. When folder is created, add a new AXPX Web Form page into it. A called this page “Viewer.aspx”.

Now we need to add some minimal amount of code and see the GroupDocs.Viewer interface. I will use a code samples from “How to use GroupDocs Viewer for .NET in an ASP.NET Project” article ( http://groupdocs.com/docs/display/viewernet/How+to+use+GroupDocs+Viewer+for+.NET+in+an+ASP.NET+Project).

Here is a code-front of this page. Code-behind is empty.

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Viewer.aspx.cs" Inherits="CMSApp.GDViewer.Viewer" %>

<%@ Import namespace="Groupdocs.Web.UI" %>

 

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">

<head runat="server">

<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

    <title>GroupDocs.Viewer page</ title>

      <%=Viewer.CreateScriptLoadBlock().LoadJquery().LoadJqueryUi().UseHttpHandlers() %>

</head>

<body style="margin:0px; padding:0px" >

    <form id="form1" runat ="server">

 

    <div id="test" style ="width: 100%; height: 700px ; position: relative; margin-bottom: 20px ; "></div>

   

<%=Viewer.ClientCode()

            .TargetElementSelector("#test")

            .FilePath("not_exists.pdf")

            .DocViewerId("doc_viewer1")

            .EnableRightClickMenu(true)

            .ShowDownload(true)

            .ShowPrint(true)

            .ShowThumbnails(true)

            .OpenThumbnails(true)

            .ZoomToFitWidth() %>    </form>

</body>

</html>

If you will try to go to this page, you will see this error (on screenshot below).

 

Must admit that this error info is not so informative as it must be. “System.ArgumentException” says nothing about the real reason.

As it was mentioned above, GroupDocs.Viewer has a caching mechanism which uses a local folder for temporary results. And for proper working it must know what exact folder it should use. For doing this you must specify a path using “ Groupdocs.Web.UI.Viewer.SetRootStoragePath” method. In the official GroupDocs.Viewer examples and in the article that I had mentioned above there is a brief instruction about how and where storage path must be specified. Particularly, in all examples storage path is specified in the “Global.asax”. From a GroupDocs.Viewer perspective there is no matter where exactly storage path is specified in the client code — the main requirement is to specify it before first use of the viewer. So, what we will do in our case?

If you will open “Global.asax.cs” file in the Kentico’s project, you will see the next code:

/// <summary>

/// Application methods.

/// </summary>

public class Global : CMSHttpApplication

{

    #region "System data (do not modify)"

 

    /// <summary>

    /// Application version, do not change.

    /// </summary>

    /// const string APP_VERSION = "7.0";

 

    #endregion

}

 

Page 2 of 2

Even Header

2 | Page

 

First page

As you can see, Kentico’s developers do not want us to modify this class. You cannot add an “Application_Start” method because in this case it will hide the original one from the base class “ CMSHttpApplication”. So, in order to do what we want we need to modify the “CMSHttpApplication” class itself. After opening it you will see all familiar event handlers from “Global.asax”. So, just set a storage path in the “ Application_Start” method. All official sample projects use folder named “testfiles” for storing documents and cache. So do I. In my case it will be “ Groupdocs.Web.UI.Viewer.SetRootStoragePath(Server.MapPath("~/testfiles/"));” at the end of the body of the “ Application_Start” method.

Also, if you have a license file, you can specify it in the same place. Just call the “Groupdocs.Web.UI.Viewer.SetLicensePath(“/path/my_license_file.lic”);” method.

Now, when you specified a storage path, it’s time to look at the result. But a result is disappointing: we have an empty page. When you look in the request/response scanner (I use Fiddler2), you should see that after successful downloading the main page “Viewer.aspx” all subsequent asynchronous requests have a “HTTP 404 Not Found” response. This is because we didn’t specify the HTTP-handlers in our “web.config” file. Let’s do this right now.

 

For IIS version 7 (or later) in the Classic Pipeline mode, or IIS version 6, or ASP.NET Development Server (a.k.a. Cassini), enter the following HTTP handler descriptions into the system.web section of the web.config:

<add verb="*" path ="document-viewer/ViewDocumentHandler" type=" Groupdocs.Web.UI.Handlers.ViewDocumentHandler, Groupdocs.Viewer, Culture=neutral " validate="false" />

      <add verb=" *" path="document-viewer/GetDocumentPageImageHandler" type="Groupdocs.Web.UI.Handlers.GetDocumentPageImageHandler, Groupdocs.Viewer, Culture=neutral" validate ="false" />

      <add verb=" *" path="document-viewer/LoadFileBrowserTreeDataHandler" type="Groupdocs.Web.UI.Handlers.LoadFileBrowserTreeDataHandler, Groupdocs.Viewer, Culture=neutral" validate ="false" />

      <add verb=" *" path="document-viewer/GetImageUrlsHandler" type ="Groupdocs.Web.UI.Handlers.GetImageUrlsHandler, Groupdocs.Viewer, Culture=neutral" validate = "false" />

      <add verb=" GET" path="document-viewer/CSS/GetCssHandler" type ="Groupdocs.Web.UI.Handlers.CssHandler, Groupdocs.Viewer, Culture=neutral"/>

      <add verb=" GET" path="document-viewer/images/*" type ="Groupdocs.Web.UI.Handlers.EmbeddedImageHandler, Groupdocs.Viewer, Culture=neutral"/>

      <add verb=" GET" path="groupdocs/images/*" type ="Groupdocs.Web.UI.Handlers.EmbeddedImageHandler, Groupdocs.Viewer, Culture=neutral"/>

      <add verb=" GET,POST" path="document-viewer/GetScriptHandler" type="Groupdocs.Web.UI.Handlers.ScriptHandler, Groupdocs.Viewer, Culture=neutral"/>

      <add verb=" GET" path="document-viewer/GetFileHandler" type ="Groupdocs.Web.UI.Handlers.GetFileHandler, Groupdocs.Viewer, Culture=neutral" validate = "false" />

      <add verb=" POST" path="document-viewer/GetPdf2XmlHandler" type="Groupdocs.Web.UI.Handlers.GetPdf2XmlHandler, Groupdocs.Viewer, Culture=neutral" />

      <add verb=" GET,POST" path="document-viewer/GetPdf2JavaScriptHandler" type="Groupdocs.Web.UI.Handlers.GetPdf2JavaScriptHandler, Groupdocs.Viewer, Culture=neutral" validate ="false" />

      <add verb=" GET,POST" path="document-viewer/GetPdfWithPrintDialogHandler " type="Groupdocs.Web.UI.Handlers.GetPdfWithPrintDialogHandler, Groupdocs.Viewer, Culture=neutral" />

      <add verb=" GET,POST" path="document-viewer/GetPrintableHtmlHandler" type="Groupdocs.Web.UI.Handlers.GetPrintableHtmlHandler, Groupdocs.Viewer, Culture=neutral" />

 

Page 2 of 2

Even Header

2 | Page

 

First page

For the IIS version 7 (or later) in the Integrated Pipeline mode, enter the following HTTP handler descriptions into the system.webServer section of the web.config:

<add name="ViewDocumentHandler" verb ="*" path="document-viewer/ViewDocumentHandler " type="Groupdocs.Web.UI.Handlers.ViewDocumentHandler, Groupdocs.Viewer, Culture=neutral " />

      <add name=" GetDocumentPageImageHandler" verb="*" path ="document-viewer/GetDocumentPageImageHandler" type=" Groupdocs.Web.UI.Handlers.GetDocumentPageImageHandler, Groupdocs.Viewer,Culture=neutral" />

      <add name=" LoadFileBrowserTreeDataHandler" verb="*" path ="document-viewer/LoadFileBrowserTreeDataHandler" type= " Groupdocs.Web.UI.Handlers.LoadFileBrowserTreeDataHandler, Groupdocs.Viewer, Culture=neutral" />

      <add name=" GetImageUrlsHandler" verb="*" path ="document-viewer/GetImageUrlsHandler" type=" Groupdocs.Web.UI.Handlers.GetImageUrlsHandler, Groupdocs.Viewer, Culture=neutral" />

      <add name=" GetCssHandler" verb="GET" path ="document-viewer/CSS/GetCssHandler" type=" Groupdocs.Web.UI.Handlers.CssHandler, Groupdocs.Viewer, Culture=neutral" />

      <add name=" images" verb="GET" path ="document-viewer/images/*" type=" Groupdocs.Web.UI.Handlers.EmbeddedImageHandler, Groupdocs.Viewer, Culture=neutral " />

      <add name=" images2" verb="GET" path ="groupdocs/images/" type="Groupdocs.Web.UI.Handlers.EmbeddedImageHandler, Groupdocs.Viewer, Culture=neutral " />

      <add name=" GetScriptHandler" verb="GET,POST" path ="document-viewer/GetScriptHandler" type=" Groupdocs.Web.UI.Handlers.ScriptHandler, Groupdocs.Viewer, Culture=neutral " />

      <add name=" GetFileHandler" verb="GET" path ="document-viewer/GetFileHandler" type=" Groupdocs.Web.UI.Handlers.GetFileHandler, Groupdocs.Viewer, Culture=neutral " />

      <add name=" GetPdf2XmlHandler" verb="POST" path ="document-viewer/GetPdf2XmlHandler" type=" Groupdocs.Web.UI.Handlers.GetPdf2XmlHandler, Groupdocs.Viewer, Culture=neutral" />

      <add name=" GetPdf2JavaScriptHandler" verb="GET,POST" path ="document-viewer/GetPdf2JavaScriptHandler" type=" Groupdocs.Web.UI.Handlers.GetPdf2JavaScriptHandler, Groupdocs.Viewer, Culture=neutral" />

      <add name=" GetPdfWithPrintDialogHandler" verb="GET,POST" path ="document-viewer/GetPdfWithPrintDialogHandler" type=" Groupdocs.Web.UI.Handlers.GetPdfWithPrintDialogHandler, Groupdocs.Viewer, Culture=neutral" />

      <add name=" GetPrintableHtmlHandler" verb="GET,POST" path ="document-viewer/GetPrintableHtmlHandler" type=" Groupdocs.Web.UI.Handlers.GetPrintableHtmlHandler, Groupdocs.Viewer, Culture=neutral" />

You can have both of these sections, but in this case don’t forget to add a <validation validateIntegratedModeConfiguration= "false" /> into system.webServer section.

Now, when all HTTP-handlers were added, let’s try to invoke the “Viewer.aspx” page again. But … results are the same: all requests to the GroupDocs.Viewer HTTP-handlers have a “404 - Not Found” response. If you will specify a direct path to the any GroupDocs.Viewer HTTP-handler in the browser (for example, http://localhost/document-viewer/ViewDocumentHandler) and if you use IIS for hosting a Kentico project, you should see a standard IIS error message:

HTTP Error 404.0 - Not Found

The resource you are looking for has been removed, had its name changed, or is temporarily unavailable.

Detailed Error Information

Module: CMSApplicationModule

Notification: MapRequestHandler

 

Page 2 of 2

Even Header

2 | Page

 

First page

Handler: ViewDocumentHandler

Error Code: 0x00000000

Please note a HTTP-module named “CMSApplicationModule”. It blocks all requests to our GroupDocs.Viewer HTTP-handlers. You can check it by yourself – just comment or delete its specification in the web.config.

But after removing a “CMSApplicationModule” Kentico web-site does not work, you should see the next error message:

Server Error in '/' Application.

[SystemMethods]: System events are not initialized. You must call CMSContext.Init() before running any of your methods. In web application, the web.config file must contain registration for the CMSApplicationModule: <add name="CMSApplicationModule" type="CMS.CMSHelper.CMSApplicationModule, CMS.CMSHelper"/>.

So, how to make Kentico site work properly and simultaneously allow custom HTTP-handlers?

Well, you need to configure it via Kentico’s web interface. Restore the “CMSApplicationModule” specification in the web.config, if you had removed it before, launch the site, then proceed to the Site Manager -> Settings tab -> URLs and SEO item -> URL format group -> Excluded URLs textbox. All GroupDocs.Viewer HTTP-handlers have a common prefix – “ document-viewer”, so you need to specify this prefix in this field as it is shown on the screenshot:

 

Save changes by clicking a “Save” button.

Now it is time to return to our “Viewer.aspx” page. Go to the “http://localhost/GDViewer/Viewer.aspx” or wherever it is located in your project.

Page 2 of 2

Even Header

2 | Page

 

First page

 

 

Finally, GroupDocs.Viewer works fine. It’s time to view some real document within it.

 

Part 2. Viewing a document with GroupDocs.Viewer

We need to send documents from the Kentico CMS to the GroupDocs.Viewer. But in order to do this we must figure out how and where these documents are stored and handled in the Kentico CMS.

In general, there are two common ways to store files in such web project. One approach is to store them locally as files in the file system. Second approach is to store them as BLOBs in the database. Both these ways have pros and cons. First is better for big files, second – for small. After release of the SQL Server 2008 new approach has arrived – FILESYSTEM. After releasing a FileTable wrap around the FILESYSTEM (introduced in the SQL Server 2012) this approach truly combines advantages from both approaches. But I digressed.

Kentico CMS stores all documents as BLOBs in the database, without any FILESYSTEM or FileTable mechanisms. There are several tables where files are stored, but for our purpose only one is interesting: “CMS_Attachment”. Here is its structure:

 

On the screenshot first column is a field name, second is a field type, and the third shows does this field allows NULL values or not.

Content of the files is stored within the “AttachmentBinary” column of the “varbinary” type. In fact, files are stored as a simple byte arrays.

How can we get these files in order to transfer them to the GroupDocs.Viewer? Of course, we can write some sort of a Data Access Layer/Repository mechanism from scratch, — open a SQL connection, create a query and execute it using SQL command, and after what take the results via SqlDataReader or SqlDataAdapter, — this is a common ADO.NET approach. In counterpart we can use some ORM, for example, Entity Framework, Linq2Sql or NHibernate. But, in fact, in our situation all these approaches are redundant, because Kentico already has its own data access layer.

Kentico allows us to get a file content as a byte array. GroupDocs.Viewer can work with byte streams, so all we need is to convert byte array to any implementation of the System.IO.Stream class. This can be easily done with System.IO.MemoryStream: it has a constructor which takes one parameter – byte array, — and creates a MemoryStream wrapper around it.

Now we know how to get a file from the database and how to put it to the GroupDocs.Viewer. But how to specify what exactly file we need to view? Please take a look on the screenshot above with the structure of the “CMS_Attachment” table. Every file has several identifiers, but the main is an “AttachmentGUID” non-nullable field of type “uniqueidentifier” (GUID). This identifier is used by the “Document library” web part and Kentico’s Data Access Layer allows us to get the file by its GUID.

So here is how we do this. We modify a “Viewer.aspx” in such way that the page must accept one GET-parameter from the URL with name “guid”. Like this: http://localhost/GDViewer/Viewer.aspx?guid=326b1336-6222-44e8-af1a-e5a60c371774

If parameter is absent, has an empty value, or value itself is invalid, we can show an error, but it will be easier just to redirect the user to the main page. After receiving a valid GUID value and with its help we take a document’s content from the database as a byte array, convert this array to the MemoryStream and specify this stream in the GroupDocs.Viewer.

Here is a code-behind of the updated page “Viewer.aspx”:

using System;

using CMS.CMSHelper;

using CMS.DocumentEngine;

 

namespace CMSApp.GDViewer

{

    public partial class Viewer : System.Web.UI. Page

    {

        public System.IO.Stream _data = null;

 

        public String _extension = null ;

 

        protected void Page_Load(object sender, EventArgs e)

        {

            String guid = this.Context.Request.QueryString.Get( "guid");

            if (String.IsNullOrWhiteSpace(guid) == true)

            {

                this.Response.Redirect(this .Request.Url.Scheme + "://" + this.Request.Url.Authority);

            }

            Tuple<System.IO.MemoryStream , String> result = Viewer.GetAttachmentStreamByGUID(guid);

            this._data = result.Item1;

            this._extension = result.Item2;

        }

 

        public static Tuple<System.IO. MemoryStream, String> GetAttachmentStreamByGUID(Object fileAttachmentGuidId)

        {

            // Get current attachment GUID

            Guid attachGuid = CMS.GlobalHelper.ValidationHelper .GetGuid(fileAttachmentGuidId, Guid.Empty);

            if (attachGuid == Guid.Empty)

            {

                throw new ArgumentException ("Input GUID is not valid", "fileAttachmentGuidId");

            }

            AttachmentInfo ai = AttachmentInfoProvider .GetAttachmentInfo(attachGuid, CMSContext.CurrentSite.SiteName);

            if (ai == null)

            {

                throw new ArgumentException (String.Format("There is no attachment with GUID = {0}", attachGuid), "fileAttachmentGuidId");

            }

            Byte[] content = ai.AttachmentBinary;

            System.IO.MemoryStream outputStream = new System.IO.MemoryStream(content);

            String ext = ai.AttachmentExtension;

            return new Tuple<System.IO. MemoryStream, String>(outputStream, ext);

        }

    }

}

And here is code-front:

<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Viewer.aspx.cs" Inherits="CMSApp.GDViewer.Viewer" %>

<%@ Import namespace="Groupdocs.Web.UI" %>

 

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">

<head runat="server">

<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

    <title>GroupDocs.Viewer page</ title>

      <%=Viewer.CreateScriptLoadBlock().LoadJquery().LoadJqueryUi().UseHttpHandlers() %>

</head>

<body style="margin:0px; padding:0px" >

    <form id="form1" runat ="server">

 

    <div id="test" style ="width: 100%; height: 700px ; position: relative; margin-bottom: 20px ;"></div>

    <%=Viewer.ClientCode()

            .TargetElementSelector("#test")

            .Stream(this._data, this ._extension)

            .DocViewerId("doc_viewer1")

            .EnableRightClickMenu(true)

            .ShowDownload(true)

            .ShowPrint(true)

            .ShowThumbnails(true)

            .OpenThumbnails(true)

            .ZoomToFitWidth() %>

    </form>

</body>

</html>

Please take a look on the GetAttachmentStreamByGUID method, which receives a file's GUID and returns a pair of two values: the document’s content as a MemoryStream and the file’s extension as a System.String. Also note that class AttachmentInfoProvider has appeared only in Kentico 7 and it is absent in the previous versions. In the Kentico 6 and prior versions its analog can be found in the AttachmentManager class, which itself is not present in the Kentico 7. Also I suggest you to avoid mixing of System.IO namespace from .NET BCL and CMS.IO namespace from Kentico’s file system access API. Kentico’s CMS.IO has types with the same name as the System.IO but with different behavior, so it is important to know what exactly you use at every moment.

And here is the final result:

 

I took this GUID “326b1336-6222-44e8-af1a-e5a60c371774” from the “CMS_Attachment” table — it is a sample document “Understanding-Our-Values.docx” from the "Intranet Portal" sample site.

Now, when GroupDocs.Viewer works successfully and is able to view documents from the database, it’s time to return to our main purpose.

Part 3. Writing a plugin

Speaking briefly, at this stage we must modify the original “Document library” web part in such way that instead of downloading the original files to the client this web part must send a request to the Viewer.aspx page with corresponding GUID of the files. But modifying an original web part is not a good decision, because this modification will have a negative effect to those who already use “Document library” and do not want any changes. The best way is to clone the original and make changes in cloned copy. Let’s investigate.

All Kentico’s web parts are located in the “CMSWebParts” folder. “Document library” is located on the path “/CMSWebParts/DocumentLibrary” respectively. After opening the code-front of the “DocumentLibrary.ascx” you can see small amount of code:

<%@ Control Language="C#" AutoEventWireup="true" Codebehind="~/CMSWebParts/DocumentLibrary/DocumentLibrary.ascx.cs"

    Inherits="CMSWebParts_DocumentLibrary_DocumentLibrary" %>

<%@ Register Src="~/CMSModules/DocumentLibrary/Controls/DocumentLibrary.ascx" TagName ="DocumentLibrary" TagPrefix="cms" %>

<cms:DocumentLibrary ID="libraryElem" runat ="server" />

Code-behind is a little bigger, but there is not any significant code that we can modify. No any HTML-layout construction, working with documents etc. We must go deeper.

From the code-front page we can see that another “DocumentLibrary.ascx” user web user control is used, but at this time it is a Kentico’s module. Let’s navigate to the “/ CMSModules/DocumentLibrary/Controls/” folder. Here is a set of several ASCX controls: “CopyDocument”, “DeleteDocument”, “DocumentLibrary”, “LibraryContextMenu”, and a “DocumentLibrary.xml” configuration file. All of these controls are parts of the “Document library” web part and the main is “DocumentLibrary.ascx”.

When you open this file, you can see really huge amount of code. Fortunately for you, you are reading this article and there is no need for you to explore all these lines of code. So open a code-behind “DocumentLibrary.ascx.cs” and go to the “ gridDocuments_OnExternalDataBound” method (line No 943). Our target is located starting from the line No 1000:

// Generate link to open document

                if (fileAttachment != Guid .Empty)

                {

                    // Get document URL

                    string attachmentUrl = CMSContext.ResolveUIUrl(AttachmentURLProvider.GetPermanentAttachmentUrl(nodeGuid, alias));

                    if (modifyPermission)

                    {

                        attachmentUrl = URLHelper.AddParameterToUrl(attachmentUrl, "latestfordocid", ValidationHelper.GetString(documentId, string.Empty));

                        attachmentUrl = URLHelper.AddParameterToUrl(attachmentUrl, "hash", ValidationHelper.GetHashString( "d" + documentId));

                    }

                    attachmentUrl = URLHelper .UpdateParameterInUrl(attachmentUrl, "chset", Guid.NewGuid().ToString());

 

                    if (! string.IsNullOrEmpty(attachmentUrl))

                    {

                        attachmentName = "<a href=\"" + URLHelper.EncodeQueryString(attachmentUrl) + "\" " + toolTip + ">" + encodedDocumentName + "</a> ";

                    }

                }

The variable “fileAttachment” contains a GUID of the processing document. And in the last line the link for the document downloading is composed.

If you want quick and dirty solution, you can simply modify this last line and put something like attachmentName = "<a href='localhost/GDViewer/Viewer.aspx?guid=" +fileAttachment+"'>"+encodedDocumentName+"</a>"; But, as I told above, it is not good to modify the existing web part.

We can make a full clone by copying the “/CMSModules/DocumentLibrary” folder with all files within it. But it is unnecessary. The only user web control where changes must be done is a “DocumentLibrary.ascx” code-behind. So I do next. I make a copy of the “DocumentLibrary.ascx” with its code-behind “DocumentLibrary.ascx.cs”, rename the copy to the “DocumentLibraryViewer.ascx” (yes, I know, this name is stupid, you can change it as you want), rename the class name from the “CMSModules_DocumentLibrary_Controls_DocumentLibrary” to the “CMSModules_DocumentLibrary_Controls_DocumentLibraryViewer”, and put this control to the “/CMSModules/DocumentLibrary/Controls/” folder, along with other web controls.

And from this moment I can modify the “DocumentLibraryViewer.ascx” without touching the original.

There is one more moment I want to discuss. Modified “Document Library” can open document within GroupDocs.Viewer in two ways: open in the existing window/tab (target = “_self”) or in the new one (target = “_blank”). In some situations the first way is preferable, in others – the second. And I am quite sure that “DocumentLibraryViewer.ascx” must allow both of these ways without need of recompiling the Kentico’s project and even touching the code. The best way is to create new option in the site settings and store its value in the DB. But this requires a lot of code, which has weak relation to the main purpose of the article. So I had chosen an intermediate and moderate solution: to store the configuration in the web.config file. In this case changing the value doesn’t require a recompilation; despite you must be able to get a write-access to the web.config file.

In the “appSettings” section of the web.config I added a new item “<add key ="UseTargetBlank" value=" false "/>”.  And in the “DocumentLibraryViewer.ascx.cs” (code-behind) I had added the next code:

private const Boolean _USE_TARGET_BLANK_BY_DEFAULT = true;

private const String _BLANK = "_blank";

private const String _SELF = "_self";

private readonly String _target;

    /// <summary>

    /// Parameterless constructor for the web user control DocumentLibraryViewer

    /// </summary>

    public CMSModules_DocumentLibrary_Controls_DocumentLibraryViewer()

    {

        String raw_value = System.Configuration.ConfigurationManager .AppSettings["UseTargetBlank"];

        if (String.IsNullOrWhiteSpace(raw_value))

        {

            this._target = _USE_TARGET_BLANK_BY_DEFAULT ? _BLANK : _SELF;

            return;

        }

        String temp = raw_value.Trim().ToLowerInvariant();

        if (temp.Equals("true", StringComparison.Ordinal))

        {

            this._target = _BLANK;

        }

        else if (temp.Equals("false" , StringComparison.Ordinal))

        {

            this._target = _SELF;

        }

        else

        {

            this._target = _USE_TARGET_BLANK_BY_DEFAULT ? _BLANK : _SELF;

        }

    }

If “UseTargetBlank” option is absent in the web.config, or it has no value, or value is incorrect, it will be used value from the “_USE_TARGET_BLANK_BY_DEFAULT” constant. Otherwise the value from web.config will be used.

I also want to show you a code, which creates a request link to the “Viewer.aspx” page that we created before.

    /// <summary>

    /// Creates the link to the GroupDocs.Viewer page

    /// </summary>

    private static String CreateLinkToViewer(HttpRequest RequestData, Guid AttachmentGUID)

    {

        if (RequestData == null) { throw new ArgumentNullException("RequestData"); }

        if (AttachmentGUID == Guid.Empty) { throw new ArgumentException("GUID cannot be empty", "AttachmentGUID" ); }

        String output = String.Format( "{0}://{1}/GDViewer/Viewer.aspx?guid={2}" , RequestData.Url.Scheme, RequestData.Url.Authority, AttachmentGUID.ToString());

        return output;

    }

This very simple piece of code takes two parameters. “AttachmentGUID” is understandable – it is the identifier of the target file. And “RequestData” needs a clarification. It is used in order not to hardcode the HTTP scheme and domain of the project web-site; all we need are extracted from the current HTTP-request.

And finally we can compose a final link with the next elegant way:

attachmentName = String.Format("<a href=\"{0}\" target=\" {1}\">{2}</a> ",

      CreateLinkToViewer(this.Request, fileAttachment), this ._target, encodedDocumentName);

New CMS module is ready. Now we need to create a new web part which will use our modified “DocumentLibraryViewer.ascx” instead of the original “DocumentLibrary.ascx”.

For the sake of similarity I had created a folder “/CMSWebParts/DocumentLibraryViewer”, after this I had made a copy of the original “DocumentLibrary” web part from the “/CMSWebParts/DocumentLibrary” folder and putted it to the new folder with renaming its class name.

I left the code-behind the same, without modifications, except renaming the class name. And made a little change in the code-front of the ““/CMSWebParts/DocumentLibraryViewer/DocumentLibraryViewer.ascx”:

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="~/CMSWebParts/DocumentLibraryViewer/DocumentLibraryViewer.ascx.cs"

    Inherits="CMSWebParts_DocumentLibraryViewer_DocumentLibraryViewer" %>

<%@ Register Src="~/CMSModules/DocumentLibrary/Controls/DocumentLibraryViewer.ascx" TagName="DocumentLibraryViewer"    TagPrefix="cms" %>

<cms:DocumentLibraryViewer ID="libraryElem" runat ="server" />

The last thing left to do: we need to register our newly created web part in the Kentico. Compile and run project. After this go to the Site Manager -> Development tab -> Web parts item. Here you can add a new category; I had added “Document Library Viewer”.

Now it’s time to register our web part “Document Library Viewer”. Click “New web part”, then add a display name to it (I had added “DocumentLibraryViewer”), check out a “Generate the code files” checkbox and click “Select” button near the “File path” textbox. New window will arrive (on the screenshot):

 

In this window navigate to the “/CMSWebParts/DocumentLibraryViewer” folder, check in “DocumentLibraryViewer” web user control and click “Select”.

You will be transferred to the previous window, which now must be like this:

 

Final action – clicking “Save” button.

Finally, new web part is ready. But how to use it?

Part 4. Using a plugin

Before showing you how to use our newly created plugin I want to tell you more about document processing mechanism in the Kentico CMS.

Please take a look on the screenshot below:

 

This is a little DB scheme where are present two tables: “CMS_Attachment” (you are already familiar with it) and “CMS_Document”. Second table is responsible especially for documents. And now you see how documents are bounded with files. Not every file from “CMS_Attachment” must be connected with document. One document can be connected to the different files during its lifetime. That is how update process works for the document: when you update a document uploading a new file, this new file is stored in the “CMS_Attachment” and takes a corresponding “DocumentID”, and old file is removed. New file means new “AttachmentGUID”, so there will be no any caching issues while using GroupDocs.Viewer.

Our newly created “Document library viewer” web part works with these tables along with original web part. Of course, it is possible to create fully autonomous web part, which can use own distinct tables and have own custom document management system, but that is not our purpose right now.

So how to add our new web part? Go to your web-site (I use Intranet portal), switch to the editing mode and go to the “CMS Desk”. Select any page you want (I prefer “Documents” page), select “Design” tab, then select a target zone for our web part.

On this zone click “Add web part”, then go to the “Document library viewer” category, select “Document library viewer” web part and click “OK”. On the next properties window just click “Ok”. Web part was added, save the changes on the page and go to the “Live site” mode.

 

Right now our web part is empty. Just click “New document”, choose your document for upload, and voila!

 

Click on the document and you will see it in the web page instead of downloading to you.

 

Please note that because of “Live site” edition mode is enabled the URL that you see on this screenshot is not as the usual. But when you will switch to the “real” user mode all must work perfectly.

Conclusion

Finally, we did it! We worked with the Kentico at the lowest level of all available. As you see, it is not as hard as it first seems. Here are a lot of possibilities to improve our web part, to add extra features to it. You also can change the Viewer.aspx page itself: add user restrictions, permissions for viewing, to transfer GUID to it not via URL, but via POST data due to security reason. But all this up to you.

I hope that this article was useful and interesting for you. Hope that it will inspire you to write new, production-ready and powerful plugins for K

 

 

 

 

 

 

 

entico CMS.

You can download the plugin I had developed here with the next link: https://www.dropbox.com/s/xw6yl374l6r5464/GroupDocs.Viewer%20Plugin%20for%20Kentico.7z