When I create a new browser view following Professional Plone Development, or using ZopeSkel’s ‘paster addcontent view’ local command, I get (basically) the following code:
from zope.interface import implements, Interface from Products.Five import BrowserView from Products.CMFCore.utils import getToolByName from Products.MyProduct import managementMessageFactory as _ class IMyView(Interface): """ IterationUserstoryAssociation view interface """ def test(): """ test method""" class MyView(BrowserView): """ IterationUserstoryAssociation browser view """ implements(IMyView) def __init__(self, context, request): self.context = context self.request = request @property def portal_catalog(self): return getToolByName(self.context, 'portal_catalog') def test(): """ test method """ return "OK"
<browser:page for="*" name="myview" class=".myview.MyView" allowed_interface=".myview.IMyView" template="myview_view.pt" permission="zope.Public" />
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en" metal:use-macro="here/main_template/macros/master" i18n:domain="Products.MyProduct"> <body> <div metal:fill-slot="main">Content Here </div> </body> </html>
This creates a view on any piece of content in the site, accessible from
@@myview. It looks like this (applied to the default Events collection that comes with Plone):
So this is great, and for most views, it’s probably all you need. But sometimes, I’d say most times, you want more from this view. Often we want to segregate our view from Plone, maybe even to the point of using a different slot in the template to toss out most of what
But honestly, I think what we usually want is what Plone already has. Every “regular” view of a content object, and its
folder_contents view, has certain user interface elements that are usually really handy to have available to your user when they are using your view.
Specifically, I’m talking about the content actions, or the “green bar dropdowns”. Here’s a close up so you know what I’m talking about:
In our view, we don’t get these. That really sucks.
After much toil, I’ve found out why, and two different ways to fix it.
Why is this happening?
First, the why. The “content actions” are actually a viewlet within a dedicated viewlet manager. We can see this through the
Along for the ride are the “content views”, which are defined via
<action> tags in our Generic Setup profile (the profiles/default/types/*.xml files, specifically). This isn’t confusing at all, is it (and the fact that the viewlet manager and content views viewlet are both named “plone.contentviews” is just icing on the cake!)?
Recall that a viewlet is registered such that it can/will only show up on a content object that implements a specific interface. Another potential limiting facotr, it turns out (my Zope 3 book was in another building today), is the specific view that the viewlet is rendered on. This has lots of potential uses, and speaks to the sweetness of the Component Architecture, but was very hard to figure out.
So the culprit in the case of our content-action-less view, is plone.app.layout. In plone.app.layout.viewlets’ configure.zcml file, lies the following code:
<browser:viewlet name="plone.contentactions" for="*" view="plone.app.layout.globals.interfaces.IViewView" manager=".interfaces.IContentViews" class=".common.ContentActionsViewlet" permission="zope2.View" /> <browser:viewlet name="plone.contentactions" for="*" view="plone.app.content.browser.interfaces.IFolderContentsView" manager=".interfaces.IContentViews" class=".common.ContentActionsViewlet" permission="zope2.View" />
What this is saying, is that the
plone.contentactions viewlet will only show up for content objects that implement
plone.app.content.browser.interfaces.IFolderContentsView. This is precisely why we don’t get content actions in our view. Our views aren’t implementing either of those interfaces.
The comment a few lines above these declarations very coyly explains things:
<!-- Content actions (menus) The default version is a blank bar; the one with real menus is registered for the main view + folder contents only. -->
So now we get to the two possible solutions. I don’t think one is better than the other, it just depends on how explicit you want to be, and how much you trust plone.app.layout :)
Solution 1: Assign the Content Actions Viewlet Directly to Your Browser View
The first method assigns the viewlet, just as if it was a custom viewlet, to your browser view’s interface. In our case, it would look like this:
<browser:viewlet name="plone.contentactions" for="*" view=".myview.IMyView" manager="plone.app.layout.viewlets.interfaces.IContentViews" class="plone.app.layout.viewlets.common.ContentActionsViewlet" permission="zope2.View" />
The advantage here is that the viewlet will render no matter what happens with
plone.app.layout. It also gives you more flexibility; you can limit the application of the viewlet in lots of different ways. You could combine the
view attribute with the
for attribute to limit it to only certain views that are registered to certain content types. You could create a special interface (instead of using the standard issue interface that represents your view class) that could be used as a marker interface to turn the viewlet on or off depending on an arbitrary state of your views. It could get really interesting.
Solution 2: Implement an Already-Registered Interface in Your View Class (This is What You Really Want to Do)
The easiest, and most straight-forward way to do this is to just add one of the interfaces, already registered to the viewlet, to your view class.
plone.app.content.browser.interfaces.IFolderContentsView. After looking at the interfaces, it becomes obvious that
plone.app.layout.globals.interfaces.IViewView is probably what we want to use. Here’s what the code looks like:
from zope.interface import implements, Interface from Products.Five import BrowserView from Products.CMFCore.utils import getToolByName from plone.app.layout.globals.interfaces import IViewView class IMyView(Interface): """ IterationUserstoryAssociation view interface """ def test(): """ test method""" class MyView(BrowserView): """ IterationUserstoryAssociation browser view """ implements(IMyView, IViewView) def __init__(self, context, request): self.context = context self.request = request @property def portal_catalog(self): return getToolByName(self.context, 'portal_catalog') def test(): """ test method """ return "OK"
The Final Result
Wait, theres (probably) another way
Instead of implementing the interface yourself, you may be able to use ZCML to mark your view class with a registered interface… more on that later.
I mistakenly posted the wrong screen shot in the “The Final Result” section. Fixed 09:00 03/11/2010