A CKEditor File Browser For ASP.NET Web Pages

CKEditor is one of the most popular rich text editors available. Out of the box, there is no file browser to help with selecting images, but you can purchase an add-on from them - CKFinder. Alternatively, you can build your own file browser component.

By default, if you click the Image button in CKEditor, you are presented with a dialogue box that requires you to enter a url for the image you want to insert. For web developers an other users with a technical bent, that shouldn't pose too many problems, but the average user won't have a clue as to what is required. What they need is a way to select an image and to have its url automatically populate the dialogue. CKEditor exposes an API that enables developers to plug in their own custom file browser to enable this.

At its simplest, a file browser can present a list of files and provide a means by which the user can indicate which one they would like to use. However, this approach would require that the user can identify which image they would like to select purely from its file name. Once any collection of image files grows, it is unlikely that the user will be able to associate images with file names unless some kind of supplementary indexing system was employed. A much more user-friendly solution would be to provide the user with a collection of thumbnails of images from which they can make their selection. The workflow is as follows:

  1. User clicks a button to browse images on the server
  2. User is presented with a list of directories that contain images from which they can select one
  3. The images in the selected directory are presented to the user in thumbnail form
  4. User can select one image by clicking it and the dialogue is populated with their selection

So you need to begin with some code to generate thumbnails of reasonable quality for display. The following code is added to a file called ThumbNails.cshtml which is placed in the App_Code folder:

@using System;
@using System.Drawing;
@using System.Drawing.Drawing2D;
@using System.IO;
@functions {
    public static byte[] GenerateTumbnail(string image, double thumbWidth) {
        try {
            using (var originalImage = Image.FromFile(image)) {
                var oWidth = originalImage.Width;
                var oHeight = originalImage.Height;
                var thumbHeight = oWidth > thumbWidth ? (thumbWidth / oWidth) * oHeight : oHeight;
                thumbWidth = oWidth > thumbWidth ? thumbWidth : oWidth;
                using (var bmp = new Bitmap((int)thumbWidth, (int)thumbHeight)) {
                    bmp.SetResolution(originalImage.HorizontalResolution, originalImage.VerticalResolution);
                    using (var graphic = Graphics.FromImage(bmp)) {
                        graphic.SmoothingMode = SmoothingMode.AntiAlias;
                        graphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
                        graphic.PixelOffsetMode = PixelOffsetMode.HighQuality;

                        var rectangle = new Rectangle(0, 0, (int)thumbWidth, (int)thumbHeight);
                        graphic.DrawImage(originalImage, rectangle, 0, 0, oWidth, oHeight, GraphicsUnit.Pixel);
                        var ms = new MemoryStream();
                        bmp.Save(ms, originalImage.RawFormat);
                        return ms.GetBuffer();
                    }
                }
            }
        }
        catch (Exception ex) {
            throw (ex);
        }
    }
}

This method picks up the the original image that is passed into it, and reduces it in size if it exceeds the width passed in as the second argument. It returns a byte array, so the new image is not stored on disk. Next, you need some code that passes in the image and thumbnail width and can do something meaningful with the byte array that is returned. This code forms Thumb.cshtml which resides in the root of the site:

@{
    if (Request["image"].IsEmpty()) {
        Response.End();
    }
    var image = Path.Combine(Server.MapPath("~/Media/"), Server.UrlDecode(Request["image"]));
    var thumbWidth = 128D;
    var buffer = ThumbNails.GenerateTumbnail(image, thumbWidth);
    Response.ContentType = string.Format("image/{0}", Path.GetExtension(image).Trim(new[]{'.'}));
    Response.OutputStream.Write(buffer, 0, buffer.Length);
    Response.End();
}

This file acts as a handler. When it is requested, it takes the image file name from the query string and uses the GenerateThumbnail function to create the thumb image and then write it to the browser. For a handler file like this to work, it needs to be set as the src of an image element. That happens in ImageViewer.cshtml:

@{
    Layout = null;
    var directory = new DirectoryInfo(Path.Combine(Server.MapPath("~/Media"), Request["directory"]));
}
@foreach (var file in directory.GetFilesByExtensions(".png", ".gif", ".jpg")) {
    <div class="thumbnail">
        <img src="/Thumb/?image=@directory.Name\@file.Name" alt="thumb" title="@directory.Name/@file.Name" />
    </div>
}

This file is responsible for locating the selected directory, represented by the value passed in the query string, and then obtaining all files that have the specified extensions and then displaying their thumbnail version. The url of the image is set as the value of the title attribute. This is visible to the user as a tooltip when they hover over an image, and is used later to populate the CKEditor image dialogue. GetFilesByExtensions is a custom extension method. It lives in a class file called FileExtensions.cs which is placed in the App_Code folder:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

public static class FileExtensions
{
    public static IEnumerable<FileInfo> GetFilesByExtensions(this DirectoryInfo directoryInfo, params string[] extensions) {
        var allowedExtensions = new HashSet<string>(extensions, StringComparer.OrdinalIgnoreCase);
        return directoryInfo.EnumerateFiles().Where(f => allowedExtensions.Contains(f.Extension));
    } 
}

The main file is called FileBrowser.cshtml. It is the file that gets called when the user clicks the insert image icon in CKEditor.

@{
    Layout = null;
    var media = new DirectoryInfo(Server.MapPath("~/Media"));
}
<html>
    <head>
        <title>Media Browser</title>
        <link href="~/Content/StyleSheet.css" type="text/css" rel="stylesheet" />
        <script src="/Scripts/jquery.js" type="text/javascript"></script>
        <script src="/Scripts/ckeditor/ckeditor.js" type="text/javascript"></script>
        <script type="text/javascript">
            var funcNum = @(Request["CKEditorFuncNum"] + ";")
            $(function() {
                $('li').click(function() {
                    $('#fileExplorer').load('/ImageViewer?directory=' + encodeURIComponent($(this).text()));
                }).hover(function() {
                    $(this).css('cursor', 'pointer');
                });
                $('#fileExplorer').on('click', 'img', function () {
                    var fileUrl = '/Media/' + $(this).attr('title');
                    window.opener.CKEDITOR.tools.callFunction(funcNum, fileUrl);
                    window.close();
                }).hover(function() {
                    $(this).css('cursor', 'pointer');
                });
            });
        </script>
    </head>
    <body>
        <div id="folderExplorer">
            <ul>
            @foreach (var dir in media.EnumerateDirectories().Where(d => d.GetFilesByExtensions(".png", ".gif", ".jpg").Any())) {
                <li>@dir.Name</li>
            }
            </ul>
        </div>
       <div id="fileExplorer"></div>
    </body>
</html>

The main images folder is called Media, and images are stored in subfolders. These are displayed in a list. Some jQuery is used to set the cursor to a pointer (hand) when you hover over the list items, indicating that they are clickable. When they are clicked, the empty fileExplorer div is loaded with the result of a request to ImageViewer.cshtml - which is the file that calls Thumb.cshtml to display thumbnail versions of the contents of the selected directory.

The next block of jQuery applies a click event handler to any image that appears in the fileExplorer div. It extracts the url of the image being clicked from its title attribute. It passes that value along with another originally sent to the FileBrowser file by CKEditor back to the editor so that it knows you are selecting a file. The url of the file is applied to the appropriate dialogue text box and then the filebrowser window is closed.

All you need to do now is tell CKEditor that you have a file browser, and where it is. You can do that in the file that features the editor. Here's a minimal file that only displays the editor and configures the file browser:

@{
    
}

<!DOCTYPE html>

<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>CKEditor File Browser FOR ASP.NET Web Pages</title>
        <link href="~/Content/StyleSheet.css" type="text/css" rel="stylesheet" />
        <script type="text/javascript" src="~/Scripts/jquery.js"></script>
        <script type="text/javascript" src="~/Scripts/ckeditor/ckeditor.js"></script>
        <script>
            $(function () {
                CKEDITOR.replace('Content', {
                    filebrowserBrowseUrl: '/FileBrowser',
                    filebrowserWindowWidth: 800,
                    filebrowserWindowHeight: 600
                });
            });
        </script>
    </head>
    <body>
        <div id="form">
            <form method="post">
                <div>
                    @Html.TextArea("Content")
                </div>
                <div>
                    <input type="submit" />
                </div>
            </form>
        </div>
    </body>
</html>

The file browser configuration requires a url to be passed to the filebrowserBrowseUrl parameter, and then optionally a height and width for the window that will open to display the resource at the specified url. If you do not specify a height or width, the default values of 80% of the available screen width, and 70% of the available height will be used.

This example, which is available at GitHub assumes that the images are in sub folders of the Media folder. The download also includes a version that works with just one images folder. You can access that version by downloading and running the site and requesting Default2.cshtml.