Dojo Framework

Last modified by Ed Callahan on 2011/12/06 14:31
  1. Dojo Framework

Why Dojo Matters

The Dojo framework is a javascript library that WaveMaker is built on top of.

In addition to providing a set of libraries useful for building applications, it also provides sets of widgets. WaveMaker tries to take the best of dojo's widgets and where we weren't satisfied with what dojo had to offer, we provided some of our own widgets as well.

WaveMaker is built using the dojo javascript toolkit; all classes are declared using dojo's object oriented class management system; and in this sense, WaveMaker and any project built with wavemaker is built using the dojo toolkit.

So where and why does WaveMaker diverge from Dojo?

Containers

Containers, panels, tabs, dialogs all depend upon a common set of base classes, so any widget whose purpose is to lay out widgets are fundamentally different between dojo and WaveMaker. Why is that?

Dojo's model of its users is as someone who opens up a text editor, enters a bunch of dijits, along with some CSS to help lay them out. These users will use containers when they want tabs. They might use them to separate the table of contents from the header from the body of a document. But within each of these containers, the dijits are laid out by the developer using CSS.

WaveMaker's model is that the WaveMaker framework handles the layout for the user, the user simply drags and drops things where they are supposed to go, and the panel helps to position things, an especially resize things dynamically if they use % sizing.

Having tried to use dojo containers to lay out every single widget, and comparing performance of dojo containers to wavemaker containers, their performance (as of dojo 1.2 or 1.3) was orders of magnitude worse than WaveMaker. This is not a criticism but simply a recognition of the different models of how a user will use their toolkit vs our designer.

By chosing not to use dojo containers, we've made it noticably harder to use their tabs, dialogs, dashboards and really, and dojo widget that depends upon their container model.

What to Know About Dojo

dojo.declare

A javascript class can be created using dojo calls:

dojo.declare("MyClass", null, {
   /* Properties */    
    budget: 50,
    year: 2025,
    name: "Jupiter",
    /* Dojo framework calls postscript after it finishes initializing your class; postscript is where your custom
     * initialization code would typically go
     */
    postscript: function() {
    },
    method1: function(arg1,arg2,arg3) {
    },
    method2: function(arg1,arg2,arg3) {
    }
});

More commonly, you'll define your class to be a subclass of a WaveMaker class, in which case you would not use postscript shown above, but would use the widget lifecycle?.

dojo.declare("MyClass", wm.ToolButton, {
    postInit: function() {
          this.inherited(arguments);
          this.setWidth(this.height);
    }
});

dojo.inherited

Common object oriented code needs a method to be able to call the parent classes version of the method; this is done in one of two ways:

mymethod: function(a,b,c) {
    this.inherited(arguments); // calls the parent class's mymethod method passing it the values of a,b,c
    this.inherited(arguments, [5,8,22]); // calls the parent class's mymethod method passing in alternate values for a,b,c
}

dojo.extend

dojo.extend provides a way of modifying the definition of a class. You can use this to add methods to a class definition or to modify an existing method. Lets take for example a trivial example where we want the wm.Lookup editor to always do an alert whenever it gets a new value:

wm.Lookup.extend({
        setDataValue: function(inData) {
            if (this.dataSet && inData) {
                this.dataSet.setData(inData ? [inData] : null);
            }
            this.inherited(arguments);
            /* This is the part that has been added to the setDataValue method */
            if (this.displayValue) {
                alert(this.dataValue[this.displayField]);
            }
        }        
    });

The question then becomes how/where would I put this extend call. Issues that need to be addressed:

  1. You want this extend code to be run before the widget is created. This is not required, but helps to insure that your new/modified method is the only one that ever gets called.
2. The class you are trying to extend may not have been loaded yet. You can not call dojo.extend on a class that does not yet exist.

The following more complicated example shows how you can do this. Going to your Source tab -> Application subtab, we can enter the following code:

/* STEP 1: Create a function that when it is run will extend wm.Lookup.  We can not execute this function
 *             until wm.Lookup exists.
 */
var project1LookupHack = function() {
        wm.Lookup.extend({
        setDataValue: function(inData) {
            if (this.dataSet && inData) {
                this.dataSet.setData(inData ? [inData] : null);
            }
            this.inherited(arguments);
            if (this.displayValue) {
                alert(this.dataValue[this.displayField]);
            }
        }        
    });
  };
  /* STEP 2: If wm.Lookup exists, call our function that updates the wm.Lookup definition */
  if (wm.Lookup) {
      project1LookupHack();
  } 
else {
      var buildPackage = wm.Array.last(wm.componentList["wm.Lookup"]);
      wm.addFrameworkFix(buildPackage, project1LookupHack);
  }
/* The rest of the application tab */
Project1.extend({
	_end: 0
});

dojo.hitch

Typically, we don't simply want to execute a method; we want to execute a method whose "this" pointer refers to the correct thing. Lets take a simple example:

dojo.declare("Main", wm.Page, {
    start: function() {
          this.button1.onclick = function() {
               this.button2.show();
          };
    }
});

Now, the concept we have to understand when looking at the above onclick function, is what does "this" refer to. Is the "this" in "this.button2" the same as the "this" in "this.button1"? In fact, the answer here is no; most commonly, "this" will refer to the window, or in this case, it refers to "button1" whose onclick function is being called. Its always important to know and control what "this" refers to, and dojo.hitch lets us do that.

dojo.declare("Main", wm.Page, {
    start: function() {
          this.button1.onclick = dojo.hitch(this,function() {
               this.button2.show();
          });
    }
});

dojo.hitch takes as parameters

  • this: the object that will be treated in the function as "this"
  • function: the function that will be executed with the specified "this" in its context.
dojo.hitch can also take a string that names a function:
dojo.declare("Main", wm.Page, {
    start: function() {
          this.button1.onclick = dojo.hitch(this,"myOnClick");
    },
    myOnClick: function() {
          this.button2.show();
    }
});

The above code specifies that button1's onclick event handler is the "myOnClick" method of "this".

dojo.connect

The connect function is a very powerful tool that lets a developer say "any time A happens, I want B to happen as well". By providing a flexible way to express this request, it becomes possible to build more flexible and modular applications. Lets compare this with a traditional development style. Lets take the case where the user clicks on a button. A traditional development approach would expect the button to know everything that must happen when it is clicked:

this.button1.onclick = function() {
   this.effect1();
   this.effect2();
   this.effect3();
   ...
   this.effectn();
};

Any time your application evolves, you have to go back, find the button1 onclick event handler and modify it to account for your changes.

In contrast, lets suppose that each component that wants to do something when the button is clicked can request to be notified of the click; the onclick code that affects a given component is controlled by that component and not by the button.

PROS: If I want to add a new side effect to clicking on the button, I don't need to further complicate my onclick event handler, I don't need my onclick event handler to build in knowledge of the components to be affected by this. The component that will be affected already knows its own internals and can connect to it without building in any module-specific logic that the button doesn't need to know about.

CONS: I may now have code to be executed onclick in a dozen different places.

Ultimately, using connect makes it far easier to build a well architected MVC style application where widgets can connect to changes in the model; the controller can connect to changes in widget state, and there is no need for the model to have widget code in it, nor the widget to have model code in it.

Parameters:

  • The object whose method we want to trigger our method
  • The name of the method that will trigger our method
  • The object whose method will be run when the trigger occurs
  • The name of the method to run when the trigger occurs
var connectionId = dojo.connect(this.button1, "onclick", this, "myOnClick");

Any time the user clicks on the button, it will call this.myOnClick().

Services Example:

start: function() {
   // Any time either service variable 1 or 2 comes back with data, call mySuccessHandler.
   // There is no limit to the number of things that can be connected to "mySuccessHandler"
   dojo.connect(this.serviceVariable1, "onSuccess", this, "mySuccessHandler");
   dojo.connect(this.serviceVariable2, "onSuccess", this, "mySuccessHandler");
   // You can also create as many connections to serviceVariable1's onSuccess event as you want;
   // in this case, serviceVariable1 will trigger calls to mySuccessHandler, anotherSuccessHandler and
   // anotherSuccessHandler2, while serviceVariable2 only calls mySuccessHandler
   dojo.connect(this.serviceVariable1, "onSuccess", this, "anotherSuccessHandler");
   dojo.connect(this.serviceVariable1, "onSuccess", this, "anotherSuccessHandler2");

dojo.disconnect

Any time you create a connection, you create pointers to objects that will need to be garbage collected. You can deallocate these pointers using dojo.disconnect(). The call to dojo.connect returns a connectionId that you can cache and use in a later call.

start: function() {
    /* By putting the disconnect inside of the function, we insure that this function is only called the first time
     * the user clicks
     */
    this.connectionId = dojo.connect(this.button1, "onclick", this, function() {
        alert("CLICK");
        dojo.disconnect(this.connectionId);
    });
},
destroy: function() {
    /* In case the user never clicked on the button, we'll still need to disconnect when this page is destroyed */
    dojo.disconnect(this.connectionId);
    this.inherited(arguments);
}

this.connect

WaveMaker objects support a simplification of dojo.connect that automatically adds the connection id to a list that needs to be disconnected when destroy is called.

start: function() {
    /* The connection id here is disconnected when "this" is destroyed (i.e. the page) */
    this.connect(this.button1, "onclick", this, "myOnClick");
    /* The connection id here is disconnected when "this.button1" is destroyed (usually but not always at 
     * the same time that the page is destroyed */
    this.button1.connect(this.button1, "onclick", this, "myOnClick");
}

dojo.subscribe/publish

Rather than connecting to a single method, its sometimes offers more flexibility to to subscribe to notifications. A notification does not let you have something called every time an event such as onclick is fired, but does let you cause something to be executed each time a call is made to dojo.publish. For purposes of a WaveMaker developer, this might be useful for one page to let other pages know that something has happened:

dojo.publish takes two parameters:

  • Notification name: The first parameter is the name of the notification; this is what you must dojo.subscribe to if you want to be notified of this particular call to dojo.publish
  • Args: an array of arguments to pass to anyone subscribed to this notification
dojo.declare("Page10", wm.Page, {
   button1click: function() {
        dojo.publish("Page10.button1Click", [this.text1.getDataValue(), this.text2.getDataValue(), this.text3.getDataValue()]);
   }
});

dojo.subscribe takes three parameters:

  • Notification name: The string that each time dojo.publish is called with it should trigger this subscription
  • context: Typically "this"; indicates what the "this" object refers to within the function
  • function: Either the name of a function (string) or a function declaration
dojo.declare("Main", wm.Page, {
    start: function() {
       /* Note that we use this.subscribe instead of dojo.subscribe; this allows us to automatically clean up
        * any subscriptions from memory when the page is destroyed
        */
       this.subscribe("Page10.button1Click", this, function(text1value, text2value, text3value) {
             this.label1.setCaption(text2value);
       });
       /* This is also valid */
       this.subscribe("Page10.button", this, "onPage10ButtonClick");
    },
    onPage10ButtonClick: function(text1value, text2value, text3value) {
        this.label3.setCaption(text3value);
    }
});
Tags:
Created by Michael mkantor on 2011/07/24 17:15

© 2012-2014 WaveMaker Inc. All Rights Reserved.
Share/Bookmark