Comunidad de diseño web y desarrollo en internet online

Programación para Windows Phone 7: Mejorar el rendimiento

Este es un artículo de una serie dedicada al desarrollo de aplicaciones para Windows Phone 7 en Silverlight. Como primer ejemplo estamos creando una aplicación sencilla de representación de datos y la iremos refinando en sucesivos episodios:


  1. Aplicación base: representación de datos y visionado de vídeos.
  2. Mejoras visuales y navegación
  3. Guardar el estado (tombstoning)
  4. Mejoras de rendimiento
  5. Interacción con otros servicios
  6. Preparación para el Marketplace


Hoy toca el episodio 4: Mejoras de rendimiento. Veremos cómo con unos pocos cambios nuestra aplicación irá como un tiro. Para ello vamos a mejorar la aplicación en tres puntos: primero daremos información al usuario sobre lo que está ocurriendo en la aplicación, luego mejoraremos la ejecución multihilos y finalmente aumentaremos la velocidad de las listas en el UI.

¿Van Halen vs Eric Clapton?


En ocasiones, más importante que la velocidad real de la aplicación es la la apreciación que tiene el usuario, así que vamos a usar el viejo truco de entretenerle mientras cargamos. Para ello utilizaremos una barra de progreso. El sdk de windows phone incluye una muy fácil de usar, pero utiliza el hilo del interfaz de usuario. Esto significa que si bloqueamos el interfaz por algún motivo, también se parará la animación.
Para evitar que la aplicación se atasque el Windows Phone puede usar la GPU para ejecutar las animaciones, combinando un storyboard y el bitmapcache haremos uso de la GPU directamente. Tranquilos, no lo tenemos que hacer nosotros: en el Silverlight Toolkit hay una PerformanceProgressBar desarrollada por Jeff Wilcox que nos funcionará a la perfección.

Para instalar el Silverlight Toolkit el mejor sistema es la herramienta NuGet, que nos permitirá activar paquetes en nuestro proyecto y distribuirlos fácilmente.

Añadiremos una referencia a la librería y luego dentro del MainPage.xaml, entre el final del panorama y el final del grid crearemos un stack panel que contendrá un texto y la barra de progreso. En la barra asignaremos el valor True a la propiedad IsIndeterminate; así obtendremos una animación cíclica para indicar que estamos realizando una operación larga.

Código :

...
</controls:Panorama>
<StackPanel x:Name="progressPanel" Orientation="Horizontal"
Margin="0,0,0,775" Visibility="Collapsed"
Background="#81000000">
<TextBlock Text="Cargando datos..." Style="{StaticResource PhoneTextSmallStyle}" />
<toolkit:PerformanceProgressBar VerticalAlignment="Center"
Name="progressBar"
Foreground="{StaticResource PhoneForegroundBrush}"
Width="300" IsIndeterminate="True" />
</StackPanel>
</Grid>

Ahora viene lo complicado, tenemos que detectar el comienzo y el final de la carga de datos para cambiar la visibilidad del panel. Como recordaréis, en el capítulos anteriores cargábamos los datos desde el MainViewModel.cs usando la clase WebClient, que contiene un método para cargar de manera asíncrona. Así que necesitaremos un indicador para cada lista para saber si está cargando o no y un evento que nos diga si ha cambiado el estado. Como son pocos valores lo haremos de una manera algo pedestre, un array de tres booleanos:

Código :

bool[] _loading = new bool[3];

public bool IsLoading
{
get { return _loading[0] || _loading[1] || _loading[2]; }
}

Una vez tenemos la propiedad necesitamos saber que la misma ha cambiado, así podremos mostrar y esconder la barra de progreso, para ello creamos un evento y dos métodos para encapsular la funcionalidad.

Código :

public event EventHandler IsLoadingChanged;

private void startLoad()
{
_loading[0] = _loading[1] = _loading[2] = true;
if (IsLoadingChanged != null)
IsLoadingChanged(this, EventArgs.Empty);
}

private void endLoad(int index)
{
_loading[index] = false;
if (!IsLoading && IsLoadingChanged != null)
{
IsLoadingChanged(this, EventArgs.Empty);
}
}
...

Dentro del método de carga notificaremos el comienzo del proceso y tras finalizar cada carga individual notificaremos un cambio en la propiedad:

Código :

public void LoadData()
{
startLoad();
fillVideos(_spain);
...

private void fillVideos(string rssUrl)
{
WebClient client = new WebClient();
client.DownloadStringCompleted += (x, e) =>
{
try
{
if (e.Error == null)
{
fillVideosList(e.Result);
}
}
finally
{
endLoad(0);
}
};
client.DownloadStringAsync(
new Uri(rssUrl));
}

Nos queda utilizar el evento para mostrar y esconder la barra de progreso, en el MainPage.xaml.cs nos engancharemos al evento y ya tendremos nuestra barra de progreso:

Código :

// Constructor
public MainPage()
{
InitializeComponent();

// Set the data context of the listbox control to the sample data
DataContext = App.ViewModel;
checkLoading();
App.ViewModel.IsLoadingChanged += new EventHandler(ViewModel_IsLoadingChanged);
this.Loaded += new RoutedEventHandler(MainPage_Loaded);
}

void ViewModel_IsLoadingChanged(object sender, EventArgs e)
{
checkLoading();
}

private void checkLoading()
{
progressPanel.Visibility = App.ViewModel.IsLoading ? Visibility.Visible :
Visibility.Collapsed;
}



La barra de progreso


Mejoras en programación multi-hilos


Una vez ya estamos dando información al usuario de que algo está ocurriendo en la aplicación, podemos empezar a arreglar la ejecución en múltiples hilos.
En la aplicación inicial usábamos la clase WebClient para descargar la información, la cual ya nos da un método de carga asíncrona para poder realizar la operación sin bloquear el UI. Los que hayáis programado aplicaciones con múltiples hilos alguna vez sabréis que para cambiar algún valor del UI hace falta sincronizar con el hilo de representación o tendremos problemas, pero en el caso de WebClient no estamos usando el Dispatcher para sincronizar. Ya habréis adivinado que el WebClient nos lo pone fácil y el evento DownloadStringCompleted ya está sincronizado con el hilo de pantalla.
Esto quiere decir que cualquier operación que hagamos dentro del evento influirá en la respuesta de la aplicación. Podemos mejorar algo utilizando otra clase para obtener los datos, la clase HttpWebRequest que no realiza automáticamente la sincronización y podemos controlar cuándo hacerla. El único inconveniente es que tendremos que escribir un poco más de código:

Código :

private void fillVideos(string rssUrl)
{
HttpWebRequest r = HttpWebRequest.CreateHttp(rssUrl);

r.BeginGetResponse((e) =>
{
if (e.IsCompleted)
{
try
{
var request = (HttpWebRequest)e.AsyncState;
using (WebResponse response = request.EndGetResponse(e))
{
using (Stream stream = response.GetResponseStream())
{
List<ElementoEntradaVideo> elements;

using (StreamReader rdr = new StreamReader(stream))
{
char[] buffer = new char[1024];
StringBuilder bldr = new StringBuilder();
int length = 0;
while ((length = rdr.Read(buffer, 0, 1024)) > 0)
{
bldr.Append(buffer, 0, length);
}
elements = ElementoEntradaVideo.GetElements(bldr.ToString());
}

System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() =>
{
try
{
fillVideosList(elements);
}
finally
{

endLoad(0);
}
});
}

}
}
catch (WebException ex)
{
_lastError = ex.Status;
System.Windows.Deployment.Current.Dispatcher.BeginInvoke(
() => endLoad(0));
}
}
}, r);
}

Mejoras en la velocidad de las listas


Hemos mejorado algo la aplicación, pero a pesar de nuestros esfuerzos seguimos teniendo problemas mientras se rellena la lista, pues la aplicación se queda uno o dos segundos atascada, dependiendo de la longitud de la misma.
Esto ocurre porque usamos una ObservableCollection, que lanza un evento cada vez que añadimos un elemento. Las ObservableCollection nos permiten notificar a un elemento, en nuestro caso un ItemsControl, que se ha modificado la lista y así se refresca automáticamente el contenido, sin que nosotros tengamos que forzarlo cada vez. El problema es que al rellenar la lista este evento se lanzará cada vez que añadamos un elemento y podría ocurrir unos cientos de veces en el hilo de UI. Podemos evitarlo con un truco que se usaba mucho en Winforms, suspender la colección, pero como la colección base no tiene métodos de suspensión tendremos que añadirlos nosotros, tal como explican en StackOverflow:

Código :

public class SuspendableObservableCollection : ObservableCollection
{
object _lock = new object();
private bool _suspended;

private bool _changedDuringSuspend;

public void Suspend()
{
lock (_lock)
{
_suspended = true;
}
}

public void Resume()
{
lock (_lock)
{
_suspended = false;
if (_changedDuringSuspend)
{
_changedDuringSuspend = false;
base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(
NotifyCollectionChangedAction.Reset));
}
}
}

protected override void OnCollectionChanged(
NotifyCollectionChangedEventArgs args)
{
lock (_lock)
{
if (!_suspended)
{
base.OnCollectionChanged(args);
}
else
{
_changedDuringSuspend = true;
}
}
}
}

Una vez hecho esto, sólo tenemos que llamar a los métodos Suspend y Resume en el momento adecuado y aceleraremos nuestra aplicación.

Código :

private void fillVideosList(List<ElementoEntradaVideo> elements)
{
this.Videos.Suspend();
try
{
this.Videos.Clear();
elements.ForEach((element) => this.Videos.Add(element));
}
finally
{
this.Videos.Resume();
}
this.LastRefresh = DateTime.Now;
}

Así habremos conseguido que la aplicación se atasque lo mínimo mientras añadimos elementos. Aparte de todo esto, hay otros métodos que nos permitirán acelerar más la carga de nuestras listas, como nos cuentan en esta entrada de WPhone.es, en futuras versiones de la aplicación iremos refinando más.

Conclusiones


Hoy hemos visto cómo mejorar la respuesta de nuestra aplicación utilizando diversas técnicas multi-hilos y evitando que nuestro código se ejecute más veces de las necesarias.
Podéis descargar el código de ejemplo y el XAP en CodePlex.

¿Sabes SQL? ¿No-SQL? Aprende MySQL, PostgreSQL, MongoDB, Redis y más con el Curso Profesional de Bases de Datos que empieza el martes, en vivo.

Publica tu comentario

o puedes...

¿Estás registrado en Cristalab y quieres
publicar tu URL y avatar?

¿No estás registrado aún pero quieres hacerlo antes de publicar tu comentario?

Registrate