views:

306

answers:

3

I have an ASP.NET MVC application where I am displaying images.

These images could be located on the file system or inside a database. This is fine as I can use Url.Action in my image, call the action on my controller and return the image from the relevant location.

However, I want to be able to support images stored in Amazon S3. In this case, I don't want my controller action to return the image, it should instead generate an image URL for Amazon S3.

Although I could just perform this logic inside my view e.g.

<%if (Model.Images[0].ImageLocation == ImageLocation.AmazonS3) {%> // render amazon image

I need to ensure that the image exists first.

Essentially I need to pass a size value to my controller so that I can check that the image exists in that size (whether it be in the database, file system or amazon s3). Once I am sure that the image exists, then I return the URL to it.

Hope that makes sense,

Ben

A: 

You can create a HttpWebRequest to load the image. Check the header in the response, if it's 200 that means it was successful, otherwise something went wrong.

Shawn Steward
Hi, the main requirement is that I am able to construct the URL dynamically based on the where the image "should" be located. I don't need to use a HttpWebRequest to check the image exists, I can use System.IO.File.Exists for this or check the Amazon S3 bucket.ThanksBen
Ben
+1  A: 

Try the following approach.

A model class for an image tag.

public class ImageModel
{
    public String Source { get; set; }
    public String Title { get; set; }
}

Helper

public static String Image(this HtmlHelper helper, String source, String title)
{
    var builder = new TagBuilder("img");
    builder.MergeAttribute("src", source);
    builder.MergeAttribute("title", title);
    return builder.ToString();
}

View with Model.Images of type IEnumerable<ImageModel>

...    
<%= Html.Image(Model.Images[0].Source, Model.Images[0].Title) %>

Action

public ActionResult ActionName(/*whatever*/)
{
    // ...
    var model = ...;
    //...

    var model0 = ImageModel();
    if (Image0.ImageLocation == ImageLocation.AmazonS3)
        model0.Source = "an amazon url";
    else
        model0.Source = Url.Action("GetImageFromDatabaseOrFileSystem", "MyController", new { Id = Image0.Id });
    model0.Title = "some title";
    model.Images.Add(model0);
    // ...
    return View(model);
}

An action is a kind of a pseudo code, however the idea should be clear.

Anton
Thanks Anton, this is good approach. I think I will create a new ViewModel for this as my current model is an IList<Product> having IList<Image>.Seems better to break this in to a new view model where I can define things like the required image size and have just a single image per product (which is all I need in my view).I will post my code once done.
Ben
A: 

After several iterations I have come up with a workable solution, although I'm still not convinced its the best solution.

Originally I followed Anton's suggestion and just set the image url accordingly within my controller action. This was simple enough with the following code:

        products.ForEach(p =>
        {
            p.Images[0].Url = _mediaService.GetImageUrl(p.Images[0], 200);
        });

However, I soon found that this approach did not give me the flexibility I needed. Often I will need to display images of different sizes and I don't want to use properties of my model for this such as Product.FullSizeImageUrl, Product.ThumbnailImageUrl.

As far as "Product" is concerned it only knows about the images that were originally uploaded. It doesn't need to know about how we manipulate and display them, or whether we are caching them in Amazon S3.

In web forms I might use a user control to display product details and then use a repeater control to display images, setting the image urls programatically in code behind.

I found that the use of RenderAction in ASP.NET MVC gave me similar flexibility:

Controller Action:

    [ChildActionOnly]
    public ActionResult CatalogImage(CatalogImage image, int targetSize)
    {
        image.Url = _mediaService.GetImageUrl(image, targetSize);
        return PartialView(image);
    }

Media Service:

         public MediaCacheLocation CacheLocation { get; set; }

     public string GetImageUrl(CatalogImage image, int targetSize)
     {
         string imageUrl;

         // check image exists
         // if not exist, load original image from store (fs or db)
         // resize and cache to relevant cache location

         switch (this.CacheLocation) {
             case MediaCacheLocation.FileSystem:
                 imageUrl = GetFileSystemImageUrl(image, targetSize);
                 break;
             case MediaCacheLocation.AmazonS3:
                 imageUrl = GetAmazonS3ImageUrl(image, targetSize);
                 break;
             default:
                 imageUrl = GetDefaultImageUrl();
                 break;
         }

         return imageUrl;
     }

Html helper:

        public static void RenderCatalogImage(this HtmlHelper helper, CatalogImage src, int size) {
        helper.RenderAction("CatalogImage", "Catalog", new { image = src, targetSize = size });
    }

Usage:

<%Html.RenderCatalogImage(Model.Images[0], 200); %>

This now gives me the flexibility I require and will support both caching the resized images to disk or saving to Amazon S3.

Could do with some url utility methods to ensure that the generated image URL supports SSL / virtual folders - I am currently using VirtualPathUtility.

Thanks Ben

Ben