Page 1 of 1

CMS improvements

Posted: Fri Apr 06, 2012 11:46 am
by bcornelis
While I was trying to work with the current implementation, I thought there are some things that can be improved. So here is my idea.

First of all, it starts with this piece of code:

Code: Select all

<cms:content contentType="Message" contentItemVar="item">
    <c:if test="${numResults > 0}">
        <h3>${item.messageText}</h3>
    </c:if>
</cms:content>

This automatically includes the assumption that for this CMS block, a content element of type "Message" will be placed. As a result, if the customer wants to change the block (instead of a symple text message, he wants an image now) he's not able to do it, cause the code expects the structured content to be an 'instance' of Message. To change this, one needs a code change.
So maybe it's better to allow a 'view' property on the StructuredContent, so that you only provide the cms:content tag in your source code, and the component will handle 'itself'.

Next thing I wanted to create (just a poc) is a rotating banner component, with the advantage that the customer can specify which images it has to include in the admin console. So I started extending the StructuredContentImpl, and doing this like:

Code: Select all

@Entity
@Table(name = "SC_ROTATING_BANNER")
public class RotatingBannerStructuredContent extends StructuredContentImpl {

   private static final long serialVersionUID = -744340426023911601L;

   @OneToMany(fetch = FetchType.LAZY, targetEntity = MediaImpl.class, cascade = { CascadeType.ALL })
   @JoinTable(name = "SC_ROTATING_BANNER_2_MEDIA", joinColumns = @JoinColumn(name = "SC_ROTATING_BANNER_ID"), inverseJoinColumns = @JoinColumn(name = "MEDIA_ID"))
   @Cascade(value = { org.hibernate.annotations.CascadeType.ALL, org.hibernate.annotations.CascadeType.DELETE_ORPHAN })
   @AdminPresentation(friendlyName="Medias")
   protected Set<Media> images = new HashSet<Media>();

   public Set<Media> getImages() {
      return images;
   }

   public void setImages(Set<Media> images) {
      this.images = images;
   }
}

But with this approach I have several issues:
- I cannot create an instance of this class using the admin console, cause the admin console will always create an instance of StructuredContentImpl with the StructuredContentType containing my 'Rotating Banner'.</li>
- I inserted an instance of my rotating banner in the database and opened this in the admin console. This does not raise any exceptions, but still I cannot see my newly added field... (I hope this is not a mistake I made? forgot some annotation or sth?)
- Another thing is that, just before passing those objects to the view, they are converted into their DTO representation. This conversion includes (by default) the conversion of the default fields (specified in the StructredContentImpl) and also the so called 'StructuredContentFields' but not the information I added in my subclass. The problem with this is that, the way I see the current solution, it's only possible to put in text fields (or simple fields) but not real domain objects; so for example link to images, link to some categories, link to some products, ...

Going together with the latest remarq about assigning domain objects to some StructureContent instance; to make the components real components, it would be a good idea to provide them with there own controller, so that if this component needs any logic, it's not nesessary to execute this code in every controller showing up the page containing the component, but the component will take care of it on its own

If you combine the three things: model-view-controller, then we really have a separate component, that we can easily place anywhere on the site, where a cms:content tag has been placed.

You can take it even further: at the moment the cms:content is specified by contentType, or by contentName which is not really flexible. Why not introduce a tag like for example: '<cms:slot id="XXX"/>'. This just defines a content slot on a certain page. Now if some advanged logic is inserted, in the admin console, when opening the page, it would be possible to see a list of those slots on the current page. Using drag and drop the customer can then add components to the slots. So that for example in the admin console, the slot 'XXX' on page 'HOME' contains 3 content elements: an image, a banner and a text component. This makes it really easy to move blocks around, and gives the customer the flexiblity to add/remove/update/... the cms components themselves.

Now I hope two things:
- you get guys get my point
- this counts as a valid 1st post ;)

Re: CMS improvements

Posted: Fri Apr 06, 2012 6:05 pm
by bpolster
Bart,

A lot of good points in your email.
[*]I can see the value in a greater separation of concerns from the view and the content item.
[*]Your extension not being available in the admin could be a defect. I've opened up BLC-414 to take a look.

The existing framework can be used to accomplish the "slot" approach you are looking for. We have two concepts that are used for structured content (contentName and contentType). Both of these can be used for placement. The contentType also determines the fields that will be available. For example, if you check the 'load_content_structure.sql' you'll see how you can create custom content types like 'Message' and 'Home Page Banner Ad' that are included with the demo.

The 'Message' type was intended as an example where the 'contentName' determined the actual placement. We were looking to answer the common use case of business user controlled page fragments (like terms and conditions or other similar messages). Compare that to the example 'Home Page Banner Ad' which is a custom type that defines the fields and the placement. This could have been constructed as a more generic type (such as 'Ad') where the "contentName" would drive its placement.

Your suggestion points out that the approach above limits your ability to provide various "contentTypes" in the same "slot". For example, a home-page-message "slot" that might show text or images or both. There are a number of ways to achieve that with the existing functionality. The easiest would be to define a new structured content type (these are data driven) that had all of the fields you desired and the content editor could only fill out those that made sense for the display such as just providing text or just providing an image. The UI would then need to contain logic to determine what was appropriate to display based on the custom type.

I modified the framework based on your post (1.6.0_SNAPSHOT) to also allow you to utilize the "contentName" field for placement. Prior to that we did not support the use case where "contentName" was used for placement but the "contentType" of the enclosed items might differ. The Taglib and Services now allow you to do this.

As for your rotating banner problem. Broadleaf considers this use case as part of the out-of-box solution. For example, you can have multiple banners each with rules that determine whether or not they are displayed. Of the banners that qualify for display they will be shown in priority order. If the priorities match, the system will randomly rotate the order the banners are displayed.

I believe the system can handle the use cases you presented. I understand that I have not addressed the idea of view aware structured content or admin UI enhancements. Will continue to think about those suggestions.

- Brian

Re: CMS improvements

Posted: Thu Apr 12, 2012 12:42 pm
by bcornelis
To show you how I implement the separate components:

In the view it starts with:

Code: Select all

<cext:component contentId="150"/>


Which executes the following code in the tag:

Code: Select all

// first retrieve the component as specified by the parameters
final StructuredContent structuredContent = getStructuredContentService().findStructuredContentById(150L);

// calculate the bean name
final String controllerBean = new StringBuilder(structuredContent.getStructuredContentType().getName()).append("Controller").toString();
pageContext.setAttribute("cmsComponent", structuredContent, 2);
pageContext.include(new StringBuilder("/cmscomponent/").append(controllerBean).toString());

The last line here is probably the most important one. It will execute the /cmscomponent/controller, which is mapped in the viewresolver. This allows me to use a separate controller for every different component. As a result, the component can be moved everywhere on different pages, without the page having to load/calculate information specific to the component.

Things I'm aware of (but in the end, it's just a POC)
* I specify the contentId as a parameter to the tag. I do this cause as you'll see in the controller code, I want to pass this entity to the model. The only method on the service to allow me to retrieve an entity is by id.
* The bean name that's executed for a certain component is composed as: contentType.name + "Controller"

The base controller:

Code: Select all

public abstract class AbstractCMSComponentController<T extends StructuredContent> implements Controller {

   @Override
   public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
      // retrieve the CMS component from the page context
      final T structuredContent = (T) request.getAttribute("cmsComponent");

      // render the component
      final ModelAndView result = render(request, response, structuredContent);
      
      return new ModelAndView("cmscomponents/" + result.getViewName(), result.getModel());
   }

   protected abstract ModelAndView render(HttpServletRequest request, HttpServletResponse response, T component);
}


Now to add a different component:
first add an entry to the viewresolver

Code: Select all

<prop key="/cmscomponent/RotatingBannerComponentController">ceRotatingBannerComponentController</prop>


Secondly, add the controller:

Code: Select all

@Controller("ceRotatingBannerComponentController")
public class RotatingBannerComponentController extends AbstractCMSComponentController<RotatingBannerStructuredContent> {

   @Override
   protected ModelAndView render(HttpServletRequest request, HttpServletResponse response,
         RotatingBannerStructuredContent component) {
      return new ModelAndView("rotatingBannerComponentView", "component", component);
   }

}


and finally create the view: /WEF-INF/cmscomponents/rotatingBannerComponentView.jsp

This structure allows me to create a lot of different components, manage them in the admin console, place them wherever I want.

Some more improvements:
* One can add a view parameter to the structured content. This way, one controller can be used to render different views
* One can add a controller parameter which specifies the bean name, so that you don't have to use the default controller-name generation strategy
* The reason why I want the view to have access to the entity, is because the default strategy uses *standard* DTO's. I can offcourse extend the existing DTO, add the values I need, but this was out of scope

This brings me to another question: Structured content instances are converted to some DTO representation, this DTO is cached and then passed to the view. Why do we convert to a DTO here? If we pass products,... to the view, there is no DTO representation, and here the entities are cached. Why is this the case for the structured content?