U ovom postu biće prikazano kako može da se napravi custom ActionResult koji vraća feed kao odgovor na zahteva browser-a.
ActionResult
Svaka akcija kontrolera vraća objekat koji proizilazi iz apstraktne ActionResult klase. ASP.NET MVC sadrži nekoliko klasa koje nasleđuju ActionResult:
- ContentResult
- EmptyResult
- FileResult (apstraktna)
- FileContentResult
- FilePathResult
- FileStreamResult
- HttpUnauthorizedResult
- JavaScriptResult
- JsonResult
- RedirectResult
- RedirectToRouteResult
- ViewResultBase (apstraktna)
- PartialViewResult
- ViewResult
ActionResult klasa sadrži samo jednu metodu, tako da ako pravimo custom ActionResult potrebno je da preklopimo samo ExecuteResult metodu. Iako, u našem primeru možemo direktno da nasledimo ActionResult klasu, mi ćemo naslediti FileResult.
S obzirom da je MVC open source projekat, evo i source-a ove dve klase:
public abstract class ActionResult {
public abstract void ExecuteResult(ControllerContext context);
}
public abstract class FileResult : ActionResult {
protected FileResult(string contentType) {
if (String.IsNullOrEmpty(contentType)) {
throw new ArgumentException(MvcResources.Common_NullOrEmpty, "contentType");
}
ContentType = contentType;
}
private string _fileDownloadName;
public string ContentType {
get;
private set;
}
public string FileDownloadName {
get {
return _fileDownloadName ?? String.Empty;
}
set {
_fileDownloadName = value;
}
}
public override void ExecuteResult(ControllerContext context) {
if (context == null) {
throw new ArgumentNullException("context");
}
HttpResponseBase response = context.HttpContext.Response;
response.ContentType = ContentType;
if (!String.IsNullOrEmpty(FileDownloadName)) {
// From RFC 2183, Sec. 2.3:
// The sender may want to suggest a filename to be used if the entity is
// detached and stored in a separate file. If the receiving MUA writes
// the entity to a file, the suggested filename should be used as a
// basis for the actual filename, where possible.
ContentDisposition disposition = new ContentDisposition() { FileName = FileDownloadName };
string headerValue = disposition.ToString();
context.HttpContext.Response.AddHeader("Content-Disposition", headerValue);
}
WriteFile(response);
}
protected abstract void WriteFile(HttpResponseBase response);
}
Kako bi browser shvatio da se radi o feed-u, potrebno je da u response-u dodamo header Content-Disposition sa vrednošću application/rss+xml
. Ovo je ujedno i razlog zašto ćemo naslediti FileResult umesto ActionResult.
Atom i RSS
Atom i Rss predstavljaju dijalekte XML-a, ali na svu sreću nije potrebno da se bavimo parsiranjem XML-a. U .NET-u postoji skup klasa koje su namenjen za rad sa strukturama podataka kakve su Atom i Rss feed-ovi. U prostoru imena System.ServiceModel.Syndication nalazi se nekoliko klasa:
- SyndicationFeed
- SyndicationItem
- SyndicationContent
- itd.
Implementacija
Uzećemo za primer blog aplikaciju, odnosno Post klasu kao model za koji ćemo generisati feed:
public class Post
{
public int PostId { get; set }
public string Title { get; set; }
public string Body { get; set; }
public string Permalink { get; set; }
public DateTime Posted { get; set; }
}
FeedController je krajnje jednostavan i mislim da nema potrebe detaljnije objašnjavati šta radi:
public class FeedController : Controller
{
private BlogContext db = new BlogContext();
public ActionResult Atom()
{
var posts = db.Posts.All();
return FeedResult(posts, FeedFormat.Atom10);
}
public ActionResult Rss()
{
var posts = db.Posts.All();
return FeedResult(posts, FeedFormat.Rss20);
}
}
FeedResult izgledaće ovako:
public class FeedResult : FileResult
{
private IEnumerable items;
private FeedFormat feedFormat;
private Uri currentUrl;
public SyndicationFeed Feed { get; set; }
public FeedResult(IEnumerable items, FeedFormat format)
: base("application/rss+xml")
{
this.items = items;
this.feedFormat = format;
}
public override void ExecuteResult(ControllerContext context)
{
this.currentUrl = context.RequestContext.HttpContext.Request.Url;
base.ExecuteResult(context);
}
protected override void WriteFile(HttpResponseBase response)
{
Feed = new SyndicationFeed("Feed title", "Feed description", this.currentUrl, GetSyndicationItems());
Feed.Authors.Add(new SyndicationPerson("my@email.com", "My Name", "MyBlogUrl"));
Save(XmlWriter.Create(response.Output));
}
private void Save(XmlWriter writer)
{
switch (this.feedFormat)
{
case FeedFormat.Atom10:
Feed.SaveAsAtom10(writer);
break;
case FeedFormat.Rss20:
Feed.SaveAsRss20(writer);
break;
default:
Feed.SaveAsRss20(writer);
break;
}
writer.Close();
}
private List GetSyndicationItems()
{
var items = new List();
foreach (var post in this.items)
{
var item = new SyndicationItem
{
Title = SyndicationContent.CreatePlaintextContent(post.Title),
Content = SyndicationContent.CreateHtmlContent(post.Body),
Id = post.PostId.ToString(),
PublishDate = post.Posted,
LastUpdatedTime = post.Posted
};
item.AddPermalink(new Uri(post.Permalink));
items.Add(item);
}
return items;
}
}
public enum FeedFormat
{
Atom10,
Rss20
}
Mislim da kod FeedResult-a dovoljno sam sebe objašnjava, treba samo obratiti pažnju na dve metode koje su preklopljne: WriteFile i ExecuteResult. ExecuteResult je metoda nasleđena od ActionResult-a i nju okida ControllerActionInvoker. WriteFile je nasleđena on FileResult klase i ona je odgovorna za pisanje sadržaja fajla u response. U slučaju nekog fajla to bi bio niz bajtova, a u našem slučaju to je sadržaj XML fajla (Atom ili RSS feed).