What is SnapGold?
All the code can be found on our GitHub page here.
The app highlights best practices in the following areas, which you can use to model your own app:
- UWP optimized for desktop & mobile
- MVVM with responsive layout
- Azure App Service
- Azure DocumentDB & Blob storage back end
- Azure App Service Authentication (w/Facebook/Twitter/Google/Microsoft account)
- Authenticated push notifications
- Instrumented with Visual Studio Application Insights (client and server)In-App-Purchase Enabled
There’s lots of great stuff in this code, even if a photo-sharing app isn’t your goal. The navigation framework, MVVM patterns, Azure integration, push notifications, and authentication are all juicy bits that you can port directly to your own apps.
Why DocumentDB?
Originally this code sample leveraged a SQL Server database to store the relational data for SnapGold. We moved the data layer to DocumentDB for a few reasons.
- DocumentDB has some awesome querying power considering all fields on your data models are indexed.
- So many apps use SQL and most developers are familiar with the nuances of setting it up. Here we tried something different. DocumentDB is relatively new and offers different bring-up steps.
- This is a code sample, and bring up for DocumentDB is much faster than bringing a new SQL Server database online. In our sample, just create a new DocumentDB in Azure, set two configs, and it creates itself when you deploy to Azure. #forthewin
We found that some queries in DocumentDB were straightforward and could be accomplished with simple Lambda queries against the collections. Here we get all the photo documents for a single user of the app for their profile page:
/// <summary> | |
/// Fetches the photo stream data for a specified user. | |
/// </summary> | |
/// <param name="userId">The user id.</param> | |
/// <param name="continuationToken">Last captured ticks in the form of a string.</param> | |
/// <returns>List of photos up to the page size.</returns> | |
public async Task<PagedResponse<PhotoContract>> GetUserPhotoStream(string userId, string continuationToken) | |
{ | |
var feedOptions = new FeedOptions | |
{ | |
MaxItemCount = PhotoStreamPageSize, | |
RequestContinuation = continuationToken | |
}; | |
var query = _documentClient.CreateDocumentQuery<PhotoDocument>(DocumentCollectionUri, | |
feedOptions) | |
.Where(d => d.DocumentType == PhotoDocument.DocumentTypeIdentifier) | |
.Where(d => d.DocumentVersion == _currentDocumentVersion) | |
.Where(p => p.UserId == userId) | |
.OrderByDescending(p => p.CreatedDateTime.Epoch) | |
.AsDocumentQuery(); | |
var documentResponse = await query.ExecuteNextAsync<PhotoDocument>(); | |
var photoContracts = await CreatePhotoContractsAndLoadUserData(documentResponse.ToList()); | |
var result = new PagedResponse<PhotoContract> | |
{ | |
Items = photoContracts, | |
ContinuationToken = documentResponse.ResponseContinuation | |
}; | |
return result; | |
} |
Conversely, some were more complicated and required some DocumentDB stored procedures. In this case, get the top 20 photos from the 20 categories most recently updated with new photos. DocumentDB uses JavaScript as their equivalent for TSQL as you will see in our code below. (More details on that are here.)
For our code sample, DocumentDB “sprocs” are stored in the Mobile App Service as Javascript files. See below:
The code for the highlighted DocumentDB sproc (getRecentPhotosForCategoriesStoredProcedure.js) is below so you can see the syntax we use for DocumentDB sprocs.
// Store Procedure for getting the most recent photos for each category | |
// @param numberOfPhotos - The number of photos to return per category | |
// @param currentDocumentVersion - The current document version number that the service is using | |
function getRecentPhotosForCategories(numberOfPhotos, currentDocumentVersion) { | |
let catCount = 0; | |
let catIndex = 0; | |
let existingPhotos = []; | |
getAllCategories(function (allCategories) { | |
for (var i = 0; i < allCategories.length; i++) { | |
// Retrieve the most recent photos for this category | |
// and append them to the response body | |
getRecentPhotosForCategory(allCategories[i].id); | |
} | |
}); | |
function getAllCategories(callback) { | |
// Perform Query using JavaScript Language Integrated Query. | |
var result =__.filter(function(doc) { | |
return doc.DocumentType == "CATEGORY" && doc.DocumentVersion == currentDocumentVersion; | |
}, function(err, documents) { | |
if (err) { | |
throw new Error("Unable to query for all categories, aborting."); | |
} | |
if (documents.length < 1) { | |
return; | |
} | |
catCount = documents.length; | |
callback(documents); | |
}); | |
if (!result.isAccepted) { | |
throw new Error("Sproc is too close to violating resource limit, aborting."); | |
} | |
} | |
function getRecentPhotosForCategory(categoryId) { | |
// Perform Query using chained JavaScript Language Integrated Query. | |
var result = __.chain() | |
.filter(function(doc) { | |
return doc.DocumentType == "PHOTO" && doc.CategoryId == categoryId && doc.DocumentVersion == currentDocumentVersion && doc.Status == 1; | |
}) | |
.sortByDescending(function (photoDoc) { return photoDoc.CreatedDateTime.Epoch }) | |
.value({ pageSize: numberOfPhotos }, function (err, documents) { | |
if (err) { | |
throw new Error("Unable to query for photos in category " + categoryId + ", aborting."); | |
} | |
// Append the documents to our total collection | |
existingPhotos = existingPhotos.concat(documents); | |
++catIndex; | |
// Once we have iterated over every category, we can return | |
// them all to the response. | |
if (catIndex >= catCount) { | |
__.response.setBody(existingPhotos); | |
} | |
}); | |
if (!result.isAccepted) { | |
throw new Error("Sproc is too close to violating resource limit, aborting."); | |
} | |
} | |
} |
When you deploy the service, most of the magic to provision our DocumentDB happens in DocumentDBRepository.cs.
We deploy these .js files automatically when you deploy the Azure App Service in the SnapGold code sample.
First we create the database:
/// <summary> | |
/// Checks if the defined document database and collection exists | |
/// and initializes them if they don't. | |
/// </summary> | |
public async Task InitializeDatabaseIfNotExisting(string serverPath) | |
{ | |
var database = _documentClient.CreateDatabaseQuery() | |
.Where(db => db.Id == _documentDataBaseId) | |
.AsEnumerable() | |
.FirstOrDefault(); | |
if (database == null) | |
{ | |
database = await CreateDocumentDbDatabase(); | |
} | |
var documentCollection = _documentClient.CreateDocumentCollectionQuery(database.SelfLink) | |
.Where(c => c.Id == _documentCollectionId) | |
.AsEnumerable() | |
.FirstOrDefault(); | |
if (documentCollection == null) | |
{ | |
await CreateDocumentDbCollection(database); | |
} | |
await InitializeStoredProceduresIfNotExisting(serverPath); | |
} |
Then we deploy the sprocs:
// Stored Procedures | |
private const string TransferGoldStoredProcedureScriptFileName = @"Models\DocumentDB\js\transferGoldStoredProcedure.js"; | |
private const string TransferGoldStoredProcedureId = "transferGold"; | |
private const string GetRecentPhotosForCategoriesStoredProcedureScriptFileName = @"Models\DocumentDB\js\getRecentPhotosForCategoriesStoredProcedure.js"; | |
private const string GetRecentPhotosForCategoriesStoredProcedureId = "getRecentPhotosForCategories"; | |
private async Task InitializeStoredProceduresInDocumentDb() | |
{ | |
// Initialize GoldTransaction Sproc | |
var sproc = new StoredProcedure | |
{ | |
Id = TransferGoldStoredProcedureId, | |
Body = File.ReadAllText(TransferGoldStoredProcedureScriptFileName) | |
}; | |
await _documentClient.UpsertStoredProcedureAsync(DocumentCollectionUri, sproc); | |
// Initialize GetCategoriesPreview Sproc | |
sproc = new StoredProcedure | |
{ | |
Id = GetRecentPhotosForCategoriesStoredProcedureId, | |
Body = File.ReadAllText(GetRecentPhotosForCategoriesStoredProcedureScriptFileName) | |
}; | |
await _documentClient.UpsertStoredProcedureAsync(DocumentCollectionUri, sproc); | |
} |
DocumentDB is much easier to self-provision in your code than a SQL Server database. This code looks for the existence of the specified DocumentDB, and if missing, sets itself up.
How easily can I port this to my own app?
Perhaps the best way to show off this code sample is to fork this sample into another photo-sharing app. In a few steps below, we’ll create SportsPics, an app for sharing sports pictures. Click here to download the free SportsPics app from the Store to get an idea of the look/feel of our code sample.
Here’s what you’ll need to get your own app started.
- Get code from GitHub.
- Open PhotoSharingApp.sln and hit Ctrl+F5 to run the UWP app. It should deploy the UWP locally and launch SnapGold using the mock service mentioned above. You’ll see a red “dummy” box at the top indicating it’s not hitting a real service. This should give you a feel for how the app functions. The next step is getting the app pointed to your own, real service.
- Create a free (for 1 month) Azure subscription to host the service, DocumentDB, and Blob Storage.
- Follow the Getting Started guide to get your own App Service hosted in Azure.
- At this point, you should have the SnapGold code sample running on your own Azure hosted App service. Now let’s tweak the app a bit.
- Change the default colors from Gold to whatever you want. We chose Blues for SportsPics:
<Color x:Key="AppAccentColor">#3498db</Color> | |
<SolidColorBrush x:Key="AppAccentColorBrush" Color="{StaticResource AppAccentColor}" /> | |
<Color x:Key="AppAccentLightColor">#2980b9</Color> | |
<SolidColorBrush x:Key="AppAccentLightColorBrush" Color="{StaticResource AppAccentLightColor}" /> | |
<Color x:Key="AppAccentForegroundColor">#333333</Color> | |
<SolidColorBrush x:Key="AppAccentForegroundColorBrush" Color="{StaticResource AppAccentForegroundColor}" /> | |
<Color x:Key="AppAccentBackgroundColor">#FFE6E6E6</Color> | |
<SolidColorBrush x:Key="AppAccentBackgroundColorBrush" Color="{StaticResource AppAccentBackgroundColor}" /> | |
<Color x:Key="BusyIndicatorBackgroundColor">#7F000000</Color> | |
<SolidColorBrush x:Key="BusyIndicatorBackgroundColorBrush" Color="{StaticResource BusyIndicatorBackgroundColor}" /> | |
<x:Double x:Key="HighResolutionImageSideLength">1000</x:Double> |
- Add a logo. I created one in PhotoShop and simply added it to the header control in the PageHeader.xaml
<UserControl | |
x:Class="PhotoSharingApp.Universal.Controls.PageHeader" | |
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | |
xmlns:local="using:PhotoSharingApp.Universal.Controls" | |
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" | |
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" | |
xmlns:valueConverters="using:PhotoSharingApp.Universal.ValueConverters" | |
VerticalAlignment="Top" | |
VerticalContentAlignment="Top" | |
HorizontalAlignment="Stretch" | |
HorizontalContentAlignment="Stretch" | |
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}" | |
Height="88" | |
x:Name="controlRoot" | |
mc:Ignorable="d" | |
d:DesignHeight="300" | |
d:DesignWidth="400"> | |
<UserControl.Resources> | |
<valueConverters:NullToVisibilityConverter x:Key="NullToVisibilityConverter" /> | |
<valueConverters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" /> | |
</UserControl.Resources> | |
<Grid x:Name="grid" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> | |
<Grid x:Name="titleBar"> | |
<Grid HorizontalAlignment="Center" VerticalAlignment="Center"> | |
<Grid.ColumnDefinitions> | |
<ColumnDefinition Width="Auto" /> | |
<ColumnDefinition /> | |
<ColumnDefinition Width="Auto" /> | |
</Grid.ColumnDefinitions> | |
<Image Source="ms-appx:///Assets/SportsPicsLogo.png" Margin="0,5" /> | |
<local:MyControl Margin="0,0,0,0" Visibility="Collapsed" /> | |
<ContentPresenter x:Name="content" Grid.Column="1" | |
VerticalAlignment="Center" | |
HorizontalAlignment="{Binding HorizontalContentAlignment, ElementName=controlRoot}" | |
Margin="0,0,16,0" | |
Content="{Binding HeaderContent, ElementName=controlRoot}" /> | |
</Grid> | |
... | |
</UserControl> | |
- Update the app name in package.appxmanifest.
- Ship it! You just created your own photo-sharing app.
Now go forth and clone
By now, you hopefully have a locally hosted UWP running against your own Azure App Service and are ready to start turning this thing into the next Instagram. You also (hopefully) now better understand how easy it is to wire up a rich UWP to an Azure App Service. If you have questions about the code sample (https://github.com/Microsoft/Appsample-Photosharing), submit comments below. The team is here to help.
Happy Windows Store coding!
Written by Eric Langland, Senior Software Engineering Lead for Universal Store