ASP.Net MVC upload file with record & validation - Step 6

upload files in mvc upload image with model data validate client side if image is selected or not re-size image before uploading in mvc upload image in different size upload image of thumb and original size

Uploading file with other model data, validate model & file before uploading by using DataAnnotation is a tricky part because there is no FileFor or any similar control in MVC. So very first question comes in mind, is that possible to use DataAnnotation with file control? Yes, it is very possible. Second question, it this possible with client side validation, even the answer is yes.

We know that we use html control input type = "file" to choose and upload any kind of file. When we need to create a text box we use TextBoxFor and it renders input type = "text", so if we will use TextBoxFor and add attribute type="file" then it will work as file upload control, is not it?

// Normal HTML file control 
<input type="file" id="image" name="image">
// Normal Text Box HTML Control
<input type="file" id="image" name="image">
// MVC: we will use this
@Html.TextBoxFor(model=>model.MyImage, new { @type = "file"} )
// It will renders for us
<input type="file" id="MyImage" name="MyImage">

This is the trick we will use to render the file control and post file to control with model data. It will be able to validate inout client side as well as server side. Do a title bit google and you will notice most of the people uses html control directly, yes, that will work if you will give the same name as the model property then it will use the model validation.

We are using Code First method so let's see our Products Table:

  • ProductId
  • ProductName
  • Price
  • CategoryId
  • Image
  • Thumb

I am planning to store image into folder in two different sizes, 1. big image and 2. thumbnail. In database we will store the path of these images. For model we will use a separate model which will accept

  • ProductName
  • Price
  • CategoryId
  • Uploading

Let's create our model, so what would be the data type for ImageUpload? we will use HttpPostedFileBase, rest of the code are normal

 public class ProductModel
 {
    public Int32 ProductId { get; set; }

    [Required]
    [Display(Name = "Product Name")]
    public String ProductName { get; set; }

    [Required]
    [Display(Name = "Price")]
    public Decimal Price { get; set; }

    [Required]
    [Display(Name = "Category")]
    public Int32 CategoryId { get; set; }

    [Required]
    [DataType(DataType.Upload)]
    [Display(Name = "Choose File")]
    public HttpPostedFileBase ImageUpload { get; set; }
 }

Create get controller: we will use ViewBag.Categories to bind the dropdown from Category table

// GET: Products/Create
public ActionResult Create()
{
    // To Bind the category drop down in search section
    ViewBag.Categories = db.Categories.Where(x => x.IsActive == true);
    var model = new ProductModel();
    return View(model);
}

alt text

Right click in side the Create action method and create view for it. Add following html into it

@model Advance.Learning.WebApp.Models.ProductModel

@{
    ViewBag.Title = "Create";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>New Product</h1>
<hr />
@using (Html.BeginForm("Create", "Products", 
    FormMethod.Post, new { enctype = "multipart/form-data" }))
{
    @Html.AntiForgeryToken()

    <div class="form-horizontal">

        <div class="form-group">
            @Html.LabelFor(m => m.ProductName, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.TextBoxFor(m => m.ProductName,
                    new { @class = "form-control", @placeholder = "Enter product name" })
                @Html.ValidationMessageFor(m => m.ProductName, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(m => m.Price, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.TextBoxFor(m => m.Price,
                    new { @class = "form-control", @placeholder = "Enter product price" })
                @Html.ValidationMessageFor(m => m.Price, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(m => m.CategoryId, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.DropDownListFor(m => m.CategoryId,
                    new SelectList(ViewBag.Categories, "CategoryId", "CategoryName"),
                    "Choose Category", new { @class = "form-control" })
                @Html.ValidationMessageFor(m => m.CategoryId, "", new { @class = "text-danger" })
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(m => m.ImageUpload, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.TextBoxFor(m => m.ImageUpload, new { type = "file", @class = "form-control" })                
                @Html.ValidationMessageFor(m => m.ImageUpload, "", new { @class = "text-danger" })
            </div>
        </div>
        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Save" class="btn btn-success" />
                @Html.ActionLink("Cancel", "Index", null, new { @class = "btn btn-warning" })
            </div>
        </div>
    </div>
}
<div>
</div>
@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

To post the selected file to control we need to add the attribute enctype

// Either we can directly use the html tag form
<form action="/Products/Create" 
      enctype="multipart/form-data" 
      method="post" 
      novalidate="novalidate">

// Same can be render by following code, which I used in view
@using (Html.BeginForm("Create", "Products", FormMethod.Post,
     new { enctype = "multipart/form-data" }))

How I tried to use the file upload control

<div class="form-group">
    @Html.LabelFor(m => m.ImageUpload, new { @class = "control-label col-md-2" })
    <div class="col-md-10">
        @Html.TextBoxFor(m => m.ImageUpload, new { type = "file", @class = "form-control" })                
        @Html.ValidationMessageFor(m => m.ImageUpload, "", new { @class = "text-danger" })
    </div>
</div>

How to bind the Category drop down:

@Html.DropDownListFor(m => m.CategoryId,
    new SelectList(ViewBag.Categories, "CategoryId", "CategoryName"),
    "Choose Category", 
            new { @class = "form-control" })

We are set to save the product record with image, so we need a folder to save the images in our application, create a folder ProductImages on the root of application and add Create post method in products controller

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create(ProductModel model)
    {
        var imageTypes = new string[]{    
                    "image/gif",
                    "image/jpeg",
                    "image/pjpeg",
                    "image/png"
                };
        if (model.ImageUpload == null || model.ImageUpload.ContentLength == 0)
        {
            ModelState.AddModelError("ImageUpload", "This field is required");
        }
        else if (!imageTypes.Contains(model.ImageUpload.ContentType))
        {
            ModelState.AddModelError("ImageUpload", "Please choose either a GIF, JPG or PNG image.");
        }

        if (ModelState.IsValid)
        {
            var product = new Product();
            product.ProductName = model.ProductName;
            product.Price = model.Price;
            product.CategoryId = model.CategoryId;

            // Save image to folder and get path
            var imageName = String.Format("{0:yyyyMMdd-HHmmssfff}", DateTime.Now);
            var extension = System.IO.Path.GetExtension(model.ImageUpload.FileName).ToLower();
            using (var img = System.Drawing.Image.FromStream(model.ImageUpload.InputStream))
            {
                product.Image = String.Format("/ProductImages/{0}{1}", imageName, extension);
                product.Thumb = String.Format("/ProductImages/{0}_thumb{1}", imageName, extension);

                // Save thumbnail size image, 100 x 100
                SaveToFolder(img, extension, new Size(100, 100), product.Thumb);

                // Save large size image, 600 x 600
                SaveToFolder(img, extension, new Size(600, 600), product.Image);
            }

            db.Products.Add(product);
            db.SaveChanges();
            return RedirectToAction("Index");
        }

        // If any error return back to the page
        ViewBag.Categories = db.Categories.Where(x => x.IsActive == true);
        return View(model);
    }
  • First create an array of image type to support
  • Check if file is available
  • Check if file type is in supported types
  • check if mode is valid: ModelState.IsValid
  • Create a file name from datetime with milliseconds, even we can use GUID
  • SaveToFolder method to save files in folder see who I resize image

    private void SaveToFolder(Image img, string extension, Size newSize, string pathToSave) { // Get new resolution Size imgSize = NewImageSize(img.Size, newSize); using (System.Drawing.Image newImg = new Bitmap(img, imgSize.Width, imgSize.Height)) { newImg.Save(Server.MapPath(pathToSave), img.RawFormat); } }

Run the application and try to add a new product, I found everything working.

Edit Record: To edit the record we cannot use the ProductModel because in it ImageUpload is mandatory and it is not necessary to change the image in every edit, might be we only want to update only price or category, so we will use our Product table model for edit. To update the image we will create a separate page.

alt text

// GET: Products/Edit/5
public ActionResult Edit(int? id)
{
    if (id == null)
    {
      return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Product product = db.Products.Find(id);
    if (product == null)
    {
      return HttpNotFound();
    }

    ViewBag.Categories = db.Categories.Where(x => x.IsActive == true);
    return View(product);
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include = "ProductId,ProductName,Price,CategoryId")] Product model)
{
  if (ModelState.IsValid)
  {
    var product = db.Products.Find(model.ProductId);
    if (product == null)
    {
      return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    product.ProductName = model.ProductName;
    product.Price = model.Price;
    product.CategoryId = model.CategoryId;
    db.SaveChanges();
    return RedirectToAction("Index");
  }

  ViewBag.Categories = db.Categories.Where(x => x.IsActive == true);
  return View(model);
}

In get method we used db.Products.Find(id), the find mehtod takes one parameter, primary key to find the record.

In post method we find the product on the basis of primary key (product Id) and update the values from model (passed values) and update the database. Here is the complete view Html

@model Advance.Learning.WebApp.Models.Product

@{
    ViewBag.Title = "Edit Product";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Edit Product</h1>
<hr />

@using (Html.BeginForm())
{
  @Html.AntiForgeryToken()

  <div class="form-horizontal">

    @Html.HiddenFor(m => m.ProductId)

    <div class="form-group">
        @Html.LabelFor(m => m.ProductName, new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.ProductName, new { @class = "form-control" })
            @Html.ValidationMessageFor(m => m.ProductName, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(m => m.Price, new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.Price, new { @class = "form-control" })
            @Html.ValidationMessageFor(m => m.Price, "", new { @class = "text-danger" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(m => m.CategoryId, new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.DropDownListFor(m => m.CategoryId, 
                new SelectList(ViewBag.Categories, "CategoryId", "CategoryName"),
               "Choose Category", new { @class = "form-control" })
            @Html.ValidationMessageFor(m => m.CategoryId, "", new { @class = "text-danger" })
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <img src="@Model.Thumb" alt="@Model.ProductName image" class="img-thumbnail" />
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" value="Save" class="btn btn-success" />
            @Html.ActionLink("Cancel", "Index", null, new { @class = "btn btn-warning" })
        </div>
    </div>
  </div>
}

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Detail Page: Simplest page with all the controls disabled and only one button to go back to listing page, action method and html

// GET: Products/Details/5
public ActionResult Details(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Product product = db.Products.Find(id);
    if (product == null)
    {
        return HttpNotFound();
    }
    ViewBag.Category = db.Categories.Find(product.CategoryId).CategoryName;
    return View(product);
}

// View

@model Advance.Learning.WebApp.Models.Product
@{
    ViewBag.Title = "Details";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Product Details</h2>
<hr />
<div>    
  <div class="form-horizontal">
    <div class="form-group">
        @Html.LabelFor(m => m.ProductName, new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.ProductName,
                         new { @class = "form-control", @disabled = "disabled" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(m => m.Price, new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.Price, 
                new { @class = "form-control", @disabled = "disabled" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(m => m.Price, new { @class = "control-label col-md-2" })
        <div class="col-md-10">
           @Html.TextBoxFor(m=>m.CategoryId, ((String)ViewBag.Category),
                new { @class = "form-control", @disabled = "disabled" })
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
          <img src="@Model.Thumb" alt="@Model.ProductName image" class="img-thumbnail" />
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
          @Html.ActionLink("Cancel", "Index", null, new { @class = "btn btn-warning" })
        </div>
    </div>
  </div>
</div>

Delete Record: Before delete show the detail of the product so user can delete record with confident

// GET: Products/Delete/5
public ActionResult Delete(int? id)
{
    if (id == null)
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);

    Product product = db.Products.Find(id);
    if (product == null)
        return HttpNotFound();

    ViewBag.Category = db.Categories.Find(product.CategoryId).CategoryName;
    return View(product);
}

// POST: Products/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
    Product product = db.Products.Find(id);
    db.Products.Remove(product);
    db.SaveChanges();
    return RedirectToAction("Index");
}

// VIEW HTML
@model Advance.Learning.WebApp.Models.Product

@{
    ViewBag.Title = "Delete";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h2>Delete Product</h2>

<h3 class="text-danger">Are you sure you want to delete this product?</h3>


<div class="form-horizontal">
    <div class="form-group">
        @Html.LabelFor(m => m.ProductName, new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.ProductName,
                            new { @class = "form-control", @disabled = "disabled" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(m => m.Price, new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.Price,
                new { @class = "form-control", @disabled = "disabled" })
        </div>
    </div>

    <div class="form-group">
        @Html.LabelFor(m => m.Price, new { @class = "control-label col-md-2" })
        <div class="col-md-10">
            @Html.TextBoxFor(m => m.CategoryId, ((String)ViewBag.Category),
                new { @class = "form-control", @disabled = "disabled" })
        </div>
    </div>

    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <img src="@Model.Thumb" alt="@Model.ProductName image" class="img-thumbnail" />
        </div>
    </div>

    @using (Html.BeginForm())
    {
        @Html.AntiForgeryToken()

            <div class="form-group">
                <div class="col-md-offset-2 col-md-10">
                    <input type="submit" value="Delete" class="btn btn-danger" /> 
                    @Html.ActionLink("Cancel", "Index", null, new { @class = "btn btn-warning" })
                </div>
            </div>
    } 
</div>

If you want to use separate image upload page we can use very similar to create page because it has everything to update the image and update record. To update we need record Id which is already in ProductModel so will be easy to use it.

Ali Adravi Having 13+ years of experience in Microsoft Technologies (C#, ASP.Net, MVC and SQL Server). Worked with Metaoption LLC, for more than 9 years and still with the same company. Always ready to learn new technologies and tricks.
  • mvc
  • file
  • sql
  • model
  • validation
By Ali Adravi On 28 Mar, 15  Viewed: 14,476

Other blogs you may like

mvc search page example with code

MVC Searh page with pagination: It’s very easy to create a search page in asp.net but when I try to create the same in MVC I faced many problems, how to create model, how to keep searched values in search controls, pagination, I found myself nowhere, so start searching for some good examples but... By Ali Adravi   On 25 Aug 2013  Viewed: 34,687

MVC insert update delete and select records

CRUD (Create, Retrieve, Update and Delete) in MVC. When we start to learn new language, first we try to run an application with “Hello World” and then CRUD functionality. So in this article we will see how to select records from database (with WebGrid, pagination and sort functionality), update a... By Ali Adravi   On 17 Aug 2013  Viewed: 96,502

How to create a single thanks page for entire controller in MVC

Sometimes we need a thanks page say we have user registration, change password, activate account functionality in our application then we need a thanks page after registering with our site, to say thanks for registering with us or showing confirmation that your password is successfully changed or... By Hamden   On 30 Jun 2013  Viewed: 3,038

MVC jquery autocomplete with value and text field

In MVC, autocomplete with jquery is the best way to pull data from database and render according to our requirements, so in this article we will use jquery to show the auto complete text box without any ajax controls. We need a method to get the data from database, a controller method to handle the... By Ali Adravi   On 29 Jun 2013  Viewed: 5,967

Upload files with model data in MVC

Upload multiple files with model data in MVC is really very easy, when I started to test by uploading some files, I though it would be more complicated but it is really not. In my previous post [ASP.Net MVC file upload][1], I promised to post soon about how to upload multiple files. When I was... By Ali Adravi   On 04 Jun 2013  Viewed: 22,233