In this post I'm going to write about building a simple Windows 8 app for administration of a Windows Azure mobile service.
This app will allow us to view all of our mobile service tables with added options to delete and edit rows.
Disclaimer: The code in my post will focus more on how to use the Windows Azure mobile services SDK to create administration apps and less on Windows 8 client code.
The first thing we want to do in an admin app is have full access to your tables, the way to do that is by using your mobile service master key as mentioned in a previous post.
using Microsoft.WindowsAzure.MobileServices; | |
using System; | |
using Windows.Foundation; | |
namespace MobileServices | |
{ | |
public class AdminServiceFilter : IServiceFilter | |
{ | |
private bool disableScripts; | |
private string masterKey; | |
public AdminServiceFilter(bool disableScripts, string masterKey) | |
{ | |
this.disableScripts = disableScripts; | |
this.masterKey = masterKey; | |
} | |
public IAsyncOperation<IServiceFilterResponse> Handle(IServiceFilterRequest request, IServiceFilterContinuation continuation) | |
{ | |
// Add master key to the request's header to have admin level control | |
request.Headers["X-ZUMO-MASTER"] = this.masterKey; | |
if (this.disableScripts) | |
{ | |
// Get the request's query and append noScript=true as the first query parameter | |
var uriBuilder = new UriBuilder(request.Uri); | |
var oldQuery = (uriBuilder.Query ?? string.Empty).Trim('?'); | |
uriBuilder.Query = ("noScript=true&" + oldQuery).Trim('&'); | |
request.Uri = uriBuilder.Uri; | |
} | |
return continuation.Handle(request); | |
} | |
} | |
} |
We wouldn't want to store the master key in our app's code so we actually use it as a pin for our app to let only users that know the master key actually be able to use this app.
So we're creating a master key input page that will show first.
<Page | |
x:Class="MobileServices.MasterKeyInputPage" | |
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | |
xmlns:local="using:MobileServices" | |
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" | |
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" | |
mc:Ignorable="d"> | |
<Grid Background="White"> | |
<Grid Margin="50,50,10,10"> | |
<Grid.RowDefinitions> | |
<RowDefinition Height="Auto" /> | |
<RowDefinition Height="*" /> | |
</Grid.RowDefinitions> | |
<Grid Grid.Row="0" Grid.ColumnSpan="2" Margin="0,0,0,20"> | |
<StackPanel> | |
<ProgressBar Name="BusyProgressBar" Visibility="Collapsed" IsIndeterminate="True" /> | |
<TextBlock Foreground="#0094ff" FontFamily="Segoe UI Light" Margin="0,0,0,6">WINDOWS AZURE MOBILE SERVICES</TextBlock> | |
<TextBlock Foreground="Gray" FontFamily="Segoe UI Light" FontSize="45" >Tables Viewer</TextBlock> | |
</StackPanel> | |
</Grid> | |
<StackPanel Grid.Row="1" Background="{StaticResource ApplicationPageBackgroundThemeBrush}" HorizontalAlignment="Center" VerticalAlignment="Center"> | |
<TextBlock Text="Enter your master key" FontSize="28" HorizontalAlignment="Center" /> | |
<TextBox Name="MasterKeyTextBox" FontSize="24" Margin="0,30,0,30" Width="500" HorizontalAlignment="Center" /> | |
<Button Click="ButtonClick" HorizontalAlignment="Center" FontSize="28">Start</Button> | |
</StackPanel> | |
</Grid> | |
</Grid> | |
</Page> |
using Windows.UI.Xaml; | |
using Windows.UI.Xaml.Controls; | |
namespace MobileServices | |
{ | |
/// <summary> | |
/// An empty page that can be used on its own or navigated to within a Frame. | |
/// </summary> | |
public sealed partial class MasterKeyInputPage : Page | |
{ | |
public MasterKeyInputPage() | |
{ | |
this.InitializeComponent(); | |
} | |
private void ButtonClick(object sender, RoutedEventArgs e) | |
{ | |
this.Frame.Navigate(typeof(MainPage), MasterKeyTextBox.Text); | |
} | |
} | |
} |
Next we want a generic way to get rows from whatever table is in our mobile service, for that we'll use the un-typed MobileServiceClient.GetTable() to get a mobile service table (IMobileServiceTable) with un-typed operations like: ReadAsync which gets as an input the odata query (empty string for all rows) and returns the rows as Json (IJsonValue).
public async Task<IJsonValue> GetTableItems(string tableName, string masterKey, bool disableServerSideScripts) | |
{ | |
// Get mobile service client with the admin filter. | |
MobileServiceClient adminMobileService = App.MobileService.WithFilter(new AdminServiceFilter(disableServerSideScripts, masterKey)); | |
// Get the selected mobile service table. | |
IMobileServiceTable mobileServiceTable = this.adminMobileService.GetTable(tableName); | |
// Read the first 10 items in this table, | |
// With disableServerSideScripts=true it's the actual first 10 rows otherwise it depends on the server side script. | |
return await mobileServiceTable.ReadAsync("$top=10"); | |
} |
Except for read there are un-typed versions for update, delete and insert, those are getting a JsonObject as an input, in our sample we use delete and update, for delete we pass the JsonObject we want to delete from the Json array of rows we read, for update we pass that JsonObject with altered values by what we want to update.
Once we know all that what is left for us to do in our admin app is to create the client code that uses this, so in order to bind the Json results to our generic table (two way bind to allow editing) we need some wrapper class over the results.
using System; | |
using System.Collections.Generic; | |
using System.ComponentModel; | |
using System.Linq; | |
using Windows.Data.Json; | |
using Windows.UI; | |
using Windows.UI.Xaml.Media; | |
namespace MobileServices | |
{ | |
public class MobileServiceTableItem | |
{ | |
public MobileServiceTableItem(JsonObject jsonObject) | |
{ | |
this.JsonObject = jsonObject; | |
} | |
public JsonObject JsonObject { get; private set; } | |
public IEnumerable<Item> Values | |
{ | |
get | |
{ | |
if (this.JsonObject != null) | |
{ | |
return this.JsonObject.Select(kv => new Item(kv.Key, JsonObject)); | |
} | |
return null; | |
} | |
} | |
} | |
public class Item : INotifyPropertyChanged | |
{ | |
public Item(string key, JsonObject jsonObject) | |
{ | |
Key = key; | |
JsonObject = jsonObject; | |
ValidationError = new SolidColorBrush(); | |
} | |
public event PropertyChangedEventHandler PropertyChanged; | |
public JsonObject JsonObject { get; private set; } | |
public string Key { get; private set; } | |
private Brush validationError; | |
public Brush ValidationError | |
{ | |
get | |
{ | |
return validationError; | |
} | |
set | |
{ | |
validationError = value; | |
OnPropertyChanged("ValidationError"); | |
} | |
} | |
public string Value | |
{ | |
get | |
{ | |
if (JsonObject[Key].ValueType == JsonValueType.String) | |
{ | |
return JsonObject[Key].GetString(); | |
} | |
return JsonObject[Key].Stringify(); | |
} | |
set | |
{ | |
try | |
{ | |
switch (JsonObject[Key].ValueType) | |
{ | |
case JsonValueType.Boolean: | |
JsonObject[Key] = JsonValue.CreateBooleanValue(bool.Parse(value)); | |
break; | |
case JsonValueType.Number: | |
JsonObject[Key] = JsonValue.CreateNumberValue(double.Parse(value)); | |
break; | |
case JsonValueType.String: | |
JsonObject[Key] = JsonValue.CreateStringValue(value); | |
break; | |
case JsonValueType.Null: | |
case JsonValueType.Object: | |
case JsonValueType.Array: | |
default: | |
throw new NotSupportedException("The value type is not supported: " + JsonObject[Key].ValueType); | |
} | |
ValidationError = new SolidColorBrush(); | |
} | |
catch (FormatException) | |
{ | |
ValidationError = new SolidColorBrush(Colors.Red); | |
} | |
OnPropertyChanged("Value"); | |
} | |
} | |
protected void OnPropertyChanged(string name) | |
{ | |
PropertyChangedEventHandler handler = PropertyChanged; | |
if (handler != null) | |
{ | |
handler(this, new PropertyChangedEventArgs(name)); | |
} | |
} | |
} | |
} |
In our main page we'll create a dynamic table that can bind to our MobileServiceTableItems and add delete, update and refresh app bar buttons.
<Page | |
x:Class="MobileServices.MainPage" | |
IsTabStop="false" | |
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" | |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" | |
xmlns:local="using:MobileServices" | |
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" | |
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" | |
mc:Ignorable="d"> | |
<Grid Background="White"> | |
<Grid Margin="50,50,10,10"> | |
<Grid.RowDefinitions> | |
<RowDefinition Height="Auto" /> | |
<RowDefinition Height="*" /> | |
</Grid.RowDefinitions> | |
<Grid Grid.Row="0" Grid.ColumnSpan="2" Margin="0,0,0,20"> | |
<StackPanel> | |
<ProgressBar Name="BusyProgressBar" Visibility="Collapsed" IsIndeterminate="True" /> | |
<TextBlock Foreground="#0094ff" FontFamily="Segoe UI Light" Margin="0,0,0,6">WINDOWS AZURE MOBILE SERVICES</TextBlock> | |
<TextBlock Foreground="Gray" FontFamily="Segoe UI Light" FontSize="45" >Tables Viewer</TextBlock> | |
<ComboBox Name="TableSelector" ItemsSource="{Binding Newa}" SelectionChanged="TableSelector_SelectionChanged"/> | |
</StackPanel> | |
</Grid> | |
<ScrollViewer Grid.Row="1"> | |
<Grid> | |
<GridView Name="ListItems" Margin="62,10,0,0" Grid.Row="1" ItemsSource="{Binding}" SelectionMode="Single"> | |
<GridView.Header> | |
<GridView Name="ItemsHeader"> | |
<GridView.ItemsPanel> | |
<ItemsPanelTemplate> | |
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Top"/> | |
</ItemsPanelTemplate> | |
</GridView.ItemsPanel> | |
<GridView.ItemTemplate> | |
<DataTemplate> | |
<TextBlock Text="{Binding}" HorizontalAlignment="Left" VerticalAlignment="Top" Width="200" FontWeight="Bold"/> | |
</DataTemplate> | |
</GridView.ItemTemplate> | |
</GridView> | |
</GridView.Header> | |
<GridView.ItemsPanel> | |
<ItemsPanelTemplate> | |
<StackPanel Orientation="Vertical" HorizontalAlignment="Left" VerticalAlignment="Top"/> | |
</ItemsPanelTemplate> | |
</GridView.ItemsPanel> | |
<GridView.ItemTemplate> | |
<DataTemplate> | |
<GridView Name="InnerGridView" ItemsSource="{Binding Values}" SelectionMode="None"> | |
<GridView.ItemsPanel> | |
<ItemsPanelTemplate> | |
<StackPanel Orientation="Horizontal" HorizontalAlignment="Left" VerticalAlignment="Top"/> | |
</ItemsPanelTemplate> | |
</GridView.ItemsPanel> | |
<GridView.ItemTemplate> | |
<DataTemplate> | |
<TextBox Text="{Binding Value, Mode=TwoWay}" Background="{Binding ValidationError}" HorizontalAlignment="Left" VerticalAlignment="Top" Width="200" IsReadOnly="False"/> | |
</DataTemplate> | |
</GridView.ItemTemplate> | |
</GridView> | |
</DataTemplate> | |
</GridView.ItemTemplate> | |
</GridView> | |
</Grid> | |
</ScrollViewer> | |
</Grid> | |
</Grid> | |
<Page.BottomAppBar> | |
<AppBar x:Name="BottomAppBar" Padding="10,0,10,0"> | |
<StackPanel Orientation="Horizontal"> | |
<Button x:Name="BottomAppBarDelete" Tag="Delete" Style="{StaticResource DeleteAppBarButtonStyle}" HorizontalAlignment="Left" Click="DeleteClick" /> | |
<Button x:Name="BottomAppBarUpdate" Tag="Update" Style="{StaticResource SaveAppBarButtonStyle}" HorizontalAlignment="Left" Click="UpdateClick" /> | |
<Button x:Name="BottomAppBarRefresh" Tag="Refresh" Style="{StaticResource RefreshAppBarButtonStyle}" HorizontalAlignment="Left" Click="RefreshClick" /> | |
</StackPanel> | |
</AppBar> | |
</Page.BottomAppBar> | |
</Page> |
And add all other code-behind implementations.
using Microsoft.WindowsAzure.MobileServices; | |
using System.Linq; | |
using System.Threading.Tasks; | |
using Windows.Data.Json; | |
using Windows.UI.Xaml; | |
using Windows.UI.Xaml.Controls; | |
using Windows.UI.Xaml.Navigation; | |
namespace MobileServices | |
{ | |
public sealed partial class MainPage : Page | |
{ | |
private MobileServiceClient adminMobileService; | |
public MainPage() | |
{ | |
this.InitializeComponent(); | |
this.TableSelector.ItemsSource = Constants.TableNames; | |
} | |
private async Task RefreshTableView() | |
{ | |
var mobileServiceTable = GetSelectedTable(); | |
if (mobileServiceTable != null) | |
{ | |
BusyProgressBar.Visibility = Windows.UI.Xaml.Visibility.Visible; | |
// Get all items as json array | |
var tableItems = await mobileServiceTable.ReadAsync("$top=10"); | |
var gridViewItems = tableItems.GetArray().Select(jsonValue => new MobileServiceTableItem(jsonValue.GetObject())); | |
var gridViewItemsHeaders = tableItems.GetArray().First().GetObject().Select(o => o.Key); | |
ListItems.ItemsSource = gridViewItems; | |
ItemsHeader.ItemsSource = gridViewItemsHeaders; | |
BusyProgressBar.Visibility = Windows.UI.Xaml.Visibility.Collapsed; | |
} | |
} | |
private IMobileServiceTable GetSelectedTable() | |
{ | |
var selectedTableName = TableSelector.SelectedValue as string; | |
if (selectedTableName != null) | |
{ | |
return this.adminMobileService.GetTable(selectedTableName); | |
} | |
else | |
{ | |
return null; | |
} | |
} | |
protected override void OnNavigatedTo(NavigationEventArgs e) | |
{ | |
base.OnNavigatedTo(e); | |
string masterKey = e.Parameter as string; | |
adminMobileService = App.MobileService.WithFilter(new AdminServiceFilter(true, masterKey)); | |
this.TableSelector.SelectedIndex = 0; | |
} | |
private async Task ButtonRefresh_Click(object sender, RoutedEventArgs e) | |
{ | |
await this.RefreshTableView(); | |
} | |
private async void TableSelector_SelectionChanged(object sender, SelectionChangedEventArgs e) | |
{ | |
await this.RefreshTableView(); | |
} | |
private async void RefreshClick(object sender, RoutedEventArgs e) | |
{ | |
await this.RefreshTableView(); | |
} | |
private async void DeleteClick(object sender, RoutedEventArgs e) | |
{ | |
var selectedTable = GetSelectedTable(); | |
if (selectedTable != null) | |
{ | |
var selectedTableItem = ListItems.SelectedItem as MobileServiceTableItem; | |
if (selectedTableItem != null) | |
{ | |
BusyProgressBar.Visibility = Windows.UI.Xaml.Visibility.Visible; | |
await selectedTable.DeleteAsync(selectedTableItem.JsonObject); | |
await this.RefreshTableView(); | |
BusyProgressBar.Visibility = Windows.UI.Xaml.Visibility.Collapsed; | |
} | |
} | |
} | |
private async void UpdateClick(object sender, RoutedEventArgs e) | |
{ | |
var selectedTable = GetSelectedTable(); | |
if (selectedTable != null) | |
{ | |
var selectedTableItem = ListItems.SelectedItem as MobileServiceTableItem; | |
if (selectedTableItem != null) | |
{ | |
BusyProgressBar.Visibility = Windows.UI.Xaml.Visibility.Visible; | |
await selectedTable.UpdateAsync(selectedTableItem.JsonObject); | |
await this.RefreshTableView(); | |
BusyProgressBar.Visibility = Windows.UI.Xaml.Visibility.Collapsed; | |
} | |
} | |
} | |
} | |
} |
You can find the entire project in this link.