Introdução
Neste post iremos consolidar o que foi ensinado nos aplicativos anteriores e para isso construiremos um aplicativo guia. Esse aplicativo deve possuir os locais e eventos que ocorrem na cidade de Ouro Preto. Como o objetivo deste aplicativo é consolidar o que foi ensinado nos tutoriais anteriores, o mesmo deverá incorporar o padrão de design panorama, comunicação com web-services e armazenamento de dados em banco de dados local, exibição de locais no mapa e notificação baseada em localização. Como o aplicativo utiliza Geofences para fazer a notificação baseada em locais o aplicativo deve ser desenvolvido para a versão 8.1 do Windows Phone.
Definindo os dados
O nosso aplicativo deve possuir os atributos importantes pra descrever os locais e eventos. Iremos aproveitar a criação das classes que representam esses tipos e já utilizar as anotações que representam as tabelas que serão usadas pelo banco de dados local da aplicação.
Descrição dos dados
Primeiro vamos descrever o local. Um local terá um nome, uma imagem associada, uma descrição e suas coordenadas geográficas. Com as anotações relativas ao banco de dados fica:
[sourcecode language=”csharp” wraplines=”false” collapse=”false”]
namespace PanoramaGuide.Models
{
[Table(Name = "Places")]
public class Place
{
[Column(IsPrimaryKey = true, IsDbGenerated = true)]
public int id { get; set; }
[Column]
public string description { get; set; }
[Column]
public string image_url { get; set; }
[Column]
public string latitude { get; set; }
[Column]
public string longitude { get; set; }
[Column]
public string name { get; set; }
}
}
[/sourcecode]
Além de locais nosso aplicativo também deverão conter eventos. Eventos devem conter um nome, descrição, imagem associada além do identificador do local e horário de inicio do evento. A classe que representa eventos ficará da seguinte forma com as anotações de banco de dados:
[sourcecode language=”csharp” wraplines=”false” collapse=”false”]
namespace PanoramaGuide.Models
{
[Table(Name = "Events")]
class Event
{
[Column(IsPrimaryKey = true, IsDbGenerated = true)]
public int id { get; set; }
[Column]
public string description { get; set; }
[Column]
public string image_url { get; set; }
[Column]
public string name { get; set; }
[Column]
public int place_id { get; set; }
[Column]
public string start { get; set; }
}
}
[/sourcecode]
Como já criamos as classes que representam os nossos dados com as anotações utilizadas pelo banco de dados local o próximo ponto é criar ele e configurar a aplicação pra instanciar ele quando é aberta.
Criando o banco de dados e conexão com Web-Service
Para representar o banco de dados vamos criar uma classe que estenda DataContext e contenha a string com o endereço do banco além das tabelas que o mesmo possui. Nesse caso ele irá conter somente duas tabelas, uma para os locais e outra para os eventos.
[sourcecode language=”csharp” wraplines=”false” collapse=”false”]
namespace PanoramaGuide.DataContexts
{
class AppDataBase : DataContext
{
public AppDataBase(string conectionString) : base(conectionString)
{ }
public static string ConectionString
{
get
{
return "isostore:/panoramaguide.sdf";
}
}
public Table<Place> Places;
public Table<Event> Events;
}
}
[/sourcecode]
Agora que já temos nosso banco de dados definido devemos instanciar ele na aplicação, para isso adicione uma chamada para o método CreateDB() no método construtor do nosso aplicativo na classe App. Agora vamos definir o método CreateDB(), para melhor modularidade do aplicativo iremos criar esse método em uma nova classe DBHelper, que conterá somente métodos auxiliares referentes ao banco de dados.
O método CreateDB() também é responsável pela conexão com o web-service para obtenção dos dados. por isso ele utiliza as strings places_list_uri e events_list_uri para armazenar o endereço que deverá acessar.
[sourcecode language=”csharp” wraplines=”false” collapse=”false”]
public static void CreateDB()
{
using (AppDataBase db = new AppDataBase(AppDataBase.ConectionString))
{
if (!db.DatabaseExists())
{
db.CreateDatabase();
}
// Conexão com web service e preenchimento da tabela de places do BD
WebClient webClientPlaces = new WebClient();
WebClient webClientEvents = new WebClient();
try
{
webClientPlaces.DownloadStringCompleted += webClient_DownloadPlacesStringCompleted;
webClientPlaces.DownloadStringAsync(new Uri(places_list_uri));
webClientEvents.DownloadStringCompleted += webClient_DownloadEventsStringCompleted;
webClientEvents.DownloadStringAsync(new Uri(events_list_uri));
}
catch (Exception)
{
MessageBox.Show("Não foi cossivel acessar o servidor");
}
}
}
[/sourcecode]
Além disso ele chama os métodos webClient_DownloadPlacesStringCompleted e webClient_DownloadEventsStringCompleted quando o download dos JSON’s são completados. Estes métodos são responsáveis por armazenar os dados recebidos no banco de dados.
Tratando os dados recebidos e armazenando localmente
O armazenamento dos dados recebidos através dos arquivos JSON’s é feito através de um parser que pega elemento a elemento dos arquivos recebido e adiciona numa lista para ser armazenado no banco de dados local. Quando acaba os dados ele armazena tudo de uma vez. O armazenamento é feito dessa maneira pra reduzir as inserções no banco e economizar recursos do dispositivo assim. Os métodos abaixo implementa esses parsers para ambos os tipos de dados.
Inserção de locais no banco de dados:
[sourcecode language=”csharp” wraplines=”false” collapse=”false”]
public static void webClient_DownloadPlacesStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
using (AppDataBase db = new AppDataBase(AppDataBase.ConectionString))
{
if (!string.IsNullOrEmpty(e.Result))
{
JArray jsonArray = JArray.Parse(e.Result);
JToken jsonArray_Item = jsonArray.First;
while (jsonArray_Item != null)
{
Place _place = new Place();
_place.name = jsonArray_Item.Value<string>("name");
_place.description = jsonArray_Item.Value<string>("description");
_place.image_url = jsonArray_Item.Value<string>("image_url");
_place.latitude = jsonArray_Item.Value<string>("latitude");
_place.longitude = jsonArray_Item.Value<string>("longitude");
db.Places.InsertOnSubmit(_place);
jsonArray_Item = jsonArray_Item.Next;
}
db.SubmitChanges();
}
}
}
[/sourcecode]
Inserção de eventos no banco de dados:
[sourcecode language=”csharp” wraplines=”false” collapse=”false”]
public static void webClient_DownloadEventsStringCompleted(object sender, DownloadStringCompletedEventArgs e)
{
using (AppDataBase db = new AppDataBase(AppDataBase.ConectionString))
{
if (!string.IsNullOrEmpty(e.Result))
{
JArray jsonArray = JArray.Parse(e.Result);
JToken jsonArray_Item = jsonArray.First;
while (jsonArray_Item != null)
{
Event _event = new Event();
_event.name = jsonArray_Item.Value<string>("name");
_event.description = jsonArray_Item.Value<string>("description");
_event.image_url = jsonArray_Item.Value<string>("image_url");
_event.start = jsonArray_Item.Value<string>("start");
try
{
_event.place_id = jsonArray_Item.Value<int>("place_id");
}
catch (InvalidCastException)
{
_event.place_id = 0;
}
db.Events.InsertOnSubmit(_event);
jsonArray_Item = jsonArray_Item.Next;
}
db.SubmitChanges();
}
}
}
[/sourcecode]
Exibição dos dados
Agora que nossa aplicação já é capaz de obter e armazenar os dados chegou a hora de exibir esses dados aos usuários. Os dados serão exibidos em dois níveis, 2 listas que contém a informação mais superficial e outra que contém os detalhes de um item especifico. Ambos os níveis de exibição serão implementados utilizando o padrão Panorama.
Model-View-ViewModel
O Windows Phone utiliza o padrão Model-View-ViewModel para exibir os dados em listagens. Este padrão possui semelhanças com Model-View-Controller, porém é voltado para o desenvolvimento usando Silverlight. Esse Padrão é composto pela camada Model que contém o modelo e as regras de negócio, a camada View que exibe os dados, e a camada ViewModel que funciona como uma ligação entre as suas anteriores.
A camada ViewModel implementa a interface INotifyPropertyChanged permitindo que quando um dado é modificado no modelo a interface receba uma notificação para poder se atualizar. O trecho abaixo corresponde a um método dessa camada para exemplificar a implementação. As classes completas podem ser vistas na implementação disponibilizada ao final deste tutorial.
[sourcecode language=”csharp” wraplines=”false” collapse=”false”]
public string EventName {
get
{
return _name;
}
set
{
if (_name != value)
{
_name = value;
NotifyPropertyChanged("Name");
}
}
}
[/sourcecode]
Nível superior: Lista de locais e eventos
Neste nível utilizaremos um panorama com dois painéis, um contendo a lista de locais e outro contendo a lista de eventos. Esse panorama possuirá como imagem de fundo uma foto da portaria da UFOP e cada lista exibe os atributos mais importantes para o tipo de item da lista.
[sourcecode language=”xml” wraplines=”false” collapse=”false”]
<Grid x:Name="LayoutRoot" Background="Transparent">
<!–Panorama control–>
<phone:Panorama Title="UGuide UFOP" Foreground="White">
<phone:Panorama.Background>
<ImageBrush ImageSource="/Assets/portaria-ufop-dark.jpg" />
</phone:Panorama.Background>
<!– Panorama Item: Places–>
<phone:PanoramaItem Header="Locais">
<phone:LongListSelector x:Name="PlacesList" ItemsSource="{Binding PlacesVM}" SelectionChanged="PlacesList_SelectionChanged" >
<phone:LongListSelector.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="10,10,10,10">
<Border BorderThickness="1" BorderBrush="White">
<Image Source="{Binding ImageUrl}" Width="100" Height="100" Stretch="UniformToFill"/>
</Border>
<StackPanel Orientation="Vertical" >
<TextBlock Text="{Binding Name}" FontSize="{StaticResource PhoneFontSizeExtraLarge}" Foreground="White"/>
<TextBlock Text="{Binding Description}" Foreground="White"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</phone:LongListSelector.ItemTemplate>
</phone:LongListSelector>
</phone:PanoramaItem>
<!– Panorama Item: Eventos –>
<phone:PanoramaItem Header="Eventos">
<phone:LongListSelector x:Name="EventsList" ItemsSource="{Binding EventsVM}" SelectionChanged="EventsList_SelectionChanged" >
<phone:LongListSelector.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="10,10,10,10">
<Border BorderThickness="1" BorderBrush="White">
<Image Source="{Binding EventImageUrl}" Width="100" Height="100" Stretch="UniformToFill"/>
</Border>
<StackPanel Orientation="Vertical" >
<TextBlock Text="{Binding EventName}" FontSize="{StaticResource PhoneFontSizeExtraLarge}" Foreground="White"/>
<TextBlock Text="{Binding EventStartAt}" Foreground="White"/>
</StackPanel>
</StackPanel>
</DataTemplate>
</phone:LongListSelector.ItemTemplate>
</phone:LongListSelector>
</phone:PanoramaItem>
</phone:Panorama>
</Grid>
[/sourcecode]
Chamando a página de detalhes
Esse aplicativo guia possui uma página de detalhes exclusiva para locais e outra para eventos. A página de detalhes é chamada quando o usuário clica sobre um item da lista. Para isso a função que responde a mudança de seleção passa o Id do item escolhido como parâmetro para o o método Navigate do NevigationService.
[sourcecode language=”csharp” wraplines=”false” collapse=”false”]
private void PlacesList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (PlacesList.SelectedItem == null)
return;
NavigationService.Navigate(new Uri("/DetailPage.xaml?selectedItem=" + ((PlacesList.SelectedItem as PlaceViewModel).Id-1), UriKind.Relative));
PlacesList.SelectedItem = null;
}
private void EventsList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (EventsList.SelectedItem == null)
return;
NavigationService.Navigate(new Uri("/EventDetailPage.xaml?selectedItem=" + ((EventsList.SelectedItem as EventViewModel).EventId-1), UriKind.Relative));
EventsList.SelectedItem = null;
}
[/sourcecode]
Pagina de detalhes de eventos
É a pagina de descrição mais simples.Ela contem um StackPanel onde são empilhados o título, a imagem a descrição e a data e hora de inicio. Ela é descrita pelo trecho de código a seguir:
[sourcecode language=”xml” wraplines=”false” collapse=”false”]
<StackPanel x:Name="LayoutRoot" Orientation="Vertical">
<StackPanel.Background>
<ImageBrush ImageSource="/Assets/escola-minas-ufop-dark.jpg" />
</StackPanel.Background>
<TextBlock x:Name="Name" Text="{Binding EventName}" Foreground="White" FontSize="72" VerticalAlignment="Top" />
<Image x:Name="Image" Source="{Binding EventImageUrl}" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="15,15,15,15"/>
<TextBlock x:Name="Description" Text="{Binding EventDescription}" VerticalAlignment="Top" Padding="10" TextWrapping="Wrap"/>
<TextBlock x:Name="StartAt" Text="{Binding EventStartAt}" VerticalAlignment="Top" Padding="10"/>
</StackPanel>
[/sourcecode]
Além disso o método OnNavigatedTo() deve definir o DataContext para o item selecionado. Assim o Windos Phone pode fazer o Biding corretamente. Essa atribuição é feita da seguinte forma:
[sourcecode language=”csharp” wraplines=”false” collapse=”false”]
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
base.OnNavigatedTo(e);
string selectedIndex;
int index = 0;
if (NavigationContext.QueryString.TryGetValue("selectedItem", out selectedIndex))
{
index = int.Parse(selectedIndex);
DataContext = App.EventListViewModel.EventsVM[index];
}
}
[/sourcecode]
Pagina de detalhes do local
Essa página é um pouco mais complexa que a anterior. Além dos dados normais ela terá um botão para ativar a notificação na próxima vez que o usuário passar próximo ao local e um mapa onde o local está marcado. Para caber o mapa junto das outras informações utilizaremos um panorama, onde um painel será exclusivo para o mapa. O código que descreve a tela de detalhes de um local é descrita a seguir:
[sourcecode language=”xml” wraplines=”false” collapse=”false”]
<Grid x:Name="LayoutRoot">
<Grid.Background>
<ImageBrush ImageSource="/Assets/escola-minas-ufop-dark.jpg" />
</Grid.Background>
<phone:Panorama x:Name="Panorama" Title="{Binding Name}" Foreground="White">
<!–Panorama item Descrição –>
<phone:PanoramaItem Header="Descrição">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Image x:Name="Image" Grid.Row="0" Source="{Binding ImageUrl}" HorizontalAlignment="Center"/>
<TextBlock x:Name="Description" Grid.Row="1" Text="{Binding Description}" VerticalAlignment="Top" Padding="10"/>
<Button x:Name="geofenceButton" Grid.Row="1" VerticalAlignment="Bottom" Content="Ativar notificação" FontSize="36" Foreground="White" BorderBrush="White" BorderThickness="5" Margin="0,0,0,6" Click="geofenceButton_Click"/>
</Grid>
</phone:PanoramaItem>
<!–Panorama item Como Chegar –>
<phone:PanoramaItem Header="Como Chegar">
<Grid>
<Border BorderThickness="5" BorderBrush="White" Margin="0,0,0,16">
<maps:Map x:Name="Mapa" VerticalAlignment="Top" Height="462" ZoomLevel="17" Center="{Binding Coordinates}"/>
</Border>
</Grid>
</phone:PanoramaItem>
</phone:Panorama>
</Grid>
[/sourcecode]
O método OnNavigatedTo() além de definir o DataContext para possibilitar o Binding irá criar o overlay que permite a marcação do local desejado no mapa. O código responsável pela definição do DataContext e pela marcação do local no mapa pode ser visto a seguir:
[sourcecode language=”csharp” wraplines=”false” collapse=”false”]
protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
base.OnNavigatedTo(e);
string selectedIndex;
int index = 0;
if (NavigationContext.QueryString.TryGetValue("selectedItem", out selectedIndex))
{
index = int.Parse(selectedIndex);
DataContext = App.PlaceListViewModel.PlacesVM[index];
}
Mapa.Center = App.PlaceListViewModel.PlacesVM[index].Coordinates;
MapOverlay overlay = new MapOverlay
{
GeoCoordinate = Mapa.Center,
Content = new Ellipse
{
Fill = new SolidColorBrush(Colors.Red),
Width = 40,
Height = 40
}
};
MapLayer layer = new MapLayer();
layer.Add(overlay);
Mapa.Layers.Add(layer);
}
[/sourcecode]
Agora que todos os dados estão carregados e exibidos corretamente devemos definir o comportamento do botão “Ativar notificação”.
Configurando Geofences
Ao clicar no botão “Ativar Notificação” o trecho de código a seguir é executado, este trecho é responsável por criar um Geofence no local com um raio de 50 metros.
[sourcecode language=”csharp” wraplines=”false” collapse=”false”]
private void geofenceButton_Click(object sender, RoutedEventArgs e)
{
PlaceViewModel place = App.PlaceListViewModel.PlacesVM[index];
BasicGeoposition position = new BasicGeoposition();
position.Latitude = place.Coordinates.Latitude;
position.Longitude = place.Coordinates.Longitude;
GeofenceMonitor geofenceMonitor = GeofenceMonitor.Current;
Geofence geofence = new Geofence(place.Name, new Geocircle(position, 50), MonitoredGeofenceStates.Entered | MonitoredGeofenceStates.Exited | MonitoredGeofenceStates.Removed, true);
try
{
geofenceMonitor.Geofences.Add(geofence);
MessageBox.Show("Notificação ativada para aproximação do local " + place.Name);
}
catch
{
MessageBox.Show("Notificação já ativada para " + place.Name);
}
}
[/sourcecode]
Agora que nosso aplicativo já é capaz de criar geofences devemos definir qual é o comportamento que deve ser assumido quando o usuário disparar algum evento do geofence. Para isso crie um método chamado initializeGeofence() na classe App e adicione uma chamada para o mesmo no construtor de App. O trecho de código a seguir é responsável por pedir autorização ao usuário para acessar a localização do usuário e define qual o handler de evento será chamado.
[sourcecode language=”csharp” wraplines=”false” collapse=”false”]
private async Task initializeGeofence()
{
GeofenceMonitor geofenceMonitor = GeofenceMonitor.Current;
Geolocator geoLocator = new Geolocator();
CancellationToken token = (new CancellationTokenSource()).Token;
try
{
Geoposition localization = await geoLocator.GetGeopositionAsync(TimeSpan.FromMinutes(5), TimeSpan.FromSeconds(30)).AsTask(token);
}
catch (Exception e)
{
throw e;
}
geofenceMonitor.GeofenceStateChanged += geofenceMonitor_GeofenceStateChanged;
}
[/sourcecode]
Para o nosso sistema de notificações baseado em geofence falta somente a implementação do handler que é disparado quando o estado de uma geofence muda. O código a seguir exibe uma mensagem quando o usuário entra na área do geofence e outra quando o mesmo sai da área.
[sourcecode language=”csharp” wraplines=”false” collapse=”false”]
void geofenceMonitor_GeofenceStateChanged(GeofenceMonitor sender, object args)
{
var reports = sender.ReadReports();
foreach (var report in reports)
{
GeofenceState state = report.NewState;
Geofence geofence = report.Geofence;
// Entrada no Geofence
if (state == GeofenceState.Entered)
{
NotifyUser("Você está proximo de: " + geofence.Id);
}
// Saida do Geofence
if (state == GeofenceState.Exited)
{
NotifyUser("Você saiu da área de: " + geofence.Id);
}
if (state == GeofenceState.Removed)
{
GeofenceMonitor.Current.Geofences.Remove(geofence);
}
}
}
public static void NotifyUser(String notification)
{
Deployment.Current.Dispatcher.BeginInvoke(() => MessageBox.Show(notification));
}
[/sourcecode]
Conclusão
Este longo tutorial teve como objetivo consolidar tudo que foi passado em um único aplicativo que engloba tudo que foi ensinado nessa série. Durante todo o tutorial foram selecionados os trechos de código mais relevantes, porém alguns ainda tiveram de ser omitidos. Abaixo segue o projeto completo, o qual pode ser aberto utilizando o Visual Studio Express 2013 ou superior. Devido a extensão desse tutorial cada etapa foi revista de forma mais superficial, caso fique dúvidas em algum ponto não exite em procurar nos outros tutoriais na categoria Windows Phone ou deixar sua dúvida nos comentários.
Download do projeto: PanoramaGuide