Tech Musings Just another tip's, tricks, and how-to's blog

15Feb/116

Orchard Shape Wizardry

While working on my employer's new website, we discovered that we had a need that didn't seem to fit into the Widget system for Orchard. Several places throughout our site, we need to be able to trigger the creation of a shape from a template, but have that shape populated with data through a controller of sorts. After a lengthy discussion with Bertrand Le Roy (who is ever so helpful), I stumbled upon the solution that we were looking for: IShapeTableProvider.

Inside of my template somewhere, I can make a call like so:
WorkContext.Layout.BeforeHeader.Add(New.HeaderBar(), "1");
This creates a new shape called HeaderBar and adds it to the BeforeHeader zone I have configured in our theme. Elsewhere (we have it in a common module we use to contain widgets & shapes that may be site-wide but don't make sense to keep in the template), I can create a shape called HeaderBar.cshtml and the shape factory will automatically use that Razor template to display the shape. However, now we need to provide data to the shape.

To do so, I created a ShapeProviders namespace in our common module and added the following class:

public class HeaderBar : IShapeTableProvider {
        private readonly IWorkContextAccessor _workContextAccessor;
        private readonly IHttpContextAccessor _httpContextAccessor;

        // not injected the usual way because this component is a 'static' dependency and any services that consume data are per-request
        private ICaseService CaseService
        {
            get
            {
                return _workContextAccessor.GetContext(_httpContextAccessor.Current()).Resolve<ICaseService>();
            }
        }
        public HeaderBar(IWorkContextAccessor workContextAccessor, IHttpContextAccessor httpContextAccessor)
        {
            _workContextAccessor = workContextAccessor;
            _httpContextAccessor = httpContextAccessor;
        }
        public void Discover(ShapeTableBuilder builder)
        {
            builder.Describe("HeaderBar").OnCreated(created =>
            {
                created.Shape.Cases = CaseService.GetFeaturedCases().Slice(0,4).ToList();                

            });
        }
    }

So what this is doing is describing the HeaderBar shape that we're using, and hooking into the OnCreated event for this shape. This allows us to call into a service and grab the data we need to populate the shape and assign it to the shape.

One thing I need to note, and this took me an entire day to discover, is that you can't just inject your data service into your IShapeTableProvider implementation. If you do, it'll try to use a transaction out of scope and that will cause you all kinds of problems. After tearing my hair out for hours on end, I finally discovered what the Orchard team is doing inside of the CoreShapes class: The trick to resolve a service dependency within the function itself by using a property that loads the service per-request.

Once I did that, I was able to use the data within my Razor view easily:

@using Orchard.ContentManagement;
@using Orchard.Core.Routable.Models;
@{
    var cases = Model.Cases;

}
Current Cases:
    @foreach (var c in cases) { var item = c as ContentItem; var route = item.As();
  • @route.Title
  • |
  • }
  • More...
Get Email Updates | Already Registered? Login
Comments (6) Trackbacks (0)
  1. Why don’t you use the standard convention for injecting your service, i.e. using the constructor invoke. The way you are doing it is when you class is not IDependency, and I know where you took the example from ;) But here it doesn’t make sense, and it would be simpler actually.

    • Sebastien,
      When I do that, it causes an exception saying that the operation is not permitted in the current state of the transaction. That was what I initially tried to do, and banged my head against a wall all day to figure out why it didn’t work. When my IShapeTableProvider is initially consumed at application start, there is no valid transaction and it threw an exception saying there was an improper ordering of transaction scope. Then any subsequent request would complain about not having a valid transaction state. The only way to make it work was to inject a service on a per request basis so it had a valid transaction in the service. If you know a better way to do it, hit me! :)

  2. I think injecting a Work would do exactly what you want

  3. Generics are stripped … I meant Work[ICaseService]

  4. I don’t think Work<> was available when I wrote this, was it?

  5. Great, this info was exactly that I needed, thanks.

    I didn’t know about the Work class either, that makes for more cleaner & safer code!


Leave a comment

(required)

No trackbacks yet.