Friday, September 20, 2013

MVC 4 - multiple image/file uploads and viewing images

Having tinkered with MVC 4 in Visual Studio 2012 I hadn't really developed an application that solved enterprise requirements.  Finally an opportunity at work came my way and provided an opportunity to use the MVC design pattern and its implementation in Visual Studio 2012.

The business requirements were rather easy.  The marketing team desired to offer rewards to our subscribers.  These rewards could be a coupon for a discounted yogurt at a local business or even free USB thumb drive with the company logo prominently displayed.

Outside of having to store the basics (e.g. titles, descriptions, start, and end dates) required for this project it was required that different types of images be stored and then later displayed.  The first image was smaller and its primary purpose was to display a small graphic of the company logo that provided the offer.  A larger image could also be added which contains a coupon or other image which needed to be displayed when the offer was redeemed by the customer.

So the first two problems arose - I wanted to be able to upload and display both images on the same form.

The image display actually was rather easy.

<div class="editor-label">  
   @Html.LabelFor(m => m.ImageId)  
 </div>  
 <div class="editor-field">  
   <img id="offerImage" class="imagePreview" src="@Url.Action("GetImage", "Offer", new {id = Model.ImageId, imageType="Image"/>  
   <input type="file" name="offerImage" />  
 </div>  
   

The code do this in the cshtml example above.  By embedding a @Url.Action in the src tag of an image element the server will invoke the method GetImage within the OfferController class.  There is also some opportunity to pass specific parameters so that each image element will display the correct type of image associated with the offer.

What wasn't as intuitive was what type of ActionResult needed to be returned by the OfferController class.  After some hunting around and experimentation I ran into the File function which uses as parameters the image (in a byte[] array) and the MIME type as illustrated below.

 public ActionResult GetImage(int id, string imageType)  
 {  
   DisplayImageModel displayImageModel   
    = _rewardsRepository.GetDisplayImageModel(id, imagetype);  
     
   if ( displayImageModel!=null )  
   {  
    return File(displayImageModel.ImageBytes, displayImageModel.ImageMimeType);  
   }  
   return HttpNotFound();  
 }  

Once that was squared away I was able to successfully display images from my data source.


The real the real trouble came with desire to upload more than one image on the same cshtml form.  There doesn't seem to be anything built into the MVC 4 implementation for this type of operation.  The first thought was to simply adjust expectations and create link that would collect these images in a separate view.  This really didn't provide the polished experience desired by the marketing team and it felt amateurish.

In order to upload any files during the HTTP POST a small change needs to take place in the cshtml file's BeginForm declaration.  The key needed for this is to ensure that enctype of "multipart/form-data" is set as shown below.

 @using (Html.BeginForm("Edit", "Offer", null, FormMethod.Post, new ( offerModel = ViewData, enctype="multipart/form-data"}))  
 {  
   @Html.ValidationSummary(true);  

For those used to ASP.NET and HTML this should be easy for you to grasp why this was needed.  However, as the problem was researched further a number of sites indicated that the method invoked when the html form posted needed to have an IEnumerable<HttpPostedFileBase> parameter.  As it turns out this wasn't at all correct.  In my experience this parameter was always NULL.  Anyone making this statement clearly hasn't actually run their 'example' code.  It didn't seem if any built in parameter would provide what was needed in order to peel off the files.  What is listed below seemed to have worked.  However, it isn't even clear that having this as parameter is really required.  In fact as I experimented even further - this parameter isn't required at all as illustrated below.  It certainly can't be used to obtain more than one file from the post data.

     [HttpPost]  
     public ActionResult Edit(OfferModel offerModel)  
     {  
       try  
       {  
         if (ModelState.IsValid)  
         {  
           GetFiles(ref offerModel);  
   
           _rewardsAdminRepository.UpdateOffer(offerModel);  
   
           return RedirectToAction("Index", "Offer");  
         }  
   
         SetSelectList(offerModel);  
         DecodeHtml(ref offerModel);  
   
         return View(offerModel);  
       }  
       catch (Exception exception)  
       {  
         _log4Net.Error("Edit(POST)", exception);  
         return ProcessError(exception.Message);  
       }  
     }  

Then it occurred to me that the Request object is in scope during any method with the [HttpPost] attribute.  And that this Request object contains a property called Files that holds the names of each of the file input elements on the html form. Rather than clutter up the Edit method above and because a Create method would also need the ability to obtain the files from the Request object a private method called GetFiles was created.  Its implementation is listed below.

     private void GetFiles(ref OfferModel offerModel)  
     {  
       foreach (string fileName in Request.Files)  
       {  
         HttpPostedFileBase hpf = Request.Files[fileName];  
   
         if (hpf != null && hpf.ContentLength > 0)  
         {  
           if (fileName.Equals("thumbnailImage"))  
           {  
             DisplayImageModel imageModel = GetImageData(hpf);  
             offerModel.ThumbnailBytes = imageModel.ImageBytes;  
             offerModel.ThumbnailMimeType = imageModel.ImageMimeType;  
           }  
           else if (fileName.Equals("offerImage"))  
           {  
             DisplayImageModel imageModel = GetImageData(hpf);  
             offerModel.ImageBytes = imageModel.ImageBytes;  
             offerModel.ImageMimeType = imageModel.ImageMimeType;  
           }  
           else if (fileName.Equals("customerListFile"))  
           {  
             // we have a list of customer's, e.g. main bill numbers...  
             List<long> mainBillNumbers = GetCustomerList(hpf);  
   
             if (mainBillNumbers != null && mainBillNumbers.Count > 0)  
             {  
               foreach (long mainBillNumber in mainBillNumbers)  
               {  
                 offerModel.CustomerList.Add(mainBillNumber);  
               }  
             }  
           }  
         }  
       }  
     }  

As you can see the you can iterate through the Files collection and get a clear understanding of which element is sending a file in the post data.  As it happens the fileName represents the name provided to the input type element in the cshtml file.  Look at the declaration of the input elements below and compare it with the code above.

     <div class="editor-group">  
       <div class="editor-label">  
         @Html.LabelFor(m => m.ThumbnailId)  
       </div>  
       <div class="editor-field">  
         <input type="file" id="upload_ThumbImage" class="find_file_button" name="thumbnailImage" />  
         <p>  
           <img id="thumbnailImage" class="imagePreview" alt="No Image Selected" src="@Url.Action("GetImage", "DisplayImage", new {id = Model.ThumbnailId, imageType = "Thumbnail"})"/>    
         </p>  
       </div>  
     </div>  
   
     <div class="editor-group">  
       <div class="editor-label">  
         @Html.LabelFor(m => m.ImageId)  
       </div>  
       <div class="editor-field">  
         <input type="file" id="upload_OfferImage" class="find_file_button" name="offerImage" />  
         <p>  
           <img id="offerImage" class="imagePreview" alt="No Image Selected" src="@Url.Action("GetImage", "DisplayImage", new {id = Model.ImageId, imageType = "Image"})"/>   
         </p>  
       </div>  
     </div>  

The code that actually pulls the image file data and saves it is implemented in the GetImageData method illustrated below.  This method also does some checking to ensure that only certain images types are used.
     private DisplayImageModel GetImageData(HttpPostedFileBase imageFile)  
     {  
       if (imageFile.ContentType.ToLower().Equals("image/jpeg") ||  
         imageFile.ContentType.ToLower().Equals("image/png"))  
       {  
         using (MemoryStream ms = new MemoryStream())  
         {  
           imageFile.InputStream.CopyTo(ms);  
           DisplayImageModel imageModel = new DisplayImageModel  
           {  
             ImageMimeType = imageFile.ContentType.ToLower(),  
             ImageBytes = ms.ToArray()  
           };  
   
           return imageModel;  
         }  
       }  
       return null;  
     }  

No comments:

Post a Comment