Wp7-Loading_thumb.gif

Loading no Windows Phone 7


Beleza galera, tudo bom!?

Ao desenvolver aplicativos que façam processamentos candidatos a demorarem, devemos nos preocupar em passar o feedback para o usuário, para que possamos prover uma boa experiência de usuário(UX). Já estamos acostumados a ver barras de progresso em instalações e downloads, também é familiar ver a famosa bolinha muito usada hoje na web ao fazer ajax.

Enfim hoje vou mostrar como fazer o uso disso no Windows Phone, para apreciação inicial segue o exemplo:

Wp7 Loading

ProgressIndicator

Essa classe é nova, ela foi acrescentada na atualização Mango, quem desenvolve usando a SDK 7.1 do aparelho pode usar ela, seu namespace é Microsoft.Phone.Shell.

Segue abaixo um simples código de uso da mesma:

[csharp]using Microsoft.Phone.Shell;

namespace SystemTrayTest
{
public partial class MainPage : PhoneApplicationPage
{
ProgressIndicator prog;

public MainPage()
{
InitializeComponent();

SystemTray.SetIsVisible(this, true);
SystemTray.SetOpacity(this, 0.5);
SystemTray.SetBackgroundColor(this, Colors.Purple);
SystemTray.SetForegroundColor(this, Colors.Yellow);

prog = new ProgressIndicator();
prog.IsVisible = true;
prog.IsIndeterminate = true;
prog.Text = "Click me…";

SystemTray.SetProgressIndicator(this, prog);
}
}
}[/csharp]

As primeiras linhas do método MainPage é só para deixar visível o SystemTray, sua cor de fundo, cor da progress bar e a opacidade. E para fazer com que a barra de progresso seja exibida nos instanciamos ela e setamos os valores das propriedades:

  • IsVisible => Usada para exibir ou ocultar a progress bar
  • IsIndeterminate => Usado para exibir a progress bar com os pontinhos “correndo” ou para setar um progresso definido, dessa maneira usamos a propriedade Value
  • Text => Usado para mostrar um texto de acompanhamento
  • Value => Usado para informar qual porcentagem a barra deve preencher

E por ultimo e não menos importante informamos que ao SystemTray que ele deve usar a nossa instancia de ProgressBar, passando 2 argumentos para o método SetProgressIndicator, a janela(no caso this) e a instancia da barra. Simples não?

Abstraindo o uso da classe

Apesar de ser fácil o uso da classe, o trabalho repetitivo de instanciar, setar os valores etc se torna cansativo, para isso eu desenvolvi em minha biblioteca particular cujo seu código fonte está no github(aqui), uma classe que abstrai o uso da barra de progresso facilitando bastante e resolvendo alguns problemas que poderia acontecer ao fazer requisições assíncronas ao consumir recursos.

Eu já desenvolvi duas aplicações para WP7, e elas já estão no marketplace, uma delas é a VidaDeProgramador, ela é uma aplicação open Soure e seu código está acessível no github(aqui). A aplicação é simples, sua objetivo é prover uma forma de visualizar as tirinhas do blog http://vidadeprogramador.com.br/ sem ter que abrir o browser para conferir as tirinhas.

Para isso consumo via XML o feed do blog a partir dessa url http://vidadeprogramador.com.br/category/tirinhas/feed/. Como estou consumindo um serviço externo, esse processo pode demorar, e para isso precisei informar o usuário que as tirinhas estavam sendo carregadas, para isso usei esse código:

[csharp]using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.ServiceModel.Syndication;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Xml;
using AlbertoMonteiroWP7Tools.Controls;

namespace VidaDeProgramador.WordpressApi
{
public class PostsService
{
const string URL = "http://vidadeprogramador.com.br/category/tirinhas/feed/?paged={0}";
const string IMAGEM = @"<img alt="" />.+)"" a";
const string CORPO = @"</pre>
<div>(?(.|\n)+)</div>
<pre>
";

public async Task<IEnumerable> GetPosts(int page)
{
GlobalLoading.Instance.PushLoading();
XmlReader reader = null;
MemoryStream contentSteam = null;
try
{
var webClient = new WebClient();
var xml = await webClient.DownloadStringTaskAsync(new Uri(string.Format(URL, page)));
contentSteam = new MemoryStream();

Encoding.UTF8.GetBytes(xml).ToList().ForEach(contentSteam.WriteByte);
contentSteam.Seek(0, SeekOrigin.Begin);

reader = XmlReader.Create(contentSteam);
var feed = SyndicationFeed.Load(reader);

var imagemRegex = new Regex(IMAGEM);
var corpoRegex = new Regex(CORPO);

return from syndicationItem in feed.Items
let html = syndicationItem.ElementExtensions.ReadElementExtensions("encoded", "http://purl.org/rss/1.0/modules/content/")
let srcImagem = imagemRegex.Match(html[0]).Groups["imagem"].Value
let body = corpoRegex.Match(html[0]).Groups["corpo"].Value
select new Tirinha
{
Title = syndicationItem.Title.Text,
Image = srcImagem,
Body = HttpUtility.HtmlDecode(body.Replace("
", Environment.NewLine)),
Link = syndicationItem.Id
};
}
catch (WebException e)
{
if (e.Response.ContentLength == 0)
throw new Exception("Impossível se conectar a internet, verifique sua conexão.");
}
finally
{
GlobalLoading.Instance.PopLoading();
if (reader != null)
{
reader.Close();
contentSteam.Close();
}
}
return null;
}
}
}[/csharp]

Esse método GetPosts ele faz o processo de recuperar as tirinhas do blog e transformar em uma lista de instancia da classe Tirinha que está no meu dominio, as linhas mais importantes desse código são:

  1. 23 => Faço um push loading, que faz com que a barra de progresso seja exibida ou continue sendo exibida.
  2. 29 => Inicio o download do feed.
  3. 60 => Faço um pop loading, que faz com que a barra de progresso seja ocultada ou que ela esta mais perto de ser ocultada.

Perai, não entendi, como assim “mais perto de  ser ocultada” ?!

Imagine o seguinte caso onde você faz 5 requisições de forma assíncrona, e no inicio de cada uma teria o PushLoading que exibiria a barra, e no final de cada teria o PopLoading que ocultaria a barra, se cada requisição demorasse respectivamente 1,2,3,4,5 minutos, apos acabar a primeira requisição a barra de progresso seria ocultada, mas ainda teríamos 4 processamentos ainda sendo executados, ou melhor em espera.

A ideia do Push e Pop é ter uma espécie de pilha, podemos empilhar os inícios de cada processo e desempilhar o fim do processo. Voltando ao mesmo caso, ao iniciar todas as 5 requisições, nos teríamos 5 itens na “pilha”, e quando o PopLoading da requisição 1 fosse executado, a pilha passaria a ter 4 processos, sendo assim a barra continuaria sendo exibida até que a pilha estivesse vazia.

PS: Estou usando C# 5 nesse código, usamos as novas palavras chaves async e await, senão conhece vale a pena estudar! Veja o blog do meu amigo @elemarjr que explica sobre isso http://elemarjr.net/2012/01/03/parallel-programming-mais-fcil-com-o-async-ctp/.

Implantando em seu aplicativo a classe GlobalLoading

Ainda não criei um pacote nuget para ela, mas basta você baixar seu fonte do github e anexar como projeto ou compilar e fazer referencia a DLL.

Depois de baixar o fonte e anexar em seu projeto, primeiro passo é referencia-ló, feito isso no método CompleteInitializePhoneApplication adicione o seguinte código:

GlobalLoading.Instace.Initialize(RootFrame), para ver um exemplo veja o código a seguir(linha 90):

[csharp]using System.Windows;
using System.Windows.Navigation;
using AlbertoMonteiroWP7Tools.Controls;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
using VidaDeProgramador.ViewModel;

namespace VidaDeProgramador
{
public partial class App : Application
{
private static MainViewModel viewModel = null;

public static MainViewModel ViewModel
{
get
{
if (viewModel == null)
viewModel = new MainViewModel();

return viewModel;
}
}

public PhoneApplicationFrame RootFrame { get; private set; }

public App()
{
UnhandledException += Application_UnhandledException;

InitializeComponent();

InitializePhoneApplication();

if (System.Diagnostics.Debugger.IsAttached)
{
Application.Current.Host.Settings.EnableFrameRateCounter = true;

PhoneApplicationService.Current.UserIdleDetectionMode = IdleDetectionMode.Disabled;
}
}

private void Application_Launching(object sender, LaunchingEventArgs e){}

private void Application_Activated(object sender, ActivatedEventArgs e){}

private void Application_Deactivated(object sender, DeactivatedEventArgs e){}

private void Application_Closing(object sender, ClosingEventArgs e){}

private void RootFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)
{
if (System.Diagnostics.Debugger.IsAttached)
{
System.Diagnostics.Debugger.Break();
}
}

private void Application_UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e)
{
if (System.Diagnostics.Debugger.IsAttached)
{
System.Diagnostics.Debugger.Break();
}
}

#region Phone application initialization

private bool phoneApplicationInitialized = false;

private void InitializePhoneApplication()
{
if (phoneApplicationInitialized)
return;

RootFrame = new PhoneApplicationFrame();
RootFrame.Navigated += CompleteInitializePhoneApplication;

RootFrame.NavigationFailed += RootFrame_NavigationFailed;

phoneApplicationInitialized = true;
}

private void CompleteInitializePhoneApplication(object sender, NavigationEventArgs e)
{
if (RootVisual != RootFrame)
RootVisual = RootFrame;

RootFrame.Navigated -= CompleteInitializePhoneApplication;
GlobalLoading.Instance.Initialize(RootFrame);
}

#endregion
}
}[/csharp]

A implementação da classe GlobalLoading

Sem mais delongas o código:

[csharp]using System.Collections.Generic;
using System.Windows.Navigation;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;

namespace AlbertoMonteiroWP7Tools.Controls
{
public class GlobalLoading
{
private static GlobalLoading _in;
private readonly Stack loadingStack;
private int loadingCount;
private ProgressIndicator mangoIndicator;

private GlobalLoading()
{
loadingStack = new Stack();
}

public static GlobalLoading Instance
{
get { return _in ?? (_in = new GlobalLoading()); }
}

public void PushLoading()
{
if (loadingStack.Count == 0)
{
++loadingCount;
}
NotifyValueChanged();
loadingStack.Push(true);
}

public void PopLoading()
{
loadingStack.Pop();
if (loadingStack.Count == 0)
{
loadingCount = 0;
NotifyValueChanged();
}
}

public void Initialize(PhoneApplicationFrame frame)
{
mangoIndicator = new ProgressIndicator();

frame.Navigated += OnRootFrameNavigated;

((PhoneApplicationPage) frame.Content).SetValue(SystemTray.ProgressIndicatorProperty, mangoIndicator);
}

private void OnRootFrameNavigated(object sender, NavigationEventArgs e)
{
var ee = e.Content;
var pp = ee as PhoneApplicationPage;
if (pp != null)
pp.SetValue(SystemTray.ProgressIndicatorProperty, mangoIndicator);
}

private void NotifyValueChanged()
{
if (mangoIndicator != null)
{
mangoIndicator.IsIndeterminate = loadingCount > 0;

if (mangoIndicator.IsVisible == false)
mangoIndicator.IsVisible = true;

mangoIndicator.Text = loadingCount > 0 ? "Carregando…" : "";
}
}
}
}[/csharp]

Implementei um Singleton nela, facilitando bastante o uso da classe.
Declarei 4 fields, a própria instancia da classe, a minha stack de “processos”, um “contador” de loading, e a barra de progresso.

Em seu construtor faço somente a instancia a pilha. Os métodos Initialize e OnRootFrameNavigated são executados pelo próprio WP7, tomei o cuidado de usar eles para que só fosse instanciado e setado os eventos quando tudo estivesse disponível.

No Initialize instancio a barra de progresso, e seto a barra de progresso no SystemTray da tela atual, e isso acontece também no evento OnRootFrameNavigated.

Os métodos Push e Pop servem para fazer o tratamento, e quando for necessário exibe ou oculta a barra a partir do método NotifyValueChanged, que atualmente está usando de forma fixa a barra com modo Indeterminado e texto “Carregando…” como vocês puderam ver na imagem no começo do post.
Se você se sentir a vontade de melhorar o código, faça um pull request!

É isso ai pessoal :D!


sobre Alberto Monteiro

Desenvolvedor no Grupo Fortes, cuja principal área de conhecimento são em tecnologias Microsoft, como Windows Forms / Services, WPF, ASP.NET(MVC/WEB API), Windows Phone, EF. Gosta de sopa de letrinhas(SOLID, DDD, TDD, BDD, IoC, SoC, UoW), possui aplicações de Windows Phone publicadas no marketplace, já contribuiu no jQuery UI. Atualmente trabalha com ASP.NET MVC / Web API, Windows Azure, Amazon AWS, jQuery/UI, Knockout, EF, Ninject, AutoMapper, Restfulie, SignalR, KendoUI.