How do I create custom logic to use IShapeTemplateHarvester?

Topics: General, Writing modules, Writing themes
Mar 14, 2013 at 8:37 PM
In this THREAD, there is discussion about storing shapes in sub-folders to avoid having a large amount of .cshtml files in the root of the ~/Views folder.

Subsequently, this WORK-ITEM was closed out with explanation that "You can implement your custom logic by implementing IShapeTemplateHarvester."

I've searched around for examples of how to do exactly that but had no luck. Do I just copy the file over to my theme and re-work it to add those sub-folders I want to utilize? Or is there some other way to do that. Much appreciated.
Mar 14, 2013 at 9:00 PM
Edited Mar 15, 2013 at 11:48 AM
You might be able to inherit BasicShapeTemplateHarvester and override the SubPaths method.

I'm not sure if Orchard will pick it up automatically (by virtue of it implementing IShapeTemplateHarvester) and use it in conjunction with BasicShapeTemplateHarvester, or if you will have to suppress the dependency using an attribute on your custom class to override BasicShapeTemplateHarvester:
[OrchardSuppressDependency("Orchard.DisplayManagement.Descriptors.ShapeTemplateStrategy.BasicShapeTemplateHarvester")] 
public class RemesqShapeTemplateHarvester : BasicShapeTemplateHarvester { 
    override IEnumerable<string> SubPaths() {
        var paths = base.SubPaths().ToList();
        paths.Add("Views/CustomFolder1");
        paths.Add("Views/CustomFolder2");
        return paths; 
    }
}
Note, the above code is not tested. Just my thought at what might work.

EDIT: added "public class" to the code sample above to make it more clear.
Mar 15, 2013 at 12:44 AM
TM, thanks for the suggestion. Haven't tried yet. Will see if that works.

Out of curiosity, if you suppress the dependency (and I am imagining that means suppressing the entire class), the won't you take away the entire ability of the class? I.e., there is no method of doing what the class is supposed to, including looking at the normal folders it already looks to. That's why I was thinking it would just have to be extended. Let me test out your suggestion without the suppression.
Mar 15, 2013 at 11:47 AM
Well the idea was that you would suppress the BasicShapeTemplateHarvester class, and replace it with your subclass. Your subclass would override one method, adding folders to the SubPaths() method on top of what the super class (BasicShapeTemplateHarvester) would return. So you wouldn't lose any functionality.

I haven't tried this yet, but I think it should work. Hope this helps. Let us know what you try and how it works out.
Mar 15, 2013 at 3:37 PM
Edited Mar 15, 2013 at 3:52 PM
Well, after trying this a bunch of different ways, I'm unable to get this working. I'm not a programmer, so I can't figure out what's wrong. Thanks for the help.

Here is what I ultimately tried. NOTE: I commented out the portion you suggested to try something different because I was getting a Visual Studio warning that I was hiding "SubPaths()" by suppressing and therefore if I intended to hide it I should use "new" (which is what you see in my code "override new IEnumerable...":
 [OrchardSuppressDependency("Orchard.DisplayManagement.Descriptors.ShapeTemplateStrategy.BasicShapeTemplateHarvester")]
    public class RemesqTemplateHarvester : BasicShapeTemplateHarvester
    {
        public new IEnumerable<string> SubPaths()
        {
            // Tried your suggest, but I got the same errors. NOTE: I got the "new" warning for "SubPaths() above
            //var paths = base.SubPaths().ToList();
            //paths.Add("Views/CustomFolder1");
            //paths.Add("Views/CustomFolder2");
            //return paths;
            // Below is what appears in original file, so I thought I'd try it and add my new folders:
            return new[] { "Views", "Views/Items", "Views/Parts", "Views/Fields", "Views/CustomFolder1", "Views/CustomFolder2" };
        }
    }
I had this in a controller:
public ActionResult MyShape()
{
        var shape = _orchardServices.New.MyShape();
        return new ShapeResult(this, shape);
}
And I added the .cshtml file to ~/Views/CustomFolder1/MyShape.cshtml.

I also tried
var shape = _orchardServices.New.CustomFolder1_MyShape();
In the end I got the same errors:
Shape type CustomFolder1_MyShape not found
Also NOTE: I receive the above error if I have a ~/CustomFolder1/MyShape RouteDescriptor in my Routes.cs file. If I delete that descriptor, I get a 404, resource not found, error. So I am not sure if I am doing something wrong (refer to the second sentence in this reply). :)
Mar 15, 2013 at 5:09 PM
I tested this out, and got it to work. Here's what I did:
public ShapeResult MyShape() {
    var shape = _services.New.CustomFolder_MyShape();
    return new ShapeResult(this, shape); 
}
    [OrchardSuppressDependency("Orchard.DisplayManagement.Descriptors.ShapeTemplateStrategy.BasicShapeTemplateHarvester")]
    public class MyShapeTemplateHarvester : BasicShapeTemplateHarvester, IShapeTemplateHarvester {
        public new IEnumerable<string> SubPaths() {
            return new[] { "Views", "Views/Items", "Views/Parts", "Views/Fields", "Views/CustomFolder" };
        }
    }
(And make sure the template file exists at: /orchard.web/modules/YOUR_MODULE/Views/CustomFolder/MyShape.cshtml)

Basically the missing part from my earlier post is that you have to declare that the custom IShapeTemplateHarvester class implements the IShapeTemplateHarvester interface. I thought it would work without the explicit interface declaration since the parent class implements that same interface but I guess not. If anyone knows why it works this way please let me know.
Mar 15, 2013 at 6:53 PM
Edited Mar 15, 2013 at 7:29 PM
Awesome! That did work (I have a follow-up below). I hope others find this useful because I know I've seen a lot of discussion about it.

Also note that your original suggestion will also work (and I guess is "cleaner"):
        public new IEnumerable<string> SubPaths()
        {
            var paths = base.SubPaths().ToList();
            paths.Add("Views/CustomFolder");
            return paths;
        }
Here is my follow-up (if I can impose further):

I have a customized theme. I can have 1) "~/My.Module/Views/Index.cshtml" and also 2) "~/MyCustomTheme/My.Module/Views/Index.cshtml".

In the latter case, the module's Index.cshtml is being overridden by my customized theme. I love this because I could also have a controller in My.Module with an ActionResult of "Something", have no "Something.cshtml" in my module, but have it instead in the theme and it gets rendered.

However, in our scenario, I have placed two "MyShape.cshtml" files (one in module, and one in theme), and only the module's gets rendered.

I copied the class file to my theme and tried changing the path to "Views/My.Module/CustomFoler" in the SubPaths() but am having no luck, my module's MyShape.cshtml is still getting rendered.

Is it possible to do the same thing with the shape as you are able to do with views from a theme (I am assuming so because you can mimic a module's parts, etc. and get the rendering)?

Thanks again for the help. I really hope this is useful to others.
Mar 15, 2013 at 7:40 PM
You're right about using paths.Add() instead of totally replacing the base class' return list with your own (this way if the base class' .SubPaths() implementation changes you inherit those changes.

I'm not sure about the theme question. I would think that this would work, I'm not sure why it doesn't. Does your theme declare your module as a dependency in Theme.txt? If not, try that and see if it helps.
Mar 15, 2013 at 8:40 PM
The last trick didn't work.
Mar 16, 2013 at 5:22 AM
Although I wasn't able to figure out how to how to do the same thing with the shape as you were able to do with views from a theme (as per my follow-up question), I was able to work around the situation.

Instead of trying to display MyShape directly from the theme, what I wanted to do was have, for example, an Index.cshtml in my theme that would have HTML markup surrounding MyShape so I could also display MyOtherShape together with it. So in Index.cshtml (from the theme) I just did @Display.CustomFolder_MyShape(). Now when I navigate to ~/Index it shows MyShape (and MyOtherShape).

I mention this because it's just crazy to me how I've got a module that's using a theme's view (not really an "override" because I can use the theme's view with no corresponding view in the module to override), which is then going back to the module for a shape to display. This helps because I can have other modules use the theme and have distinct web-sites providing different services. The shape itself will be unique to the module.

Thanks!

FYI: This is possible because I am using a ThemeSelector class with a ModuleFilter class similar to what has been discussed about overriding the AdminFilter class (Orchard.Framework.UI.Admin). If this is overkill or not the right way, please chime in.