Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Jira
serverMagnolia - Issue tracker
columnskey,summary,type,created,updated,due,assignee,reporter,priority,status,resolution
serverId500b06a6-e204-3125-b989-2d75b973d05f
keyDEV-910

Main goals

  • Adapt UI framework to Vaadin 8 concepts
  • Data binding in forms, dialogs and grids - simplify the existing solution

Module View Presenter

Reasons to botherWhat changesBenefits
  • Way too many interfaces where a mere class would do (see IoC part as well)
  • The way we used it before is cumbersome and not too beneficial.
  • Concept of Model is not well defined.
  • Poor component isolation patterns
    • views know about sub-views, presenters know about sub-presenters [...and sub-views].
    • poor state management (the parent views/presenters may just have most of the child view's state/business logic).
  • We try to prefer classes over the interfaces. If we introduce interfaces, we try to keep them small and function-oriented.
  • Instead of MVP we use something like MVVMP (Model-View-ViewModel-Presenter)
    • Model is the good ol' datasource like JCR workspace.
    • View is now the entry point and the driver of the pattern. Views may compose and even interact with each other (though we we should not encourage that!)
    • ViewModel is the state of the View (all the properties that define the view in real time)
    • Presenter - optional, completely isolated part which is used to help View interact with Model/ViewModel
  • We implement the ViewModel part via so called ViewContexts - interfaces that describe a certain part of mutable state (selection, value, location etc).
    • We provide the views with the ability to bind contexts and share them with the sub-views (see more in the IoC section).
    • For the ease of use, we do not enforce the developer to actually implement the interfaces, we generate them. (question) We use some reactive technology for convenient subscription.
  • Views are more component-like and are easier to compose.
    • Interfaces/classes do not expose methods like #refresh(), that encourage external control over the view.
    • All the internal view state management happens in the view itself (no case when sub-app reacts on events and updates the state of the sub-views/components).
    • All the necessary context can be injected via ViewContexts
  • Some synergy with how client-side frameworks manage the UI's (Redux/React contexts do similar things though in more conventional way)
  • Less code to write (less noisy listener interfaces, less abstractions)

...

Reasons to botherWhat changesOutcome

Quirky way of communicating the data source changes

  • Requires manual actions of both sending the events and handling them. I.e. if I change smth in JCR and forget to send CCE, no one will ever know.
  • Apps that are not related to the changed data source still have to handle its CCE and see if they should react (hence ContentConnector#canHandleItem).

We should try to expose the data source observation utilities and subscribe to them where needed.

  • e.g. we have already the JCR observation mechanism that Vaadin's DataProvider can subscribe to in this or that way and just push the change notification to the UI automatically.
  • Whenever we change smth in the data source the UI is notified eventually.
  • For the data sources that do not have real time observation, we could easily provide a timer-based implementation that merely refreshes the UI periodically.

Benefits

  • Clear and re-usable data source observation mechanism.
  • Less boilerplate for the app developers

Questions

  • (question) Need to provide un-registration strategy. For some cases a mere weak-hashmap solution might work (question). We though should also make sure that once the view is dead, no observation left-overs are still hanging around

THE FOLLOWING STILL REQUIRES GROOMING

(plus) Affected parts:
UI framework in Magnolia involves different components and abstractions that function on different levels. In the scope of the current effort we plan to target/re-work only some of them.

  • Content app de
  • Content app implementation.

Main goals

The main purpose of the effort is to simplify the existing concepts

Current state overview

Framework should help the developers to build their apps with Magnolia faster, get access to the components and features, framework though should not be all the time in the way of the developer.

The current offering of the UI framework:

  • Content app implementation 
  • Form framework 
  • Flexibility via configuration of the content apps
  • Dependency injection via Guice and factory capabilities of the ComponentProvider
  • EventBus API to share events between the UI parts

The problems:

...

  • Solution proposal - let the views drive the process, treat presenters as optional, view-specific detail that merely helps to separate communication with the backend.

...

  • As a result - lot's of redundant Listener interfaces between views and presenters.
  • Each view and a presenter as a result is a Guice component mapped in the module descriptor, making it hard to override the parts with custom implementations.
  • Can we treat views more as custom Vaadin components? Can we rely more on ComponentProvider#newInstance more when creating the views and merely using the impl classes coming from e.g. definitions?
  • Possible solution: provide views with out-of-the-box component provider and #create() API, let them create sub-views easily.

...

How to share context and provide injectable components

...

  • Views rarely require 'components' with specific lifecycle (typically they just need an instance of smth).
  • Views, especially the ones that are bound to generic data, require generic instances to be provided. Guice does not support that easily (and our XML configs completely block that).

...

Code Pro
languagejava
titleFrameworkView
public interface UiFrameworkView extends View {

    ...

    /**
     * Wrapper around {@link ComponentProvider} capabilities.
     */
    default <T> T create(Class type, Object... args) {
        return getComponentProvider().newInstance((Class<T>) type, args);
    }

    default ViewProvider getViewProvider() {
        return new ViewProvider.Impl(getCurrentViewReference());
    }

    default ComponentProvider getComponentProvider() {
        return new ViewComponentProvider(getCurrentViewReference());
    }

    default UiContextReference getCurrentViewReference() {
        return ViewContextKeyRegistry.access()
                .lookUp(this)
                .orElseGet(() -> CurrentUiContextReference.get().getUiContextReference());
    }

    default <T extends ViewContext> T bindContext(Class<? extends T> contextClass) {
        final T context = new ViewContextProxy().createViewContext(contextClass);
        SessionStore.access().getBeanStore(getCurrentViewReference()).put(contextClass, context);
        return context;
    }

    default void bindDatasource(Object definition) {
        SessionStore.access().getBeanStore(getCurrentViewReference()).put(DatasourceHolder.class, new DatasourceHolder(definition));
    }
}


How to bind to different data sources

The following could be a looser and more flexible replacement for the ContentConnector abstraction. Instead of having the monolithic interface, we rather could facilitate a DatasourceSupport that could provide data-binding implementations based on the exposed configuration by e.g. delegating to the registered reference implementations. 

Image Removed

Code Pro
languagejava
@Singleton
public class DatasourceSupport {

    private final Map<Class, DatasourceBundle> bundles;

    @Inject
    public DatasourceSupport(Set<DatasourceBundle> bundles) {
        this.bundles = bundles
                .stream()
                .collect(toMap(
                        DatasourceBundle::supportedDataSourceType,
                        identity()));
    }

    @SuppressWarnings("unchecked")
    public <DEF> DatasourceBundle<DEF> getDatasourceBundle(DEF def) {
        Objects.requireNonNull(def);
        return Optional.ofNullable(bundles.get(def.getClass())).orElseThrow(() -> new IllegalArgumentException("No such bundle for the type " + def.getClass()));
    }
}



@Multibinding
public abstract class DatasourceBundle<DEF> {

    private final Class<DEF> type;

    public DatasourceBundle(Class<DEF> type) {
        this.type = type;
    }

    public Class<DEF> supportedDataSourceType() {
        return this.type;
    }

    public abstract <T> T lookup(Class<T> type, DEF definition);
}
Code Pro
languagejava
titleProvisioning of datasource components
public class DatasourceComponentParameterResolver implements ParameterResolver {

    private final DatasourceSupport datasourceSupport;
    private final Object datasourceDefinition;

    public DatasourceComponentParameterResolver(DatasourceSupport datasourceSupport, Object datasourceDefinition) {
        this.datasourceSupport = datasourceSupport;
        this.datasourceDefinition = datasourceDefinition;
    }

    @Override
    public Object resolveParameter(ParameterInfo parameter) {
        boolean isDatasourceComponent = Stream.of(parameter.getParameterAnnotations()).anyMatch(DatasourceComponent.class::isInstance);
        if (isDatasourceComponent) {
            return datasourceSupport
                    .getDatasourceBundle(datasourceDefinition)
                    .lookup(parameter.getParameterType(), datasourceDefinition);
        }

        return UNRESOLVED;
    }
}

Questions:

...

Databinding in Grids

  • Datasource support introduction might be a nice matching for the grid databinding requirements, since what is mostly required is the DataProvider (to fetch the row data) and the PropertySet (for the column rendering).
  • Apparently it is possible to pass the PropertySet instance directly into the Grid making it aware of the underlying datasource properties, then configuration of the columns is just about assigning the captions and renderers.

...