Udostępnij za pośrednictwem


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!

Comments

  • Anonymous
    May 01, 2014
    Hi Beth Nice Post. But instead of using Public access permission for blobs, Is it possible to view the images on demand by using the blob Shared Access Signature. I think public blobs access may be a security leak for the application. Regards

  • Anonymous
    May 01, 2014
    @babloo1436 - The client in this case is the .NET server so it's probably easier to just use the AccessKey and read the images from storage via the middle-tier query interceptor. FWIW though, that's why I just made the blobs themselves public and not the container. Trying to guess at GUIDs is pretty tough so this solution may suit some situations just fine and it avoids the read in the middle-tier.

  • Anonymous
    May 15, 2014
    Hi Beth Really nice. Would you look at returning the selected file URL and Type as well. I can see its in the Data at the end as 'type='  but no source URL? I have successfully used this to download any file Type to the local Db as well and its AWESOME.  .xlsx .docx  .pdf and I need Type to unpack it from there. In fact I would have thought this to be a tutorial for any file which is what we need in this great product Lightswitch HTML.

  • Anonymous
    July 23, 2014
    I need to customize the UI of my app, is there any guide that give more than just changing font or background colors? I'm seriously considering using LS as a BackEnd solution and Bootstrap as a FrontEnd... I don't know why I can't have other Libraries instead of JQuery Mobile... it just does not work for me since I use the Silverlight UI for my App.... JQuery Mobile is just too big for my forms... Thanks Beth for sharing.. hope you can read and comment my sugestion. amatosolivo@gmail.com, alex_sotam@hotmail.com

  • Anonymous
    August 07, 2014
    Hi Beth, This is how we can upload the image in the blob. If I want to upload any other format of file like .pdf, .txt then what should we have to change in this code? Thanks.

  • Anonymous
    September 10, 2014
    I would like to retain the original file name and extension  as a field in the database. How can you pass that from the file uploader to the database? I'm sure there is a way but I'm just not seeing it clearly.

  • Anonymous
    October 28, 2014
    The comment has been removed

  • Anonymous
    December 14, 2014
    The comment has been removed

  • Anonymous
    December 23, 2014
    I already have the images stored in Azure blob. I need to Display images in Datagrid row from Azure storage. Images name stored in Database. I am using LightSwitch Desktop application with Visual Studio 2013. Could you please guide how to show the images into Grid column.

  • Anonymous
    December 23, 2014
    Thanks Bath, Nice article. Now I am able to show the image inside DataGrid of Desktop Application. Can you guide me how to open the large image on click of  small image on Grid Column.

  • Anonymous
    February 11, 2015
    Hi, Beth on Pictures_Updating  Orignanl value is null for Image. Can you give me the idea

  • Anonymous
    February 26, 2015
    Hello All, Nice post Champion, I can see some of the comment has said they can use this to the LightSwitch desktop client, but i am not able to figure out how i can use this in desktop client. So Can Beth/Anyone else can show me a step by step configuration to add different type of files to blob and download the file to browser or client machine on button click.? Kindly reply to this comment. Thank you. Regards, Nirav

  • Anonymous
    April 05, 2015
    Beth, thank you much!

  • Anonymous
    January 27, 2016
    I'm confused on where to place the static constructor. If we have the Solution Explorer open, which file to we open and place that code?  I understand where to put the Pictures_Inserting and Pictures_Updating code, but I'm a little lost after that.

  • Anonymous
    May 24, 2016
    Hi,It worked but just save buttom doesnt work .is a part code need for fix it?thx

  • Anonymous
    September 03, 2016
    I got what you intend,saved to favorites, very decent web site.