ClientBundle for multiple "themes"
Asked Answered
E

1

8

We have a web application that needs a different theme for each major client. The original developer did this by looking at the URL in javascript and adding a stylesheet to override the default theme.

One problem with this is the site has the default look for a few seconds then suddenly swaps to the correct theme. Another is that it seems to waste a lot of bandwidth/time.

My current idea is to create a "default" ClientBundle with our default look and feel extend that interface and override each entry (as needed) with the client's images using the various annotations like @ImageResouce and pointing to a different location.

Has anybody had experience doing this? One problem I forsee is not being able to use the uibinder style tags as they statically point to a specific resource bundle.

Any ideas?

Expostulate answered 18/4, 2011 at 21:48 Comment(0)
A
17

Overriden bundles

Yes you can.

I've did the override thing with ClientBundles and works fine. One thing you MUST do is inherit the types of the properties too. By example:

BigBundle {
  Nestedundle otherBundle();
  ImageResource otherImage();
  Styles css();
}

And then you must inherit this way:

OtherBigBundle extends BigBundle {
  OtherNestedBundle otherBundle(); // if you want to change it
  ImageResource otherImage(); // of you want to change it
  OtherStyles css(); // of you want to change it
}

and OtherNestedBundle extends NestedBundle and OtherStyles extends Styles

At least with css's: if the properties are declared NOT USING the child interface they will produce styles for the same CSS classname and all will be mixed. So declare overriden styles with the child interfaces :)

Flexible UIBinders

You can set from outside the bundle to use if you use UiField(provided=true) annotation. In this way you first set the bundle and then call the uibindler. It will use the resource field assuming it's already created.

Deferred binding

You could use GWT.runAsync for loading just the correct bundle.

Some example

The ui.xml

<ui:with field='res' type='your.package.TheBundle'/>

the corresponding class

@UiField(provided=true) TheBundle bundle;

private void createTheThing() {
  this.bundle = factory.createBundle();
  MyUiBindler binder = GWT.create(MyUiBindler.class);
  this.panel = binder.createAndBindUi(this);
  ...
}

Some bundle interfaces

interface TheBundle extends ClientBundle {
  @ImageResource("default.png")
  ImageResource image1();

  @Source("default.css")
  TheCss css();
}

interface Theme1Bundle extends TheBundle {
  @ImageResource("one.png")
  ImageResource image1(); // type: imageresource is ok

  @Source("one.css")
  OneCss css(); // type: OneCss => use other compiled css class-names

  interface OneCss extends TheCss { // inner-interface, just for fun
     // don't need to declare each String method
  }
}

If you don't override something it's ok

Options for the bundle factory

1) just altogether

if (...) {
  return GWT.create(TheBundle.class);
} else if (...) {
  return GWT.create(Theme1Bundle.class);
}

2) runAsync (just load the needed part... but after the initial part is executed)

if (...) {
   GWT.runAsync(new RunAsyncCallback() {
      public void onSuccess() {
        return GWT.create(TheBundle.class);
      }
      // please program the onFailure method
   });
} else if (...) {
   GWT.runAsync(new RunAsyncCallback() {
      public void onSuccess() {
        return GWT.create(Theme1Bundle.class);
      }
      // please program the onFailure method
   });
}

3) use deferred-binding and generators for autogenerating factory in compile-time based on annotated bundles like @ThemeBundle("one")

This example is from the real world. I use a DynamicEntryPointWidgetFactory (DEPWidgetFactory for short) for creating widget based on an identifier string. Each widget is an application screen and each main menu ítem has the widgetName it has to create.

In your case the id will be the theme to create.

Important: if you use runAsync you cannot create the resourcebundle just before creating the UI like in the sample code before. You must ask for the theme and when it's ready (in the callback) pass it to your widget constructor and your widget can assign it to its field.

The factory interface:

public interface DynamicEntryPointWidgetFactory
{
   public void buildWidget(String widgetName, AsyncCallback<Widget> callback);
}

The annotation for widgets to generate:

@Target(ElementType.TYPE)
public @interface EntryPointWidget 
{
    /**
     * The name wich will be used to identify this widget.
     */
    String value();
}

The module configuration:

It says: the implementation for the Factory will be generated with this class (the other option is to use replace-with, but in our case we don't have predefined options for each locale or browser, but something more dynamic).

<generate-with class="com.dia.nexdia.services.gwt.rebind.entrypoint.DynamicEntryPointFactoryGenerator">
  <when-type-assignable class="com.dia.nexdia.services.gwt.client.entrypoint.DynamicEntryPointWidgetFactory" />
</generate-with>

The generator:

public class DynamicEntryPointFactoryGenerator extends Generator {
    @Override
    public String generate(TreeLogger logger, GeneratorContext context,
            String typeName) throws UnableToCompleteException {
        PrintWriter pw = context.tryCreate(logger,
                "x.services.gwt.client.entrypoint",
                "DynamicEntryPointWidgetFactoryImpl");

        if (pw != null) {
            // write package, imports, whatever
            pw.append("package x.services.gwt.client.entrypoint;");
            pw.append("import x.services.gwt.client.entrypoint.DynamicEntryPointWidgetFactory;");
            pw.append("import com.google.gwt.core.client.GWT;");
            pw.append("import com.google.gwt.core.client.RunAsyncCallback;");
            pw.append("import com.google.gwt.user.client.rpc.AsyncCallback;");
            pw.append("import com.google.gwt.user.client.ui.Widget;");

            // the class
            pw.append("public class DynamicEntryPointWidgetFactoryImpl implements DynamicEntryPointWidgetFactory {");

            // buildWidget method
            pw.append("   public void buildWidget(String widgetName, final AsyncCallback<Widget> callback) {");

            // iterates over all the classes to find those with EntryPointWidget annotation
            TypeOracle oracle = context.getTypeOracle();
            JPackage[] packages = oracle.getPackages();
            for (JPackage pack : packages) 
            {
                JClassType[] classes = pack.getTypes();
                for (JClassType classtype : classes) 
                {
                    EntryPointWidget annotation = classtype.getAnnotation(EntryPointWidget.class);
                    if (annotation != null) 
                    {
                        String fullName = classtype.getQualifiedSourceName();
                        logger.log(TreeLogger.INFO, "Entry-point widget found: " + fullName);

                        pw.append("if (\"" + annotation.value() + "\".equals(widgetName)) {");
                        pw.append("   GWT.runAsync(" + fullName + ".class, new RunAsyncCallback() {");
                        pw.append("      public void onFailure(Throwable t) {");
                        pw.append("         callback.onFailure(t);");
                        pw.append("      }");
                        pw.append("      public void onSuccess() {");
                        pw.append("         callback.onSuccess(new " + fullName + "());");
                        pw.append("      }");
                        pw.append("   });");
                        pw.append("   return;");
                        pw.append("}");
                    }
                }
            }
            pw.append("callback.onFailure(new IllegalArgumentException(\"Widget '\" + widgetName + \"' not recognized.\"));");

            pw.append("   }");
            pw.append("}");

            context.commit(logger, pw);         
        }

        // return the name of the generated class
        return "x.services.gwt.client.entrypoint.DynamicEntryPointWidgetFactoryImpl";
    }
Ahmedahmedabad answered 18/4, 2011 at 21:58 Comment(6)
How would option 3 work? You register a generator in the GWT xml config file?Expostulate
Exactly. You specify which is the "generator-class" for the BundleFactory interface. That generator class must implement a method that outputs the code. That code writes the "if's". It can use some GWT utility classes to find Bundle interfaces tagged in some way (like the @ThemeBundle example). Tomorrow I can post some sample code if you like.Ahmedahmedabad
With traditional css swapping wouldn't changing which css file is loaded instantly reaload the entire GUI? Is there a way to acheive this same effect or would I have to "re-set" each style to the new bundle.Expostulate
I'm not sure what are you asking. But if you are proposing that using one or other usual css external file would work as a theme swap... yes it will work. The advantage of Clientbundles is the compilation (it is: optimization) and the inclusion of images and other resources inside the theme. But if what you need is only changing an external css file, it's ok.Ahmedahmedabad
What I was trying to ask is if it is possible with clientbundles to swap the themes (via drop box perhaps) and have the GUI show all of the different theme's styles without having to call widget.setStyle(MyBundleUtil.getCurrentBundle().css().someStyle()) on each widget that can have the theme look changed.Expostulate
Good point. 1) One aproach would be using the same class names (declaring the same CssResource type in the child resource interface) and "de-injecting" the previous css. But I think you can't. 2) Other aproach would be simply having a setAllStyles(Bundle) method that you could call :) or 3) You could refresh the page ;-) I think it's the simplest solution of all...Ahmedahmedabad

© 2022 - 2024 — McMap. All rights reserved.