Nested Projections - Is Sanity Check Needed

Topics: General
Aug 15, 2012 at 3:25 AM
Edited Aug 15, 2012 at 1:14 PM

I am a big fan of the Projector Module so I wanted to see if I could do "Nested Projections". What this means is that I have a Query that is returning a Content Type that contains a Projection so you end up with results like this:

  • Level 1
    • Level 1a
    • Level 1b
  • Level 2
    • Level 2a
    • Level 2b
    • Level 2c
  • Level ...

In short, it does pose a risk of a long standing query (and yes, it poses a risk of an infinite loop. However, the Preview of the Query skips right over the "sanity check" line in "Orchard.Projections.Drivers.ProjectionPartDriver" (line 114 with comment on sanity check - love the comment) and works perfectly (assuming there is no infinite loop of course).  Because I love the results so much, I feel one of two things should be done:

  • Fix it so that the Query Preview does not tease me by showing the nested list
  • Make a way for Projector to be nested that prevents inifinite loops

I am just now looking into how infinite loops could be prevented and am wondering whether anyone else has ideas (or would also desire to have this be available in the Projector module):

  • Idea 1: A persistent counter that terminates the process after so many iterations
    • This could even be defaulted to off/0 iterations and the user would have to override the setting in the Projector module (or globally) with the number of iterations they accept since this also impacts performance

I welcome anyone elses thoughts on anything I might have missed. 

Aug 15, 2012 at 5:56 PM

I have made some progress on the coding front and need some assistance. 

I have edited "Orchard.Projections.Drivers.ProjectionPartDriver" as seen here:

// Check to see if the contentItem has already been used 
//  - if it has, remove it and continue on
if (ProjectionGlobal.ProcessedContentItems.Count > 0) {
    // Use for loop since we cannot remove items inside a foreach loop since enumerator is locked
    for (int i = contentItems.Count - 1; i >= 0; i--) {
        if (ProjectionGlobal.ProcessedContentItems.Contains(contentItems[i])) {
            contentItems.Remove(contentItems[i]);
        }
    }
}
// Add the current ContentItems into the global variable for the next round
ProjectionGlobal.ProcessedContentItems.AddRange(contentItems);

// ORIGINAL CODE
// sanity check so that content items with ProjectionPart can't be added here, or it will result in an infinite loop
//contentItems = contentItems.Where(x => !x.Has<ProjectionPart>()).ToList();

The only part that is not visible in this view is the ProjectionGlobal.ProcessedContentItems and that "global" class is here:

public class ProjectionGlobal {
    public static List<ContentItem> ProcessedContentItems = new List<ContentItem>();
}

As you can see, this is a "global" variable to store the content items that have already been processed. By recording all of the content items that have been utilized I can later remove them if they come up again (and thus prevent an infinite loop).  Everything works great (I have set up a nested loop and proven it works to prevent it).  However, here is the issue:

  • The "global" variable is across all tenants which would cause an issue later on
  • There is no great spot to clear the contents of ProcessedContentItems to remove the data in memory (so they will cause an issue later)

In short, I need a better tool than a "global" variable. I need something that is tenant aware and clears itself with each page loading cycle. If anyone can give me some advice I think this could be finished pretty easily.

Aug 15, 2012 at 8:08 PM
Edited Aug 15, 2012 at 8:09 PM

Why not just use a local variable?  Or, if you really want it in a separate class, just create an interface implementing IDependency to ensure that it's a per-request instance.  Then, inject it in the constructor.

Aug 17, 2012 at 5:01 PM

Hi Brandon,

Because I am losing it. You are absolutely right, just used a local variable and problem done.  Here is the final code for anyone looking to use this (or maybe it could be used in Orchard.Projections even):

// Check to see if the contentItem has already been used - if it has, remove it and continue on
if (ProcessedContentItems.Count > 0) {
    // Use for loop since we cannot remove items inside a foreach loop since enumerator is locked
    for (int i = contentItems.Count - 1; i >= 0; i--) {
        if (ProcessedContentItems.Contains(contentItems[i])) {
            contentItems.Remove(contentItems[i]);
        }
    }
}
// Add the current ContentItems into the class variable for the next round
ProcessedContentItems.AddRange(contentItems);

// ORIGINAL CODE
// sanity check so that content items with ProjectionPart can't be added here, or it will result in an infinite loop
//contentItems = contentItems.Where(x => !x.Has<ProjectionPart>()).ToList();

This only other bit of information that is needed is to include the class variable as seen here:

protected List ProcessedContentItems = new List();

Thanks Brandon for pointing out what should have been obvious to me.

Oct 5, 2012 at 2:10 PM

Final comments from observations after using this a while. The code above presents one problem, it hides content items that would appear twice. This is problematic when you have a widget that projects the same content items as another projection on the page (as they will be filtered out).  My last bit of comfort in getting a working solution is to just limit the number of levels for which Projections reveals nested projections for - here is the code for that:

At the top of the file (outside any methods) - of course, you can change your "MaximumLevel" to however many nested levels you want:

        // EMS CHANGE - start
        protected List<ContentItem> ProcessedContentItems = new List<ContentItem>();
        int CurrentLevel = 0;
        const int MaximumLevel = 1;
        // EMS CHANGE - end

At the start of "Display"):

            // EMS CHANGE - start
            if (CurrentLevel >= MaximumLevel) {
                return null;
            }
            // EMS CHANGE - end

Inside the ContentShape area:

                    // EMS CHANGE - start
                    CurrentLevel++;

                    try {

                        // ORIGINAL CODE
                        // sanity check so that content items with ProjectionPart can't be added here, or it will result in an infinite loop
                        //contentItems = contentItems.Where(x => !x.Has<ProjectionPart>()).ToList();

                        // EMS CHANGE - end

Note that I have the try { so at the bottom of the ContentShape area I have the finally (to ensure the CurrentLevel is reduced):

                        // EMS CHAGE - start
                    }
                    finally {
                        CurrentLevel--;
                    }
                    // EMS CHANGE - end

And that does it, no more removing content items, just limiting the number of nested projections that can appear.  This would go nicely into a Projections Setting (though I have not coded it yet).

 

Developer
Feb 25, 2013 at 5:13 PM
I just came across a scenario where nested projections would be extremely useful:

I have a single page website where the Homepage is a ProjectionPage, which projects all pages as a series of slides.
One slide is a custom content type which itself has a ProjectionPart that projects a number of VIPS.

Limiting the number of nested Projections looks like a better solution then simply skipping a nested projection entirely.

I created a workitem here: http://orchard.codeplex.com/workitem/19486