A Practical Guide to Photo Sharing Apps using Windows Phone and Azure

The previous post in this series was called Theory of a Cloud Based Photo Sharing App using Windows Phone and Azure. We went through the architecture for a simple image sharing application. Steps 1 to 5 in this process involve taking a photo with the camera and uploading it to blob storage. To follow along with this article you’ll need to install both the Windows Phone SDK and the Windows Azure SDK for Visual Studio.

Step 1: Creating the Phone Application

Note: It’s recommended that you run Visual Studio “As Administrator” for the duration of this tutorial. Whilst it is not required for the development of the phone application, it is required for working with the Windows Azure development environment. Also note that if Visual Studio is running “As Administrator” it will also run the Windows Phone Emulator in this mode – if you already have the emulator running, remember to close it before attempting to run your phone application from Visual Studio running in this mode.

Let’s start with the phone part. We’ll start with a new Windows Phone project and take an image with the camera. Windows Phone has a concept called Tasks which are designed to expose common features of the platform using a standard, consistent look and feel. They’re broken into two categories: Launchers, which simply launch specific functionality such as the maps or search applications, and Choosers, which are designed to return data to the application. In this case we’re going to use the CameraCaptureTask to prompt the user to take a photo with the camera, which is then returned to the application. Note that in Windows Phone 7.5 (Mango) you do have direct access to the camera feed if you want to provide your own custom user experience for taking a photo or for capturing video.

We’ll keep the layout for the MainPage of the application relatively simple, consisting of an Image control, in which the photo captured will be displayed, and two Buttons, the first to initiate the photo capture and the second to upload the captured image to the cloud.

<StackPanel>
    <Image x:Name="CaptureImage" Height="200" Width="200" />
    <Button Content="Capture" Click="CaptureClick" />
    <Button Content="Upload" Click="UploadClick" />
</StackPanel>

In the code for the MainPage we need to create an instance of the CameraCaptureTask, assign an event handler for the Completed event, and then call the Show method on the task when the user clicks the Capture button.

private CameraCaptureTask camera = new CameraCaptureTask();
private string filename;

public MainPage() {
    InitializeComponent();

    camera.Completed += CameraCompleted;
}

private void CameraCompleted(object sender, PhotoResult e)  {
    var bmp = PictureDecoder.DecodeJpeg(e.ChosenPhoto);
    filename = Guid.NewGuid() + ".jpg";
    using (var file = IsolatedStorageFile.GetUserStoreForApplication().OpenFile(filename, FileMode.Create, FileAccess.Write)) {
        bmp.SaveJpeg(file, bmp.PixelWidth, bmp.PixelHeight, 0, 100);
    }
    CaptureImage.Source = bmp;
}

private void CaptureClick(object sender, RoutedEventArgs e) {
    camera.Show();
}

private void UploadClick(object sender, RoutedEventArgs e) { }

When the CameraCaptureTask returns we’re taking the returned stream (e.ChosenPhoto) and decoding it into an image that is both saved to IsolatedStorage (local storage for the application) and displayed in the image control, CaptureImage. If you run this code in the emulator you should be able to use the camera simulation to return a default image that will be displayed within your application, as in the right image of Figure 1.

Blob Storage Figure 1

Figure 1

Step 2: Creating the Cloud Application

The next step is to create our cloud project, which can be added to the same solution that contains your phone application. Right-click on the Solution node in Solution Explorer and select Add > New Project, and in the Add New Project dialog select the Windows Azure Project template from within the Cloud node. Give the new project a new name, for example “ImageSharingCloud”, and click OK to proceed. You’ll then be prompted to add one or more Windows Azure Projects based on a set of existing templates. In this case we’re going to create a WCF Service Web Role, called ImageServices, and a Worker Role, called ImageWorker. Add these by selecting the role and clicking the right arrow (see Figure 2); you can then rename them so that they’re easier to identify within your solution.

Blob Storage Figure 2

Figure 2

Step 3: Generating a Shared Access Signature

In order to upload the photo from our Windows Phone application directly into Windows Azure Blob Storage we need to know one of the two access keys issued via the Windows Azure Management Console for the storage account you want to upload to. These keys are designed to be use within web sites or services that are located on a server, not for inclusion within client applications. Silverlight, whether it is used to build desktop applications or applications for Windows Phone, is a client technology and you should not include access keys within these applications.

Windows Azure provides an alternative strategy for uploading content to Blob Storage through the use of Shared Access Signatures (SAS). When an application wants to upload content to Blob Storage it requests a SAS from a service; the service, uses one of the access keys to generate a SAS which is returned to the calling application; the application uses the SAS in place of an access key when uploading the content.

We’re going to work with the newly created ImageServices project to expose a service which can be called by our Windows Phone application in order to return an SAS. Firstly, you need to tell the ImageServices project which storage account you want to use. Double-click the Properties node for the ImageServices project in Solution Explorer to open the Properties dialog. On the Settings tab you need to create a new entry, DataConnectionString, with the Type set to Connection String. Clicking the ellipses button will open the Storage Account Connection String dialog, shown in Figure 3. Here we’re going to use the storage emulator that comes with the Windows Azure SDK. However, when you eventually go to publish your application you will need to change this to use the account name and key of your storage account.

Blob Storage Figure 3

Figure 3

The next step is to create the service that will return a SAS to the application. Right-click on the ImageServices project in Solution Explorer and select Add > New Item, and then select the WCF Service template in the Add New Item dialog. Give the new service a name, UploadService.svc, and click Add to create the new service. Change the service interface to include a method called UploadUriWithSharedAccessSignature as follows:

[ServiceContract]
public interface IUploadService {
    [OperationContract]
    Uri UploadUriWithSharedAccessSignature(string userId);
}

This method accepts a single parameter which will be used to separate uploads from different users within Blob Storage. The method will return the Uri, complete with Shared Access Signature, of the Blob Storage container into which the application can upload files. The following code provides the implementation of this method, along with the private method InitializeStorage, which is used to load the cloud configuration information and create an instance of the CloudBlobClient class which is a wrapper class for working with Blob Storage.

public class UploadService : IUploadService {
    private const SharedAccessPermissions ContainerSharedAccessPermissions = 
        SharedAccessPermissions.Write | SharedAccessPermissions.Delete | SharedAccessPermissions.List;
    private CloudBlobClient cloudBlobClient;

    private void InitializeStorage() {
        CloudStorageAccount.SetConfigurationSettingPublisher((configName, configSetter) => {
            configSetter(RoleEnvironment.GetConfigurationSettingValue(configName));
        });
        var storageAccount = CloudStorageAccount.FromConfigurationSetting("DataConnectionString");
        cloudBlobClient = storageAccount.CreateCloudBlobClient();
    }

    public Uri UploadUriWithSharedAccessSignature(string userId) {
        try {
            InitializeStorage();

            var container = this.cloudBlobClient.GetContainerReference("imageservice");
            container.CreateIfNotExist();
            var sas = container.GetSharedAccessSignature(new SharedAccessPolicy() {
                Permissions = ContainerSharedAccessPermissions,
                SharedAccessExpiryTime = DateTime.UtcNow + TimeSpan.FromMinutes(5)
            });

            var uriBuilder = new UriBuilder(container.Uri + "/" + userId) { Query = sas.TrimStart('?') };
            return uriBuilder.Uri;
        }
        catch (Exception exception) {
            throw new WebFaultException<string>(exception.Message, HttpStatusCode.InternalServerError);
        }
    }
}

In this scenario the SharedAccessSignature is created with a 5 minute expiry. This means that our Windows Phone application will only be able to upload files within that window. This significantly reduces the risk of exposing an access key to the Windows Phone application.

Step 4: Retrieving the SharedAccessSignature

Now that we have a service which will generate the shared access signature we need to call that from within our Windows Phone application. In order to do this we need to add a reference to the UploadService to the Windows Phone project. Make sure you have built the solution and can browse to the UploadService (right-click on UploadService.svc and select View in Browser). Right-click on the Windows Phone project and select Add Service Reference. In the Add Service Reference dialog, shown in Figure 4, click the Discover button. Once the services have been discovered, select the UploadService, specify a namespace, in this case we’ll use UploadService, and click OK.

Blob Storage Figure 4

Figure 4

This will not only create the necessary proxy classes to wrap the calls to the UploadService, it will also create, and add to the Windows Phone project, a file called ServiceReferences.ClientConfig. In this file you’ll see the configuration information for the UploadService endpoint. You will need to adjust this to change it from pointing to the Visual Studio Development Server (the default) to the Windows Azure Compute Emulator. Typically this involves changing the endpoint address to http://127.0.0.1:81/UploadService.svc; although the actual port number may vary, so check to see what port number your service runs on when you run the ImageSharingCloud project.

Step 5: Completing the Upload

The last step is to call the UploadUriWithSharedAccessSignature method to retrieve the SAS, which is then used to upload the photo to Blob Storage. The following code starts by calling this method, then when response is received, the photo is uploaded to Blob Storage using the CloudBlobUploader class. The full code for this class is available at the end of this post.

private void UploadClick(object sender, RoutedEventArgs e) {
    var client = new UploadServiceClient();
    client.UploadUriWithSharedAccessSignatureCompleted += UploadUriWithSharedAccessSignatureCompleted;
    client.UploadUriWithSharedAccessSignatureAsync(Guid.NewGuid().ToString());
}

private void UploadUriWithSharedAccessSignatureCompleted(object sender,
                                                            UploadUriWithSharedAccessSignatureCompletedEventArgs e) {
    if (e.Error == null) {
        // Determine upload path - Add filename to container path
        var builder = new UriBuilder(e.Result);
        builder.Path = builder.Path + "/" + filename;
        var blobUrl = builder.Uri;

        // Open the image file from isolates storage to read from
        IsolatedStorageFileStream file =
IsolatedStorageFile.GetUserStoreForApplication().OpenFile(filename, FileMode.Open, FileAccess.Read);

        // Create the uploader and kick off the uploader
        var uploader = new CloudBlobUploader(file, blobUrl.AbsoluteUri);
        uploader.UploadFinished += (s, args) => {
                     this.Dispatcher.BeginInvoke(() => {
                                  MessageBox.Show("Upload complete!"); });
                                        };
        uploader.StartUpload();
    }
}

In order to run this application you will need to run both the ImageSharingCloud project and the ImageSharing Windows Phone project. Capture an image by clicking the Capture button, then click the Upload button. If this is successfull you should see a message displayed stating that the upload is complete. At this point you can browse the Windows Azure storage emulator and inspect the image that has been uploaded. The following code is the CloudBlobUploader class that is used in the final step of this tutorial. This can be reused in any application to upload content into Blob Storage.

public class CloudBlobUploader
{
    private const long ChunkSize = 4194304;
    private readonly IList<string> blockIds = new List<string>();

    private readonly long dataLength;
    private readonly Stream fileStream;
    private readonly string uploadUrl;

    private readonly bool useBlocks;
    private string currentBlockId;
    private long dataSent;

    public CloudBlobUploader(Stream fileStream, string uploadUrl) //, IStorageCredentials credentials)
    {
        this.fileStream = fileStream;
        this.uploadUrl = uploadUrl;
        //                this.StorageCredentials = credentials;
        dataLength = this.fileStream.Length;
        dataSent = 0;

        // Upload the blob in smaller blocks if it's a "big" file.
        useBlocks = (dataLength - dataSent) > ChunkSize;
    }

    public event EventHandler<ParameterEventArgs<bool>> UploadFinished;
    public event EventHandler<ParameterEventArgs<double>> UploadProgress;

    //public IStorageCredentials StorageCredentials { get; set; }

    public void StartUpload()
    {
        if (uploadUrl == null)
        {
            RaiseUploadFinished(false);
        }
        var uriBuilder = new UriBuilder(uploadUrl);

        // Set a timeout query string parameter.
        uriBuilder.Query = string.Format(
            CultureInfo.InvariantCulture,
            "{0}{1}",
            uriBuilder.Query.TrimStart('?'),
            string.IsNullOrEmpty(uriBuilder.Query) ? "timeout=10000" : "&timeout=10000");

        if (useBlocks)
        {
            // Encode the block name and add it to the query string.
            currentBlockId = Convert.ToBase64String(Encoding.UTF8.GetBytes(Guid.NewGuid().ToString()));
            uriBuilder.Query = string.Format(
                CultureInfo.InvariantCulture,
                "{0}&comp=block&blockid={1}",
                uriBuilder.Query.TrimStart('?'),
                currentBlockId);
        }

        // With or without using blocks, we'll make a PUT request with the data.
        var request = (HttpWebRequest) WebRequestCreator.ClientHttp.Create(uriBuilder.Uri);
        request.Method = "PUT";
        request.BeginGetRequestStream(WriteToStreamCallback, request);
    }

    private void WriteToStreamCallback(IAsyncResult asynchronousResult)
    {
        try
        {
            var request = (HttpWebRequest) asynchronousResult.AsyncState;
            Stream requestStream = request.EndGetRequestStream(asynchronousResult);
            var buffer = new byte[4096];
            int bytesRead = 0;
            int tempTotal = 0;
            fileStream.Position = dataSent;

            while (((bytesRead = fileStream.Read(buffer, 0, buffer.Length)) != 0) &&
                    (tempTotal + bytesRead < ChunkSize))
            {
                requestStream.Write(buffer, 0, bytesRead);
                requestStream.Flush();

                dataSent += bytesRead;
                tempTotal += bytesRead;
            }

            requestStream.Close();

            request.BeginGetResponse(ReadHttpResponseCallback, request);
        }
        catch (Exception exception)
        {
            RaiseUploadFinished(false);
        }
    }

    private void ReadHttpResponseCallback(IAsyncResult asynchronousResult)
    {
        try
        {
            var request = (HttpWebRequest) asynchronousResult.AsyncState;
            var response = (HttpWebResponse) request.EndGetResponse(asynchronousResult);

            if (useBlocks)
            {
                blockIds.Add(currentBlockId);
            }

            RaiseUploadProgress(100.0*dataSent/dataLength);

            // If there is more data, send another request.
            if (dataSent < dataLength)
            {
                StartUpload();
            }
            else
            {
                fileStream.Close();
                fileStream.Dispose();

                if (useBlocks)
                {
                    // Commit the blocks into the blob.
                    PutBlockList();
                }
                else
                {
                    RaiseUploadFinished(true);
                }
            }
        }
        catch (Exception exception)
        {
            RaiseUploadFinished(false);
        }
    }

    private void PutBlockList()
    {
        if (uploadUrl == null)
        {
            RaiseUploadFinished(false);
        }
        var uriBuilder = new UriBuilder(uploadUrl);
        uriBuilder.Query = string.Format(
            CultureInfo.InvariantCulture,
            "{0}{1}",
            uriBuilder.Query.TrimStart('?'),
            string.IsNullOrEmpty(uriBuilder.Query) ? "comp=blocklist" : "&comp=blocklist");

        var request = (HttpWebRequest) WebRequestCreator.ClientHttp.Create(uriBuilder.Uri);
        request.Method = "PUT";

        // x-ms-version is required for put block list
        request.Headers["x-ms-version"] = "2009-09-19";

        request.BeginGetRequestStream(BlockListWriteToStreamCallback, request);
    }

    private void BlockListWriteToStreamCallback(IAsyncResult asynchronousResult)
    {
        try
        {
            var request = (HttpWebRequest) asynchronousResult.AsyncState;
            Stream requestStream = request.EndGetRequestStream(asynchronousResult);
            var document =
                new XDocument(new XElement("BlockList",
                                            from blockId in blockIds
                                            select new XElement("Uncommitted", blockId)));
            XmlWriter writer = XmlWriter.Create(requestStream, new XmlWriterSettings {Encoding = Encoding.UTF8});
            long length = 0L;

            document.Save(writer);
            writer.Flush();

            length = requestStream.Length;
            requestStream.Close();

            request.BeginGetResponse(BlockListReadHttpResponseCallback, request);
        }
        catch (Exception exception)
        {
            RaiseUploadFinished(false);
        }
    }

    private void BlockListReadHttpResponseCallback(IAsyncResult asynchronousResult)
    {
        try
        {
            var request = (HttpWebRequest) asynchronousResult.AsyncState;
            var response = (HttpWebResponse) request.EndGetResponse(asynchronousResult);

            RaiseUploadFinished(true);
        }
        catch (Exception exception)
        {
            RaiseUploadFinished(false);
        }
    }

    private void RaiseUploadProgress(double progress)
    {
        if (UploadProgress != null)
        {
            UploadProgress(this, progress);
        }
    }

    private void RaiseUploadFinished(bool response)
    {
        if (UploadFinished != null)
        {
            UploadFinished(this, response);
        }
    }
}

public class ParameterEventArgs<T> : EventArgs
{
    public ParameterEventArgs(T parameter)
    {
        Parameter1 = parameter;
    }

    public T Parameter1 { get; set; }

    /// <summary>
    /// Converts the parameter into a ParameterEventArgs
    /// </summary>
    public static implicit operator ParameterEventArgs<T>(T parameter)
    {
        return new ParameterEventArgs<T>(parameter);
    }
}

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Harry

    Thank you very much for your tutorial :) This is exactly what I need for my app. I can now successfully upload the photo to azure, but how can I view it again in the app?

  • XueBao

    thanks for that! it’s realy good article!! best of the best…