HtmlWidget - customizing per zone

Topics: Customizing Orchard, Writing themes
Mar 16, 2011 at 3:32 PM

Hello,

I'm creating my custom theme and I need to implement different html stucture for HtmlWidget depending on zone.

I know I can override file: Widget-Featured.cshtml but what with the rest of files tied with widget
- Widget.ControlWrapper.cshtml
- Widget.Wrapper.cshtml

Can I override them as well or schould I implement all logic in Widget-Featured.cshtml and ignore them?

Coordinator
Mar 16, 2011 at 6:18 PM

Sure, you should be able to override those.

Mar 16, 2011 at 11:14 PM

Can you tell me how it can be accomplished?
I analyzed this part of docoumentation http://www.orchardproject.net/docs/Accessing-and-rendering-shapes.ashx but
it seems that Widget.ControlWrapper-Feautured.cshtml and Widget.Wrapper-Feautured.cshtml has no effect.

How should I construct file name in this case?

Coordinator
Mar 17, 2011 at 12:40 AM

I misunderstood, I thought you just wanted to override the template for all widget wrappers. We do not currently have alternates for those wrappers. What you can do is override the widget template for a specific zone (widget-myzone.cshtml) or for a specific widget type (widget-mywidgettype.cshtml).

Developer
Apr 28, 2011 at 9:45 PM

It can actually easily be done if you create a class that implements IShapeTableProvider. For my site, I'm using 3 RecentBlogPosts widgets in 3 different zones. For one zone, I required custom rendering (a tickertape displaying recent posts of a specific blog).
What I needed was an extra Alternate that targeted the RecentBlogPosts widget and a specific zone.
The following class generates exactly such alternates:

public class Shapes : IShapeTableProvider
    {

        public void Discover(ShapeTableBuilder builder)
        {
            builder.Describe("Parts_Blogs_RecentBlogPosts").OnDisplaying(displaying =>
            {
                if (displaying.ShapeMetadata.DisplayType == "Detail")
                {
                    ContentItem contentItem = displaying.Shape.ContentItem;
                    if (contentItem != null)
                    {
                        var zoneName = contentItem.As<WidgetPart>().Zone;
                        displaying.ShapeMetadata.Alternates.Add("Parts_Blogs_RecentBlogPosts__" + zoneName);
                    }
                }
            });
        }
    }

Put this class somewhere in your theme (any module will do actually).

For example, if you put the widget in a zone named "ContentAside" then create a shape named "Parts.Blogs.RecentBlogPosts-ContentAside.cshtml"
What would be nice is to be able to make this shape provider more generic, so that we could for example use the Placement.info file to create more alternates that target specific types of widgets for specific zones. 


Developer
Apr 28, 2011 at 9:54 PM
Edited Apr 28, 2011 at 10:12 PM

If we could somehow describe "Widget" and get the ContentType name, this custom shape provider could be generalized. Something like:

public class Shapes : IShapeTableProvider
    {

        public void Discover(ShapeTableBuilder builder)
        {
            builder.Describe("Widget").OnDisplaying(displaying =>
            {
                if (displaying.ShapeMetadata.DisplayType == "Detail")
                {
                    ContentItem contentItem = displaying.Shape.ContentItem;
                    if (contentItem != null)
                    {
                        var zoneName = contentItem.As<WidgetPart>().Zone;
                        var shapeName = displaying.ShapeMetaData.Type; // (don't know yet if this works, will try out)
                        displaying.ShapeMetadata.Alternates.Add(shapeName + "__" + zoneName);
                    }
                }
            });
        }
    }

 

Coordinator
Apr 28, 2011 at 9:59 PM

I think it's doable, and might be provided as a feature, like fpr Url Alternates.

Bertrand, can you check the elligibility of it ? Or a Contrib module if not ....

Coordinator
Apr 28, 2011 at 10:00 PM

File a bug. (you knew I was going to say that don't you?)

Apr 28, 2011 at 10:03 PM

Hi All,

Workitem is created http://orchard.codeplex.com/workitem/17784

and here is the fork http://orchard.codeplex.com/SourceControl/network/Forks/ipavlovi/CustomWidgetWrapperForSpecificZone

 

 

Developer
Apr 29, 2011 at 12:00 AM
Edited Apr 29, 2011 at 12:04 AM

Nice! And I found a working generic solution:

public class WidgetAlternatesFactory : ShapeDisplayEvents
    {
        public override void Displaying(ShapeDisplayingContext context)
        {
            context.ShapeMetadata.OnDisplaying(displayedContext =>
            {
                // We don't want the widget itself, but the content item that consists of the Widget part (e.g. Parts.Blogs.RecentBlogPosts)
                if(displayedContext.ShapeMetadata.Type != "Widget")
                {
                    ContentItem contentItem = displayedContext.Shape.ContentItem;
                    if (contentItem != null)
                    {
		      // Is the contentItem a widget? (we could probably test for the stereotype setting, don't know if that is more efficient than selecting the WidgetPart)
                        var widgetPart = contentItem.As<WidgetPart>();
                        if (widgetPart != null)
                        {
                            var zoneName = widgetPart.Zone;
                            var shapeName = displayedContext.ShapeMetadata.Type;
 
			// Add 2 alternates for flexible shape naming, e.g.
// [ShapeName]-[ZoneName].cshtml: "Parts.Blogs.RecentBlogPosts-myZoneName.cshtml"
// [ContentTypeName]-[ZoneName].cshtml: "RecentBlogPosts-myZoneName.cshtml"                             displayedContext.ShapeMetadata.Alternates.Add(contentItem.ContentType + "__" + zoneName);                             displayedContext.ShapeMetadata.Alternates.Add(shapeName + "__" + zoneName);                         }                        }                 }             });         }

 This could be submitted to the Orchard core team for consideration taking into one of the existing modules (perhaps to be enabled as a feature).

Developer
Apr 29, 2011 at 2:28 AM

Above code has been submitted to the afore mentioned fork: http://orchard.codeplex.com/SourceControl/network/Forks/ipavlovi/CustomWidgetWrapperForSpecificZone

Jun 29, 2011 at 5:57 PM

Could a similar thing be done but within a Parts_ContainerWidget, i.e. I want to provide an alternate shape but only to Location content types that are in the Footer Zone:

 

public void Discover(ShapeTableBuilder builder){
            builder.Describe("Parts_ContainerWidget").OnDisplaying(displaying =>
            {
                var items = displaying.Shape.ContentItems.Items as IList<dynamic>;
                if (items != null)
                {
                    var item = items.FirstOrDefault();
                    if (item == null) return;
                    ContentItem contentItem = item.ContentItem;
                    if (contentItem.ContentType == "Location")
                    {
                        displaying.ShapeMetadata.Alternates.Add("Parts_ContainerWidget__List__Location");
                    }
                }
            });
        }

 

I need to get the Zone that the Parts_ContainerWidget is in and append it to the alternate shape but I cannot find a way of doing this.

Coordinator
Jun 29, 2011 at 7:25 PM

Sure, those shapes are just one or two levels down the tree, but the container part itself could transmit the zone information down through alternates of the items, following a technique similar as http://weblogs.asp.net/bleroy/archive/2011/05/23/orchard-list-customization-first-item-template.aspx.

Jun 30, 2011 at 12:20 PM

Hi, I have had another look but still cannot see a way of getting the Zone that the Parts_ContainerWidget is in. Can this expose a parent property that provides the parent Widget so the zone can be extracted as per the example provided by sfmskywalker?

var zoneName = contentItem.As<WidgetPart>().Zone;
Or would it be better to Describe the Widget itself to find the Zone and use the alternative to provide different markup?

Coordinator
Jun 30, 2011 at 7:58 PM

There is no parent property on most shapes at the moment, which is why in my post I'm acting at the list level. In your case, you should be able to do As<WidgetPart>() on the container part and then get the Zone from that.

Jul 6, 2011 at 3:57 PM

Hello, still unable to work this out. 

I am unable to access any properties than can be applied As<WidgetPart> within this code.

The scenario is that I have footer that displays on all pages and contains a widget of containable 'Location'. I have also added a widget of containable 'Location' on the body of a page. As a result both lists (in content and footer) on this page use the same shape and I cannot find a way of determining the shape per zone. This is in regards to the the 'layout zone', not local zones as in content.cshtml.

I must be missing something fundamental here as this must be a very common scenario. I would be really grateful if someone could point me in the right direction.

Coordinator
Jul 7, 2011 at 12:25 AM

How does sfmskywalker's solution not work for you?

Jul 7, 2011 at 11:11 AM

It does work for me but I need it to be a bit more explicit. Currently I can get shapes such as Widget-Footer and ContainerWidget-Footer but what I need is for this to be named based on the type that is contained in the Container Widget i.e. Location-Footer or ContainerWidget-Location-Footer, 'Location' being the list. I cannot find a way of getting to this from the displayedContent.Shape.ContentItem - I am currently using the ID of the widget or the Name, but this is not very stable, if the widget is renamed, or deleted and re-added this would break the view.

Is the only way of doing this within the view itself rather than the shape provider, i.e. check the type in the view and change the markup accordingly?

Jul 7, 2011 at 12:20 PM

i.e. 

I have added an alternate shape called Widget-ContainerWidget-Content.cshtml. This gets the list items type then displays a different shape based on the type.

@{
    Model.Heading = "header";
    Model.Classes.Add("small");
    Model.Classes.Remove("widget");
    Model.Classes.Remove("widget-container-widget");
    // get content type
    var type = Model.Content.Items[1].ContentItems.Items[0].ContentItem.ContentType;
}
@if (type != null)
{
    if (type == "Links")
    {
        @Display.Content_Small_Links(Content: Model.Content)
    }
    else { 
        @Display(Model.Content)
    }
}
else { 
    @Display(Model.Content)
}

Coordinator
Jul 7, 2011 at 8:18 PM

Well, if it's the whole widget template that you want to alternate and not just a part, that should be even easier. In a shape table provider, describe the widget shape and add an alternate that is including the content type and the zone. Look at the code in Orchard.Widgets.Shapes. The last two lines are creating alternates for the zone and the content type. In your own provider, do one that combines them:

displaying.ShapeMetadata.Alternates.Add("Widget__"+ zoneName + "_" + contentItem.ContentType);