
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.
- To get started, we need to create a new ASP.NET MVC application.
- In the
Models
folder, we create a new class namedImageResult
. - Then we have our new
ImageResult
class inherit fromActionResult
. - In order to implement
ActionResult
, we need to create a method namedExecuteResult
that looks like this:ImageResult.cs:
public override void ExecuteResult(ControllerContext context) { }
- 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; }
- 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 theControllerContext
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); }
- 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 aswitch
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; }
- With this new method in place, we can then add an additional line to our
ExecuteResult
method where we set the result ofGetContentTypeFromFile
into aString
variable.ImageResult.cs:
public override void ExecuteResult(ControllerContext context) { ... string contentType = GetContentTypeFromFile(); ...
- Next, we are going to set the
ContentType
of the response to the browser. We do this by getting a handle to theHttpContext.Response
instance.ImageResult.cs:
public override void ExecuteResult(ControllerContext context) { ... HttpResponseBase response = context.HttpContext.Response; response.ContentType = contentType;
- 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(); }
- With our
ImageResult
completed, we can now turn our attention to the HomeController in our MVC application. Add a newActionResult
under the default "About action" of a new project (or wherever in your controller). This time, rather than specifying that the action will return anActionResult
, we want to specify that we plan to return anImageResult
and we will pass the path to our image to the constructor of our newImageResult
.HomeController.cs:
public ImageResult GetImage() { return new ImageResult(@"C:\{pathToImage}.jpg"); }
- Hit F5 and browse to your new action at
localhost:{port}/Home/GetImage
.
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 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.