Telerik blogs
photo_gallery_header

In this tutorial, I am going to show how to create the administrative UI for managing a photo gallery that could be part of a simple CMS built with ASP.NET MVC and Kendo MVC wrappers. The simple photo gallery can be used for for managing albums and photos.

To keep this article manageable, I am not going to cover creating and editing album properties. If you'd like to create this part yourself, it is essentially a Kendo Grid and Kendo Window to create the new album. When you click to edit an album, this is where the photo admin will live. Let's look at how to build this.

Creating the Photo List

For the photo list, we'll use a Kendo ListView to show the photos in a grid. We'll create a Kendo template as well to display the images with their title and some editing options.

@(Html.Kendo().ListView()
    .Name("album-photos-list")
    .Deferred()
    .TagName("div")
    .ClientTemplateId("photo-template")
    .DataSource(ds => 
    {
       ds.Read("AlbumPhotos", "Albums", new { albumId = Model.Album.Id }); 
       ds.Sort(x => x.Add(s => s.SortOrder));
    })
    .HtmlAttributes(new { @class = "clearfix" }))

This is pretty straightforward, but let's look at it in a little more detail. First, we set Deferred on the ListView so that the script will appear at the bottom of the page, which is where we call Html.Kendo().DeferredScripts() (this comes after including the necessary jQuery and the Kendo script files).

Next we call TagName("div") and ClientTemplateId("photo-template") which reference the Kendo template for each photo item, as we'll see below. After that, we set our DataSource to define where the data is coming from as well as the sorting for the photos. Finally, we have a simple HtmlAttributes() call that adds a clearfix CSS class - this is necessary because elements inside the list are going to be floated.

As part of the photo list, we have the Kendo template which defines how the photo items will look.

<script type="text/x-kendo-tmpl" id="photo-template">
    <div class="photo-item">
        <img src="@Url.Content(Model.ImageBaseUrl)#:FileName#?w=170&h=100&mode=crop" />
        <span class="overlay">
        <span class="title">#:Title#</span>
        <span class="options">
            <a href="\\#" class="glyphicon glyphicon-pencil pull-right photo-edit-button" style="color: white; text-decoration: none;" data-photo-id="#:Id#"></a>
       </span>
       </span>
    </div>
</script>

The important thing to point out here is that we're "injecting" MVC in order to get the base image URL from the server-side, which is being passed through the model. The ?w=170&h=100&mode=crop portion of the code is because we are using ImageResizer to resize (and process) images on the fly as they are being served (we'll see ImageResizer being used more later).

Below is the CSS that goes with the above template:

.album-photos #album-photos-list { min-width: 938px; min-height: 329px; }
.album-photos .photo-item { float: left; position: relative; width: 170px; height: 100px; margin: 5px; }
.album-photos .photo-item img { position: relative; }
.album-photos .photo-item .overlay { position: absolute; visibility: hidden; left: 0; top: 81px; z-index: 100; width: 166px; height: 15px; padding: 2px; background: #000000; background: rgba(0, 0, 0, 0.75); font-size: x-small; color: #FFF; }
.album-photos .photo-item .overlay .title { display: inline-block; width: 140px; height: 15px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.album-photos .photo-item:hover .overlay { visibility: visible; }

On the server side, let's look at the controller action that is used to return the data for the list of images.

public ActionResult AlbumPhotos(Guid albumId, [DataSourceRequest] DataSourceRequest request)
{
   var album = _photoService.GetAlbum(albumId);
   if (album == null)
   {
       Log.Error(string.Format("Could not find album {0}", albumId));
       return null;
   }

   var photos = Mapper.Map<IEnumerable<Photo>, IEnumerable<PhotoModel>>(album.Photos);
   return Json(photos.ToDataSourceResult(request));
}

This action is getting the album by it's ID (passed in from the UI) and returning the mapped (using AutoMapper) photos using the Kendo DataSourceResult.

Uploading Photos

Now it's time for the fun part - uploading new photos. We'll be using the Kendo Upload widget, as shown below.

@(Html.Kendo().Upload()
     .Name("photos")
     .Deferred()
     .Multiple(true)
     .Async(async => async.Save("SavePhotos", "Albums", new { albumId = Model.Album.Id })
     .AutoUpload(true))
     .Events(evt => evt.Success("uploadCompleted")))

As before, we're calling Deferred() so that the script for the widget is placed at the bottom of the page. We want allow multiple uploads at once, so we specify Multiple(true) and then we specify what controller action should be called when the photos are uploaded. Along with that, we're enabling AutoUpload, which means that, as soon as the file(s) are selected, they are automatically uploaded.

Lastly, we reference a JavaScript function (uploadCompleted()) that will refresh the list. Let's look at that function.

function uploadCompleted() {
    refreshPhotos();
}

function refreshPhotos() {
    $('#album-photos-list').data('kendoListView').dataSource.read();
}

You might ask, why we didn't just call the code that's in the refreshPhotos() method directl? This is because we have other events that will trigger a refresh of the photo list and now we can reuse the function rather than repeat the code.

Let's see the code for the saving of photos on the Controller side:

public ActionResult SavePhotos(Guid albumId, IEnumerable<HttpPostedFileBase> photos)
{
    var album = _photoService.GetAlbum(albumId);
    EnsureAlbumFolderExists(album);
    var albumPath = GetAlbumPath(album);

    var resizeSettings = new ResizeSettings();
    resizeSettings.MaxWidth = PhotoSettings.MaxWidth;
    resizeSettings.MaxHeight = PhotoSettings.MaxHeight;
    resizeSettings.Quality = PhotoSettings.MaxQuality;
    resizeSettings.Mode = FitMode.Max; // TODO: change to use config setting

    foreach (var photoFile in photos)
    {
        var filename = Path.GetFileName(photoFile.FileName);
        // If we can't get the filename for some reason, just generate one using a guid - yuck
        if (string.IsNullOrEmpty(filename))
        {
            filename = string.Format("{0}{1}", Guid.NewGuid(), Path.GetExtension(filename));
        }

        filename = filename.ToLower();

        var path = Path.Combine(albumPath, filename);

        ImageBuilder.Current.Build(photoFile, path, resizeSettings);

        var photo = new Photo
        {
            Album = album,
            Title = filename,
            FileName = filename,
            SortOrder = 1
        };

        album.Photos.Add(photo);
    }

    _photoService.UpdateAlbum(album);

    return Content("");
}

What's this method doing? First, it's getting the album that we are adding the photos to, ensuring that the folder for the photos exists on the server and then retrieving it's path. On my server, the path looks like "/media/photos/albums/{album-slug}/".

After that, we're setting some ImageResizer settings from the settings contained in my web.config. This is so that we can resize the photos on upload. We loop through all of the photos to get their file name (or generate one if we can't get it for some reason), and then processing and saving it with the image resizer.

Once the photo has been saved, we create a new record in the album's photo collection. When all the photos have been uploaded, we save the album, which, in turn, saves all it's photo records. The method returns an empty content result - as Kendo expects. Once that happens, the success event on the file upload triggers the photo list to be refreshed.

Conclusion

So, there you have it. A simple photo uploading tool using Kendo's ASP.NET MVC wrappers. Try it out today and feel free to leave comments with questions.


Telerik Blogging Ninja
About the Author

The Telerik Team

 

Comments

Comments are disabled in preview mode.