Today we will see how a more involved application like a picture viewer can potentially use Azure Blob Storage. For the front end, we will build an ASP.NET MVC application and for the UI we will use CSS and KnockOut JS to spin up a decent looking UI. We will also use Ninject as our DI Container.
The functional Requirement
We are building an image service that allows users the ability to Upload photos to their Accounts and label them. So apart from having registered users, we will have multiple images stored per user.
When the user logs in, they should be presented with a nice ‘wall’ of their uploaded images and there should be an un-obtrusive method for uploading more images if desired and these should get added to the photo wall.
Application Design
We will create a standard three tiered structure using the Repository pattern to store the data in databases. The Repositories will be injected into the Controllers using Ninject. Finally in the View layer, we will use a mix of standard scaffolding and custom UI to load and view images in the system.
Image files will be uploaded to Windows Azure Blob storage while the metadata will be stored in the Database.
Project Setup
Getting Started – The Application Structure
Step 1: We start off with an MVC 4 application Internet Application Template, this pulls in the basic forms authentication implementation.
Step 2: We install MvcScaffolding using the Nuget Package Manager. This will help us scaffold the repositories for our Entities.
Step 3: We install Ninject as follows
PM> install-package ninject
PM> install-package ninject.mvc3
Step 4: (Optional) Update jQuery to 1.9.1, jQuery UI, jQuery Validator to their respective latest versions using Nuget Package Management Console.
Step 5: Add the following setting to the web.config for Azure Blob Storage
<add key="StorageConnectionString" value="UseDevelopmentStorage=true" />
Implementation
Step 1: Add the class Photo in the Models folder with the following properties
public class Photo
{
public int Id { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public string Notes { get; set; }
public string UserId { get; set; }
}
Step 2: Build the application. Now right click on the Controllers folder and select Add Controller. In the Add Controller wizard, select the options as follows. Note the MvcScaffolding Template that we are using to generate the Repositories for us.
This will generate the Controller, the Views, the Repository interface and the Repository implementation. We will move the PhotoRepository to a separate Repository folder.
Integrating Ninject
To get Ninject to create the Repository dependencies for us, we need to make a single line of change. In the App_Start\NinjectWebCommon.cs class, add the mapping function in the RegisterServices method as follows
/// <summary>
/// Load your modules or register your services here!
/// </summary>
/// <param name="kernel">The kernel.</param>
private static void RegisterServices(IKernel kernel)
{
kernel.Bind<IPhotoRepository>().To<PhotoRepository>();
}
This ensures whenever MVC is looking for IPhotoRepository Ninject is able to supply an instance of it.
Adding Upload Controller and Views
Once the repository is configured, the data can be saved to database. Let’s take care of file upload. We will use the same implementation of uploading multiple files using the chunked upload technique that we saw earlier. We will look at the key differences here.
The View
Instead of just the progress bars, we’ll have a place holder for the image as well which will show the image once uploaded. Additionally we’ll have an editable input box that has the Notes property of the Photo. By default Name is set to the Notes field as well.
The View markup is as follows, we can see the additional fields that we have added to the view model i.e. uploadedUrl, name and the Save function that is bound to the click of the Save method.
<div class="content-wrapper">
<h2>Upload files to your Library</h2>
<div class="content-wrapper">
<input id="selectImages" type="file" multiple="multiple" value="Select Files" />
<input id="uploadImages" type="button" value="Upload" />
</div>
<div data-bind="foreach: uploaderCollection" style="width: 100%; height: 100%">
<div data-bind="with: $data" style="border: 1px solid gray; float: left; width: 400px; height: 300px; text-align: center; margin-right: auto; margin-left: auto;">
<div style="width: 300px; height: 200px; margin-right: auto; margin-left: auto;">
<img data-bind="attr: { src: uploadedUrl }" alt="Uploading" style="width: 100%; height: 100%" />
</div>
<div class="progressBar" style="width: 300px; height: 10px; background-color: grey;
margin-right: auto; margin-left: auto;"
data-bind="attr: { id: 'progressBar' + fileIndex }">
</div>
<label style="font-size: x-small" data-bind="attr: { id: 'statusMessage' + fileIndex
}"></label>
<input type="text" data-bind="value: name" />
<input type="button" data-bind="click: save" value="Save" />
<a data-bind="attr: { src: uploadedUrl }, text: uploadedUrl"></a>
</div>
</div>
</div>
@section Scripts{
<script src="~/Scripts/knockout-2.2.0.debug.js"></script>
<script src="~/Scripts/apm-uploader.js"></script>
}
The View Model
The view model has changes required to support the updated view. These are highlighted below, I have removed most of the unchanged parts for brevity:
/// <reference path="jquery-1.8.2.js" />
/// <reference path="_references.js" />
$(document).ready(function ()
{
…
});
var beginUpload = function ()
{
…
}
var uploaders = {
…
}
var chunkedFileUploader =
{
maxRetries: 3,
blockLength: 1048576,
numberOfBlocks: 1,
currentChunk: 1,
retryAfterSeconds: 3,
fileToBeUploaded: null,
size: 0,
fileIndex: 0,
name: "",
init: function (file, index)
{
this.fileToBeUploaded = file;
this.size = file.size;
this.name = file.name;
this.fileIndex = index;
this.uploadedUrl = ko.observable();
this.id = -1;
},
uploadMetaData: function ()
{
this.numberOfBlocks = Math.ceil(this.size / this.blockLength);
this.currentChunk = 1;
$.ajax({
type: "POST",
async: true,
url: "/Upload/SetMetadata?blocksCount=" + this.numberOfBlocks
+ "&fileName=" + this.name
+ "&fileSize=" + this.size
+ "&fileIndex=" + this.fileIndex,
}).done(function (state)
{
if (state.success == true)
{
var cufl = uploaders.uploaderCollection()[state.index]
cufl.displayStatusMessage(cufl, "Starting Upload");
cufl.sendFile(cufl);
}
}).fail(function ()
{
this.displayStatusMessage("Failed to send MetaData");
});
},
save: function(uploader)
{
$.ajax({
url: "/Photos/Edit",
type: "POST",
async: true,
contentType: "application/json",
data: JSON.stringify( {
Id: uploader.id,
Url: uploader.uploadedUrl(),
Notes: uploader.name
})
});
},
sendFile: function (uploader)
{
…
jqxhr = $.ajax({
async: true,
url: ('/Upload/UploadChunk?id=' + uploader.currentChunk + "&fileIndex=" +
uploader.fileIndex),
data: fileChunk,
cache: false,
contentType: false,
processData: false,
type: 'POST'
}).fail(function (request, error)
{
…
}).done(function (state)
{
if (state.error || state.isLastBlock)
{
cful.displayStatusMessage(cful, state.message);
cful.updateProgress(cful);
cful.uploadedUrl(state.url);
cful.id = state.id;
return;
}
++cful.currentChunk;
start = (cful.currentChunk - 1) * cful.blockLength;
end = Math.min(cful.currentChunk * cful.blockLength, cful.fileToBeUploaded.size);
retryCount = 0;
cful.updateProgress(cful);
if (cful.currentChunk <= cful.numberOfBlocks)
{
sendNextChunk();
}
});
}
sendNextChunk();
},
displayStatusMessage: function (uploader, message)
{
$("#statusMessage" + uploader.fileIndex).text(message);
},
updateProgress: function (uploader)
{
var progress = uploader.currentChunk / uploader.numberOfBlocks * 100;
if (progress <= 100)
{
$("#progressBar" + uploader.fileIndex).progressbar("option", "value", parseInt(progress));
uploader.displayStatusMessage(uploader, "Uploaded " + progress + "%");
}
}
}
The Controller
In the upload Controller we made a critical change on naming of our blob. Earlier we used to dump everything in a hard-coded blob, but now since the photostreams should not be mixed up, we will separate our blobs per user. We change changed the blob creation code as highlighted below
[HttpPost]
[Authorize]
public ActionResult SetMetadata(int blocksCount, string fileName, long fileSize, int fileIndex)
{
var container = CloudStorageAccount.Parse(
ConfigurationManager.AppSettings["StorageConnectionString"]).CreateCloudBlobClient()
.GetContainerReference(Request.RequestContext.HttpContext.User.Identity.Name);
container.CreateIfNotExists();
var fileToUpload = new CloudFile()
{
BlockCount = blocksCount,
FileName = fileName,
Size = fileSize,
BlockBlob = container.GetBlockBlobReference(fileName),
StartTime = DateTime.Now,
IsUploadCompleted = false,
UploadStatusMessage = string.Empty,
FileKey = "CurrentFile" + fileIndex.ToString(),
FileIndex = fileIndex
};
Session.Add(fileToUpload.FileKey, fileToUpload);
return Json(new { success = true, index = fileIndex });
}
We have also added properties to the object that is returned once the entire file is upload. Also at this point, we have saved the image data to the repository. Both these changes are in the CommitAllChunks method and is highlighted below. Also note the new Constructor in which an instance of the PhotosRepository is injected
private ActionResult CommitAllChunks(CloudFile model)
{
IPhotoRepository _photosRepository;
public UploadController(IPhotoRepository photos)
{
_photosRepository = photos;
}
…
_photosRepository.InsertOrUpdate(new Photo
{
Name = model.FileName,
Notes = model.FileName,
Url = model.BlockBlob.Uri.ToString(),
UserId = Request.RequestContext.HttpContext.User.Identity.Name
});
_photosRepository.Save();
}
catch (StorageException e)
{
model.UploadStatusMessage = "Failed to Upload file. Exception - " + e.Message;
errorInOperation = true;
}
finally
{
Session.Remove(model.FileKey);
}
return Json(new
{
error = errorInOperation,
isLastBlock = model.IsUploadCompleted,
message = model.UploadStatusMessage,
index = model.FileIndex,
url = model.BlockBlob.Uri
});
}
Updating the Repository
In the Photos repository, we update the default implementation of InsertOrUpdate implementation. This is because the default implementation bases the Insert or Update based on Id value = 0. However in our case, if Id value is 0 but the name is same, the file will get overwritten in blob storage. So we shouldn’t be inserting a new database entry in that case. To do this we update the InsertOfUpdate method as follows
public void InsertOrUpdate(Photo photo)
{
using (AzurePhotoManagerContext context = new AzurePhotoManagerContext())
{
Photo current = null;
if (photo.Id == default(int))
{
// New entity
current = context.Photos.FirstOrDefault<Photo>(p => p.Name == photo.Name);
if (current == null)
{
context.Photos.Add(photo);
}
else
{
photo.Id = current.Id;
context.Entry(photo).State = EntityState.Modified;
}
}
else
{
// Existing entity
context.Entry(photo).State = EntityState.Modified;
}
context.SaveChanges();
}
}
Notice another change, we are using a local EF Context. This is required because Repository lifecycle is managed by Ninject and Ninject by default does not create a new instance on every request. Though it can be setup to do so quite easily.
On similar lines we update the Delete method
public void Delete(int id)
{
using (AzurePhotoManagerContext context = new AzurePhotoManagerContext())
{
var photo = context.Photos.Find(id);
context.Photos.Remove(photo);
context.SaveChanges();
}
}
This makes the Save method redundant, we can remove it we want to. We leave the shared context for doing selects and searches.
The Home Page and the Home Controller
Now that the Upload Controller is good to go, we can setup the Index page and get data to be displayed on the Home Page. Mind you authentication is required in every step and a user can see only their Images.
Markup Changes
The Home\Index.cshtml that comes up by default is updated to render images only if the user is authenticated. So, when the User is not Authenticated, we just show them a message requesting them to Log on.
@{
ViewBag.Title = "Azure Photo Stream";
}
@if (!Request.IsAuthenticated)
{
@section featured {
<section class="featured">
<div class="content-wrapper">
<hgroup class="title">
<h1>Log in to view your photo stream</h1>
</hgroup>
<p>
For more Azure Tutorials Visit
<a href="https://www.dotnetcurry.com/BrowseArticles.aspx?CatID=73" title="DotNetCurry - Windows Azure Tutorials">www.dotnetcurry.com</a>.
</p>
</div>
</section>
}
}
Once logged on, we have setup the UI to databind to the list of photos returned by the PhotosController’s Home method
else
{
<h1>Your Photo Stream</h1>
<div data-bind="foreach: photos" style="width: 100%; height: 100%; margin-right: auto; margin-left: auto;">
<div data-bind="with: $data" style="border: 1px solid gray; float: left; width: 350px; height: 300px; padding: 5px;
text-align: center; margin-right: auto; margin-left: auto;">
<div style="width: 300px; height: 200px; margin-right: auto; margin-left: auto;">
<a data-bind="attr: { href: Url }">
<img data-bind="attr: { src: Url }" alt="Uploading" style="width: auto; height: 100%" />
</a>
</div>
<div>
<label data-bind="text: Name" />
</div>
<div>
<label data-bind="text: Notes" />
</div>
</div>
</div>
}
@section Scripts{
<script src="~/Scripts/knockout-2.2.0.js"></script>
<script src="~/Scripts/apm-home.js"></script>
}
The Photos Controller Home Method
We add a new Home action method that returns a JsonResult to our home page with the top 20 photos for the user. For brevity, we won’t implement pagination today.
The Home action method is as follows
public JsonResult Home()
{
var userId = Request.RequestContext.HttpContext.User.Identity.Name;
return Json(photoRepository.All.Where<Photo>(l=>l.UserId==userId).Take<Photo>(20),
JsonRequestBehavior.AllowGet);
}
With that we wrap up all the new code and changes required. Time for a test run.
Testing it all out
First thing is to start the Azure Storage Emulator. From the Azure Command Prompt you can type
csrun /devstore
This starts the Azure storage emulator. All set, let’s run the App. Also using the Visual Studio DB Explorer, ensure that you have navigated
Step 1: From home page click on Register to create a new account and login
Step 2: Click on upload to navigate to the Upload page, select a set of files and upload. We already saw the UI for the Upload page above.
Step 3: Now click on ‘Azure Photo Studio’ to go to your home page
Editing the Name and Labels
We can use the Scaffolder generated pages to update Name and Labels. Click on ‘Library’ on the top right corner page to go to your image library. Click on Edit and edit the Notes for one of the images
Save and navigate back to your home page. As seen below, we have the update note coming up in the home page
From the Library page you can manage your images and delete them if required.
Our Photo Management portal is ready! Of course it is rather crude at the moment and can have lots of enhancements, but overall we have moved one step closer towards building complete applications in ASP.NET MVC using Azure Blob storage.
Conclusion
Using Azure blob storage in an ASP.NET MVC app is rather seamless and once we plan our application out, it’s pretty easy to build upon. Today we used some of our past experience to quickly put together a functional Image Library service, which albeit can do with a lot more features in the future.
Download the entire source code of this article (Github)
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Suprotim Agarwal, MCSD, MCAD, MCDBA, MCSE, is the founder of
DotNetCurry,
DNC Magazine for Developers,
SQLServerCurry and
DevCurry. He has also authored a couple of books
51 Recipes using jQuery with ASP.NET Controls and
The Absolutely Awesome jQuery CookBook.
Suprotim has received the prestigious Microsoft MVP award for Sixteen consecutive years. In a professional capacity, he is the CEO of A2Z Knowledge Visuals Pvt Ltd, a digital group that offers Digital Marketing and Branding services to businesses, both in a start-up and enterprise environment.
Get in touch with him on Twitter @suprotimagarwal or at LinkedIn