Welcome to AspAdvice Sign in | Join | Help

Alessandro Gallo

.NET & Beyond
Handling the context menu with a custom Atlas Behavior
One of the goals of Atlas is making easier to build DHTML-enabled controls, while helping to easily develop web sites and web applications by providing features like OOP patterns, components, bindings, transformers and more.

For example, Atlas controls can have behaviors, where a Behavior is a component used to dynamically add/remove client side functionality (mainly DHTML). Since Atlas components expose a behaviors collection, their client side capabilities can be defined and controlled programmatically by adding/removing behaviors from it. Examples of behaviors are the PopupBehavior that shows/hides a control, the FloatingBehavior that transforms a control into a floating control, the AutoCompleteBehavior that helps to fill text fields from a list of suggestions.


In this article we'll see how to code a simple Behavior (called ContextMenuBehavior) that allows to associate a context menu to a control by setting a contextMenuID property. When the user right-clicks on a control with a ContextMenuBehavior, a context menu is showed and a contextMenuShow event is raised.

Note: this Behavior works in browsers that support the oncontextmenu event, like for example IE6 and FireFox1.5.

Examining the code (you will find it at the end of this post), we'll overview some OOP patterns introduced by the Atlas framework, including namespaces, inheritance and other features like events, getters/setters, declarative usage in XML script. When dealing with OOP patterns, I will refer to Javascript's typed constructors as classes, and to variables and functions declared inside the class as fields and methods.

To be able to use the features provided by the Atlas framework, a custom class must implement one or more interfaces defined in the framework, or inherit from one of the base classes provided (tipically Sys.Component or Sys.UI.Control). The Sys.Component class provides the basic Atlas functionality while Web.UI.Control is the base class for all the UI Controls (wrappers for DOM elements like TextBox or Button, or composite controls).
The ContextMenuBehavior class that we are going to implement inherits from the Sys.UI.Behavior class (that is the base class for all behaviors), which in turn inherits from the Web.Component class.

Overview of the code
First of all, we have to register the namespaces that we intend to use. In this case, we register the namespace AtlasNotes.UI that will be the namespace for our class.

Type.registerNamespace('AtlasNotes.UI');

The namespace registration, done with the call to registerNamespace(), is followed by the declaration of our class:

AtlasNotes.UI.ContextMenuBehavior = function() {
    AtlasNotes.UI.ContextMenuBehavior.initializeBase(this);
...

where the first statement is a call to initializeBase(), that allows to properly setup instances within the Atlas framework.

Then, I've declared some fields supposed to have private scope (Javascript doesn't support access modifiers, so it is a convention to prefix with an underscore fields or methods with private scope).

var _contextMenuHandler;
var _mousedownHandler;
var _contextMenuID;
var _contextMenu;
var _popupBehavior;

After private fields declaration, some public properties are exposed. Public properties are getters or setters in the Atlas framework. There is a convention that must be followed when naming public properties: a setter must have the prefix set_, while a getter must have the prefix get_. This ensures that our Behavior will work properly with declarative script or bindings.

I've also declared an event to be raised by the ContextMenuBehavior. An event is created with the createEvent() method:

this.contextMenuShow = this.createEvent();

The event contextMenuShow is raised when the context menu is made visible after the user's right-click. Events in the Atlas framework behave in a manner similar to events in the .NET framework. They are handled by declaring an event handler (a function) for that particular event. When the event is raised, if an event handler is found, it is called with two arguments: a reference to the object that raised the event (the "sender") and a state object (the "event arguments"). For example, the following function acts as an event handler for the contextMenuShow event:

function onContextMenuShow(sender, e) {
    // Get the source control.
    var src = sender.control.element;
}

Two important methods are those related to the lifecycle of our object. We can partecipate in initialization and disposing of the object, by overriding the initialize() and dispose() methods of our base class (Sys.UI.Behavior). In Javascript, we can "override" a method by declaring a function with the same signature. Moreover, the Atlas framework allows to call the method implementation of the base class, by invoking the callBaseMethod(), passing a reference to the current instance and the name of the base method to call (to be able to call the implementation of the base class, the corresponding method must have been registered in the base class with a call to registerBaseMethod()). In the code below, we are overriding the initialize()/dispose() methods, calling their base implementations and then adding custom code:

this.initialize = function() {
    AtlasNotes.UI.ContextMenuBehavior.callBaseMethod(this, 'initialize');
...

this.dispose = function() {
    AtlasNotes.UI.ContextMenuBehavior.callBaseMethod(this, 'dispose');
...

In the initialize() method, I've setup handlers for the oncontextmenu and onmousedown events of the document DOM element. In these event handlers, the context menu is made visible, or hided if one left-clicks outside it. The event handlers are the mousedownHandler() and contextmenuHandler() methods defined in the class.

For example, look at this statement in the mousedownHandler():

var bounds = Sys.UI.Control.getBounds(_contextMenu.element);

There I invoked the getBounds() method of  the Sys.UI.Control class. This method (you can think of it as a "static" method) returns the bounds of a DOM element in an object with four properties: x, y, width, height). I've used these properties to test if the user clicked outside the menu region. In the initialize() method I've also upgraded the context menu element to an Atlas control and added a PopupBehavior to it:

_contextMenu = new Sys.UI.Control($(_contextMenuID));

_popupBehavior = new Sys.UI.PopupBehavior();
_popupBehavior.set_parentElement(this.control.element);
_contextMenu.get_behaviors().add(_popupBehavior);
_popupBehavior.initialize();

_contextMenu.initialize();

In the dispose() method, event handlers are detached from the corresponding DOM events and set to null for garbage collection.

An important method inherited from our base class is getDescriptor(). This method is defined in the Sys.Component class and allows to add support for usage of our Behavior in declarative XML script (the Atlas markup). To do that, we have to override the getDescriptor() method and add properties, methods and events to be mapped to XML tags and attributes:

this.getDescriptor = function() {
   var td = AtlasNotes.UI.ContextMenuBehavior.callBaseMethod(this, 'getDescriptor');
       
   td.addProperty('contextMenuID', String);
   td.addEvent('contextMenuShow', true);
   return td;
}

The getDescriptor method has been overridden above. We first call the base class implementation, that returns a reference to the type descriptor (td) and then add the property contextMenuID and the event contextMenuShow. Now, we can use the ContextMenuBehavior in the Atlas markup:

<contextMenuBehavior contextMenuID="contextMenu1"
                                contextMenuShow="onContextMenu" />

At the end of the code, after the class declaration, we find two other important statements:

AtlasNotes.UI.ContextMenuBehavior.registerClass('AtlasNotes.UI.ContextMenuBehavior', Sys.UI.Behavior);
Sys.TypeDescriptor.addType('script', 'contextMenuBehavior', AtlasNotes.UI.ContextMenuBehavior);

With the first statement, we make our class inherit from the Sys.UI.Behavior class. The second argument passed to the registerClass() method is the base type (Sys.UI.Behavior). If our class implements any interfaces, we have to pass their names as arguments after the base type.
Finally, with the call to addType() in the last statement, we register the ContextMenuBehavior to be recognized as a valid tag when used in XML script.

Summary
Behaviors are classes that inherit from the base Sys.UI.Behavior class. Basic steps to build a custom type within the Atlas framework are

  • register the needed namespaces using the Type.registerNamespace() method;
  • declare the class as a typed constructor using its full name (namespace.className);
  • call the initializeBase() method of the base class to let the Atlas framework properly setup the instance;
  • define fields, properties (using the getter/setter convention), events (using the createEvent() method), methods for the class;
  • override the initialize()/dispose() methods of the base class to partecipate in the instance's lifecycle;
  • override the getDescriptor() method of the base class to add support for use in XML script;
  • register the class as a derived class calling the registerClass() method;
  • register the class to be recognized as a valid element in XML script by invoking the Sys.TypeDescriptor.addType() method.
ContextMenuBehavior.js

Type.registerNamespace('AtlasNotes.UI');

AtlasNotes.UI.ContextMenuBehavior = function() {
    AtlasNotes.UI.ContextMenuBehavior.initializeBase(this);
  
    // Private fields.
    var _contextMenuHandler;
    var _mousedownHandler;
    var _contextMenuID;
    var _contextMenu;
    var _popupBehavior;
  
    // Properties.
    this.get_contextMenuID = function() {
        return _contextMenuID;
    }
    this.set_contextMenuID = function(value) {
        if(value != _contextMenuID) {
            _contextMenuID = value;
            this.raisePropertyChanged('contextMenuID');
        }
    }
  
    // Events.
    this.contextMenuShow = this.createEvent();
  
    // Initialize / Dispose
    this.initialize = function() {
        AtlasNotes.UI.ContextMenuBehavior.callBaseMethod(this, 'initialize');
        _contextMenu = new Sys.UI.Control($(_contextMenuID));
        _popupBehavior = new Sys.UI.PopupBehavior();
        _popupBehavior.set_parentElement(this.control.element);
        _contextMenu.get_behaviors().add(_popupBehavior);
        _popupBehavior.initialize();
        _contextMenu.initialize();
      
        _contextMenuHandler = Function.createDelegate(this, contextMenuHandler);
        document.attachEvent('oncontextmenu', _contextMenuHandler);
      
        _mousedownHandler = Function.createDelegate(this, mousedownHandler);
        document.attachEvent('onmousedown', _mousedownHandler);
    }
    this.dispose = function() {
        AtlasNotes.UI.ContextMenuBehavior.callBaseMethod(this, 'dispose');
      
        document.detachEvent('oncontextmenu', _contextMenuHandler);
        document.detachEvent('onmousedown', _mousedownHandler);
      
        _contextMenuHandler = null;
        _mousedownHandler = null;
    }
  
    // Handlers.
    function contextMenuHandler() {
        var evt = window.event;
        var target = evt.srcElement;
      
        if(target == this.control.element) {
            _popupBehavior.set_x(evt.offsetX);
            _popupBehavior.set_y(evt.offsetY);
            _popupBehavior.show();
          
            this.contextMenuShow.invoke(this, Sys.EventArgs.Empty);
          
            evt.returnValue = false;
        }
    }
    function mousedownHandler() {
          var x = window.event.clientX;
          var y = window.event.clientY;
          var bounds = Sys.UI.Control.getBounds(_contextMenu.element);
        
          if(!((x >= bounds.x) && (x <= bounds.x + bounds.width)
           && (y >= bounds.y) && (y <= bounds.y + bounds.height))) {
              this.hideMenu();
          }
    }
  
    // Public methods.
    this.hideMenu = function() {
        _popupBehavior.hide();
    }
  
    // getDescriptor.
    this.getDescriptor = function() {
        var td = AtlasNotes.UI.ContextMenuBehavior.callBaseMethod(this, 'getDescriptor');
      
        td.addProperty('contextMenuID', String);
        td.addEvent('contextMenuShow', true);
        td.addMethod('hideMenu');
       
        return td;
    }
}
AtlasNotes.UI.ContextMenuBehavior.registerClass('AtlasNotes.UI.ContextMenuBehavior', Sys.UI.Behavior);
Sys.TypeDescriptor.addType('script', 'contextMenuBehavior', AtlasNotes.UI.ContextMenuBehavior);

UPDATES:

  • 03/21/2006 - Updated both the post and the code to the March CTP.
  • 02/21/2006 - ContextMenuBehavior.js, fixed a problem with context menu positioning (thanks to Mirko).
  • 01/24/2006 - ContextMenuBehavior.js, added the hideMenu() method in getDescriptor() to be used in declarative script.

 

Posted: Monday, January 23, 2006 8:47 PM by Garbin

Comments

Mirko said:

Thanx for your work!...
It's very usefull but I found an error in ContextMenuBehavior.js:
I tried hook more than one control at time.. and on the second control the context menu doesn't work ok.. the Y position was wrong.. I changed the code:
from:
_popupBehavior.set_x(evt.clientX);
_popupBehavior.set_y(evt.clientY);
to:
_popupBehavior.set_x(evt.offsetX);
_popupBehavior.set_y(evt.offsetY);
now it works fine..
I hope this helps..
Bye..
# February 16, 2006 9:08 AM

Garbin said:

Mirko,
thank you very much for the feedback. As you suggested, the correct way is to get the mouse coordinates relative to the element that fired the event.
# February 20, 2006 7:39 PM

Recon_609 said:

Thanks for the code sample - very helpful with your good explanations.

I like the way you break out each piece and explain.

Is there a list of possible 'behaviors' somewhere that I can get bigger picture of the possibilities?

Thanks again - great work!
# March 17, 2006 11:23 PM

davidw said:

It is broken on March CTP

thanks
# March 20, 2006 5:19 PM

Garbin said:

David,

thanks for reporting this. The code and the post have been updated to the March CTP.
Garbin
# March 21, 2006 7:02 AM

Atlas notes said:

A user on the ASP.NET Forums (dleffel, in this thread) has an interesting requirement. Basically, he...
# March 23, 2006 7:10 AM

Atlas notes said:

A user on the ASP.NET Forums (dleffel, in this thread) has an interesting requirement. Basically, he...
# March 23, 2006 7:13 AM

Atlas notes said:

A user on the ASP.NET Forums (dleffel, in this thread) has an interesting requirement. Basically, he...
# March 23, 2006 7:14 AM

Atlas notes said:

A user on the ASP.NET Forums (dleffel, in this thread) has an interesting requirement. Basically, he...
# March 23, 2006 7:14 AM

Atlas notes said:

In a previous post I gave a quick introduction to the Atlas Drag&amp;amp;Drop system. The core of this system...
# March 29, 2006 6:21 AM

jared said:

Fantastic post! I was wondering if you noticed the contextmenu shows up well below the element that invoked it when using IE. I can probably throw together a hack to get around this, but I'd like to see your approach using the Atlas library.

thanks!
# March 29, 2006 1:03 PM

Garbin said:

jared,

thanks for your feedback. I've noticed that this happens in IE when using inline elements or if element bounds aren't explicitly set. On Firefox it works correctly. I will investigate this issue and try to fix it ASAP.
Garbin
# March 29, 2006 2:10 PM

Amit said:

Hi. I am new to atlas. Do you have any example code of this behaviour being used?

I need to make right click context menu appear when some one right clicks on a repeater item, and the values displayed in the menu depend on the item the right click event were fired from. How can this be done?
# March 30, 2006 3:00 AM

Atlas notes said:

In this article, we'll see how we can raise and handle events using the Atlas framework.
When we talk...
# June 19, 2006 5:35 PM

sanjay said:

thanks
# July 14, 2006 8:58 AM

creativesanjay said:

Hi,
I hv created the following code for context meny  activation, but it is not working. Can anyone help me out?

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="ContextMenuBehavior.aspx.cs" Inherits="ContextMenuBehavior" %>
<%@ Register Assembly="Microsoft.Web.Atlas" Namespace="Microsoft.Web.UI" TagPrefix="cc1" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
<head runat="server">
   <title>ContextMenuBehavior Example</title>
</head>
<body>
   <form id="form1" runat="server">
   <div id="popup">
       <span style="font-size: 10pt"><span style="font-family: Verdana"><span style="color: #cc0033">
           <strong>Note:</strong> </span>
           this Behavior works in browsers that support the <b>oncontextmenu</b> event, like
           for example IE6 and FireFox1.5.</span>
       </span>
   </div>
   <div>
       <cc1:ScriptManager ID="SM" runat="server">
           <Scripts>
               <cc1:ScriptReference Path="js/ContextMenuBehavior.js" />
           </Scripts>
       </cc1:ScriptManager>
       <br />
   </div>
   <div id="lbl1">
       <asp:Label ID="myLbl" runat="server" Text="Label"></asp:Label>
   </div>
   </form>
   
   <script type="text/xml-script">
       <page>
           <components>
               <control id="popup">
                   <behaviors>
                       <contextMenuBehavior contextMenuID="popup" contextMenuShow="lbl1" />
                   </behaviors>
               </control>
           </components>
       </page>
   </script>
</body>
</html>
# July 17, 2006 6:34 AM

Ben said:

the problem with that last post is that the control id should reference lbl1, not popup.
# August 3, 2006 12:49 AM

Atlas notes said:

As you know (if not, please check this post ) an Atlas behavior is a class that inherits from the base

# September 26, 2006 7:09 PM

kk.miral said:

Hello,

i have similar kind of req.i need context menu on right click of tree node in treeview.

so can any one explain me that how can i use this .js file with tree view.

i dont have much knowledge of xml what there is mentioned in xml script.

which id i have to map and where ??

thanks

Miral

# December 8, 2006 10:25 AM
New Comments to this post are disabled