Widget without a record or part?

Topics: Writing modules
May 6, 2011 at 3:05 PM

Is it possible to create a widget with just a driver and display template? IE: without a ContentPart or ContentPartRecord?

I have need for a widget that has no configuration or settings etc, you add it to the page, the driver should build a shape based on some external data, and the template then renders it.

Cheers
Tony 

May 6, 2011 at 3:10 PM

You need a ContentPart to build a driver.

However there are lots of other ways to hook into shape building.

What you should probably do is write your own result filter and just push your own shape directly into whichever zone you need. This is how Orchard.Widgets actually adds all the widget layers to the page.

May 6, 2011 at 3:22 PM

Thanks, I'm guessing that approach won't let site admins use the widget/layer manager to choose where and when the widget should appear. In that case I guess I'd be better with just an empty ContentPart and ContentPartRecord?

Cheers

May 6, 2011 at 3:27 PM

Exactly. If you want it to work properly with the admin system then you have to follow the prescribed structure. Like I say there are a number of other ways you could approach it, but none of them would give you the full UI features. It's a "have your cake and eat it" thing ;)

May 6, 2011 at 3:29 PM

Thanks mate, will opt to eat the cake ;)

Coordinator
May 6, 2011 at 6:24 PM

Although a widget does not require a specific part, so it doesn't necessitate a driver either. If your widget is just an assemblage of existing parts plus some specific logic, it can be implemented as a migration that creates the widget type, and a handler that specializes the shape on the fly. No driver, part or record required.

May 9, 2011 at 8:24 AM

Thanks guys, will take a look at just using a handler and a migration. Any examples of this in the code?

May 9, 2011 at 11:02 AM

You could look at Orchard.Core/Shapes/CoreShapes.cs for examples of using IShapeTableProvider to customise shapes, you could hook into the Widget shape (but check the ContentItem is the same type as your target widget, since those events get run for everything.) Actually another way would be to write your own driver for WidgetPart.

May 9, 2011 at 11:26 AM

Thanks for the guidance, can you validate this is correct? I've built the following in my module;

migrations.cs

 

ContentDefinitionManager.AlterTypeDefinition("BasketInfoWidget",
		cfg => cfg
			.WithPart("WidgetPart")
			.WithPart("CommonPart")
			.WithSetting("Stereotype", "Widget"));

basketInfoWidgetHandler.cs

 

public class BasketInfoWidgetHandler : ContentHandler
{
	protected override void BuildDisplayShape(BuildDisplayContext context)
	{
		base.BuildDisplayShape(context);
		if (context.ContentItem.ContentType == "BasketInfoWidget")
		{
			context.Shape.BasketCount = 5;
			context.Shape.BasketValue = "549.99";
		}
	}
}

Widget-BasketInfoWidget.cshtml

@if (Model.BasketCount < 1)
{
	<div>Your shopping basket it empty</div>
}
else
{
	<div>Your shopping basket contains @Model.BasketCount items (@Model.BasketValue)</div>
}

This appears to be working correctly, but is this the recommended approach? If so, it kind of leads into my other thread where I need to get this same widget rendered from my custom controller (http://orchard.codeplex.com/discussions/256646)

Thanks for helping a newbie out. I'm hoping to take all this (and answers to all my earlier questions) and share it in a series of blog posts to recreate the site I'm building.

May 9, 2011 at 12:09 PM

** Solved **

Thanks for the help. In order to get this working, and to allow the shape to be rendered from a custom controller action, I simply did this;

1. Created a migration to define my partless, driverless widget;

ContentDefinitionManager.AlterTypeDefinition("BasketInfoWidget",
			cfg => cfg
				.WithPart("WidgetPart")
				.WithPart("CommonPart")
				.WithSetting("Stereotype", "Widget"));

2. Created a handler to inject my custom shape whenever this widget is rendered;

	public class BasketInfoWidgetHandler : ContentHandler
	{
		private readonly IOrchardServices _orchard;

		public BasketInfoWidgetHandler(IOrchardServices orchard)
		{
			_orchard = orchard;
		}

		protected override void BuildDisplayShape(BuildDisplayContext context)
		{
			base.BuildDisplayShape(context);
			
			if (context.ContentItem.ContentType == "BasketInfoWidget")
			{
				dynamic packageDisplay = _orchard.New.BasketInfo(
					BasketCount: 10,
					BasketTotal: 699.57
				);

				context.Shape.BasketInfo = packageDisplay;
			}
		}
	}

3. Created a template for the widget itself that just chains into rendering the custom shape (MyModule/Views/Widget-BasketInfoWidget.cshtml);

@Display(Model.BasketInfo)

4. Created a template for the BasketInfo shape (MyModule/Views/BasketInfo.cshtml);

@if (Model.BasketCount < 1)
{
	<div>Your shopping basket it empty</div>
}
else
{
	<div>Your shopping basket contains @Model.BasketCount items (@Model.BasketTotal)</div>
}

Now, adding the basket info part onto a page renders as expected. Next job, getting a custom controller action to render the same thing turned out to be quite easy.

5. In my custom controller;

/// <summary>
/// Partial view that will render the basket summary
/// - number of items in the basket and total value
/// </summary>
/// <returns></returns>
public ActionResult BasketSummary()
{
	dynamic packageDisplay = _orchard.New.BasketInfo(
		BasketCount: 10,
		BasketTotal: 689.57
		);
			
	return new ShapeResult(this, packageDisplay); 
}

The end result - my widget can be added to pages, the info shape can be themed, and I can render the same, themed partial from my custom action using AJAX.

Firstly, thanks for all the help, secondly can someone "in the know" (bertrand/pete) just validate this approach is sound.

Cheers

Tony

May 9, 2011 at 12:15 PM

What's not recommended about this, is that you've completely overidden the Widget template. Therefore if you (or anyone using this feature if you packaged as a module) wanted to add extra parts to your widget, they wouldn't get rendered.

Additionally, a more normal usage of ContentHandler is to attach to display events instead of overriding the methods (I don't know if this really matters too much!)

I'd be inclined to suggest something like this:

public class BasketInfoWidgetHandler : ContentHandler
{
        public BasketInfoWidgetHandler(){
                OnDisplaying(context=>{
                        
		if (context.ContentItem.ContentType == "BasketInfoWidget")
		{
                        context.Shape.Zones["Content"].Add(
                                context.New.BasketInfo(BasketCount:5,BasketValue:"549.99"),"5");
		}
                });
        }
}

What's going on there is creating an entirely new shape called BasketInfo. Then you can rename Widget-BasketInfoWidget.cshtml to BasketInfo.cshtml. Finally you can also render that shape from your controller but I'm not sure the best way to get a partial view of a shape. If you don't have any luck experimenting I can have a look. I was writing an AJAX system but it worked by wrapping around shapes and capturing the data during a normal page render, for something this simple you possibly want a more direct method anyway.

May 9, 2011 at 12:24 PM

I posted the above before I saw your last message and yes, that's exactly what I was suggesting about building the shape. Just see my other notes.

Also, you didn't really need an IOrchardServices, since BuildDisplayContext has a New property which is also an IShapeFactory. You can also just inject IShapeFactory directly.

The only other problem with the code I showed (i.e. pushing the shape straight into a Zone) is that you can't use Placement.info to position BasketInfo within your widget. To get around that you'd want to use a ContentPartDriver<WidgetPart> and use a normal ContentShape return.

May 9, 2011 at 12:47 PM

Can't seem to find the OnDisplaying event within the ContentHandler....

May 9, 2011 at 12:52 PM

So, what I've done now, is got rid of the widget template entirely (so it uses the default rendering, and therefore additional parts should get rendered if I understand correctly) and then changed my handler as follows;

public class BasketInfoWidgetHandler : ContentHandler
	{
		protected override void BuildDisplayShape(BuildDisplayContext context)
		{
			base.BuildDisplayShape(context);

			if (context.ContentItem.ContentType == "BasketInfoWidget")
			{
				dynamic packageDisplay = context.New.BasketInfo(
					BasketCount: 10,
					BasketTotal: 699.57
				);

				context.Shape.Zones["Content"].Add(packageDisplay);
			}
		}
	}

So now, I think standard widget rendering would kick in, which in turn would render any other associated parts, and by adding the new shape into the local content zone, I assume placement.info could be used. Only think outstanding therefore I think is how to move from overriding BuildDisplayShape to using an event....

Thanks

Tony

May 9, 2011 at 1:27 PM

Yes I was mistaken about OnDisplaying, that's only available when using an IShapeTableProvider (instead of a Handler).

Placement.info won't be taken into account. You're adding the shape directly into the zone, so the placement process is "skipped". Placement usually happens when DriverResults are applied. In this case you're not using a DriverResult, because you're not using a driver. It's pretty complicated to utilise placement outside of drivers, I've actually done it for certain bits of MediaGarden.

May 9, 2011 at 2:26 PM

Fair enough on that last point, I think, all things considered, I can live without the placement.info for this particular widget - it delivers on the other items now (thanks to your help), namely;

1. Used as a standard widget

2. Is themeable

3. Can be rendered from a custom controller action as a partial view to update it with AJAX

All in all, pretty happy with it. Going to build the actual functionality now (BasketService) so it does something useful. Once I'm done I'll write all this up on a blog post or two.

Cheers

May 20, 2011 at 9:59 AM

I try doing the exact same things but the MyModule/Views/Widget-BasketInfoWidget.cshtml is not used.

With the Shape Tracing feature, I see that it look for this file only in the Theme/Views directory and not in the module view directory. If I create the alternate template in theme directory, then it works.


Active Template
~/Themes/TheThemeMachine/Views/Widget-BasketInfoWidget.cshtml
Original Template Template
~/Modules/Orchard.Widgets/Views/Widget.cshtml

Am I missing something ?

Coordinator
May 20, 2011 at 6:16 PM

Why don't you do BasketInfo.cshtml?

May 20, 2011 at 7:27 PM

I have it too and removing the Widget-BasketInfoWidget.cshtml works. I have done it because ukdevcom did it

ukdevcom wrote:
3. Created a template for the widget itself that just chains into rendering the custom shape (MyModule/Views/Widget-BasketInfoWidget.cshtml);
@Display(Model.BasketInfo)
May 20, 2011 at 7:42 PM

Yup - I actually have both.... point 3 renders the widget straight up, forget AJAX, it just renders the widget by deferring to the BasketInfo.cshtml (see point 4). Point 4 is used both in the widget rendering and then when it refreshes the widget using AJAX to re-create the html fragment......

I wrote: 

3. Created a template for the widget itself that just chains into rendering the custom shape (MyModule/Views/Widget-BasketInfoWidget.cshtml);

 @Display(Model.BasketInfo)

and then:

4. Created a template for the BasketInfo shape (MyModule/Views/BasketInfo.cshtml);

@if (Model.BasketCount < 1)
{
	<div>Your shopping basket it empty</div>
}
else
{
	<div>Your shopping basket contains @Model.BasketCount items (@Model.BasketTotal)</div>
}
Hope that helps
May 20, 2011 at 8:11 PM
Edited May 20, 2011 at 8:11 PM

So if I understand it well, your ajax controller use the BasketInfo.cshtml as a partial view and the widget handler use the Widget-BasketInfoWidget.cshtml as its view (which defer to the common partial view BasketInfo.cshtml).

If that's right, why having the widget view file in my module clear the output and having the same file but in the theme view folder makes it work. Without it, it also works and correctly render the BasketInfo.cshtml.

May 20, 2011 at 8:25 PM

Yes, that's how it's happening in the above, but on reflection, you probably don't need the widget template - the widget itself isn't going to render anything, the basketInfo shape is injected into the shapes from the content handler, and it will use it's template as normal, so...... yep, you probably don't need the widget template, just basketinfo.cshtml would be enough.....

Sep 22, 2011 at 9:52 AM

Hello, I am working with orchard for not so long time and not so good to ajax, so sorry for a newbie question:

I am trying to make a widget with the same concepsion: it should be updated from external links. I've read many topics here to know how to acsess controller throw url without updateing page.

But how can i update the whole widget ?

Coordinator
Sep 22, 2011 at 6:56 PM

What do you mean?

Sep 22, 2011 at 8:35 PM

I mean that if i call a controller like one from this theme via ajax:

public ActionResult BasketSummary()
{
	dynamic packageDisplay = _orchard.New.BasketInfo(
		BasketCount: 10,
		BasketTotal: 689.57
		);
			
	return new ShapeResult(this, packageDisplay); 
}

If i am wright, it returns themed shape. But i can't put it into div.

How to access a content html string in returning object ?

Coordinator
Sep 22, 2011 at 8:57 PM

Did you try a JsonResult?

Sep 22, 2011 at 9:06 PM

Yes, but as i understand - it returns a set of paramatrs. I already have some examples how that works, but i wanted to get themed widget.

 

However i am not sure, what's better for server efficiency: 

-to return the whole widget with several included divs and all parametrs inside

-or to return a set of ~7 parametrs and update each of them using ajax ?

Coordinator
Sep 22, 2011 at 9:12 PM

The second option is a million times better.

Sep 22, 2011 at 9:18 PM

Okay, than i will use that.

Thanks a lot! 

Oct 27, 2011 at 6:00 PM

I have a similar problem to the original post. In our system we have promotions that we want to manage through the orchard CMS so I have a content part to represent each promotion and have it assigned to a content type with route and body so each promotion can have a page by default. This all works great. Next step is to create a widget that will display the current promotion for the user. The problem is I don't want it backed by a promotion part because if I do that every time I create a version of that widget I'm creating a NEW promotion not selecting from an existing list. Also I can't do the selecting because the promotion management (other than the content of the promotion) is done through a 3rd party. So what I need to do is call a webservice to identify what promotion should be loaded then load that promotion content part and display it. So what I'm doing right now is this.

        protected override DriverResult Display(PromotionWidgetPart part, string displayType, dynamic shapeHelper) {
            var promotion = _promotionService.GetHeroPromotionForUser("someone@example.com");
            return ContentShape("Parts_PromotionPart", () => shapeHelper.Parts_PromotionPart(
                Promotion:promotion
                ));
        }

The content part for the widget is empty and everything is handled in the driver accessing a service. This works great but it feels wrong. It feels like the driver should only be handling display and I should instead be overriding the handler to access the promotionService that gets the results from the 3rd party and then in the handler decide which part to load to give to the widget. Unfortunately I haven't been able to find any documentation on how the handlers actually work so I'm not quite sure what the best way to go about doing this is.

Thanks

Coordinator
Oct 27, 2011 at 7:56 PM

Not at all, this is perfectly all right. The driver is not just supposed to handle display. It's supposed to orchestrate things, like a controller in MVC. Whether the data comes from the part or from somewhere else doesn't really matter. The handler is not particularly better for this, quite the contrary in fact. To continue the MVC parallel, you wouldn't do that in a filter in MVC would you? Well, driver ~= controller, and handler ~= filter.