Skip to content

Instantly share code, notes, and snippets.

@sma
Created May 15, 2009 13:35
Show Gist options
  • Save sma/112212 to your computer and use it in GitHub Desktop.
Save sma/112212 to your computer and use it in GitHub Desktop.

Webapps a la Seaside

Ich will versuchen, die Essenz von Seaside in Python-Syntax zu übertragen.

Anwendungen werden aus Komponenten zusammengesetzt. Klassen beschreiben diese Komponenten. Die Klassen werden an URLs gebunden. Wird so eine URL angesprochen, wird eine neue Komponente erzeugt und für diesen Anwender in eine Session gepackt, wo sie solange lebt, die Session explizit oder durch einen Timeout beendet wird.

Das Rahmenwerk ruft für jede Komponente die Methode render auf. Diese ist dafür zuständig, die Komponte zu "zeichnen", sprich, das HTML durch Aufrufe von Methoden des übergebenen html-Objekts zu erzeugen.

class HelloWorld(seaside.Component):
    def render(self, html):
        html.text("Hello, World!")

Das kanonische Beispiel für Seaside ist ein Zähler:

class Counter(seaside.Component):
    def __init__(self):
        self.value = 0

    def render(self, html):
        html.heading(level=1, text=self.value)
        with html.paragraph():
            html.anchor(send='increment', to=self, text="++")
            html.space()
            html.anchor(send='decrement', to=self, text="--")

    def increment(self): self.value += 1
    def decrement(self): self.value -= 1

Wie funktioniert's? Die Methode heading erzeugt ein <h1>-Element mit den angegebenen Text. Durch paragraph wird ein <p>-Element erzeugt. Ich nutze with dazu, das Element am Ende des Blocks wieder zu schließen. Die Methode anchor erzeugt einen Link mit einer automatisch generierten URL und registriert diese, damit ein Klick dann die gebundene Methode der Komponente aufruft. Danach wird diese dann wieder mittels render gezeichnet, was neue URLs generiert und für den nächsten Roundtripp registiert. Die "lebende" Komponente wird mit allem, was dazu gehört, in der Session aufbewahrt. Der Vorteil dieses Ansatz ist, dass man sich um URLs, HTML und all die Details eines HTTP-Requests nicht kümmern muss.

Ich könnte den Aufruf auch zu html.anchor(call=self.increment, text="++") vereinfachen, fällt mir gerade auf. Liese sich allerdings so eine gebundene Methode in ein Session-Objekt serialisieren?

Komponenten lassen sich kombinieren.

class MultiCounter(seaside.Container):
    def __init__(self):
        self.children = [Counter() for i in range(10)]

    def render(self, html):
        for child in self.children:
            html.render(child)

Eine Komponente kann eine andere Komponente auch kapseln, um etwa wie ein Fenster auszusehen. Das nötige HTML und JavaScript, um so ein Fenster in einem Browser wie ein Desktop-Fenster aussehen zu lassen, spare ich mir hier. Es wäre jedoch in dieser Komponente wiederverwendbar gekapselt:

class Window(seaside.Component):
    def __init__(self, title, component):
        self.title, self.component = title, component

    def render(self, html):
        with html.div(cls='window'):
            self.render_title(html)
            self.render_body(html)

    def render_title(self, html):
        with html.div(cls='title'):
            with html.span(cls='closebutton'):
                html.anchor(send='close', to=self, image='close.png')
            html.span(cls='text', text=self.title)

    def render_body(self, html):
        html.div(cls='body', component=self.component)

    def close(self):
        pass

Seaside erlaubt es außerdem, CSS-Definitionen pro Komponente zu machen. Diese werden dann automatisch in einer CSS-Datei gesammelt, die im Header geladen wird. Da Smalltalk ja nicht mit Dateien zur Programmspeicherung arbeitet, sondern mit einer (eigenen) Datenbank, kann man diese Definitionen auch zur Laufzeit direkt im Browser anpassen und so unmittelbar die Änderungen sehen.

Eine Komponente kann eine andere Komponente überlagern. Ich kann so etwa einen Zähler haben, der bei einer bestimmten Zahl ein Fenster öffnet. Technisch heißt das einfach, dass zwar die Zähler-Komponente noch im Komponentenbaum enthalten ist, aber zugunsten der Fenster-Komponente nicht mehr gezeichnet wird. Man kann sich natürlich auch komplexere Szenarien überlegen, etwa das man per CSS und z-index sicherstellt, dass das übereinander dargestellt wird. Standard ist aber eine Verdrängung.

class Confirmation(seaside.Component):
    def __init__(self, question):
        self.question = question

    def render(self, html):
        html.heading(level=3, text=question)
        with html.paragraph():
            html.anchor(call=self.yes, text="YES")
            html.space()
            html.anchor(call=self.no, text="NO")

    def yes(self): self.answer(True)
    def no(self): self.answer(False)

class ConfirmingCounter(Counter):
    def decrement(self):
        self.open(Confirmation("Really?"), then=self.after_confirmation)

    def after_confirmation(self):
        if self.result: self.counter -= 1

Mit der Methode open kann ich eine Komponente überlagern. Die Confirmation-Komponente stellt eine Frage und zwei Links für Antworten dar. Da Seaside Continuations hat, die es erlauben, an der Stelle, wo open aufgerufen wird, den Programmfluss einzufrieren und ihn dort später mit der Antwort fortzusetzen, sieht das recht elegant aus. Ich muss mir damit behelfen, eine Funktion anzugeben, die aufgerufen werden soll, wenn die Antwort zur Verfügung steht. In diesem einfachen Fall geht es noch, aber ich befürchte, hier hat Seaside einen klaren Vorteil, denn dort sähe es so aus:

def decrement(self):
    if self.open(Confirmation("Really?")):
        self.counter -= 1

Als letztes Beispiel möchte ich zeigen, wie man mit Formularen umgehen kann. Die Inspector-Komponente kann alle Attribute eines Objekts darstellen und erlaubt es, sie zu ändern (so lange es Strings sind). So könnte es aussehen:

class Inspector(seaside.BoxedComponent):
    def __init__(self, object):
        self.object = object

    def render_title(self, html):
        html.text("Inspector on ", self.object)

    def render_content(self, html):
        with html.form():
            with html.table():
                for key in sorted(self.object.__dict__.keys()):
                    with html.table_row():
                        self.render_attribute(html, key)
                with html.table_row(colspan=2):
                    with html.table_data():
                        html.button(text="Save", writeback=True)
                        html.button(text="Cancel")

    def render_attribute(self, html, key):
        html.table_header(text=key)
        with html.table_data():
            html.input_field(key=key, object=self.object)

Erneut muss man sich um keine HTML-Details kümmern. Das Rahmenwerk registiert eine automatisch generierte URL für das Formular und weiß, wie es für die Eingabefelder die Werte aus der Komponente zur Anzeige holen kann und wie sie nach einem POST und Click auf "Save" automatisch wieder setzt.

Für eine ganze Anwendung mit mehreren "Seiten" wären wieder Continuations praktisch. Ich muss jetzt eine State-Maschine bauen, die sehr als die XML-Konfiguration von JSF erinnert. Nun ja:

class TodoList(seaside.Application):
    def __init__(self):
        self.login()

    def login(self):
        self.set_current(Login(), success='main')

    def main(self):
        self.set_current(MainScreen(), help='help', logout='login')

    def help(self):
        self.set_current(HelpScreen(), close='main')

class Login(seaside.Component):
    def render(self, html):
        ...
        html.button(text="Login", call=self.login)
        ...

    def login(self):
        ...
        if self.backend.authenticate(self.name, self.password):
            self.answer('success')

Seaside selbst hier ist dank Continuations eleganter, dort gibt es eine Task-Klasse mit einer Methode go, in der das Programm mit Schleifen und Bedingungen einfach abläuft.

Stefan

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment