Generating Html for Content Item records

Topics: Core, Writing modules
Jan 18, 2012 at 5:09 PM

Hi there,

I am implementing a controller within Orchard that will serve as a kind of basic Api for Orchard content. The idea is that given an Id or a Slug it will respond with the display markup representing the requested content item. I only want to return the markup specific to the content item - I don't want any layout whatsoever returned. The goal is to be able to embed markup from Orchard into certain parts of our existing web tier until they are reimplemented as Orchard modules.

This is relatively easy to implement for specific content types by checking for the presence of a BodyPart and returning the BodyPart.Text value, but I would like to implement this for any arbitrary content type including Menus and third-party content types. 

Is there a clever way I could make use of the Orchard framework to look up a content item and from that generate the appropriate display markup from within a controller action method? 

Jan 18, 2012 at 5:47 PM

Same as the ordinary ItemController - IContentManager.BuildDisplay(...) - see Core\Contents\Controllers\ItemController ..

Jan 18, 2012 at 5:49 PM

Oh right, didn't see the bit about not having layout.

To do this you create your own implementation of ShapeResult as follows:

    public class ShapePartialResult : PartialViewResult {
        public ShapePartialResult(ControllerBase controller, dynamic shape) {
            ViewData = controller.ViewData;
            TempData = controller.TempData;
            ViewData.Model = shape;
            ViewName = "~/Modules/Downplay.Origami/Views/PartialShapeResult/Display.cshtml";
        }
    }

Then from your controller return new ShapePartialResult(this, shape);

Display.cshtml just looks like this:

@Display(Model)

I'm using this in Origami successfully for some new AJAX behaviour that I need to announce soon :)

Jan 18, 2012 at 5:56 PM

Thanks - I need to know the appropriate view ahead of time though yes? But I'm sure there's a method somewhere that will find the appropriate view for me? :)

Jan 18, 2012 at 5:58 PM

Also I wasn't intending to use a view page anywhere, so I wouldn't have anywhere to call @Display(Model) from.

And I'm having trouble figuring out how to get the shape for a content item.

Jan 18, 2012 at 6:12 PM

You misunderstand - you have to return a View from the controller, in this case the view contains nothing except the @Display which displays the shape you passed into it (which could be any shape). So it's always the same view, but it's required to render the shape to HTML (it's extremely difficult if not practically impossible to do so otherwise).

Jan 18, 2012 at 6:14 PM
Edited Jan 18, 2012 at 6:14 PM

The important thing to notice is that the ShapePartialResult is a PartialViewResult, as opposed to the ordinary ShapeResult which inherits just from ViewResult (otherwise it's identical). By emitting a PartialViewResult you avoid having the rest of the layout and just get your shape.

Jan 18, 2012 at 6:14 PM

If I get it working I will kiss you..

thanks!

Jan 18, 2012 at 6:15 PM
andrewmyhre wrote:

And I'm having trouble figuring out how to get the shape for a content item.

I already said - IContentManager.BuildDisplay(..). Have you looked at the ItemController that I mentioned?

Jan 20, 2012 at 3:54 PM

Note: ShapePartialResult is now in Orchard core on the 1.x branch.

Jan 20, 2012 at 5:35 PM

I owe you a big kiss. Here's my action method:

[ActionName("html-byslug")]
        public ActionResult HtmlFor(string slug)
        {
            var content = _services.ContentManager.Query()
                .Join<RoutePartRecord>()
                .Where(m => m.Slug == slug)
                .Slice(0, 1).FirstOrDefault();

            if (content == null
                || !(content.Has<BodyPart>() || content.Has<MenuWidgetPart>()))
            {
                return HttpNotFound();
            }

            dynamic model = _contentManager.BuildDisplay(content);


            return new ShapePartialResult(this, "~/Modules/MyFeature/Views/Display.cshtml", model);
        }

I'm a little bothered that I have to whitelist content which has the appropriate parts to be able to render, but for now it's great.

The view just has a single @Display(Model)

Very simple!

I was making it more complicated because although there's a lot of sample code to look at it's difficult to connect the dots when dynamics are being passed around the framework rather than concrete types.

Jan 20, 2012 at 5:57 PM

Great stuff! Couple of points:

- I'm not sure why you need to whitelist at all. All content items can be viewed anyway using /Content/Item/{id}.

- Querying by slug is a bad move. For a start, not everything is guaranteed to have RoutePart (e.g. if you wanted to render a Widget). Also Slug is just the end part of the Url; e.g. for my-blog/blog-post, Slug will just be "blog-post". Finally, in Orchard 1.4, RoutePart will get superceded by AutoroutePart. So you're best off just with an Id (I read a comment somewhere that surfacing Ids in the front end is bad, but I think in some cases it's unavoidable. In 1.4 you can use Alias to mask the Id to a named path anyway).

- You don't need to pass the ViewName into ShapePartialResult. See my original code for it - ViewName is a fixed view which only needs to exist once since it's always just @Display(Model), so you can just set the default in ShapePartialResult itself. I guess might want an optional parameter in case you wanted to e.g. wrap the output with some additional HTML.

Anyway, thanks for the big kiss ... I think ;)

Jan 25, 2012 at 12:03 PM

Okay well my story is that I'm embedding Orchard content into views in another ASP.Net site, like so:

@Html.ContentFor("global-navigation")

I work in a large team and this project will likely be inherited by others so readability and transparency is a concern. Also I'm considering that Id's can change (a content manager might delete a piece of content to replace with something new) but the url would generally not change for SEO reasons. Trying to futureproof here.

So with that in mind the migrations class in my feature is explicitly adding RoutePart to a few particular content types, for example HtmlWidget.

I know it's not ideal from a data integrity point of view but with this setup when something does go wrong it's easier to discover what and why...

Anyway, about that kiss...

Jan 25, 2012 at 1:08 PM

Yes I see the problem. Actually a slug is just as much at risk of changing. You're better off using something specially denoted as "identity" and Orchard already includes a part for exactly this purpose: IdentityPart. It has an "Identity" field which can be any string value, you can then query directly on this.

Jan 25, 2012 at 5:34 PM

Well a URL to me is an identity so it should (*should*) amount to the same thing.

Got some weird behaviour going on though - I added the RoutePart to the StyledMenuWidget content type and saved a widget with a url specified. But when I query and have a look at the route part only the Slug is set, the Path value is null...

Any ideas?

I'll try the same with IdentityPart and if that works I'll use that

Jan 25, 2012 at 5:48 PM

Well Identity is practically the same as Id, but without the feature of being memorable. Ah well

Jan 25, 2012 at 6:15 PM

Identity lets you store any string you like; it only generates a Guid if you leave it blank.

Yes, urls are a kind of identity. Actually they make up part of the total identity of an item. But importantly, they're not defined as a unique identifier. For your purposes, yes, jury-rigging this part that's not intended for your purpose might work, but RoutePart implies a lot of other behaviour which you might not want. Identity however is perfect for your purposes and additionally is intended for this purpose.

Have a look at my new Blocks module: it also uses Identity to store and retrieve blocks based on the name you specify.

Jan 25, 2012 at 10:26 PM
Okay thanks for the good summary...

I'll poke around with identities tomorrow, though I don't recall
seeing a new field pop up in the edit form for an identity. Pretty
sure I looked for it...

Will reply tomorrow

On Wed, Jan 25, 2012 at 6:15 PM, [email removed] wrote:
> From: randompete
>
> Identity lets you store any string you like; it only generates a Guid if you
> leave it blank.
>
> Yes, urls are a kind of identity. Actually they make up part of the total
> identity of an item. But importantly, they're not defined as a unique
> identifier. For your purposes, yes, jury-rigging this part that's not
> intended for your purpose might work, but RoutePart implies a lot of
> other behaviour which you might not want. Identity however is perfect for
> your purposes and additionally is intended for this purpose.
>
> Have a look at my new Blocks module: it also uses Identity to store and
> retrieve blocks based on the name you specify.
>
> Read the full discussion online.
>
> To add a post to this discussion, reply to this email
> ([email removed])
>
> To start a new discussion for this project, email
> [email removed]
>
> You are receiving this email because you subscribed to this discussion on
> CodePlex. You can unsubscribe on CodePlex.com.
>
> Please note: Images and attachments will be removed from emails. Any posts
> to this discussion will also be available online at CodePlex.com
Jan 25, 2012 at 11:18 PM

Oh ... Identity doesn't actually have an editor view. I was setting it manually from code. Well, perhaps you could have your controller create an item with correct identity if it doesn't find an existing one ... as a bonus that way there is no chance for your users to accidentally change it ... Then you can edit in the CMS once it's generated.

Well, whatever works for you; this can be solved with a custom part, or providing your own view for the identity part, or maybe with CustomPropertiesPart instead ... I'm only explaining why I personally wouldn't recommend Urls, and particularly not Slug which isn't even unique. (e.g. /blog/my-post and /foo/my-post both have the same Slug 'my-post')