ASP.NET MVC 2 Cookbook
上QQ阅读APP看书,第一时间看更新

A custom ActionResult to return an image

I think the title pretty much sums this one up! In this recipe, we are going to implement our own custom ActionResult, whose sole purpose in life is to take a path to an image on the file system and render it to the web page directly. This is useful when you need to render images that are outside your website or when you want to control access to premium content that you would prefer not be leached off your site.

There are many implementations to this recipe on the net. However, most of them involve a semi-messy approach requiring you to create a memory stream to your file, to determine the file's content type, and to access the code through some form of extension method. In this recipe, we will create a self-contained ImageResult that accepts the path to an image somewhere on the server, and then renders it directly.

Getting ready

To pull this recipe off you don't need anything special. It's all code!

How to do it...

  1. To get started, we need to create a new ASP.NET MVC application.
  2. In the Models folder, we create a new class named ImageResult.
  3. Then we have our new ImageResult class inherit from ActionResult.
  4. In order to implement ActionResult, we need to create a method named ExecuteResult that looks like this:

    ImageResult.cs:

    public override void ExecuteResult(ControllerContext context)
    {
    }
    
  5. For our custom ActionResult, we need to be able to pass in the path to the image that we want to render. We will do this in the constructor of our class like this:

    ImageResult.cs:

    private string _path;
    public ImageResult(string path)
    {
    _path = path;
    }
    
  6. Now we are ready to create the guts of our ImageResult. To get started, we need to do some checking on some of our required bits. Because we expect to have an instance of the ControllerContext passed to us, we need to verify that it is not null. Also, we want to make sure that the path that was passed to us actually exists.

    ImageResult.cs:

    public override void ExecuteResult(ControllerContext context)
    {
    byte[] bytes;
    //no context? stop processing
    if (context == null)
    throw new ArgumentNullException("context");
    //check for file
    if(File.Exists(_path)) { bytes = File.ReadAllBytes(_path); }
    else { throw new FileNotFoundException(_path); }
    
  7. Next, we need to determine the content type of the file that we intend to send down to the browser. We will do this in another method named GetContentTypeFromFile. In this method, we are simply getting the extension of the file that was passed in and then running it through a switch statement to make a best guess at the content type (supporting JPG and GIF at the moment).

    ImageResult.cs:

    private string GetContentTypeFromFile()
    {
    //get extension from path to determine contentType
    string[] parts = _path.Split('.');
    string extension = Path.GetExtension(_path).Substring(1);
    string contentType;
    switch (extension.ToLower())
    {
    case "jpeg":
    case "jpg":
    contentType = "image/jpeg";
    break;
    case "gif":
    contentType = "image/gif";
    break;
    default:
    throw new NotImplementedException(extension + "not handled");
    }
    return contentType;
    }
    
  8. With this new method in place, we can then add an additional line to our ExecuteResult method where we set the result of GetContentTypeFromFile into a String variable.

    ImageResult.cs:

    public override void ExecuteResult(ControllerContext context)
    {
    ...
    string contentType = GetContentTypeFromFile();
    ...
    
  9. Next, we are going to set the ContentType of the response to the browser. We do this by getting a handle to the HttpContext.Response instance.

    ImageResult.cs:

    public override void ExecuteResult(ControllerContext context)
    {
    ...
    HttpResponseBase response = context.HttpContext.Response;
    response.ContentType = contentType;
    
  10. Finally, we can wrap up this method by creating a MemoryStream from our file and then send it down to the client.

    ImageResult.cs:

    MemoryStream imageStream = new MemoryStream(bytes);
    byte[] buffer = new byte[4096];
    while (true)
    {
    int read = imageStream.Read(buffer, 0, buffer.Length);
    if (read == 0)
    break;
    response.OutputStream.Write(buffer, 0, read);
    }
    response.End();
    }
    
  11. With our ImageResult completed, we can now turn our attention to the HomeController in our MVC application. Add a new ActionResult under the default "About action" of a new project (or wherever in your controller). This time, rather than specifying that the action will return an ActionResult, we want to specify that we plan to return an ImageResult and we will pass the path to our image to the constructor of our new ImageResult.

    HomeController.cs:

    public ImageResult GetImage()
    {
    return new ImageResult(@"C:\{pathToImage}.jpg");
    }
    
  12. Hit F5 and browse to your new action at localhost:{port}/Home/GetImage.
    How to do it...

How it works...

We have basically used one of the many extensibility points in the MVC framework. In this case, we simply extended ActionResult to flush the contents of an image down to the client via the Response object.

The nice thing is that a good chunk of the code was guard code to ensure that items, as well as code to determine the content type of the image to load, existed. Pretty much all of the code is there to allow us to provide added functionality. Notice that there isn't any code to beat the framework into submission.

There's more...

There are many ways to implement this. Some examples will have many overloads to handle processing memory streams directly or via byte arrays. Others use various forms of extension methods dangled off of the controller itself.

An example of why you might want to keep this bit of complexity outside of the ImageResult is if you are pulling image data out of your database. In this case, you may want to create an overloaded constructor on the ImageResult class that will take a MemoryStream and the ContentType of the file. Then, inside the ImageResult class, you can bypass the loading of the image and just go straight to outputting the data in the response stream.