|
Etude de cas : application Silverlight avec Twitter et MVVM Light
Richard Clark
16/10/09
(Vu
3262)
08:00 Début du développement de ma petite application Twitter Silverlight. Dans un premier temps, savoir ou se trouve les API de Twitter. Une petite recherche rapide sur Google (pardon, sur Bing), et je trouve le site très bien documenté de Twitter (sans doute l'une des raisons de son succès) : http://apiwiki.twitter.com/Twitter-API-Documentation. Je constate que j'ai l'accès à un flux Xml, json, rss ou atom dénommé statuses/user_timeline. Il suffit pour l'obtenir de faire une simple requête Http GET vers l'url : http://twitter.com/statuses/user_timeline.format ou format est xml, json, rss ou atom. Simple. Par exemple pour mon compte Twitter, je n'ai qu'à aspirer mon flux à http://twitter.com/user_timeline.xml?screen_name=c2iClark&count=10. NB : Remarquez l'ajout de count=10 pour limiter aux 10 derniers tweets. Donc je vais essayer de créer un petit bout de code qui me permettra de faire cela. Je crée donc mon projet Silverlight avec mon site web de test dans Visual Studio 2008. Je me dis dans un premier temps que je vais utiliser HttpWebRequest comme classe de System.Net mais un rapide coup d'oeil sur MSDN me dit qu'il vaut mieux utiliser WebClient. Malheureusement, comme je vais demander au client d'aspirer un flux Xml qui n'est pas dans mon domaine, la sécurité de Silverlight va m'interdir cette aspiration. Surtout que le CrossDomainPolicy de twitter interdit toute aspiration exterieure (voir le fichier http://twitter.com/crossdomain.xml). J'aurais la solution de créer un feed chez FeedBurner, mais je n'aurais pas le temps réel (FeedBurner met en cache, heureusement pour lui). Je suis donc obliger de créer un service sur mon site Web. Je vais choisir de créer un WebService, et pas un Service WCF, car j'ai des petits soucis avec mon hébergement à ce sujet. Le Web Service Je crée donc un rapide WebService qui va juste aspirer le flux Xml et me retourner son contenu sous forme de string. Il n'y a donc qu'une seule méthode dans ce WebService : [WebService(Namespace = "http://www.c2i.fr/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[System.ComponentModel.ToolboxItem(false)]
public class c2iTwitterService : System.Web.Services.WebService
{
private const string MyTweetsCacheKey = "myTweets";
[WebMethod]
public string GetMyTweets()
{
if (HttpContext.Current.Cache[MyTweetsCacheKey] == null)
{
string url = "http://twitter.com/statuses/user_timeline.xml?screen_name=c2iClark&count=10";
string result = XDocument.Load(url).ToString();
HttpContext.Current.Cache.Add(MyTweetsCacheKey, result, null, DateTime.Now.AddMinutes(5),
Cache.NoSlidingExpiration, CacheItemPriority.Normal, null);
}
return (string)HttpContext.Current.Cache[MyTweetsCacheKey];
}
}
Remarquez que je met en cache le résultat pour une réinterrogation que toutes les 5 minutes. J'ajoute enfin un fichier d'autorisation d'accès pour mon site, un fichier nommé clientaccesspolicy.xml que je place à la racine de mon site. Son contenu est : <?xml version="1.0" encoding="utf-8"?>
<access-policy>
<cross-domain-access>
<policy >
<allow-from http-methods="*">
<domain uri="*"/>
</allow-from>
<grant-to>
<resource path="/" include-subpaths="true"/>
</grant-to>
</policy>
<policy>
<allow-from http-request-headers="SOAPAction">
<domain uri="*"/>
</allow-from>
<grant-to>
<resource path="/" include-subpaths="true"/>
</grant-to>
</policy>
</cross-domain-access>
</access-policy>
Je pourrais optimiser (en précisant le domain uri="*.c2i.fr" par exemple). Mon WebService est donc prêt, je peux ajouter sa référence dans mon projet Silverlight. 8:30 Comme vous l'aurez deviner, je vais utiliser le framework MVVM de Laurent Bugnion pour mon projet Silverlight. Sa structure globale est la suivante :
9:15 Oui je sais, j'ai un peu trainé mais il y avait Nicolas Canteloup sur Europe1 et café avec ma femme ;-) Interrogation du WebService Comme toute interrogation d'un WebService sous Silverlight, cet appel se fera de façon asynchrône. La structure de ma méthode est donc la suivante : public void ExecuteLoadTweetsCommand()
{
if (IsWorking)
return;
c2iTwitterServiceSoapClient srv = new c2iTwitterServiceSoapClient();
srv.GetMyTweetsCompleted += delegate(object sender, GetMyTweetsCompletedEventArgs e)
{
// le code d'interrogation, cf plus loin
}
this.Tweets = myNewTweets;
this.IsWorking = false;
};
this.IsWorking = true;
srv.GetMyTweetsAsync();
}
Au début de l'appel, je place la propriété IsWorking à True. Comme cela, dans ma View, je pourrais facilement indiquer que je suis en train de travailler : je mettrais une animation d'un sablier sur le devant de la scène. J'ai un Border pour cela : <Border Visibility="{Binding IsWorking, Converter={StaticResource BoolVisibilityConverter}, Mode=OneWay}">
<Path x:Name="path" Stretch="Fill" >
<!--le dessin du sablier-->
</Path>
</Border>
Le converter de type BoolVisibilityConverter converti le boolean en Visibility (Collapsed == false, Visible = true). Il ne reste plus qu'à transformer le string du flux Xml en collection d'objet Tweet. J'ai fais simple, un Tweet n'est représenté que par son texte et sa date de création : public class Tweet
{
public DateTime CreatedAt { get; set; }
public string Text { get; set; }
}
Pour aller vite dans la lecture du flux Xml, je vais utiliser un XDocument de Linq To Xml : XDocument doc = XDocument.Parse(e.Result);
IEnumerable<XElement> items =
from XElement item in doc.Root.Elements("status")
select item;
Parser la date : Vu sur http://www.wduffy.co.uk/blog/parsing-dates-when-aspnets-datetimeparse-doesnt-work/. La date est dans un format UTC mais qui n'est pas reconnu explicitement par le Framework. En revanche, son format est précis et donc la méthode ParseExact de DateTime permet de récupérer la date correctement : DateTime createdAt =
DateTime.ParseExact(
item.Element("created_at").Value,
"ddd MMM dd HH:mm:ss zz00 yyyy",
CultureInfo.InvariantCulture);
Le User et son image Pour chaque status dans le flux Xml, on a les informations concernant le TwitterUser. Son nom est facilement récupérable, en revanche, son image est une url. Il faut la convertir en BitmapImage pour qu'elle soit utilisable par un contrôle Image. On crée donc dynamiquement cette image : this.UserImage =
new BitmapImage(new Uri(items.First().Element("user").Element("profile_image_url").Value));
Et voila, j'ai donc fini par aspirer mon flux. Le code complet de cette méthode est donc le suivant : public void ExecuteLoadTweetsCommand()
{
if (IsWorking)
return;
c2iTwitterServiceSoapClient srv = new c2iTwitterServiceSoapClient();
srv.GetMyTweetsCompleted += delegate(object sender, GetMyTweetsCompletedEventArgs e)
{
ObservableCollection<Tweet> myNewTweets = new ObservableCollection<Tweet>();
try
{
XDocument doc = XDocument.Parse(e.Result);
IEnumerable<XElement> items = from XElement item in doc.Root.Elements("status")
select item;
foreach (XElement item in items)
{
DateTime createdAt =
DateTime.ParseExact(
item.Element("created_at").Value,
"ddd MMM dd HH:mm:ss zz00 yyyy",
CultureInfo.InvariantCulture);
string text = item.Element("text").Value;
myNewTweets.Add(new Tweet() { Text = text, CreatedAt = createdAt });
}
if (items.Count() > 0)
{
this.UserName = items.First().Element("user").Element("screen_name").Value;
this.UserImage =
new BitmapImage(
new Uri(items.First().Element("user").Element("profile_image_url").Value));
}
}
catch (Exception ex)
{
myNewTweets = new ObservableCollection<Tweet>();
myNewTweets.Add(new Tweet(){CreatedAt=DateTime.Now, Text="ERREUR!"});
}
this.Tweets = myNewTweets;
this.IsWorking = false;
};
this.IsWorking = true;
srv.GetMyTweetsAsync();
}
10:30 Il me reste maintenant une problématique, c'est le contenu du Tweet. Si je mets juste un TextBlock, j'aurais bien l'info d'afficher mais sans aucun lien hypertexte à l'intérieur. Cette fois-ci, l'idée est venue d'un projet sur Codeplex ou également, il affiche des tweets (désolé mais j'ai perdu l'url de ce projet. Mais faites une recherche sur Codeplex et je suis sur que vous le retrouverez). Le principe est simple :
J'ai donc crée un contrôle utilisateur TweetViewer qui contient juste un WrapPanel (nommé wp) et une DepedencyProperty TweetText : public partial class TweetViewer : UserControl
{
public TweetViewer()
{ InitializeComponent();}
public string TweetText
{
get { return (string)GetValue(TweetTextProperty); }
set { SetValue(TweetTextProperty, value); }
}
public static readonly DependencyProperty TweetTextProperty =
DependencyProperty.Register(
"TweetText",
typeof(string),
typeof(TweetViewer),
new PropertyMetadata(null,OnTweetChanged) );
Quand la propriété TweetText change, je crée dynamiquement les contrôles dans le WrapPanel : private static void OnTweetChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
TweetViewer ctrl = sender as TweetViewer;
ctrl.wp.Children.Clear();
if (e.NewValue == null)
return;
string text = (string) e.NewValue;
string[] words = text.Split(new char[] {' '});
foreach (string word in words)
{
UIElement controlToAdd = null;
if (word.StartsWith("http://") || word.StartsWith("https://"))
{
controlToAdd = new HyperlinkButton()
{
Content = word + " ",
NavigateUri = new Uri(word),
TargetName = "_blank",
Foreground = Application.Current.Resources["linkColorBrush"] as Brush
};
}
else if (word.StartsWith("@") && word.Length>1)
{
Uri uri = new Uri(string.Format(@"http://twitter.com/{0}", word.Substring(1)));
controlToAdd = new HyperlinkButton()
{
Content = word + " ",
NavigateUri =uri,
TargetName = "_blank",
Foreground = Application.Current.Resources["linkColorBrush"] as Brush
};
}
else
{
controlToAdd = new TextBlock() {Text = word + " "};
}
ctrl.wp.Children.Add(controlToAdd);
}
}
Maintenant, il ne me reste plus qu'à créer l'interface, ie la View. C'est le plus dur car je suis tout sauf Designer :P Pour afficher la liste des Tweets, j'utiliserai un ItemsControl bindé à ma propriété Tweets de mon ViewModel et dans le DataTemplate de mon ItemsControl, j'utiliserai mon nouveau contrôle TweetViewer. 11:30 Et voila le résultat :
Bon, OK, je suis pas designer, je vous avais prévenu !!! 11:42 Fin de la rédaction de l'article (Note de ma femme : MENTEUR!!!!) Richard Clark
Vos commentaires
Vous devez être identifié pour pouvoir commenter l'article. |



