Autorenarchiv

Universeller Feldupdater mit Rails

Dienstag, 24. August 2010

Ich erstelle momentan ein kleines, sehr spezielles CMS mit Ruby on Rails, mein erstes komplettes Rails Projekt. Der Umstieg von Java in die Rails-Welt war zwar gewöhnungsbedürftig und hatte einige Hürden, aber es hat sich gelohnt. Hier ein kleines, schönes Beispiel wie man mit wenigen Zeilen Ruby ziemlich universellen Code erzeugen kann.

Die Anforderung war, alle Eingabefelder im CMS, z.B. die Beschriftung der Bilder oder Überschriften und Artikel, einzeln editierbar zu machen. D.h. man klickt bzw. doppelklickt das Feld an, editiert es und die Änderung wird beim Verlassen des Feldes gespeichert.
Undenkbar hier für jedes Feld eigene Methoden im Controller oder JavaScript anzulegen.

Die Lösung besteht aus drei Teilen wobei ich hier nur den Controller näher erläutern möchte.
Teil 1 ist eine Helpermethode die ein entspr. Eingabefeld erzeugt.
Teil 2 ist eine kleine JavaScript Funktion die den Wert des Feldes per Ajax an den Controller sendet.
Teil 3 ist der Controller der den neuen Wert speichern soll.

Idee war es dem Controller folgende Werte zu liefern:
- Name der Model Klasse
- ID des Datensatzes
- Name des Attributes
- Wert des Attributes
Die ersten drei Werte werden durch einen Punkt getrennt als ID des HTML input gesetzt, z.B.: Image.34.caption

Jetzt aber zum Controller, der nur aus einer public Methode besteht:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class FieldsController < ApplicationController
 
  layout nil
 
  def update
    a = params[:key].split('.')
    update_field a[0], a[1], a[2], params[:value]
    head :status => 200
  end
 
  private
  def update_field classname, id, attr, value
    # must be a ActiveRecord::Base
    obj = ActiveRecord::Base.const_get(classname).find(id)
    obj.update_attribute(attr, value)
  end
end

Die als “key” übergebenen Werte werden erstmal zerlegt und der Methode update_field übergeben. Hier wird die Model-Klasse über die Kernel Funktion const_get geladen (Klassennamen sind in Ruby Konstanten). Das machen wir direkt auf ActiveRecord::Base, weil wir dort alle Model Klassen finden werden und somit nicht Kernel.const_get verwenden müssen. Auf der Modelklasse rufen wir direkt ein find mit der ID auf und bekommen die Instanz unseres Datenobjekts. Auf diesem setzen wir den Wert des Attributs mit update_attribute neu. Fertig.
Der Controller liefert dann noch den Status 200 zurück, damit unser AjaxRequest weiß, dass alles ok ist.

Auf ActiveRecord::Base gibt es noch weitere update Methoden, so könnte man z.B. auch mehrere Attribute updaten.


Christof Aenderl


JavaScript: Tastatureingaben mit prototype auswerten

Montag, 15. März 2010

Im folgenden Abschnitt möchte ich anhand des Beispiels einer Mehrfachauswahl von Eingabefeldern erläutern, wie einfach es ist, in JavaScript einen Tastendruck auszuwerten.
Ich verwende hier die prototype Bibliothek. Für alle, die prototype nicht kennen, ganz kurz:
$(’name’) entspricht der Methode getElementById(’name’)

var KeyEvent = {
   ctrl: false,
   keyCode: 0,
   element: null,
 
   /**
    * Am uebergebenen Element z.B. einem div Container oder document selbst
    * werden die Methoden fuer die Events keydown und keyup registriert.
    *
    * element: HTML-Element, an dem die Events registriert werden sollen
    * z.B. Tabelle oder Div.
    */
   init: function(element) {
      KeyEvent.element = element;
      // Taste gedrueckt
      Event.observe(KeyEvent.element, 'keydown', KeyEvent.registerDown);
      // Taste wieder losgelassen
      Event.observe(KeyEvent.element, 'keyup', KeyEvent.registerUp);
   },
 
   /**
    * Function, die auf den keydown event reagiert
    */	
   registerDown: function(event) {
      // falls die Taste gedrueckt bleibt, sollen keine weiteren 
      // keydown events verarbeitet werden
      KeyEvent.element.stopObserving('keydown');
      KeyEvent.keyCode = event.keyCode;
      if (event.ctrlKey) {
         KeyEvent.ctrl = true;
      }
   },
 
    /**
     * Function, die auf den keyup event reagiert
     */
   registerUp: function(event) {
      KeyEvent.ctrl = false;
      // wird die Taste losgelassen, starten wir den Observer fuer keydown wieder
      Event.observe(KeyEvent.element, 'keydown', KeyEvent.registerDown);
   }
};
 
/**
 * Wird beim onfocus des Eingabefeldes aufgerufen
 */
function selectField(element) {
   if (KeyEvent.ctrl) {
      // multiselect ... z.B. könnte man hier das Feld mit einer CSS class markieren.
   }
}

Im HTML könnte das dann wie folgt verwendet werden:

<body onload="KeyEvent.init($('content'))">
 
   <div id="content">
      <form ....>
         <input type="text" name="f1" onfocus="selectField(this)" />
         <input type="text" name="f2" onfocus="selectField(this)" />
         ...
      </form>
   </div>
</body>

Das obige Beispiel verwendet jetzt nur den speziellen event.ctrlKey. Eine denkbare Erweiterung wäre z.B. die Shift-Taste auzuwerten um die Felder von – bis zu markieren. Natürlich könnte man auch beliebige andere Tasten auswerten, dafür muss aber dann der numerische keyCode verwendet werden.

Mehr Infos zur Verwendung von Prototype finden sie unter http://prototypejs.org


Christof Aenderl


XStream: Object -> XML -> Object

Donnerstag, 19. November 2009

Mit der Bibliothek XStream lassen sich auf einfache Weise Java Objekte in XML zu serialisieren und wieder zurück.
Das kann z.B. dazu dienen, Datensätze als Textdatei abzuspeichern oder XML für Unit-Tests zu erzeugen.
Vorteile von XStream sind die sehr einfache Anwendung und die gute Performance. Neuere Versionen von XStream bieten zudem die Möglichkeit auch JSON zu erzeugen oder die Integration in andere XML APIs.

Nun ein paar Beispiele zur Verwendung von XStream.

XStream verwendet per default die Pull-Parser Implementierung XPP3. Das entsprechende JAR muss hierzu separat heruntergeladen und im Classpath hinzugefügt werden. (Die XPP3 Implementierung am Besten über eine Suchmaschine im Internet suchen.)

Anmerkung:
Wir verwenden hier Aliase für Klassennamen. Das hat den Vorteil, dass das XML lesbarer wird und vor allem eine Deserialisierung auch nach einem Umbenennen oder Verschieben der Klasse funktioniert.

Objekt nach XML serialisieren

  final XStream xstream = new XStream();
  // set alias for the MyData class 
  xstream.alias("my-data", MyData.class);
  // MyData myData = new MyData(....);
  final String xml = xstream.toXML(myData);

Objekt aus XML deserialisieren

  final XStream xstream = new XStream();
  // set alias for the MyData class 
  xstream.alias("my-data", MyData.class);
  // String xml = 
  MyData myData = (MyData) xstream.fromXML(xml);

Interaktion mit dom4J

Mit XStream lassen sich auch Objekte in ein bestehendes XML Document einfügen. In diesem Beispiel wird ein dom4j Element ermittelt und an die Methode addToElement() übergeben, welche das Objekt myData in das Element serialisiert:

  public void addToElement(final MyData myData, final org.dom4j.Element element) {
    final XStream xstream = new XStream();
    xstream.alias("my-data", MyData.class);
    // create new com.thoughtworks.xstream.io.xml.Dom4JWriter
    final Dom4JWriter writer = new Dom4JWriter(element);
    xstream.marshal(myData, writer);
    writer.close();
  }

Natürlich ist der Weg zurück zum Objekt aus dem XML genauso einfach:

  public MyData getFromElement(final org.dom4j.Element element) {
    final XStream xstream = new XStream();
    xstream.alias("my-data", MyData.class);
    final Dom4JReader reader = new Dom4JReader(element);
    return (MyData) xstream.unmarshal(reader);
  }

Viel Spaß und Erfolg beim Ausprobieren und Anwenden.


Christof Aenderl


Integration von Ext JS (AJAX) in eine bestehende Webanwendung

Montag, 02. November 2009

Das man mit JavaScript und AJAX nicht nur tolle, sondern auch wirklich nützliche Funktionalität in einer Webanwendung unterbringen kann, ist nicht von der Hand zu weisen. Eingabefelder, die schon beim Eintippen mögliche Treffer anzeigen, sind nur ein Beispiel und auf fast allen bekannten Webseiten zu finden.
Solche Funktionalität lässt sich auch in bestehenden Anwendungen nachrüsten, ohne das komplette Projekt umzukrempeln. Die folgenden Beispiele stammen aus der Erweiterung einer Struts 1 Anwendung, in die wir nachträglich Ext JS (Version 2.2) integriert hatten, um einige der Widgets von Ext JS zu nutzen.

1. Ext JS zum Projekt hinzufügen
2. Einbinden von Ext JS in die JSP’s
3. AJAX Request mit Ext JS (Beispiel)
4. Response (HTML oder JSON)

Anmerkung: Expression Language (EL) muss vom Server unterstützt werden.

1. Ext JS zum Projekt hinzufügen

Das Archiv, das man von der Ext JS Seite herunterladen kann, enthält neben den eigentlichen Bibliotheken auch zahlreiche Beispiele und Sourcen, die von der Anwendung nicht benötigt werden. Folgende Dateien* und Verzeichnisse* kopiert man in sein Projekt z.B. in das Unterverzeichnis WebContent/js/ext:
- ext-all.js
- ext-core.js
- /adapter
- /build
- /resources

* Ext JS Version 2.2 oder 2.3 ohne Debug

Man kann auch die oben erwähnten Dateien und Verzeichnisse in ein ZIP packen, z.B. ext-2.2.zip und dieses beim Build der Anwendung durch Ant enstspr. auspacken und zum WAR hinzufügen lassen. Damit ist einigermaßen sichergestellt, dass die Ext-Dateien nicht editiert werden und zudem hat man nur eine Datei auf dem SVN bzw. CVS liegen (erleichtert auch den Wechsel auf eine andere Ext-Version). Das ist aber absolut optional.

2. Einbinden von Ext JS in die JSP’s

Um Ext verfügbar zu machen und den Standardadapter zu nutzen sind folgende Zeilen im HTML-Head nötig:

<script type="text/javascript" 
src="${pageContext.request.contextPath}/js/ext/ext-all.js"></script>
<script type="text/javascript" 
src="${pageContext.request.contextPath}/js/ext/adapter/ext/ext-base.js"></script>

Will man einige der Widgets von Ext nutzen, ist zusätzlich das CSS von Ext einzubinden:

<link rel="stylesheet" type="text/css" 
href="${pageContext.request.contextPath}/js/ext/resources/css/ext-all.css" />

und ggf. ein Theme auszuwählen, wenn man nicht das Standard-Blau verwenden möchte:

<link rel="stylesheet" type="text/css" id="theme" 
href="${pageContext.request.contextPath}/js/ext/resources/css/xtheme-gray.css" />

Eine Besonderheit: Ich weiß nicht, ob die Ext Entwickler das mit Absicht gemacht haben und welchen Zweck es haben soll, aber Ext “funkt” nach Hause. An einigen Stellen wird ein 1×1 Pixel Image von der Ext-Seite abgerufen. Will man das umgehen und stattdessen das Image lokal laden, sollte man diese Zeilen im Header unterbringen:

<script type="text/javascript">
// url for empty image otherwise http://extjs.com will be called
Ext.BLANK_IMAGE_URL = '${pageContext.request.contextPath}/js/ext/resources/images/default/s.gif';
</script>

3. AJAX Request mit Ext JS (Beispiel)

Nun werden wir einen Ajax Request ausführen um z.B. den Wert eines Formularfelds an den Server zu übermitteln, ohne die komplette Seite neu laden zu müssen (also kein Form.submit()).
Die Funktion würde wie folgt aussehen:

function sendValue(value) {
  Ext.Ajax.request({
    url: '${pageContext.request.contextPath}/sendValue.do',
    params: { newValue: value },
    success: function(result, response) {
      // in diesem Fall soll nichts gemacht werden	
    },
    failure: function(result, request) {
      Ext.Msg.alert('Fehler', 'Request konnte nicht ausgeführt werden');
    }
  });
}

Serverseitig rufen wir hiermit eine entspr. gemappte StrutsAction auf.
Auszug aus der struts-config.xml:

<action path="/sendValue"
  type="de.baderundjene.example.ValueChangeAction">
</action>

Ein Form kann man sich sogar sparen und den Parameter direkt auslesen:

public final class ValueChangeAction extends Action {
  @Override
  public final ActionForward execute(....) {
    final String newValue = request.getParameter("newValue");
    // irgendwas mit dem newValue machen
 
    return null;
  }
}

Am Ende geben wir null zurück, es soll ja kein ActionForward erfolgen und der Response soll leer sein.

4. Response (HTML oder JSON)

Möchten wir aber einen Wert, Werte oder ein HTML-Fragment zurückgeben, muss dieses als String in den Response geschrieben werden.
Um Daten an den Client (Browser) zu schicken, die dort per JavaScript verarbeitet werden sollen um z.B. die Werte einer Select-Box zu verändern, eignet sich JSON (JavaScript Object Notation) besonders gut. Details zu JSON findet man ganz leicht im Internet. Bevor wir also in unserer Action null zurückgeben, schreiben wir unseren JSON-String in den Response:

// ein Attribut data mit dem Wert Test
final String json = "({data:'Test'})";
// HttpServletResponse response;
// hier können wir auch das Encoding auf z.B. UTF-8 stellen
response.setContentType("text/json; charset=UTF-8");
final PrintWriter out = response.getWriter();
out.println(json);
out.flush();

Im JavaScript erweitern wir unsere success Methode:

function sendValue(value) {
  Ext.Ajax.request({
    url: '${pageContext.request.contextPath}/sendValue.do',
    params: { newValue: value },
    success: function(result, response) {
      var responseObj = Ext.decode(result.responseText);
      Ext.Msg.alert('Erfolg', responseObj.data);
    },
    failure: function(result, request) {
      Ext.Msg.alert('Fehler', 'Request konnte nicht ausgeführt werden');
    }
  });
}

Möchte man ein HTML-Fragment zurückgeben, um z.B. einen Teil der Seite neu zu laden, kann man in der Action einen ActionForward auf eine entspr. JSP durchführen. Diese JSP enthält dann eben keine ganze HTML-Seite sondern nur den Teil, den wir neu laden möchten, z.B. eine Tabelle mit den Kundendaten.
D.h. struts-config.xml anpassen:

<action path="/sendValue"
  type="de.baderundjene.example.ValueChangeAction">
  <forward name="success" path="inc/customerTable.jsp"/>
</action>

und statt null den ActionForward mapping.findForward(”success”); zurückgeben.
Unsere angepasste JavaScript Methode:

function sendValue(value) {
  Ext.Ajax.request({
    url: '${pageContext.request.contextPath}/sendValue.do',
    params: { newValue: value },
    success: function(result, response) {
      // <div id="customer-data">&nbsp;</div>
      var customerDiv = Ext.get('customer-data');
      customerDiv.update(response.responseText, true);
    },
    failure: function(result, request) {
      Ext.Msg.alert('Fehler', 'Request konnte nicht ausgeführt werden');
    }
  });
}

Fazit

Ein Projekt kann sehr leicht mit Ajax erweitert werden ohne bestehende Funktionalität anzufassen. Welches MVC-Framework hier zum Einsatz kommt, spielt eigentlich keine Rolle. Modernere Frameworks bieten allerdings teilweise out-of-the-box Unterstützung für Ajax und JSON an.
Bei unserem Projekt haben wir neue Erweiterungen direkt mit Ext JS implementiert, möglichst alle eigenen oder sonstiges JavaScript durch Ext Funktionen ersetzt und bauen jetzt bei Bedarf und wenn es sinnvoll ist, ältere Teile der Anwendung auf Ext um. Z.B. verwenden wir das Tree Widget inkl. Drag and Drop und Kontextmenü, welches sehr mächtig ist und einen sehr stabilen Eindruck macht.
Zu wünschen wäre jetzt nur noch eine bessere Unterstützung durch eclipse, aber das kommt vielleicht auch noch.
Falls es sich ergibt, werde ich auch noch näher auf interessante Widgets von Ext JS eingehen.

Viel Spaß und Erfolg beim Ausprobieren und Anwenden.


Christof Aenderl