Data Binding

The Channels data binding framework automates the process of tying Django models into frontend views, such as javascript-powered website UIs. It provides a quick and flexible way to generate messages on Groups for model changes and to accept messages that change models themselves.

The main target for the moment is WebSockets, but the framework is flexible enough to be used over any protocol.

What does data binding allow?

Data binding in Channels works two ways:

  • Outbound, where model changes made through Django are sent out to listening clients. This includes creation, updates and deletion of instances.
  • Inbound, where a standardised message format allows creation, update and deletion of instances to be made by clients sending messages.

Combined, these allow a UI to be designed that automatically updates to reflect new values and reflects across clients. A live blog is easily done using data binding against the post object, for example, or an edit interface can show data live as it’s edited by other users.

It has some limitations:

  • Signals are used to power outbound binding, so if you change the values of a model outside of Django (or use the .update() method on a QuerySet), the signals are not triggered and the change will not be sent out. You can trigger changes yourself, but you’ll need to source the events from the right place for your system.
  • The built-in serializers are based on the built-in Django ones and can only handle certain field types; for more flexibility, you can plug in something like the Django REST Framework serializers.

Getting Started

A single Binding subclass will handle outbound and inbound binding for a model, and you can have multiple bindings per model (if you want different formats or permission checks, for example).

You can inherit from the base Binding and provide all the methods needed, but we’ll focus on the WebSocket JSON variant here, as it’s the easiest thing to get started and likely close to what you want.

Start off like this:

from django.db import models
from channels.binding.websockets import WebsocketBinding

class IntegerValue(models.Model):

    name = models.CharField(max_length=100, unique=True)
    value = models.IntegerField(default=0)

class IntegerValueBinding(WebsocketBinding):

    model = IntegerValue
    stream = "intval"
    fields = ["name", "value"]

    @classmethod
    def group_names(cls, instance):
        return ["intval-updates"]

    def has_permission(self, user, action, pk):
        return True

This defines a WebSocket binding - so it knows to send outgoing messages formatted as JSON WebSocket frames - and provides the three things you must always provide:

  • fields is a whitelist of fields to return in the serialized request. Channels does not default to all fields for security concerns; if you want this, set it to the value ["__all__"]. As an alternative, exclude acts as a blacklist of fields.
  • group_names returns a list of groups to send outbound updates to based on the instance. For example, you could dispatch posts on different liveblogs to groups that included the parent blog ID in the name; here, we just use a fixed group name. Based on how group_names changes as the instance changes, Channels will work out if clients need create, update or delete messages (or if the change is hidden from them).
  • has_permission returns if an inbound binding update is allowed to actually be carried out on the model. We’ve been very unsafe and made it always return True, but here is where you would check against either Django’s or your own permission system to see if the user is allowed that action.

For reference, action is always one of the unicode strings "create", "update" or "delete". You also supply the WebSocket Multiplexing stream name to provide to the client - you must use multiplexing if you use WebSocket data binding.

Just adding the binding like this in a place where it will be imported will get outbound messages sending, but you still need a Consumer that will both accept incoming binding updates and add people to the right Groups when they connect. The WebSocket binding classes use the standard WebSocket Multiplexing, so you just need to use that:

from channels.generic.websockets import WebsocketDemultiplexer
from .binding import IntegerValueBinding

class Demultiplexer(WebsocketDemultiplexer):

    consumers = {
        "intval": IntegerValueBinding.consumer,
    }

    def connection_groups(self):
        return ["intval-updates"]

As well as the standard stream-to-consumer mapping, you also need to set connection_groups, a list of groups to put people in when they connect. This should match the logic of group_names on your binding - we’ve used our fixed group name again. Notice that the binding has a .consumer attribute; this is a standard WebSocket-JSON consumer, that the demultiplexer can pass demultiplexed websocket.receive messages to.

Tie that into your routing, and you’re ready to go:

from channels import route_class, route
from .consumers import Demultiplexer
from .models import IntegerValueBinding

channel_routing = [
    route_class(Demultiplexer, path="^/binding/"),
]

Frontend Considerations

You can use the standard Channels WebSocket wrapper to automatically run demultiplexing, and then tie the events you receive into your frontend framework of choice based on action, pk and data.

Note

Common plugins for data binding against popular JavaScript frameworks are wanted; if you’re interested, please get in touch.

Custom Serialization/Protocols

Rather than inheriting from the WebsocketBinding, you can inherit directly from the base Binding class and implement serialization and deserialization yourself. Until proper reference documentation for this is written, we recommend looking at the source code in channels/bindings/base.py; it’s reasonably well-commented.

Dealing with Disconnection

Because the data binding Channels ships with has no history of events, it means that when a disconnection happens you may miss events that happen during your offline time. For this reason, it’s recommended you reload data directly using an API call once connection has been re-established, don’t rely on the live updates for critical functionality, or have UI designs that cope well with missing data (e.g. ones where it’s all updates and no creates, so the next update will correct everything).