lundi 1 novembre 2010

Rss Converter (Asp.net MVC AsyncController sample)

L’exercice du jour, c’est d’explorer le contrôleur asynchrone ASP.net MVC 2 (et autres nouveautés). Pour cela, le sujet du jour c’est de construire un convertisseur de flux RSS qui transforme les podcasts Channel 9 de base au format wmv, vers d’autres formats, mp4 entre autre, sachant que l’information du nouveau fichier est déjà dans le flux original mais pas dans le bon tag XML. Cette fonctionnalité est déjà fournie par le site Channel 9 lui même en ajoutant /mp3 ou /ipod à l’adresse des flux mais cela fait un bon exercice.

EditorForModel

Commençons par créer un objet conteneur des champs du formulaire de demande de conversion d’un flux.

namespace Channel9Converter.Models
{
public enum TargetType : byte
{
Mp3,
Mp4,
Wmv
}

public class ConverterRequest
{
[DisplayName("Original feed url"), StringLength(200)]
public string OriginalFeedUrl { get; set; }
[DisplayName("Target file format"), UIHint("Enum")]
public TargetType TargetType { get; set; }
}
}

Cette classe porte sur ses propriétés, des attributs qui permettront aux méthodes d'extension du HtmlHelper de générer des labl contenant un texte d’édition, l’attribut UIHint permet de choisir de template qui sera utilisé pour afficher l’éditeur de la propriété TargetType qui est une énumération.
Cet éditeur le voici : un simple fichier Enum.ascx placé dans le répertoire Views\Shared\EditorTemplates du projet.

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<object>" %>
<%
if (Model != null)
{
var enumType = Model.GetType();
if (enumType.IsEnum)
{
%>
<%:
Html.DropDownList(
"",
Enum.GetNames(enumType).Select(
n => new SelectListItem() {
Text = n,
Value = n,
Selected = n == Model.ToString()
})
)
%>
<%
}
}
%>

La vue d'index du site affiche un formulaire montrant deux champs : un pour l'adresse du flux à convertir, et un pour le format cible

<%@ Page Language="C#" Inherits="System.Web.Mvc.ViewPage<Channel9Converter.Models.ConverterRequest>" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title>Channel 9 feed converter</title>
</head>
<body>
<h1>
Welcome to the channel 9 feed converter</h1>
<% using (Html.BeginForm("Convert", "Rss", FormMethod.Get))
{%>
Enter a Channel 9 RSS Feed below and click the "Convert !" button<br />
in order to display the same rss feed converted to the file format you selected.<br />
Then copy the new url in the adress bar and use it in any Rss reader you want.
<br /><br />
<%: Html.ValidationSummary(true) %>
<%: Html.EditorForModel() %>
<br /><br />
<input type="submit" value="Convert !" />
<% } %>

<span style="color:red;">You actually only need to add "/ipod" at the end of the original podcast adress</span>
</body>
</html>

Ce qui a pour rendu :

image


AsyncController

On en vient au contrôleur asynchrone, objet principal de ce post. Le but des pages asynchrones, déjà implémentée sous ASP.net depuis le Framework 2, est de ne pas monopoliser des threads IIS (utilisés pour “servir” les requêtes HTTP) à l’attente d’opérations pouvant être longues (dans notre exemple un téléchargement de fichier sur un autre serveur). Cette gestion asynchrone permet, sous certaines conditions (de ne pas rediriger l’attente vers un autre goulot d’étranglement encore plus critique) d’augmenter le nombre de requêtes simultanés que pourra gérer votre site.

Si dessous le code de ce contrôleur asynchrone :

namespace Channel9Converter.Controllers
{
public partial class RssController : AsyncController
{
[OutputCache(Duration = 300, VaryByParam = "*")]
public virtual void ConvertAsync(ConverterRequest request)
{
if (request == null || request.OriginalFeedUrl == null)
return;

AsyncManager.OutstandingOperations.Increment();

var web = new WebClient();
web.OpenReadCompleted += (sender, e) =>
{
AsyncManager.Parameters["stream"] = e.Result;
AsyncManager.Parameters["request"] = request;

AsyncManager.OutstandingOperations.Decrement();
};

web.OpenReadAsync(new Uri(request.OriginalFeedUrl, UriKind.Absolute));
}

string translate_to_content_type(TargetType targetType)
{
switch (targetType)
{
case TargetType.Mp3:
return "audio/mp3";
case TargetType.Mp4:
return "video/mp4";
case TargetType.Wmv:
return "x-ms-wmv";
default:
throw new NotImplementedException();
}
}

public virtual ActionResult ConvertCompleted(Stream stream, ConverterRequest request)
{
var reader = XmlReader.Create(stream, new XmlReaderSettings() { DtdProcessing = System.Xml.DtdProcessing.Parse, ValidationType = ValidationType.None });
var channel9 = SyndicationFeed.Load(reader);

foreach (var item in channel9.Items)
{
var media_group =
item.ElementExtensions.ReadElementExtensions<XElement>("group", @"http://search.yahoo.com/mrss/").FirstOrDefault();
if (media_group == null) continue;

Func<XElement, string, string> attValueOrEmpty = (xe, att_name) =>
{
var att = xe.Attribute(att_name);
if (att == null) return "";
else return att.Value;
};

var media_contents =
from mc in media_group.Descendants()
select new
{
url = attValueOrEmpty(mc, "url"),
expression = attValueOrEmpty(mc, "expression"),
duration = attValueOrEmpty(mc, "duration"),
fileSize = attValueOrEmpty(mc, "fileSize"),
type = attValueOrEmpty(mc, "type"),
medium = attValueOrEmpty(mc, "medium"),
};

var content_type = translate_to_content_type(request.TargetType);
var media = media_contents.Where(obj => obj.type == content_type).FirstOrDefault();
if (media == null) continue;

var link = item.Links.Where(l => l.RelationshipType == "enclosure").FirstOrDefault();
if (link == null) continue;

link.Uri = new Uri(media.url);
link.MediaType = media.type;
link.Length = int.Parse(media.fileSize);
}

return new RssResult(channel9);
}
}
}

L’attribut OutputCache permet encore d’optimiser la charge serveur et surtout dans ce cas le temps de réponse en mettant en cache le rendu de la page. Ici le cache est rafraichi toutes les 5 minutes (300 secondes).

Le résultat de la deuxième méthode est un RssResult, directement inspiré du RssResult proposé par l’excellent Nerd Dinner (http://nerddinner.codeplex.com/) et dont le code est ci dessous.


namespace Channel9Converter.Controllers
{
public class RssResult : FileResult
{
private Uri currentUrl;
private SyndicationFeed _feed;

public RssResult() : base("application/rss+xml") { }

public RssResult(SyndicationFeed feed)
: this()
{
this._feed = feed;
}

public override void ExecuteResult(ControllerContext context)
{
currentUrl = context.RequestContext.HttpContext.Request.Url;
base.ExecuteResult(context);
}

protected override void WriteFile(System.Web.HttpResponseBase response)
{
Rss20FeedFormatter formatter = new Rss20FeedFormatter(_feed);

using (XmlWriter writer = XmlWriter.Create(response.Output))
{
formatter.WriteTo(writer);
}
}
}
}

Note : T4MVC ne me semble pas compatible avec les contrôleurs asynchrones.

Aucun commentaire: