TreeControl
Baumstrukturen mit nachladenden Knoten, Kontextaktionen und Auswahl.
TreeControl in 10 Schritten einbauen
Die Tour zeigt, wie sich das Kontrollelement auch nachträglich in eine bestehende Anwendung integrieren lässt.
TreeControl - Gegenstand
Diese Übung demonstriert den Einsatz des TreeControls. Dieses Kontrollelement erzeugt einen Baum, dessen Knoten sich auf- und zugeklappen lassen. Der Programmierer stellt hierfür lediglich die Anzeigedaten (das Datenmodell) durch Implementierung eines einfachen Interfaces bereit.
Das TreeControl bietet die folgenden Features:
- Die Linien auf der obersten Ebene können ein oder ausgeblendet werden. Zu den Knoten und den Blättern lassen sich unterschiedliche Bilder in einer ImageMap hinterlegen. Dabei wird die Zuordnung der Bilder zu dem jeweiligen Baumknoten mit Hilfe von Regulären Ausdrücken vorgenommen.
- Das Kontrollelement verwaltet selbstständig alle notwendigen Zustandsdaten über mehrere Server Roundtrips hinweg. Dazu zählt beispielsweise der auf- oder zugeklappte Status der Baumknoten.
- Vor den Baumeinträgen können Checkboxen ein oder ausgeblendet werden. Bei der Auswahl eines Knotens auf einer unteren Ebene, werden automatisch alle übergeordneten Knoten markiert.
Zum Einsatz des TreeControls sind lediglich folgende Schritte notwendig:
- Auswahl des Designs der Benutzeroberfläche
- Erstellung einer Actionklasse
- Instanziierung eines TreeControls
- Bereitstellung der Anzeigedaten
- Konfiguration des Baumes innerhalb der JSP-Seite
1. Registrierung der Painterfactory
Zuerst erfolgt die Registrierung der Painterfactory. Sie legt fest, welches Design die Benutzeroberfläche erhält. Dies kann in der init()-Methode des Frontcontroler-Servlets applikationsweit geschehen.1 Wir wählen hier das Standarddesign, welches uns der DefaultPainter bereitstellt.2
import javax.servlet.ServletExceptionimport org.apache.struts.action.ActionServlet;import com.cc.framework.ui.painter.PainterFactoryimport com.cc.framework.ui.painter.def.DefPainterFactory;import com.cc.framework.ui.painter.html.HtmlPainterFactory;public class MyFrontController extends ActionServlet { public void init() throws ServletException { super.init(); // Register all Painter Factories // with the preferred GUI-Layout // In this case we only use the Default-Layout. PainterFactory.registerApplicationPainter ( getServletContext(), DefPainterFactory.instance()); PainterFactory.registerApplicationPainter ( getServletContext(), HtmlPainterFactory.instance()); }}
*1) Wenn der einzelne Benutzer zwischen verschiedenen Oberflächendesigns wählen können soll, dann werden zusätzliche PainterFactorys in der Benutzersession registriert. Dies erfolgt meist in der LoginAction mit PainterFactory.registerSessionPainter() im Session Scope.
*2) Weitere Designs (PainterFactories) sind im Lieferumfang der Professional Edition enthalten, oder können selbst entwickelt werden.
2. Ableitung der Action Klasse für den Struts Adapter
In unserem Baum wollen wir Produktgruppen und Produkte anzeigen. Daher soll die Action-Klasse, welche das Laden und Befüllen des TreeControls übernimmt, die Bezeichnung "ProductTreeBrowseAction" tragen. Die Actionklasse wird von der Klasse FWAction abgeleitet, welche die Struts-Action Klasse kapselt und um Funktionalitäten des Präsentationsframeworks erweitert. Dabei wird anstelle der execute()-Methode die doExecute()-Methode aufgerufen. Sie erhält beim Aufruf den ActionContext, über den der Zugriff auf weitere Objekte, wie das Request-, Session- und Response-Objekt gekapselt ist.
import java.io.IOException;import javax.servlet.ServletException;import com.cc.framework.adapter.struts.FWActionimport com.cc.framework.adapter.struts.ActionContextpublic class ProductTreeBrowseAction extends FWAction { /** * @see com.cc.framework.adapter.struts.FWAction#doExecute(ActionContext) */ public void doExecute(ActionContext ctx) throws IOException, ServletException { // In the next chapter, we will instantiate // our TreeControls with the DisplayData }}
3. Instanziierung des TreeControls
Nun wird das TreeControl innerhalb unserer Action instanziiert und mit den Anzeigedaten gefüllt. Das Datenmodell wird dem Kontrollelement über die setDataModel()-Methode zugeordnet. Die Methode nimmt als Argument ein Objekt vom Typ TreeGroupDataModel entgegen. Dabei handelt es sich um ein Interface welches den Zugriff auf die Anzeigedaten des Baumes bereitstellt. Es ist die Aufgabe des Anwendungsentwicklers eine entsprechende Implementierung zur Verfügung zu stellen.
import java.io.IOException;import javax.servlet.ServletException;import com.cc.framework.adapter.struts.ActionContext;import com.cc.framework.adapter.struts.FWAction;import com.cc.framework.ui.control.TreeControl;public class ProductTreeBrowseAction extends FWAction { /** * @see com.cc.framework.adapter.struts.FWAction#doExecute(ActionContext) */ public void doExecute(ActionContext ctx) throws IOException, ServletException { try { // first we get the Data for our Tree ProductGroupDsp data = DBProduct.fetch(); // secondly create the TreeControl and populate it // with the Data to display TreeControl products = new TreeControl(); products.setDataModel(data); // third put the TreeControl into the Session-Object. // Our Control is a statefull Object. ctx.session().setAttribute("products", products); } catch (Throwable t) { ctx.addGlobalError("Error: ", t); } // Display the Page with the Tree ctx.forwardToInput(); }}
4. Bereitstellen der Anzeigedaten
Der Baum besteht aus Gruppen- und Blattknoten. Gruppenknoten können wieder weitere Knoten enthalten (Composite Pattern). Entsprechend stehen für die beiden Knotentypen die Interfaces TreeGroupDataModel und TreeNodeDataModel bereit (TreeGroupDataModel erweitert dabei TreeNodeDatamodel). Mit ihrer Hilfe lässt sich die Baumstruktur einfach erzeugen.
Dabei wird zuerst der Wurzelknoten erzeugt, unter dem dann weitere Gruppen oder Blätter eingehängt werden. Der Wurzelknoten wird dem TreeControl als Datenmodell übergeben.
// RootProductGroupDsp root = new ProductGroupDsp("0", "Products", "Root");ProductGroupDsp group = null;ProductGroupDsp subgroup = null;// First Group under the Root-Elementgroup = new ProductGroupDsp("1201", "Workstations & Monitors");subgroup = new ProductGroupDsp("2102", "Workstations");subgroup.addChild( new ProductDsp("3005", "XWR4000", "product description"));subgroup.addChild( new ProductDsp("3005", "XWR4010", "product description"));group.addChild(subgroup);subgroup = new ProductGroupDsp("2103", "Monitors");subgroup.addChild( new ProductDsp("3101", "ITV 2800") );subgroup.addChild( new ProductDsp("3102", "ITV 2820i") );subgroup.addChild( new ProductDsp("3103", "ITV 3220") );group.addChild(subgroup);root.addChild(group);// Secound Group under the Root-Elementgroup = new ProductGroupDsp("1204", "Printing & Multifunction");root.addChild(group);
Die folgenden Klassen bilden die entsprechenden Modelle für dieses Beispiel ab:
- ProductGroupDsp
- ProductDsp
- ProductBaseDsp
5. Konfiguration des TreeControls innerhalb der JSP-Seite
Um das TreeControl-Tag auf einer JSP Seite einzusetzen, muss am Anfang der Seite die entsprechende Tag Library deklariert werden. Anschließend können die Common-Controls mit dem Präfix <ctrl:tagname /> verwendet werden. [Zudem muss die Aufnahme der Tag Bibliotheken im Deployment-Deskriptor, der WEB-INF/web.xml Datei, erfolgen]
<%@ taglib uri="/WEB-INF/tlds/cc-controls.tld" prefix="ctrl" %><ctrl:tree id="prodtree1" name="products" action="sample201/productBrowse" root="true" linesAtRoot="true" labelProperty="name" imageProperty="type" expandMode="multiple" groupselect="true" checkboxes="false"/>
Damit sind alle notwendigen Schritte zur Nutzung des TreeControls abgeschlossen. Das Auf- und Zuklappverhalten muss nicht selbst implementiert werden, es wird durch das Kontrollelement verwaltet. Dies gilt auch für die Selektionszustände von Checkboxen, die über das Attribut checkboxes="true" aktiviert werden können. Der Programmierer kann sich also auf die fachlichen Abläufe und die Bereitstellung der Anzeigedaten konzentrieren.
Professional Edition: Mit dem Attribut runat="client" wird der Baum in einer JavaScript Version generiert und kann somit ohne Server Roundtrips auf- und zugeklappt werden. Diese Erhöhung des Benutzerkomforts bedarf jedoch keinerlei Änderungen im Anwendungsprogramm.
Tour Ende
Das TreeControl lässt sich schnell und einfach integrieren. Sein Standardverhalten lässt sich bei Bedarf überschreiben. So lassen sich auch spezielle TreeControl-Objekte erstellen, die bereits den Zugriff auf bestimmte fachliche Daten kapseln und wiederkehrend innerhalb eines Anwendungsprojektes verwendet werden können.
Durch die Konfigurationsmöglichkeiten in der JSP-Seite kann das Verhalten des TreeControls schnell geändert werden. Alternative Designs lassen sich durch eine Anpassung der bestehenden Painter einfach integrieren. Dabei werden unterschiedliche Designs auch parallel unterstützt.
Features des TreeControls:
- Implementiert ein automatisches Auf- und Zuklappen von Knoten.
- Verwaltet den Zustand von optionalen Checkboxen.
- Verschiedene Konfigurationsmöglichkeiten (Ein- oder Ausblenden des Wurzelknotens, Änderung des Auf- und Zuklappverhalten einstellbar, Verbindungslinien auf oberster Ebene ausblendbar)
- Daten unterhalb eines Gruppenknotens können auch erst bei Öffnen der Gruppe geladen werden. Der Baum muss also nicht von Anfang an vollständig bekannt sein. Dies ist beispielsweise in Verbindung mit Datenbanken hilfreich. Beim ersten Aufklappen eines Knoten mit unbekannter Kinderzahl wird der Anwendung ein onExpandEx Ereignis gesendet.
- Design des TreeControls in der JSP oder auch serverseitig definierbar!
- Bildet Aktion, die auf dem Baum ausgeführt werden auf CallBack-Methoden in der Action-Klasse ab (Beispiele: onCheck, onExpand, onCollapse, onExpandEx).
- Bilder vor den Knoten/Blättern über Reguläre Ausdrücke zuordbar.
- Berechtigungsprüfung auf Knotenebene. Knoten können damit automatisch für unberechtigte Anwender ausgeblendet werden (siehe Security-Dokumentation).
- Layout durch Painterfactory an eigenen StyleGuide (Corporate Identity) anpassbar.
- Optimierter HTML-Code.
- Gleiches Look and Feel in Microsoft® InternetExplorer > 5.x und Netscape™ Navigator > 7.x
Exkurs: Implementierung einer Callback-Methode
Das TreeControl generiert bei einem Klick auf ein Label automatisch ein onDrilldown-Event auf das der Programmierer innerhalb der Action-Klasse reagieren kann.
Um in unserem Beispiel auf dieses Ereignis zu reagieren, nehmen wir eine entsprechende Callback-Methode in der ProductTreeBrowseAction auf. Da wir die BusinessLogik nicht an dieser Stelle implementieren möchten, leiten wir das Ereignis an eine andere Action - ProductDisplayAction - weiter.
import java.io.IOException;import javax.servlet.ServletException;import com.cc.framework.adapter.struts.ActionContext;import com.cc.framework.adapter.struts.FWAction;import com.cc.framework.ui.control.ControlActionContext;import com.cc.framework.ui.control.TreeControl;import com.cc.sampleapp.common.Forwards;public class ProductTreeBrowseAction extends FWAction { /** * @see com.cc.framework.adapter.struts.FWAction#doExecute(ActionContext) */ public void doExecute(ActionContext ctx) throws IOException, ServletException { try { ProductGroupDsp data = DBProduct.fetch(); TreeControl products = new TreeControl(); products.setDataModel(data); ctx.session().setAttribute("products", products); } catch (Throwable t) { ctx.addGlobalError("Error: ", t); } // Display the Page with the Tree ctx.forwardToInput(); } // ------------------------------------------------ // Tree-Control Event Handler // ------------------------------------------------ /** * This Method is called when the TreeLabel is clicked * In our Example we switch to the DetailView, which shows * more Information about the node. * @param ctx ControlActionContext * @param key UniqueKey, as created in the Datamodel */ public void products_onDrilldown(ControlActionContext ctx, String key) { ctx.forwardByName(Forwards.DRILLDOWN, key); }}
Der Name der CallBack-Methode setzt sich dabei aus dem Property-Namen des TreeControls - dem Namen der Bean - und dem eingetretenen Event zusammen. Da das TreeControl unter dem Namen "products" in der Session abgelegt wurde, lautet der Name der CallBack-Methode products_onDrilldown.
Exkurs: Verwendung einer ImageMap
Das TreeControl verwendet bei der Darstellung für geöffnete und geschlossene Knoten vordefinierte Bilder, die sich bei Bedarf einfach austauschen lassen. Jedem Eintrag innerhalb des Baumes kann ein eigenes Bild zugeordnet werden. Eine Möglichkeit hierzu stellt die Verwendung einer ImageMap dar. Diese wird außerhalb des Baumes in der JSP-Seite deklariert und dem TreeControl über das Attribut "imagemap" zugeordnet.
<%@ taglib uri="/WEB-INF/tlds/cc-controls.tld" prefix="ctrl" %><%@ taglib uri="/WEB-INF/tlds/cc-utility.tld" prefix="util" %><util:imagemap name="imap_products"> <util:imagemapping rule="prodgroup.open" src="images/imgBoxOpen.gif" width="16" height="16"/> <util:imagemapping rule="prodgroup.closed" src="images/imgBoxClosed.gif" width="16" height="16"/> <util:imagemapping rule="product" src=" images/imgItem.gif" width="16" height="16"/></util:imagemap><ctrl:tree id="prodtree1" name="products" action="sample201/productBrowse" root="true" linesAtRoot="true" labelProperty="name" imageProperty="type" imagemap="imap_products" expandMode="multiple" groupselect="true" checkboxes="true"/>
Arbeitsweise: Bei der Darstellung des Baumes liefert das Datenmodel über die Methode getType() jeweils einen Ausdruck zurück, der mit der ImageMap verglichen wird. Bei einer Übereinstimmung mit einer Regel wird das entsprechende Bild gezeichnet. Für geschlossene und geöffnete Knoten, werden diese Ausdrücke automatisch um das Suffix ".open" bzw. ".closed" erweitert, so dass sich unterschiedliche Bilder für die unterschiedlichen Zustände verwenden lassen. Die Angabe der Regeln erfolgt mit regulären Ausdrücken.
Die Methode, welche den Ausdruck für das zuzeichnende Bild liefert, wird über das Attribut "imageProperty" bestimmt. In unserem Beispiel wird das type-Property verwendet, welches für Gruppen stets "prodgroup" und für einzelne Blätter stets "product" liefert.
Alternative Layouts
Andere Layouts lassen sich über die Implementierung und Registrierung eigener Painterfactorys erzeugen, wie das folgende Beispiel zeigt:
Klassen aus dem Beispiel
Die in der Tour verwendeten Klassen vollständig.
Die Klasse ProductGroupDsp
public class ProductGroupDsp extends ProductBaseDsp implements TreeGroupDataModel { /** * ParentNode */ private TreeGroupDataModel parent = null; /** * ChildNodes */ private Vector children = new Vector(); // ------------------------------------------------ // Methods // ------------------------------------------------ /** * Constructor * @param key Unique Key for the Group * @param name Name of the Group * @param unknownChildren true if the Childs should be loaded later * false if the Childnodes exists */ public ProductGroupDsp(String key, String name) { super(); this.key = key; this.name = name; this.type = "prodgroup"; } /** * Constructor * @param key Unique Key for the Group * @param name Name of the Group * @param description description for the Group * @param unknownChildren true if the Childs should be loaded later * false if the Childnodes exists */ public ProductGroupDsp(String key, String name, String description) { super(); this.key = key; this.name = name; this.description = description; this.type = "prodgroup"; } /** * @see TreeGroupDataModel#getChild(int) */ public TreeNodeDataModel getChild(int index) { return (TreeNodeDataModel) children.elementAt(index); } /** * @see TreeGroupDataModel#addChild(TreeNodeDataModel) */ public void addChild(TreeNodeDataModel child) { children.add(child); child.setParent(this); } /** * Returns the Number of ChildNodes * -1 = The Number of ChildNodes is unknown. * When the Node opens an onExpandEx Event is generated * and the Childs can be loaded at runtime * 0 = This Node has no ChildNodes * >0 = This Node has ChildNodes * * @see TreeGroupDataModel#size() */ public int size() { return children.size(); } /** * @see TreeNodeDataModel#getParent() */ public TreeGroupDataModel getParent() { return parent; } /** * @see TreeNodeDataModel#setParent(TreeGroupDataModel) */ public void setParent(TreeGroupDataModel parent) { this.parent = parent; } /** * @see TreeNodeDataModel#getParentKey() */ public String getParentKey() { return parent.getUniqueKey(); } /** * @see TreeNodeDataModel#getUniqueKey() */ public String getUniqueKey() { return this.key; }}
Die Klasse ProductDsp
public class ProductDsp extends ProductBaseDsp implements TreeNodeDataModel { /** * ParentNode */ private TreeGroupDataModel parent = null; /** * Constructor * @param key Unique Productkey * @param name Productname */ public ProductDsp(String key, String name) { super(); this.key = key; this.name = name; this.type = "product"; } /** * Constructor * @param key Unique Productkey * @param name Productname * @param description Product description */ public ProductDsp(String key, String name, String description) { super(); this.key = key; this.name = name; this.description = description; this.type = "product"; } public void setParent(TreeGroupDataModel parent) { this.parent = parent; } public TreeGroupDataModel getParent() { return parent; } public String getParentKey() { return parent.getUniqueKey(); } public String getUniqueKey() { return this.key; }}
Die Klasse ProductBaseDsp
public class ProductBaseDsp { /** * ProductKey */ protected String key = ""; /** * Name of the Product */ protected String name = ""; /** * Description for the Product */ protected String description = ""; /** * Type for the Node */ protected String type = ""; /** * Constructor */ public ProductBaseDsp() { super(); } public String getName() { return name; } public String getDescription() { return description; } public String getType() { return type; }}
Fertige Konfigurationen zum Übernehmen
Screenshot, Konfiguration und der zugehörige JSP-Code.
Konfiguration
- Anzeige des Wurzelknotens (root="true").
- Anzeige der Verbindungslinien auf der obersten sichtbaren Ebene (linesAtRoot="true").
- Keine Anzeige von CheckBoxen vor Gruppen und Blättern (checkboxes="false").
- Verwendung der Default Images für geöffnete, geschlossene Knoten und Blätter.
- Durch die Angabe von expandMode="multiple" werden geöffnete Knoten beim Aufklappen weitere Knoten nicht mehr geschlossen. Mit der Einstellung expandMode="single" wird immer nur ein Knoten aufgekplappt gezeichnet, d.h. alle andere Knoten werden immer automatisch geschlossen.
- Die Instanz des TreeControls wird unter seinem Namen im Scope (Session/Request) gesucht. Bei Verwendung des Property-Attributs wird das Control aus der FormBean ermittelt.
<ctrl:tree id="prod1" name="products" action="sample201/productBrowse" root="true" linesAtRoot="true" labelProperty="name" imageProperty="type" expandMode="multiple" groupselect="true" checkboxes="false"/>
Konfiguration
- Siehe Konfigurationsbeispiel A.
- Deklaration einer ImageMap mit benutzerspezifischen Bildern für Gruppen und Blättern. Hierzu wird die ImageMap innerhalb der Tree-Spalte über das Attribut "imagemap" referenziert (imagemap="im_products). Jeder Eintrag im Baum liefert über die in der imageProperty spezifizierten Methode einen Ausdruck zurück, der mit der ImageMap verglichen wird. Bei Übereinstimmung wird das entsprechende Image verwendet.
<util:imagemap name="im_products"> <util:imagemapping rule="group.open" src="app/images/imgBoxOpen.gif" width="16" height="16"/> <util:imagemapping rule="group.closed" src="app/images/imgBoxClosed.gif" width="16" height="16"/> <util:imagemapping rule="single" src="app/images/imgItem.gif" width="16" height="16"/> <util:imagemapping rule="modem" src="app/images/tree/modem.gif" width="16" height="16"/> <util:imagemapping rule="mouse" src="app/images/tree/mouse.gif" width="16" height="16"/> <util:imagemapping rule="backup" src="app/images/tree/backup.gif" width="16" height="16"/></util:imagemap><ctrl:tree id="prod2" name="hardware" action="sample202/hardwareBrowse" root="true" linesAtRoot="true" labelProperty="name" imageProperty="type" imagemap="im_products" expandMode="multiple" groupselect="true" checkboxes="false"/>
Konfiguration
- Siehe Konfigurationsbeispiel A.
- Verwendung von Checkboxen (checkboxes="true").
- Es wird nur ein Knoten aufgeklappt gezeichnet (expandMode="single").
<util:imagemap name="im_products"> <util:imagemapping rule="group.open" src="app/images/imgBoxOpen.gif" width="16" height="16"/> <util:imagemapping rule="group.closed" src="app/images/imgBoxClosed.gif" width="16" height="16"/> <util:imagemapping rule="single" src="app/images/imgItem.gif" width="16" height="16"/></util:imagemap><ctrl:tree id="prod2" name="hardware" action="sample202/hardwareBrowse" root="true" linesAtRoot="true" labelProperty="name" imageProperty="type" imagemap="im_products" expandMode="single" groupselect="true" checkboxes="true"/>
Konfiguration
- Ausgeblendeter Wurzelknoten (root="false").
- Weitere Einstellungen vergleiche Konfigurationsbeispiel A.
<ctrl:tree id="prod1" name="products" action="sample201/productBrowse" root="false" linesAtRoot="true" labelProperty="name" imageProperty="type" expandMode="multiple" groupselect="true" checkboxes="false"/>
Konfiguration
- Auf der Ebene des Wurzelknoten wird kein Öffnen- bzw. Schließ-Symbol gezeichnet (linesAtRoot="false").
- Die Instanz des TreeControls wird in dem der Action zugeordneten Formbean gesucht. Statt des Name-Attributes wird daher das Property-Attribute angegeben.
- Weitere Einstellungen vergleiche Konfigurationsbeispiel A.
<html:form action="/sample201/productBrowse"><ctrl:tree id="prod1" property="products" root="true" linesAtRoot="false" labelProperty="name" imageProperty="type" expandMode="multiple" groupselect="true" checkboxes="false"/></html:form>