Storing Images in Azure Blob Storage in a LightSwitch Application

LightSwitch has always had support for storing pictures in a database through its “Image” business type. However, often it is not feasible to store images in a database, due to size and/or accessibility. In this post I’ll show you how you can leverage Azure blob storage to store images used in your HTML client applications. This is a good architecture particularly if your application is hosted in an Azure Website already. It’s also pretty easy to do.

Setting up Azure Blob Storage

Setting up storage is easy, just follow these directions: Create an Azure Storage account

That article also explains how to access storage programmatically from .NET and how to store settings in your web.config so I encourage you to read through all of it. For the purposes of this post, I’m going to focus on the pieces needed to integrate this into your LightSwitch HTML app.

After you create & name your storage account, click “Manage Access Keys” to grab the access key you will need to supply in your connection string.

image

Once you have the storage account set up, you can programmatically create containers and save blobs to them from your LightSwitch .NET Server project. Let’s take a look at an example.

Setting up the Data Model

To get this to work elegantly in LightSwitch, we’re going to utilize a couple business types: Image and Web Address. The Image type will only be used to “transport” the bytes into storage for us. We’ll use the Web Address for viewing the image. We can set up the blog container so that we can address blobs directly via a URL as you will see shortly. 

For this example, assume a User can have many Pictures. Here’s our data model. Notice the Picture entity has three important properties: Image, ImageUrl, and ImageGuid.

image

The ImageGuid is used to generate a unique Id that becomes part of the addressable URL of the image in Azure blob storage. It’s necessary so we can find the correct blob. Of course you can come up with your own unique naming, and you can even store them in different containers if you want.

Creating the Screens

When you create your screens, make sure that the ImageUrl is being used to display the picture and not the Image property. A Web Address business type also allows you to choose a Image control to display it. For instance, I’ll create a Common Screen Set for my User table and include Pictures.

Then open up the ViewUser screen and on the Pictures tab, remove the Image and instead display the ImageUrl as an Image control.

image

Then add a tap action on the Tile List to viewSelected on the Picture. Choose a new screen, Select View Details Screen and select Picture as the screen data. On this screen, you can display the picture as actual size using the same technique as above, but setting the ImageUrl image control to “Fit to Content” in the appearance section of the properties window. You can also set the Label position to “None”.

image

Finally, we’ll want to allow adding and editing pictures. On the ViewUser screen, add a button to the Command Bar and set the Tap action to addAndEditNew for the Picture. Create a new screen, add a new Add Edit Details screen and set the screen data to Picture. On the View Picture screen, add a button and set the Tap action to edit for the Picture and use the same Add Edit Picture Screen.

Custom JavaScript Control for Uploading Files

Now that our screens are in place and are displaying the ImageUrl how we like, it’s time to incorporate a custom JavaScript control for uploading the files. This utilizes the Image property which is storing the bytes for us on the client side. I’ll show you in a minute how we can intercept this on the Server tier and send it to Azure storage instead of the database, but first we need a control to get the bytes from our users.

Luckily, you don’t have to write this yourself. There’s a control that the LightSwitch team wrote a wile back that will work swimmingly with modern browsers as well as older ones that don’t support the HTML5 method of reading files. It’s part of a tutorial, but you can access the files directly and copy the code you need.

image-uploader.js
image-uploader-base64-encoder.aspx

Put them in your HTMLClient\Scripts folder. Then add a reference to the JavaScript in your default.htm file.

 
<script type="text/javascript" src="Scripts/image-uploader.js"></script>

Finally, add the custom control to the Add Edit Picture screen. This time, make sure you use the Image property, not ImageUrl. We will be setting the bytes of this property using the image-uploader control.

image

While the custom control is selected, drop down the “Write Code” button from the top of screen designer and set the code in the _render method like so:

 myapp.AddEditPicture.Image_render = function (element, contentItem) {
    // Write code here.
    createImageUploader(element, contentItem, "max-width: 300px; max-height: 300px");
};

Now for the fun part. Saving to Azure blob storage from your LightSwitch Server tier.

Saving to Blob Storage from the LightSwitch Update Pipeline

That’s all we have to do with the client – now it’s time for some server coding in .NET. First, we’ll need some references to the Azure storage client libraries for .NET. You can grab these from NuGet. Right-click on your Server project and select “Manage NuGet Packages”. Install the Windows.Azure.Storage 3.1 package.

image

Now the trick is to intercept the data going through the LightSwitch update pipeline, taking the Image data and saving it to storage and then setting the Image data to null. From there, LightSwitch takes care of sending the rest of the data to the database.

Open the Data Designer to the Picture entity and drop down the Write Code button in order to override a couple methods “Picture_Inserting” and “Picture_Updating” on the ApplicationDataService class. (We could also support Delete, but I’ll leave that as an exercise for the reader – or a follow up post :-)).

On insert, first we need to create a new Guid and use that in the URL. Then we can save to storage. On update, we’re just checking if the value has changed before saving to storage.

VB:

 Private Sub Pictures_Inserting(entity As Picture)
    'This guid becomes the name of the blob (image) in storage
    Dim guid = System.Guid.NewGuid.ToString()
    entity.ImageGuid = guid
    'We use this to display the image in our app.
    entity.ImageUrl = BlobContainerURL + guid

    'Save actual picture to blob storage 
    SaveImageToBlob(entity)
End Sub

Private Sub Pictures_Updating(entity As Picture)
    'Save actual picture to blob storage only if it's changed
    Dim prop As Microsoft.LightSwitch.Details.IEntityStorageProperty = 
        entity.Details.Properties("Image")

    If Not Object.Equals(prop.Value, prop.OriginalValue) Then
        SaveImageToBlob(entity)
    End If
End Sub

C#:

 partial void Pictures_Inserting(Picture entity)
{
    //This guid becomes the name of the blob (image) in storage
    string guid = System.Guid.NewGuid().ToString();
    entity.ImageGuid = guid;
    //We use this to display the image in our app.
    entity.ImageUrl = BlobContainerURL + guid;

    //Save actual picture to blob storage 
    SaveImageToBlob(entity);
}
       
partial void Pictures_Updating(Picture entity)
{
    //Save actual picture to blob storage only if it's changed
    Microsoft.LightSwitch.Details.IEntityStorageProperty prop = 
    (Microsoft.LightSwitch.Details.IEntityStorageProperty)entity.Details.Properties["Image"];
    if(!Object.Equals(prop.Value, prop.OriginalValue)){
        SaveImageToBlob(entity);
    }
}

Working with Azure Blob Storage

The article I referenced above walks you through working with blob storage but here’s the basics of how we can create a new container if it doesn’t exist, and save the bytes. First, include the Azure storage references at the top of the ApplicationDataService file.

VB:

 Imports Microsoft.WindowsAzure.Storage
Imports Microsoft.WindowsAzure.Storage.Blob

C#:

 using Microsoft.WindowsAzure.Storage;
using Microsoft.WindowsAzure.Storage.Blob;

Next, get the connection string, the container name, and the base URL to the container. You can read these from your web.config, but for this example I am just hardcoding them in some constants on the ApplicationDataService class.

VB:

 'TODO put in configuration file
Const BlobConnStr = "DefaultEndpointsProtocol=https;AccountName=mystorage;AccountKey=*****"
Const BlobContainerName = "pics"
Const BlobContainerURL = https://mystorage.blob.core.windows.net/pics/

C#:

 const string BlobConnStr = "DefaultEndpointsProtocol=https;AccountName=mystorage;AccountKey=****";
const string BlobContainerName = "pics";
const string BlobContainerURL = "https://mystorage.blob.core.windows.net/pics/";

Next create a static constructor to check if the container exists and if not, create it. It will also set the container’s blobs so they are publically accessible via direct URLs. This will only run once when the web application is first started (or restarted).

VB:

 Shared Sub New()

    'Get our blob storage account
    Dim storageAccount As CloudStorageAccount = CloudStorageAccount.Parse(BlobConnStr)
    'Create the blob client.
    Dim blobClient As CloudBlobClient = storageAccount.CreateCloudBlobClient()
    'Retrieve reference to blob container. Create if it doesn't exist.
    Dim blobContainer = blobClient.GetContainerReference(BlobContainerName)

    If Not blobContainer.Exists Then
        blobContainer.Create()

        'Set public access to the blobs in the container so we can use the picture 
        '  URLs in the HTML client.
        blobContainer.SetPermissions(New BlobContainerPermissions With
                         {.PublicAccess = BlobContainerPublicAccessType.Blob})
    End If
End Sub

C#:

 static ApplicationDataService() {

    //Get our blob storage account
    CloudStorageAccount storageAccount = CloudStorageAccount.Parse(BlobConnStr);
    //Create the blob client.
    CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();

    //Retrieve reference to blob container. Create if it doesn't exist.
    CloudBlobContainer blobContainer = blobClient.GetContainerReference(BlobContainerName);

    if(!blobContainer.Exists())
    {
        blobContainer.Create();

        //Set public access to the blobs in the container so we can use the picture 
        //   URLs in the HTML client.
        blobContainer.SetPermissions(new BlobContainerPermissions { 
            PublicAccess = BlobContainerPublicAccessType.Blob });
    }
}

Now that we have created the container on our storage account, the saving is pretty easy. Notice the “trick” here. After we save the image to storage, set the Image property to null so that it isn’t saved into the database.

VB:

 Private Sub SaveImageToBlob(p As Picture)
    Dim blobContainer = GetBlobContainer()

    'Get reference to the picture blob or create if not exists. 
    Dim blockBlob As CloudBlockBlob = blobContainer.GetBlockBlobReference(p.ImageGuid)

    'Save to storage
    blockBlob.UploadFromByteArray(p.Image, 0, p.Image.Length)

    'Now that it's saved to storage, set the Image database property null
    p.Image = Nothing
End Sub

Private Function GetBlobContainer() As CloudBlobContainer
    'Get our blob storage account
    Dim storageAccount As CloudStorageAccount = CloudStorageAccount.Parse(BlobConnStr)
    'Create the blob client
    Dim blobClient As CloudBlobClient = storageAccount.CreateCloudBlobClient()
    'Retrieve reference to a previously created container.
    Return blobClient.GetContainerReference(BlobContainerName)
End Function

C#:

 private void SaveImageToBlob(Picture p)
{
    CloudBlobContainer blobContainer = GetBlobContainer();

    //Get reference to the picture blob or create if not exists. 
    CloudBlockBlob blockBlob = blobContainer.GetBlockBlobReference(p.ImageGuid);

    //Save to storage
    blockBlob.UploadFromByteArray(p.Image, 0, p.Image.Length);

    //Now that it's saved to storage, set the Image database property null
    p.Image = null;
}

private CloudBlobContainer GetBlobContainer()
{
    //Get our blob storage account
    CloudStorageAccount storageAccount = CloudStorageAccount.Parse(BlobConnStr);
    //Create the blob client
    CloudBlobClient blobClient = storageAccount.CreateCloudBlobClient();
    //Retrieve reference to a previously created container.
    return blobClient.GetContainerReference(BlobContainerName);
}

That’s all here is to it. Run the application and upload a picture.

image

Then go back to your Azure Management Portal and inspect your storage account. You will see the pictures in the storage container. And if you check your database, the Image will be null.

image

Desktop Client Considerations: Reading from Blob Storage from the LightSwitch Query Pipeline

If you want this to work with the Desktop (Silverlight) client, you’ll need to retrieve the Image bytes from storage into the Image property directly. This is because the built-in LightSwitch control for this client works with bytes, not URLs. You can use the same code to save the images above, but you’ll also need to read from blob storage anytime a Picture entity is queried from the database by tapping into the query pipeline. Here’s some code you can use in the same ApplicationDataService class.

VB:

 Private Sub Query_Executed(queryDescriptor As QueryExecutedDescriptor)
'This would be necessary if using the Silverlight client.
    If queryDescriptor.SourceElementType.Name = "Picture" Then

        For Each p As Picture In queryDescriptor.Results
            ReadImageFromBlob(p)
        Next
    End If
End Sub

Private Sub ReadImageFromBlob(p As Picture)
    'Retrieve reference to the picture blob named after it's Guid.
    If p.ImageGuid IsNot Nothing Then
        Dim blobContainer = GetBlobContainer()
        Dim blockBlob As CloudBlockBlob = blobContainer.GetBlockBlobReference(p.ImageGuid)

        If blockBlob.Exists Then
            Dim buffer(blockBlob.StreamWriteSizeInBytes - 1) As Byte
            blockBlob.DownloadToByteArray(buffer, 0)

            p.Image = buffer
        End If
      End If
End Sub

C#:

 partial void Query_Executed(QueryExecutedDescriptor queryDescriptor)
{
    //This would be necessary if using the Silverlight client.
    if(queryDescriptor.SourceElementType.Name == "Picture")
    {
        foreach (Picture p in (IEnumerable<Picture>)queryDescriptor.Results)
        {
            ReadImageFromBlob(p);
        }
    }
}

private void ReadImageFromBlob(Picture p)
{
    //Retrieve reference to the picture blob named after it's Guid.
    if (p.ImageGuid != null)
    {
        CloudBlobContainer blobContainer = GetBlobContainer();
        CloudBlockBlob blockBlob = blobContainer.GetBlockBlobReference(p.ImageGuid);

        if (blockBlob.Exists())
        {
            byte[] buffer = new byte[blockBlob.StreamWriteSizeInBytes - 1];
            blockBlob.DownloadToByteArray(buffer, 0);

            p.Image = buffer;
        }
    }
}

Wrap Up

Working with Azure Blob Storage is really easy. Have fun adapting this code to suit your needs, making it more robust with error logging, etc. I encourage you to play with storing other types of data as well. It makes a lot of sense to utilize Azure blob storage, particularly if your LightSwitch app is already being hosted in Azure.

Enjoy!