How do you collect all notifications on step 2 to fire them on step 3 such that UI does not re-render itself too much? E.g. updating a title of each item in a list of 100 items should not trigger 100 renders. Or 100 layout calculations (which I think is harder to avoid).
How do you deal with situations where on step 4 UI triggers an event that your model happens to listen and the cycle repeats while killing performance?
Because you rely on events how do you avoid “event hell”? That is, a situation when an event handler triggers a change that triggers another event handler that triggers a change and so on. Sometimes it is scrolling or typing, sometimes it is parts of the model subscribed to each other bubbling events to UI.
> UI does not re-render itself too much?
Glad you asked! In my Blackbird reference architecture (which is an instance of MVC), I use a coalescing queue to capture the updates. The coalescing is two-level: first, simple duplicates are weeded out. Second, if the update queue gets very full, it becomes coarser-grained, and weeds out duplicates based on that coarser grain. This has multiple steps of grain up to "just re-render the whole UI". Worked like magic in Wunderlist. Except it wasn't magic at all and very simple, inspectable and tractable.
> step 4 UI triggers an event that your model happens to listen
That's not allowed in MVC.
> Because you rely on events how do you avoid “event hell”?
I don't "rely" on events and there is no "event hell". Events are only used in the M→V communication part and there are no subsequent triggers, because the only event is "the model has changed", with an optional payload specifying which part of the model. Important: it must not contain the data that changed, this the view has to fetch from the model once it processes the update event.
Since the only event used is "the model changed", the view cannot ever be a source of those events, so no "event hell".
I don't. And I don't have to, as I delegate that sort of stuff (mostly) to Cocoa/CocoaTouch etc.
https://blog.metaobject.com/2018/12/uis-are-not-pure-functio...
When you have stateful view objects, these stateful view objects maintain the view state. When updating themselves with new data due to a ModelDidChange notification, they take care of reconciling their current display state with the underlying model state.
> When displaying a scrollable and selectable list of items
So for example an NSTableView or NSCollectionView. I personally use a subclass that interacts directly with a table representation, meaning a lot of the glue code that Cocoa(Touch) requires disappears.
> Who updates the UI state accordingly to make it valid again?
Always the view. Who else?
> In the general case, application code needs to be involved in choosing the desired valid UI state when the underlying model state changes.
How so? The view is always a reflection of the model data. Whether that is a "change" is actually mostly irrelevant, even though the notification is called ModelDidChange in my case. In Smalltalk MVC it is the #changed message. It means "you are out of date, please make yourself reflect the model".
This same mechanism also handles the model being changed by some other party without any further code. "The model has changed, please update yourself to reflect the current state of the model". That's it, modulo optimizations.
> How is the corresponding application code prevented from triggering further events?
Model code isn't involved. A ModelDidChange event is only triggered when...er...the model changes.
That said nothing prevents you from manually invoking the ModelDidChange notification, just like nothing prevents you from calling abort(), running an infinite loop, creating an unbounded recursion or reading from /dev/random until it is exhausted ...
Doing it by accident, though, is very hard, because it just isn't part of the programming model.
This is not about duplicates. For example, sync updates 100 items in a list changing their titles. Items are bound to a list in the UI. Thus, 100 unique title update events triggered.
>Events are only used in the M→V communication
I don’t understand. Button clicked -> model change -> view update -> new event triggered -> model or view updated again … This is not something one would code on purpose, but often an attempt to create relationships between view. Like a custom layout code. Might not include model at all, just views being updated in an event handler trigger more events and more updates to views.
Those "updates" go in the queue. When the UI gets around to updating itself, it looks at the queue and invalidates all the UI elements that refer to the model items in the queue.
It then updates those elements, using the coarsening to update larger elements in bulk if that becomes better.
> Button clicked -> model change -> view update -> new event triggered -> model or view updated again
Once again, that is not allowed. View updates are not allowed to trigger any events in MVC. A model → view update updates the view. That's it.
The only event is "model changed", so it also doesn't make sense for the view to generate those events.
class DataModel:
def __init__(self, counter):
self.counter = counter
def layout(data, info):
return Dom.create_div()
.with_child(Dom.create_text(str(data.counter)))
.with_css("font-size: 32px;")
def on_click(data, info):
data.counter += 1
return Update.RefreshDom
model = DataModel(5)
window = WindowCreateOptions.create(layout)
app = App.create(model, AppConfig.create())
app.run(window)
So, there's no "automatic" re-render, a callback has to return "Update.RefreshDom" or "Update.DoNothing" (default).Now to your questions:
> How do you collect all notifications on step 2 to fire them on step 3 such that UI does not re-render itself too much?
Diffing, and then caching very aggressively. The click causes the model to re-call the layout() fn to return the entire DOM, however, there are ways to make this step very fast (arena allocation / no allocation). Then this gets diffed with the previous DOM state and the framework internally reuses everything it can (with user providing keys for list items, like React does).
> How do you deal with situations where on step 4 UI triggers an event that your model happens to listen and the cycle repeats while killing performance?
Azul has a "max recursion depth" of 5 and then just throws an error (infinite cycle). So, it will invoke all the relevant callbacks for a frame, then "sum up" all of the Update enums (i.e. one callback returned RefreshDom -> now we need to repaint).
> Sometimes it is scrolling or typing, sometimes it is parts of the model subscribed to each other bubbling events to UI.
Scrolling, selection, typing, etc. are handled by the framework. To make something editable, you need to set "contenteditable=true" on the Dom node (like on the web). Then, on text editing (which can also come from IME, a11y input, copy-paste), you get a "text changeset". The callback can then "reject" the changeset or allow it (default, since you already set contenteditable before).
Azul has a "dual update pattern" for performance here, i.e. the DOM itself is immutable until the next layout() call, however for "quick edits" like dragging a node you obviously don't want to call layout() again and construct an entire new DOM tree. So there, you just (conceptually, don't know the current API for this):
def on_div_dragged(data, info):
mouse = info.get_window_state().mouse_state
info.set_css_property(info.get_hit_node(), "transform: translate(%s, %s)", mouse_state.x, mouse_state.y)
# store in data model or node if necessary
data.user_mouse_pos = mouse_state
return Update.DoNothing # no re-render here
So, if another callback fires in between, the data model is still properly up to date. Azul also aggressively reconciles focus, scroll position, selection, text cursor position, etc. But Azul does not allow "one event auto-triggers another" like SolidJS does, it looks nice on a slide deck and then is a pain to debug Rube-Goldberg state machines.This also works for text input or updating images (i.e. you don't need to call layout again on text input). Update.RefreshDom is for "larger / structural" changes, i.e. something like a route switch in a SPA-style app. Azul tracks the text cursor position by diffing the actual text, so the user code doesn't have to track the text cursor and state is preserved during a diff (it can also retain heavy elements).
For large lists, there is a native "virtualized view" DOM node with a callback that is being called "during" layout (after the size of the container has been determined, then the framework asks you to render your DOM, given the scroll position). So, that can be diffed, too. You never render in the DOM more than ends up on screen, so the perf is manageable.
Scrolling and retaining scroll positions inside a virtualized view is still an ongoing topic (not impossible, you just have to have functions to measure the DOM items before you return them, to estimate how much you need to render, and then do the math for "where are we right now, where is the scrollbar, how big is the virtualized view in relation to what we're rendering" - so the framework can set the right scrollbar size and position).
Again: please don't use or post Azul here on HN yet, docs are still slop and undergoing review, API is unstable until I have some apps going, but I just wanted to answer these questions.