A very common taks in web development is uploading images. This isn't realy a hard task in it self, but it can be pretty frustrating to write the same code over and over again even if its only a few lines.
In one of my current projects, I need to handle a lot of images. In many cases there are two or more image uploads in a view. That is code that I don't want to write more then once and I want to keep it out of my controller. Therefore I decided to create a model binder to handle this.
I started of by building a simple binder that just handeled the needs of our application. It was a binder for our Image object that pretty much just set the properties of the object (just like the default model binder except that I got the name from the uploaded file) and uploaded the file to our upload map on the server.
Then I wanted to generalize the the binder so that I may reuse it in a easy manner in my other projects. I talked to my friend Magnus about this, and together we came up with the following idea.
One new requirement was that we wanted to save the image from the controller (after we save the data to the database) by just calling a save method on the image object. This will gain us the advantage of beeing able to not save the image to the disk if, for some reason, the database save generates a error and the data isn't saved. Another advantage is that we are now able to select where we should save the image from the controller, so we can save different images in different maps.
I decide to create a base model for a image that you can inherit from if you need to save some other data about the image more then just the file name. Here is how that class looks like:
public class ImageFile {
protected ImageFile(HttpPostedFileBase file) {
File = file;
Name = file.FileName;
}
public virtual string Name { get; protected set; }
protected HttpPostedFileBase File { get; private set; }
public virtual void Save(string virtualPath) {
var path = HostingEnvironment.MapPath(virtualPath);
if (string.IsNullOrEmpty(path)) throw new FileNotFoundException();
File.SaveAs(string.Format("{0}{1}{2}", path, (path.EndsWith("/") ? string.Empty : "/"), File.FileName));
}
}
This class simply takes a HttpPostedFileBase and has the ability to save it to the disk with a simple method call. Then we have the model binder it self that looks like this:
public class ImageFileModelBinder<T> : IModelBinder where T : ImageFile {
public virtual object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
var key = GetKay(controllerContext, bindingContext); if (string.IsNullOrEmpty(key)) return null;
var file = controllerContext.HttpContext.Request.Files[key];
if (file == null || file.ContentLength < 1) return NoFileFound(controllerContext, bindingContext);
var image = GetFile(controllerContext, bindingContext, file);
BindProperties(image, controllerContext, bindingContext, file);
return image;
}
protected virtual T NoFileFound(ControllerContext controllerContext, ModelBindingContext bindingContext) { return null; }
protected virtual string GetKay(ControllerContext controllerContext, ModelBindingContext bindingContext) {
return controllerContext.HttpContext.Request.Files.AllKeys.Where( x => x.ToLower().Contains(bindingContext.ModelName.ToLower())).FirstOrDefault();
}
protected virtual T GetFile(ControllerContext controllerContext, ModelBindingContext bindingContext, HttpPostedFileBase file) {
var construct = typeof(T).GetConstructor(new[] { typeof (HttpPostedFileBase) });
return construct.Invoke(new object[] { file }) as T;
}
protected virtual void BindProperties(T image, ControllerContext controllerContext, ModelBindingContext bindingContext, HttpPostedFileBase file) { }
}
This is a generic class where the generic type must inherit from ImageFile. As you can see I decided to put in a few extension points in this binder. First we have the method "NoFilFound()". This method basicly is there if you want a default image to be returned if there is no uploaded file. In this default binder we simply return null. Then we have have the "GetKey()" method. This is there to provide the key for the uploaded image, or the name of the file input field. It has a default implementation that is based on the name of the model. After that we have a "GetFile()" method. This is the method where we acctually initialize the image object. In this defaul implementation we use reflection and use the constructor of the ImageFile object. If you don't have a constructor that takes only the posted file as a parameter, you need to override this method. Our last method is "BindProperties()". This is simply there to add extra data to our implementation of ImageFile if we need any.
Now that we have our base classes we can start using them in a real world applicaiton. In the application I was talking about here, we needed to know the width and height of the images we uploaded as well. So I needed to create my own classes. Here is how my image object looked like:
public class ImageFileFormModel : ImageFile {
private readonly IFileSystem _fileSystem;
public ImageFileFormModel(HttpPostedFileBase file, IFileSystem fileSystem) : base(file, bindingContext) {
_fileSystem = fileSystem;
}
public int Width { get; set; }
public int Height { get; set; }
public override void Save(string virtualPath) {
_fileSystem.WriteFile(string.Format("{0}{1}{2}", virtualPath, (virtualPath.EndsWith("/") ? string.Empty : "/"), File.FileName), File.InputStream);
}
}
Other then the Width and Height properties I also override the default Save() method and use a IFileSystem interface. This is basicly a wrapper with methods that handle different file system tasks, like saving a file. I take that interface as a parameter in my constructor.
I also needed to create my own binder. The binder looks like this:
public class ImageFileFormModelBinder : ImageFileModelBinder<ImageFileFormModel> {
private readonly IFileSystem _fileSystem;
public ImageFileFormModelBinder(IFileSystem fileSystem) {
_fileSystem = fileSystem;
}
protected override ImageFileFormModel GetFile(ControllerContext controllerContext, ModelBindingContext bindingContext, HttpPostedFileBase file) {
return new ImageFileFormModel(file, _fileSystem);
}
protected override void BindProperties(ImageFileFormModel image, ControllerContext controllerContext, ModelBindingContext bindingContext, HttpPostedFileBase file) {
var img = Image.FromStream(file.InputStream);
image.Width = img.Width;
image.Height = img.Height;
}
}
This is pretty simple. I override the GetFile() method as I have a constructor with two parameters (the IFileSystem as well) and I override the BindProperties() method as I need to set the width and height of the image as well.
When you have this set up and you have registered the model binder you can easily use it in any action. Here is a example from our application where we have two images we need to save in one action:
[AcceptVerbs(HttpVerbs.Post), Authorize] public ActionResult Create(CreateFormModel house) {
var saved = _houseService.Add(Mapper.Map<CreateFormModel, HouseDto>(house));
house.MapImage.Save("~/Upload/Images/");
house.ProductImage.Save("~/Upload/Images/");
return RedirectToAction("HouseImages", new { id = saved.Id });
}
This is only a first version, and there is probably some things that will changes in the future as we use it in more applications. But right now I'm pretty happy with it and it serves my needs very well. If you have any sugestions, feel free to post them here.