From 49a9c6cc98fcdd20c662186914c1bf71be0cad81 Mon Sep 17 00:00:00 2001 From: Suha Can Date: Tue, 2 Mar 2010 12:10:49 -0800 Subject: [PATCH] - Finish up live writer support for blogs. --HG-- branch : dev --- .../Core/XmlRpc/Controllers/HomeController.cs | 2 +- .../Core/XmlRpc/Services/XmlRpcWriter.cs | 3 +- src/Orchard.Web/Core/XmlRpc/XmlRpcContext.cs | 2 + .../Controllers/BlogController.cs | 51 ++++- .../Extensions/UrlHelperExtensions.cs | 5 + .../Modules/Orchard.Blogs/Models/BlogPost.cs | 10 + .../Modules/Orchard.Blogs/Routes.cs | 16 ++ .../Orchard.Blogs/Services/XmlRpcHandler.cs | 213 +++++++++++++----- .../DisplayTemplates/Items/Blogs.Blog.ascx | 1 + .../Items/Blogs.BlogPost.Summary.ascx | 2 +- .../Items/Blogs.BlogPost.SummaryAdmin.ascx | 2 +- .../DisplayTemplates/Items/Blogs.Blog.ascx | 1 + .../Items/Blogs.BlogPost.Summary.ascx | 2 +- .../DisplayTemplates/Items/Blogs.Blog.ascx | 1 + .../Items/Blogs.BlogPost.Summary.ascx | 2 +- .../Mvc/Extensions/UrlHelperExtensions.cs | 15 ++ src/Orchard/Orchard.csproj | 1 + src/Orchard/UI/Resources/LinkEntry.cs | 1 + src/Orchard/UI/Resources/ResourceManager.cs | 7 + 19 files changed, 276 insertions(+), 61 deletions(-) create mode 100644 src/Orchard/Mvc/Extensions/UrlHelperExtensions.cs diff --git a/src/Orchard.Web/Core/XmlRpc/Controllers/HomeController.cs b/src/Orchard.Web/Core/XmlRpc/Controllers/HomeController.cs index 3c2fe192e..632b304f6 100644 --- a/src/Orchard.Web/Core/XmlRpc/Controllers/HomeController.cs +++ b/src/Orchard.Web/Core/XmlRpc/Controllers/HomeController.cs @@ -40,7 +40,7 @@ namespace Orchard.Core.XmlRpc.Controllers { } private XRpcMethodResponse Dispatch(XRpcMethodCall request) { - var context = new XmlRpcContext { HttpContext = HttpContext, Request = request }; + var context = new XmlRpcContext { ControllerContext = ControllerContext, HttpContext = HttpContext, Request = request }; foreach (var handler in _xmlRpcHandlers) handler.Process(context); return context.Response; diff --git a/src/Orchard.Web/Core/XmlRpc/Services/XmlRpcWriter.cs b/src/Orchard.Web/Core/XmlRpc/Services/XmlRpcWriter.cs index 13c4ec58b..e2d3c419f 100644 --- a/src/Orchard.Web/Core/XmlRpc/Services/XmlRpcWriter.cs +++ b/src/Orchard.Web/Core/XmlRpc/Services/XmlRpcWriter.cs @@ -16,9 +16,10 @@ namespace Orchard.Core.XmlRpc.Services { { {typeof(int), p=>new XElement("int", (int)p.Value)}, {typeof(bool), p=>new XElement("boolean", (bool)p.Value?"1":"0")}, - {typeof(string), p=>new XElement("string", (string)p.Value)}, + {typeof(string), p=>new XElement("string", p.Value)}, {typeof(double), p=>new XElement("double", (double)p.Value)}, {typeof(DateTime), p=>new XElement("dateTime.iso8601", ((DateTime)p.Value).ToString("o"))}, + {typeof(DateTime?), p=>new XElement("dateTime.iso8601", ((DateTime?)p.Value).Value.ToString("o"))}, {typeof(byte[]), p=>new XElement("base64", Convert.ToBase64String((byte[])p.Value))}, {typeof(XRpcStruct), p=>Map((XRpcStruct)p.Value)}, {typeof(XRpcArray), p=>Map((XRpcArray)p.Value)}, diff --git a/src/Orchard.Web/Core/XmlRpc/XmlRpcContext.cs b/src/Orchard.Web/Core/XmlRpc/XmlRpcContext.cs index b71f4643c..a8d8d52ed 100644 --- a/src/Orchard.Web/Core/XmlRpc/XmlRpcContext.cs +++ b/src/Orchard.Web/Core/XmlRpc/XmlRpcContext.cs @@ -1,8 +1,10 @@ using System.Web; +using System.Web.Mvc; using Orchard.Core.XmlRpc.Models; namespace Orchard.Core.XmlRpc { public class XmlRpcContext { + public ControllerContext ControllerContext { get; set; } public HttpContextBase HttpContext { get; set; } public XRpcMethodCall Request { get; set; } public XRpcMethodResponse Response { get; set; } diff --git a/src/Orchard.Web/Modules/Orchard.Blogs/Controllers/BlogController.cs b/src/Orchard.Web/Modules/Orchard.Blogs/Controllers/BlogController.cs index 8411c6b89..529217c4b 100644 --- a/src/Orchard.Web/Modules/Orchard.Blogs/Controllers/BlogController.cs +++ b/src/Orchard.Web/Modules/Orchard.Blogs/Controllers/BlogController.cs @@ -1,6 +1,7 @@ using System.Linq; using System.Web; using System.Web.Mvc; +using System.Web.Routing; using System.Xml.Linq; using Orchard.Blogs.Extensions; using Orchard.Blogs.Models; @@ -15,14 +16,17 @@ namespace Orchard.Blogs.Controllers { private readonly IOrchardServices _services; private readonly IBlogService _blogService; private readonly IFeedManager _feedManager; + private readonly RouteCollection _routeCollection; public BlogController( IOrchardServices services, IBlogService blogService, - IFeedManager feedManager) { + IFeedManager feedManager, + RouteCollection routeCollection) { _services = services; _blogService = blogService; _feedManager = feedManager; + _routeCollection = routeCollection; Logger = NullLogger.Instance; } @@ -52,15 +56,22 @@ namespace Orchard.Blogs.Controllers { return View(model); } - public ActionResult LiveWriterManifest() { + public ActionResult LiveWriterManifest(string blogSlug) { Logger.Debug("Live Writer Manifest requested"); + Blog blog = _blogService.Get(blogSlug); + + if (blog == null) + return new NotFoundResult(); + const string manifestUri = "http://schemas.microsoft.com/wlw/manifest/weblog"; var options = new XElement( XName.Get("options", manifestUri), new XElement(XName.Get("clientType", manifestUri), "Metaweblog"), - new XElement(XName.Get("supportsSlug", manifestUri), "Yes")); + new XElement(XName.Get("supportsSlug", manifestUri), "Yes"), + new XElement(XName.Get("supportsKeywords", manifestUri), "Yes")); + var doc = new XDocument(new XElement( XName.Get("manifest", manifestUri), @@ -69,5 +80,39 @@ namespace Orchard.Blogs.Controllers { Response.Cache.SetCacheability(HttpCacheability.NoCache); return Content(doc.ToString(), "text/xml"); } + + public ActionResult Rsd(string blogSlug) { + Logger.Debug("RSD requested"); + + Blog blog = _blogService.Get(blogSlug); + + if (blog == null) + return new NotFoundResult(); + + const string manifestUri = "http://archipelago.phrasewise.com/rsd"; + + var urlHelper = new UrlHelper(ControllerContext.RequestContext, _routeCollection); + var url = urlHelper.Action("", "", new { Area = "XmlRpc" }); + + var options = new XElement( + XName.Get("service", manifestUri), + new XElement(XName.Get("engineName", manifestUri), "Orchar CMS"), + new XElement(XName.Get("engineLink", manifestUri), "http://orchardproject.net"), + new XElement(XName.Get("homePageLink", manifestUri), "http://orchardproject.net"), + new XElement(XName.Get("apis", manifestUri), + new XElement(XName.Get("api", manifestUri), + new XAttribute("name", "MetaWeblog"), + new XAttribute("preferred", true), + new XAttribute("apiLink", url), + new XAttribute("blogID", blog.Id)))); + + var doc = new XDocument(new XElement( + XName.Get("rsd", manifestUri), + new XAttribute("version", "1.0"), + options)); + + Response.Cache.SetCacheability(HttpCacheability.NoCache); + return Content(doc.ToString(), "text/xml"); + } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Blogs/Extensions/UrlHelperExtensions.cs b/src/Orchard.Web/Modules/Orchard.Blogs/Extensions/UrlHelperExtensions.cs index 30cfda0fe..f1affc2f4 100644 --- a/src/Orchard.Web/Modules/Orchard.Blogs/Extensions/UrlHelperExtensions.cs +++ b/src/Orchard.Web/Modules/Orchard.Blogs/Extensions/UrlHelperExtensions.cs @@ -1,4 +1,5 @@ using System.Web.Mvc; +using Orchard.Mvc.Extensions; namespace Orchard.Blogs.Extensions { public static class UrlHelperExtensions { @@ -18,6 +19,10 @@ namespace Orchard.Blogs.Extensions { return urlHelper.Action("LiveWriterManifest", "Blog", new { blogSlug, area = "Orchard.Blogs" }); } + public static string BlogRsd(this UrlHelper urlHelper, string blogSlug) { + return urlHelper.AbsoluteAction(() => urlHelper.Action("Rsd", "Blog", new { blogSlug, area = "Orchard.Blogs" })); + } + public static string BlogArchiveYear(this UrlHelper urlHelper, string blogSlug, int year) { return urlHelper.Action("ListByArchive", "BlogPost", new { blogSlug, archiveData = year.ToString(), area = "Orchard.Blogs" }); } diff --git a/src/Orchard.Web/Modules/Orchard.Blogs/Models/BlogPost.cs b/src/Orchard.Web/Modules/Orchard.Blogs/Models/BlogPost.cs index cf4ca0672..5f53fb84e 100644 --- a/src/Orchard.Web/Modules/Orchard.Blogs/Models/BlogPost.cs +++ b/src/Orchard.Web/Modules/Orchard.Blogs/Models/BlogPost.cs @@ -14,6 +14,7 @@ namespace Orchard.Blogs.Models { public string Title { get { return this.As().Title; } + set { this.As().Title = value; } } public string Slug { @@ -21,6 +22,11 @@ namespace Orchard.Blogs.Models { set { this.As().Slug = value; } } + public string Text { + get { return this.As().Text; } + set { this.As().Text = value; } + } + public Blog Blog { get { return this.As().Container.As(); } set { this.As().Container = value; } @@ -50,6 +56,10 @@ namespace Orchard.Blogs.Models { } } + public DateTime? CreatedUtc { + get { return this.As().CreatedUtc; } + } + public DateTime? PublishedUtc { get { return this.As().VersionPublishedUtc; } } diff --git a/src/Orchard.Web/Modules/Orchard.Blogs/Routes.cs b/src/Orchard.Web/Modules/Orchard.Blogs/Routes.cs index 60997e830..d1798f8b6 100644 --- a/src/Orchard.Web/Modules/Orchard.Blogs/Routes.cs +++ b/src/Orchard.Web/Modules/Orchard.Blogs/Routes.cs @@ -208,6 +208,22 @@ namespace Orchard.Blogs { }, new MvcRouteHandler()) }, + new RouteDescriptor { + Route = new Route( + "{blogSlug}/rsd", + new RouteValueDictionary { + {"area", "Orchard.Blogs"}, + {"controller", "Blog"}, + {"action", "Rsd"} + }, + new RouteValueDictionary { + {"blogSlug", new IsBlogConstraint(_containerProvider)} + }, + new RouteValueDictionary { + {"area", "Orchard.Blogs"} + }, + new MvcRouteHandler()) + }, new RouteDescriptor { Route = new Route( "{blogSlug}/{postSlug}", diff --git a/src/Orchard.Web/Modules/Orchard.Blogs/Services/XmlRpcHandler.cs b/src/Orchard.Web/Modules/Orchard.Blogs/Services/XmlRpcHandler.cs index ec788b7c1..888a315ac 100644 --- a/src/Orchard.Web/Modules/Orchard.Blogs/Services/XmlRpcHandler.cs +++ b/src/Orchard.Web/Modules/Orchard.Blogs/Services/XmlRpcHandler.cs @@ -1,38 +1,60 @@ using System; +using System.Linq; +using System.Web.Mvc; +using System.Web.Routing; +using Orchard.Blogs.Controllers; +using Orchard.Blogs.Models; +using Orchard.ContentManagement; using Orchard.Core.XmlRpc; using Orchard.Core.XmlRpc.Models; using Orchard.Logging; +using Orchard.Mvc.Extensions; +using Orchard.Security; +using Orchard.Blogs.Extensions; namespace Orchard.Blogs.Services { public class XmlRpcHandler : IXmlRpcHandler { private readonly IBlogService _blogService; + private readonly IBlogPostService _blogPostService; + private readonly IContentManager _contentManager; + private readonly IAuthorizationService _authorizationService; + private readonly IMembershipService _membershipService; + private readonly RouteCollection _routeCollection; - public XmlRpcHandler(IBlogService blogService) { + public XmlRpcHandler(IBlogService blogService, IBlogPostService blogPostService, IContentManager contentManager, + IAuthorizationService authorizationService, IMembershipService membershipService, + RouteCollection routeCollection) { _blogService = blogService; + _blogPostService = blogPostService; + _contentManager = contentManager; + _authorizationService = authorizationService; + _membershipService = membershipService; + _routeCollection = routeCollection; Logger = NullLogger.Instance; } public ILogger Logger { get; set; } public void Process(XmlRpcContext context) { - var uriBuilder = new UriBuilder(context.HttpContext.Request.Url) { - Path = - context.HttpContext.Request. - ApplicationPath, - Query = string.Empty - }; - + var urlHelper = new UrlHelper(context.ControllerContext.RequestContext, _routeCollection); if (context.Request.MethodName == "blogger.getUsersBlogs") { - var a = new XRpcArray(); - foreach (var blog in _blogService.Get()) { - a.Add(new XRpcStruct() - .Set("url", uriBuilder.Path + blog.Slug) - .Set("blogid", blog.Id) - .Set("blogName", blog.Name)); - } + var result = MetaWeblogGetUserBlogs(urlHelper, + Convert.ToString(context.Request.Params[0].Value), + Convert.ToString(context.Request.Params[1].Value), + Convert.ToString(context.Request.Params[2].Value)); - context.Response = new XRpcMethodResponse().Add(a); + context.Response = new XRpcMethodResponse().Add(result); + } + + if (context.Request.MethodName == "metaWeblog.getRecentPosts") { + var result = MetaWeblogGetRecentPosts(urlHelper, + Convert.ToString(context.Request.Params[0].Value), + Convert.ToString(context.Request.Params[1].Value), + Convert.ToString(context.Request.Params[2].Value), + Convert.ToInt32(context.Request.Params[3].Value)); + + context.Response = new XRpcMethodResponse().Add(result); } if (context.Request.MethodName == "metaWeblog.newPost") { @@ -48,6 +70,7 @@ namespace Orchard.Blogs.Services { if (context.Request.MethodName == "metaWeblog.getPost") { var result = MetaWeblogGetPost( + urlHelper, Convert.ToInt32(context.Request.Params[0].Value), Convert.ToString(context.Request.Params[1].Value), Convert.ToString(context.Request.Params[2].Value)); @@ -63,76 +86,162 @@ namespace Orchard.Blogs.Services { Convert.ToBoolean(context.Request.Params[4].Value)); context.Response = new XRpcMethodResponse().Add(result); } + + if (context.Request.MethodName == "blogger.deletePost") { + var result = MetaWeblogDeletePost( + Convert.ToString(context.Request.Params[0].Value), + Convert.ToString(context.Request.Params[1].Value), + Convert.ToString(context.Request.Params[2].Value), + Convert.ToString(context.Request.Params[3].Value), + Convert.ToBoolean(context.Request.Params[4].Value)); + context.Response = new XRpcMethodResponse().Add(result); + } + } + + private XRpcArray MetaWeblogGetUserBlogs(UrlHelper urlHelper, + string appkey, + string userName, + string password) { + + var user = _membershipService.ValidateUser(userName, password); + _authorizationService.CheckAccess(StandardPermissions.AccessFrontEnd, user, null); + + var array = new XRpcArray(); + foreach (var blog in _blogService.Get()) { + array.Add(new XRpcStruct() + .Set("url", urlHelper.AbsoluteAction(() => urlHelper.Blog(blog.Slug))) + .Set("blogid", blog.Id) + .Set("blogName", blog.Name)); + } + return array; + } + + private XRpcArray MetaWeblogGetRecentPosts( + UrlHelper urlHelper, + string blogId, + string userName, + string password, + int numberOfPosts) { + + var user = _membershipService.ValidateUser(userName, password); + _authorizationService.CheckAccess(StandardPermissions.AccessFrontEnd, user, null); + + var blog = _contentManager.Get(Convert.ToInt32(blogId)); + if (blog == null) + throw new ArgumentException(); + + var array = new XRpcArray(); + foreach (var blogPost in _blogPostService.Get(blog).Take(numberOfPosts)) { + array.Add(CreateBlogStruct(blogPost, urlHelper)); + } + return array; } private int MetaWeblogNewPost( string blogId, - string user, + string userName, string password, XRpcStruct content, bool publish) { - //var title = content.Optional("title"); - //var description = content.Optional("description"); - //var pageRevision = _pageManager.CreatePage(new CreatePageParams(title, null, "TwoColumns")); - //pageRevision.Contents.First().Content = description; + var user = _membershipService.ValidateUser(userName, password); + _authorizationService.CheckAccess(Permissions.EditBlogPost, user, null); - //if (publish) { - // if (string.IsNullOrEmpty(pageRevision.Slug)) - // pageRevision.Slug = "slug" + pageRevision.Page.Id; + var blog = _contentManager.Get(Convert.ToInt32(blogId)); + if (blog == null) + throw new ArgumentException(); - // _pageManager.Publish(pageRevision, new PublishOptions()); - //} + var title = content.Optional("title"); + var description = content.Optional("description"); + var slug = content.Optional("wp_slug"); - //return pageRevision.Page.Id; - return 1; + var blogPost = _contentManager.New(BlogPostDriver.ContentType.Name); + blogPost.Blog = blog; + blogPost.Title = title; + blogPost.Slug = slug; + blogPost.Text = description; + + _contentManager.Create(blogPost.ContentItem, VersionOptions.Draft); + + if (publish) + _blogPostService.Publish(blogPost); + + return blogPost.Id; } private XRpcStruct MetaWeblogGetPost( + UrlHelper urlHelper, int postId, - string user, + string userName, string password) { - //var pageRevision = _pageManager.GetLastRevision(postId); + var user = _membershipService.ValidateUser(userName, password); + _authorizationService.CheckAccess(StandardPermissions.AccessFrontEnd, user, null); - //var url = "http://localhost/orchard/" + pageRevision.Slug; - //return new XRpcStruct() - // .Set("userid", 37) - // .Set("postid", pageRevision.Page.Id) - // .Set("description", pageRevision.Contents.First().Content) - // .Set("title", pageRevision.Title) - // .Set("link", url) - // .Set("permaLink", url); + var blogPost = _blogPostService.Get(postId); + if (blogPost == null) + throw new ArgumentException(); - throw new NotImplementedException(); + return CreateBlogStruct(blogPost, urlHelper); } private bool MetaWeblogEditPost( int postId, - string user, + string userName, string password, XRpcStruct content, bool publish) { - //var pageRevision = _pageManager.AcquireDraft(postId); + var user = _membershipService.ValidateUser(userName, password); + _authorizationService.CheckAccess(StandardPermissions.AccessFrontEnd, user, null); - //var title = content.Optional("title"); - //var description = content.Optional("description"); + var blogPost = _blogPostService.Get(postId, VersionOptions.DraftRequired); + if (blogPost == null) + throw new ArgumentException(); - //pageRevision.Title = title; - //pageRevision.Contents.First().Content = description; - //if (publish) { - // if (string.IsNullOrEmpty(pageRevision.Slug)) - // pageRevision.Slug = "slug" + postId; + var title = content.Optional("title"); + var description = content.Optional("description"); + var slug = content.Optional("wp_slug"); - // _pageManager.Publish(pageRevision, new PublishOptions()); - //} + blogPost.Title = title; + blogPost.Slug = slug; + blogPost.Text = description; - //return true; + if (publish) { + _blogPostService.Publish(blogPost); + } - throw new NotImplementedException(); + return true; } + private bool MetaWeblogDeletePost( + string appkey, + string postId, + string userName, + string password, + bool publish) { + var user = _membershipService.ValidateUser(userName, password); + _authorizationService.CheckAccess(StandardPermissions.AccessFrontEnd, user, null); + + var blogPost = _blogPostService.Get(Convert.ToInt32(postId), VersionOptions.Latest); + if (blogPost == null) + throw new ArgumentException(); + + _blogPostService.Delete(blogPost); + return true; + } + + private static XRpcStruct CreateBlogStruct(BlogPost blogPost, UrlHelper urlHelper) { + var url = urlHelper.AbsoluteAction(() => urlHelper.BlogPost(blogPost.Blog.Slug, blogPost.Slug)); + return new XRpcStruct() + .Set("postid", blogPost.Id) + .Set("dateCreated", blogPost.CreatedUtc) + .Set("title", blogPost.Title) + .Set("wp_slug", blogPost.Slug) + .Set("description", blogPost.Text) + .Set("link", url) + .Set("permaLink", url); + } } } diff --git a/src/Orchard.Web/Modules/Orchard.Blogs/Views/DisplayTemplates/Items/Blogs.Blog.ascx b/src/Orchard.Web/Modules/Orchard.Blogs/Views/DisplayTemplates/Items/Blogs.Blog.ascx index ce92b3036..d1080fe6f 100644 --- a/src/Orchard.Web/Modules/Orchard.Blogs/Views/DisplayTemplates/Items/Blogs.Blog.ascx +++ b/src/Orchard.Web/Modules/Orchard.Blogs/Views/DisplayTemplates/Items/Blogs.Blog.ascx @@ -5,5 +5,6 @@ <%@ Import Namespace="Orchard.Blogs.Models"%>

<%=Html.TitleForPage(Model.Item.Name) %>

<% Html.RegisterLink(new LinkEntry { Rel = "wlwmanifest", Type = "application/wlwmanifest+xml", Href = Url.BlogLiveWriterManifest(Model.Item.Slug) });%> +<% Html.RegisterLink(new LinkEntry { Rel = "EditURI", Type = "application/rsd+xml", Title = "RSD", Href = Url.BlogRsd(Model.Item.Slug) });%> <% Html.Zone("primary", ":manage :metadata"); Html.ZonesAny(); %> \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Blogs/Views/DisplayTemplates/Items/Blogs.BlogPost.Summary.ascx b/src/Orchard.Web/Modules/Orchard.Blogs/Views/DisplayTemplates/Items/Blogs.BlogPost.Summary.ascx index afdb28469..469d4fdfa 100644 --- a/src/Orchard.Web/Modules/Orchard.Blogs/Views/DisplayTemplates/Items/Blogs.BlogPost.Summary.ascx +++ b/src/Orchard.Web/Modules/Orchard.Blogs/Views/DisplayTemplates/Items/Blogs.BlogPost.Summary.ascx @@ -6,4 +6,4 @@ <%@ Import Namespace="Orchard.Blogs.Models"%>

<%=Html.Link(Html.Encode(Model.Item.Title), Url.BlogPost(Model.Item.Blog.Slug, Model.Item.Slug)) %>

<%=Html.PublishedState(Model.Item) %> | <%Html.Zone("meta");%>
-
<%=Model.Item.As().Text ?? string.Format("

{0}

", _Encoded("there's no content for this blog post"))%>
\ No newline at end of file +
<%=Model.Item.Text ?? string.Format("

{0}

", _Encoded("there's no content for this blog post"))%>
diff --git a/src/Orchard.Web/Modules/Orchard.Blogs/Views/DisplayTemplates/Items/Blogs.BlogPost.SummaryAdmin.ascx b/src/Orchard.Web/Modules/Orchard.Blogs/Views/DisplayTemplates/Items/Blogs.BlogPost.SummaryAdmin.ascx index 4b4f39c76..875db73d2 100644 --- a/src/Orchard.Web/Modules/Orchard.Blogs/Views/DisplayTemplates/Items/Blogs.BlogPost.SummaryAdmin.ascx +++ b/src/Orchard.Web/Modules/Orchard.Blogs/Views/DisplayTemplates/Items/Blogs.BlogPost.SummaryAdmin.ascx @@ -36,7 +36,7 @@ <%=_Encoded("Last modified: ") + Html.DateTimeRelative(Model.Item.As().ModifiedUtc.Value) %><% } %> |  -
  • <%=_Encoded("By {0}", Model.Item.Creator.UserName)%>
  • +
  • <%=_Encoded("By {0}", Model.Item.Creator == null ? String.Empty : Model.Item.Creator.UserName)%>