Introdução
No ano passado, durante o PDC09, Ray Ozzie, Chief Software Architect da Microsoft, apresentou ao público a estratégia da Microsoft em relação a construção de Software de hoje em diante. Essa estratégia consiste do “lema” “3 Screens and the Cloud”, ou seja, aplicações que são acessíveis e integram-se através de tres dispositivos (TV, PC e Celular). E essas aplicações terão interfaces ricas, e com a centralização dos dados na nuvem, assim provendo uma grande User-Experience.
E para nós desenvolvedores que temos que começar a pensar em como construir essas aplicações, devemos começar a aprender como integrar nossas Rich Internet Applications com serviços hospedados no Azure Services Plataform, e assim centralizar nossos dados e torná-los acessíveis de qualquer lugar, através da internet.
E é sobre essa integração entre o Azure e Silverlight que este post irá tratar. Irei explicar como utilizar o Table Storage do Azure e disponibiliar esses dados através de serviços RESTful utilizando WCF Data Services e acessá-los em uma aplicação Silverlight.
Pré-Requisitos
O código-fonte deste tutorial pode ser baixado aqui:
http://cid-1498c467c14dc20b.skydrive.live.com/self.aspx/BrSilverlight/Tutoriais/CloudGameStorage.Silverlight.zip
Definindo os serviços e acessando o Table Storage
Para começar crie um novo projeto Silverlight no Visual Studio. Agora, crie um projeto do tipo “WCF Service Application”.
Voce pode deletar o serviço criado automaticamente pelo template (IService1.cs e IService1.svc). Agora, voce irá adicionar duas classes a esse projeto, uma chamada Game e outra chamada GameDataContext.
Game.cs
- public class Game : TableServiceEntity
- {
- public String Name { get; set; }
- public String Website { get; set; }
- public Int32 NumberOfPlayers { get; set; }
- public Double Rating { get; set; }
- public String Category { get; set; }
- }
Snippet 1
Essa classe define o tipo de dados que será salvo no Table Storage do Azure. Note que a classe herda de TableServiceEntity. Assim, o runtime do Azure saberá qual o nome da Table a ser criada e quais os tipos dos dados a ser guardados nessa Table.
Quando herdamos da classe TableServiceEntity, herdamos tres propriedade muito importantes:
PartitionKey: utilizado pelo Load Balancer do Azure para particionar e segmentar as tables no Storage. Assim, cada item das Tables com a mesma PartitionKey será salva no mesmo local e de maneira sequencial, garantindo assim um acesso mais eficiente aos dados.
RowKey: a RowKey é utilizada para identificar uma entidade específica no Table Storage.
Timestamp. essa propriedade é gerenciada internamente pelo Azure e não deve ser modificada. Ela é um valor DateTime que guarda a última modificação a ser feita na entidade.
Obs: o desenvolvedor deverá setar os valores das propriedade PartitionKey e RowKey, e esses valores deverão ser uma String de até no máximo 1K de tamanho.
Note que apesar do nome ser Tables, e cada Table possuir linhas (cada objeto do tipo Game salvo seria uma linha na Table) e colunas (cada propriedade da nossa classe), isso não possui nenhuma ligação com tabelas de banco de dados. Não é possível fazer Joins entre Tables nem ter Primary Keys ou Foreign Keys. Se voce deseja um banco de dados na nuvem, utilize o SQL Azure.
Na classe GameDataContext, adicione o código abaixo:
GameDataContext.cs
- public class GameDataContext : TableServiceContext, IUpdatable
- {
- private static CloudStorageAccount account = CloudStorageAccount.DevelopmentStorageAccount;
-
- public GameDataContext(String baseAddress, StorageCredentials credentials)
- : base(baseAddress, credentials)
- {
- CloudTableClient.CreateTablesFromModel(typeof(GameDataContext),
- baseAddress,
- credentials);
- }
-
- public IQueryable<Game> Games
- {
- get { return this.CreateQuery<Game>("Games"); }
- }
-
- #region IUpdatable Members
- //thanks to Aleksey Savateyev
- //from his blob post at http://blogs.msdn.com/ales/archive/2009/12/17/update-porting-silverlight-ria-to-windows-azure-data-access.aspx
-
- public void AddReferenceToCollection(object targetResource, string propertyName, object resourceToBeAdded)
- {
- AddLink(targetResource, propertyName, resourceToBeAdded);
- }
-
- public void ClearChanges()
- {
- // clear out links
- foreach (LinkDescriptor link in Links)
- DetachLink(link.Source, link.SourceProperty, link.Target);
-
- // clear out entities
- foreach (EntityDescriptor entity in Entities)
- Detach(entity.Entity);
- }
-
- public object CreateResource(string containerName, string fullTypeName)
- {
- object obj = Activator.CreateInstance(GetType().Assembly.GetType(fullTypeName));
-
- base.AddObject(containerName, obj);
-
- return obj;
- }
-
- public void DeleteResource(object targetResource)
- {
- base.DeleteObject(targetResource);
- }
-
- public object GetResource(IQueryable query, string fullTypeName)
- {
- object resource = null;
-
- // fullTypeName can be null for deletes
- if (fullTypeName == null)
- {
- resource = query.Cast<Game>().SingleOrDefault();
- }
- else
- {
- // Check for types that will be updated or deleted
- if (fullTypeName == typeof(Game).FullName)
- {
- resource = query.Cast<Game>().SingleOrDefault();
- }
-
- UpdateObject(resource);
- }
-
- return resource;
- }
-
- public object GetValue(object targetResource, string propertyName)
- {
- Type type = targetResource.GetType();
-
- PropertyInfo propInfo = type.GetProperty(propertyName);
-
- if (propInfo == null)
- {
- throw new Exception(string.Format("Can't find given property '{0}'.", propertyName ?? "NULL"));
- }
-
- object val = propInfo.GetValue(targetResource, null);
-
- return val;
- }
-
- public void RemoveReferenceFromCollection(object targetResource, string propertyName, object resourceToBeRemoved)
- {
- DeleteLink(targetResource, propertyName, resourceToBeRemoved);
- }
-
- public object ResetResource(object resource)
- {
- Detach(resource);
-
- return resource;
- }
-
- public object ResolveResource(object resource)
- {
- // Already gave object in CreateResource
- return resource;
- }
-
- public new void SaveChanges()
- {
- try
- {
- base.SaveChanges();
- }
- catch (KeyNotFoundException e)
- {
- var exception = e;
- }
- }
-
- public void SetReference(object targetResource, string propertyName, object propertyValue)
- {
- SetLink(targetResource, propertyName, propertyValue);
- }
-
- public void SetValue(object targetResource, string propertyName, object propertyValue)
- {
- PropertyInfo propInfo = targetResource.GetType().GetProperty(propertyName);
-
- if (propInfo == null)
- {
- throw new Exception(string.Format("Can't find property '{0}'", propertyName ?? String.Empty));
- }
-
- propInfo.SetValue(targetResource, propertyValue, null);
- }
- #endregion
- }
Snippet 2 Esta classe será responsável por fazer toda a comunicação com os serviços REST do Azure Table Storage. Por isso, herdamos nossa classe de TableServiceContext, que por sua vez, herda de DataServiceContext. O que isso quer dizer é que estamos utilizando o WCF Data Services para fazer as requisições HTTP com os serviços do Azure Table Storage.
Mas somente herdar de TableServiceContext não resolve todos os problemas. Precisamos implementar a Interface IUpdatable. Assim poderemos executar operações além de somente leitura, como inserts, deletes e updates nos nossos dados.
Agora precisamos colocar os dados de forma acessível aos clientes. Para isso, iremos criar um novo WCF Data Service.
Adicione o seguinte código ao serviço:
GameDataService.svc.cs
- public class GameDataService : DataService< GameDataContext >
- {
- // This method is called only once to initialize service-wide policies.
- public static void InitializeService(DataServiceConfiguration config)
- {
- config.SetEntitySetAccessRule("*", EntitySetRights.All);
- config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V2;
- }
-
- protected override GameDataContext CreateDataSource()
- {
- var account = CloudStorageAccount.DevelopmentStorageAccount;
-
- var address = account.TableEndpoint.AbsoluteUri;
- var credentials = account.Credentials;
-
- GameDataContext context = new GameDataContext(address, credentials);
- return context;
- }
- }
Snippet 3
Note que aqui nós criamos um serviço que irá expor os dados obtidos com o nosso DataContext criado anteriormente que acessa o TableStorage do Azure.
Importante: o método CreateDataSource é chamado quando o serviço é iniciado. Nesse método passamos as credenciais necessárias para acessar-se o TableStorage. No nosso caso, estamos utilizando as credenciais padrões usadas no ambiente de desenvolvimento provido pelo SDK do Azure. Em ambiente de produção, será necessário passar as suas credenciais para acessar os serviços do Azure.
Acessando os dados no Silverlight
Agora iremos criar nossa camada de apresentação em Silverlight. Para isso crie um novo projeto Silverlight no Visual Studio.
Agora, precisamos adicionar a referência ao nosso serviço ao projeto Silverlight. Para isso, clique com o botão direito no projeto e vá em “Add Service Reference…”.
Na janela que abrir, clique em “Discovery” e o Visual Studio irá achar nosso serviço. Dê um nome a referencia do serviço e clique em OK.
Nosso xaml será composto do controle DataForm (caso voce não conheça ou não sabe como usá-lo, veja meu tutorial explicando como ele funciona, que irá mostrar e coletar os dados do usuário, e um botão que irá salvar os dados no Table Storage. Para isso, adicione o seguinte código ao MainPage.xaml:
MainPage.xaml
- <UserControl x:Class="CloudGameStorage.Silverlight.MainPage"
- xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
- xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
- xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
- xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
- xmlns:df="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data.DataForm.Toolkit"
- xmlns:in="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Input.Toolkit"
- mc:Ignorable="d"
- Loaded="UserControl_Loaded"
- d:DesignHeight="400" d:DesignWidth="572">
-
- <Grid x:Name="LayoutRoot" Background="White">
- <df:DataForm x:Name="gameForm" AutoEdit="False" AutoGenerateFields="False" EditEnding="gameForm_EditEnding" DeletingItem="gameForm_DeletingItem">
- <df:DataForm.EditTemplate>
- <DataTemplate>
- <StackPanel df:DataField.IsFieldGroup="True">
- <df:DataField Label="Name:" Description="Name of the game">
- <TextBox Text="{Binding Path=Name, Mode=TwoWay}" />
- </df:DataField>
- <df:DataField Label="Website:" Description="Website of the game">
- <TextBox Text="{Binding Path=Website, Mode=TwoWay}" />
- </df:DataField>
- <df:DataField Label="Number of Players" Description="Number of players supported by the game" >
- <in:NumericUpDown Minimum="1" Value="{Binding Path=NumberOfPlayers, Mode=TwoWay}" Width="70" HorizontalAlignment="Left" />
- </df:DataField>
- <df:DataField Label="Rating" Description="How good is this game?">
- <in:Rating ItemCount="5" Value="{Binding Path=Rating, Mode=TwoWay}" />
- </df:DataField>
- <df:DataField Label="Category" Description="Game category (Shooter, Sports, Action, ...)">
- <TextBox Text="{Binding Path=Category, Mode=TwoWay}" />
- </df:DataField>
- <!--<df:DataField Label="Image" Description="Game box cover">
- <Image Cursor="Hand" mouselef />
- </df:DataField>-->
- </StackPanel>
- </DataTemplate>
- </df:DataForm.EditTemplate>
- </df:DataForm>
- <Button Width="50" Height="20" HorizontalAlignment="Left" VerticalAlignment="Top" Content="Save" Click="Button_Click" />
- </Grid>
- </UserControl>
Snippet 4
Aqui criamos um DataForm fazendo DataBinding com as propriedades do nosso modelo de dados. Note que tratamos os eventos EditEnding e DeletingItem. Esses eventos irão adicionar novos items e atualizar seus dados e deletá-los da fonte de dados. O botão será responsável por sincronizar as mudanças com o serviço que acessa o TableStorage.
Agora adicione o seguinte código ao code-behind da MainPage:
MainPage.xaml.cs
- public partial class MainPage : UserControl
- {
- private GameDataContext dataContext;
- private Uri serviceUri;
-
- public MainPage()
- {
- InitializeComponent();
-
- //creates the data service context
- this.serviceUri = new Uri("http://localhost:3213/GameDataService.svc");
- this.dataContext = new GameDataContext(this.serviceUri);
- }
-
- /// <summary>
- /// Gets or sets the list of games currently registered
- /// </summary>
- public List<Game> Games { get; set; }
-
- private void UserControl_Loaded(object sender, RoutedEventArgs e)
- {
- //creates a query to get all games saved.
- var query = this.dataContext.CreateQuery<Game>("Games");
-
- //asynchronously executes the query, passing a lambda exp. to get the results.
- query.BeginExecute((result) =>
- {
- var games = query.EndExecute(result);
-
- //gets the list of games returned by the data service and
- //set the ItemsSource of the DataForm to use DataBinding
- this.Games = games.ToList();
- this.gameForm.ItemsSource = this.Games;
- }, null);
- }
-
- private void Button_Click(object sender, RoutedEventArgs e)
- {
- //Saves the changes made to the games (inserts, updates, deletes).
- this.dataContext.BeginSaveChanges((result) =>
- {
- this.dataContext.EndSaveChanges(result);
- }, null);
- }
-
- private void gameForm_DeletingItem(object sender, System.ComponentModel.CancelEventArgs e)
- {
- //deletes a game.
- var game = this.gameForm.CurrentItem as Game;
-
- this.dataContext.DeleteObject(game);
- }
-
- private void gameForm_EditEnding(object sender, DataFormEditEndingEventArgs e)
- {
- if (e.EditAction == DataFormEditAction.Commit)
- {
- //gets the game being edited.
- var currentGame = this.gameForm.CurrentItem as Game;
-
- //if the PartitionKey and RowKey is null, then it's a new game.
- if (currentGame.PartitionKey == null && currentGame.RowKey == null)
- {
- currentGame.PartitionKey = DateTime.Now.ToString("MMddyyy");
-
- // Row key allows sorting, so we make sure the rows come back in time order.
- currentGame.RowKey = String.Format("{0:10}_{1}", DateTime.MaxValue.Ticks - DateTime.Now.Ticks, Guid.NewGuid());
-
- //adds a new game.
- this.dataContext.AddToGames(currentGame);
- }
- else
- //updates the game data.
- this.dataContext.UpdateObject(currentGame);
- }
- }
- }
Snippet 5 No construtor, criamos o nosso DataContext, que será uma espécie de cliente do nosso serviço, e irá fazer toda a comunicação com o serviço, fazer a serialização dos objetos por debaixo dos panos para nós. Isso ajuda bastante, pois senão teríamos que fazer toda a comunicação e a leitura do ATOM retornado pelo serviço (o que pode ser bem trabalhoso!). Note que precisamos passar a URI do serviço no construtor do DataContext. No meu ambiente de desenvolvimento, o serviço estava rodando na porta 3213, no seu caso, ela pode mudar, então preste atenção nisso e faça as alterações necessárias.
No evento Loaded do UserControl, chamamos o nosso serviço e retornamos os games já cadastrados. Para isso, criamos uma query passando o nome da entidade que desejamos, nesse caso “Games”.
Para receber os dados, precisamos fazer a requisição de maneira assíncrona, então chamamos o método BeginExecute do nossa query, e passamos uma lambda expression que será chamada quando a requisição terminar trazendo os resultados. Esses resultados então são guardados na nossa propriedade Games (do tipo List<Game>) e usado como fonte de dados (ItemsSource) do nosso DataForm para fazermos o DataBinding.
Nos eventos DeletingItem e EditEnding do DataForm, criamos, editamos e deletamos um item do TableStorage. Isso é feito chamando-se o método AddToGames, UpdateObejct e DeleteObject respectivamente. Note que ao chamar esses métodos, a requisição salvar as alterações não é realmente feita. Os dados só são sincronizados quando chamamos o método SaveChanges no evento do click do botão.
Importante: no evento EditEnding definimos as duas propriedades necessárias para todos os items salvos no TableStorage do Azure. Então checamos se esses valores já foram criados para o item que foi editado. Caso esses valores sejam null, setamos a PartitionKey para a data de hoje (assim os objetos criados em um determinado dia serão armazenados sequencialmente e no mesmo lugar no storage do Azure) e a RowKey para um valor que possa ser ordenado pela hora em que eles foram criados, adicionando-se uma GUID que irá identificá-los unicamente.
Conclusão
Neste tutorial, vimos como expor e acessar dados salvos no Table Storage do Windows Azure em uma aplicação Silverlight. Vimos que não é uma tarefa tão complicada e que pode agregar grande valor as aplicações. Principalmente agora, que podemos criar aplicações Silverlight que rodam tanto no PC e Windows Phone. Ou seja, podemos ter nossa aplicação rodando em dois dispositivos acessando os mesmos dados, que estão salvos na núvem, assim acessíveis de qualquer lugar.