Creating Bezier Curves in an Ajax DiagramView (Part 2)
In the previous article, I have explained the fundations of the Ajax interaction framework of ILOG Diagrammer for .NET. It is now time to dive into the implementation details.
From the first article, we know an AjaxDiagramView interactor inherits from the System.Web.UI.ExtenderControl class. As such, it is composed of two parts: an ASP.NET server control and a JavaScript client control.
The server control
The ASP.NET server control is responsible for describing the client resources to the underlying ASP.NET framework (the script resources to deploy, the optional initialization tasks to perform on the client, etc.), and for handling the possible actions coming from the client control. In our case, the server control implementation handles the client action (i.e. create an ILOG.Diagrammer.Graphic.BezierCurve instance and add it to the view content) via an asynchronous callback. Let’s first begin with the implementation of the client resources description.
For this purpose, the ILOG.Diagrammer.Web.UI.ViewInteractor class defines the following methods:
ScriptBehaviorDescriptor CreateScriptBehaviorDescriptor(Control targetControl) IEnumerable<ScriptReference> GetScriptReferences()
The CreateScriptBehaviorDescriptor method must be overriden to return a ScriptBehaviorDescriptor instance that specifies the name of the client-side class of the interactor (in other words the name of the JavaScript class).
The GetScriptReferences method returns the script resources needed by your client control. This is needed by ASP.NET to be able to automatically deploy the required client resources.
Here is a first implementation of the MakeBezierCurveInteractor class that illustrates the implementation of these methods :
public class MakeCurveInteractor : ViewInteractor {
protected override ScriptBehaviorDescriptor CreateScriptBehaviorDescriptor(Control targetControl) {
return new ScriptBehaviorDescriptor("AjaxBezierSample.MakeCurveBehavior",
targetControl.ClientID);
}
protected override IEnumerable<scriptreference> GetScriptReferences() {
IEnumerable<ScriptReference> references = base.GetScriptReferences();
List<ScriptReference> list = new List<ScriptReference>(references);
list.Add(new ScriptReference("AjaxBezierSample.script.MakeCurveBehavior.js",
Assembly.GetExecutingAssembly().FullName));
return list;
}
which means the client control implementation is the AjaxBezierSample.MakeCurveBehavior class defined in the MakeCurveBehavior.js file, packaged as an embedded resource in the application assembly (packaging a script resource in the assembly requires the .js file be built with the Embedded Resource build action, and the resource declared in the AssemblyInfo.cs).
The next step, and it completes the server control implementation, is to handle the action. As explained above, we made the choice to process the interaction via an asynchronous ASP.NET callback, which is possible because the ViewInteractor base class implements the ASP.NET 2.0 ICallbackEventHandler interface. Therefore, our custom interactor must override the ViewInteractor.RaiseCallBackEvent method that is called when a callback request is received by our control.
The implementation is quite straightforward: it first reads the curve definition points from the method parameter (passed by the client control as a JSON string), converts these points from the view coordinate system to the view graphic container coordinate system, then creates the corresponding Diagrammer for .NET graphic object (a BezierCurve instance) and finally adds it to the view graphic container.
Here is the code:
protected override void RaiseCallbackEvent(string eventArgument) {
ProcessRequest(eventArgument);
base.RaiseCallbackEvent(eventArgument);
}
protected virtual void ProcessRequest(string eventArgument) {
if (string.IsNullOrEmpty(eventArgument))
return;
DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(BezierArg));
BezierArg args = null;
using (Stream input = new MemoryStream(Encoding.Unicode.GetBytes(eventArgument))) {
args = serializer.ReadObject(input) as BezierArg;
}
if (args != null) {
Point2D[] points = args.points;
if (points.Length == 0)
return;
Transform t = View.Transform.Inverse();
t.TransformPoints(points);
BezierCurve curve = new BezierCurve(points);
IManageChildren container = View.Content as IManageChildren;
if (container != null) {
container.AddChildren(new GraphicObject[] { curve });
}
}
}
The server control implementation is done. It’s now time to wear our JavaScript developer suit for the client side control implementation. From now on, all class names refer to the Microsoft JavaScript Library.
The client control
Implementing the client part of a ViewInteractor consists on writing a JavaScript class inheriting from the ILOG.Diagrammer.Web.UI.ViewBehavior class which offers the basic services to communicate with the server control. In the Microsoft JavaScript world, writing a new class consists on :
- defining the namespace
- defining the class object
- implementing the class methods via the class object prototype
- registering the class in the class hierarchy
In terms of code, it means:
- defining the name space:
Type.registerNamespace(‘AjaxBezierSample’);
This line of code creates a new namespace called ‘AjaxBezierSample’ and registers it in the Type object.
- defining the class object:
AjaxBezierSample.MakeCurveBehavior = function(element) {
…
AjaxBezierSample.MakeCurveBehavior.initializeBase(this, [element]);
};
The namespace object being created, we define the MakeCurveBehavior class object (the constructor) and call the initializeBase() method to initialize the base class. Latter, we will add to the constructor the fields initialization code.
- implementing the class methods via the class object prototype
AjaxBezierSample.MakeCurveBehavior.prototype = {
initialize : function() {
AjaxBezierSample.MakeCurveBehavior.callBaseMethod(this, ‘initialize’);
},
dispose : function() {
AjaxBezierSample.MakeCurveBehavior.callBaseMethod(this, ‘dispose’);
},
…
}
The methods of the class are defined on the prototype following the prototype model. For more information on the prototype model, see the title=”Prototype model”>MSDN documentation. For the time being, we only need to call the initialize() and dispose() methods of the base class.
- registering the class in the class hierarchy
AjaxBezierSample.MakeCurveBehavior.registerClass(‘AjaxBezierSample.MakeCurveBehavior’, ILOG.Diagrammer.Web.UI.ViewBehavior);
Finally, we register our new class in the class hierarchy, specifying its fully qualified name (that is the class name prefixed with the namespace) and the base class it inherits from. This skeleton is the minimum code required to fully define a new ViewBehavior subclass.
The last step is to eventually implement the code needed to create bezier curves.
Drawing Bezier curves in DHTML
As explained in the first part of this article, the interaction ghost of the bezier curves will be implemented either in VML or SVG depending on the browser. The ghost will be composed of three primitive objects:
- a circle that represents the edited passage point.
- two rectangles that represent the control points.
These primitives will be added to a parent container positionned in absolute above the image. To ease the primitives update, both the primitives and the container will have a corresponding class field (a reference to the corresponding node in the DOM).
The code below shows the updated constructor (with the primitive fields) and the DOM initialization :
AjaxBezierSample.MakeCurveBehavior = function(element) {
this._ghostContainer = null;
this._ptMarker = null;
this._ctrl1 = null;
this._ctrl2 = null;
this._useVML = Sys.Browser.agent == Sys.Browser.InternetExplorer;
this._curveCount = 0;
this._svg = null;
...
AjaxBezierSample.MakeCurveBehavior.initializeBase(this, [element]);
};
AjaxBezierSample.MakeCurveBehavior.prototype = {
initialize : function() {
AjaxBezierSample.MakeCurveBehavior.callBaseMethod(this, 'initialize');
...
this.initDOM();
},
initDOM : function() {
this._ghostContainer = document.createElement("div");
this.get_element().appendChild(this._ghostContainer);
this._ghostContainer.style.zIndex = 3;
this._ghostContainer.style.position = "absolute";
this._ghostContainer.style.left="0px";
this._ghostContainer.style.top="0px";
this._ghostContainer.style.width="100%";
this._ghostContainer.style.height="100%";
if (!this._useVML) {
this._svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
this._ghostContainer.appendChild(this._svg);
}
var parentNode = this._useVML ? this._ghostContainer : this._svg;
// passage point marker
this._ptMarker = this.createMarker(parentNode);
// control points markers
this._ctrl1 = this.createControlPt(parentNode, 'ctrl1');
this._ctrl2 = this.createControlPt(parentNode, 'ctrl2');
},
createMarker : function(parentNode) {
var marker = this._useVML ? this._createMarkerVML(parentNode) : this._createMarkerSVG(parentNode);
return marker;
},
_createMarkerVML : function(parentNode) {
var marker = document.createElement("oval");
parentNode.appendChild(marker);
marker.id = "ptmarker";
marker.style.behavior = "url(#default#VML)";
marker.strokeweight = 1;
marker.filled = 'true';
marker.fillcolor = 'black';
marker.style.position = "absolute";
marker.style.width = "7px";
marker.style.height = "7px";
marker.style.display='none';
return marker;
},
_createMarkerSVG : function(parentNode) {
var marker = document.createElementNS("http://www.w3.org/2000/svg", "circle");
parentNode.appendChild(marker);
marker.style.display = 'none';
marker.setAttributeNS(null, 'id', "ptmarker");
marker.setAttributeNS(null, 'r', 3);
marker.setAttributeNS(null, "fill", 'black');
marker.setAttributeNS(null, "stroke", "none");
return marker;
},
createControlPt : function(parentNode, id) {
var ctrl = this._useVML ? this._createControlVML(parentNode, id) : this._createControlSVG(parentNode, id);
return ctrl;
},
_createControlVML : function(parentNode, id) {
var ctrl = document.createElement("rect");
parentNode.appendChild(ctrl);
ctrl.id = id;
ctrl.style.behavior = "url(#default#VML)";
ctrl.strokeweight = 1;
ctrl.filled = 'false';
ctrl.style.position = 'absolute';
ctrl.style.width = '11px';
ctrl.style.height = '11px';
ctrl.style.display='none';
return ctrl;
},
_createControlSVG : function(parentNode, id) {
var ctrl = document.createElementNS("http://www.w3.org/2000/svg", "rect");
parentNode.appendChild(ctrl);
ctrl.id = id;
ctrl.setAttributeNS(null, "width", "11px");
ctrl.setAttributeNS(null, "height", "11px");
ctrl.setAttributeNS(null, "stroke-width", 1);
ctrl.setAttributeNS(null, "fill", "none");
ctrl.setAttributeNS(null, "stroke", "black");
ctrl.style.display='none';
return ctrl;
},
Handling interactions
The interaction process follows the following sequences:
- When the left mouse button is pressed, a new passage point and two control points are created.
- When the mouse is dragged, the position of the control points is adjusted accordingly.
- On a double-click, the curve is validated and a request is sent to the server with the curve points.
- When the ESC key is pressed, the interaction is cancelled.
Therefore, the interactor has to listen the mousedown, mousemove, mouseup and keydown events. For this purpose, we define four handlers that will be wired/unwired depending on the value of the active property.
The code below shows the handler initialization and the required methods to wire/unwire the handlers on a active property change:
AjaxBezierSample.MakeCurveBehavior = function(element) {
this._mouseDownHandler = null;
this._mouseMoveHandler = null;
this._mouseUpHandler = null;
this._keyDownHandler = null;
this._propertyChangedHandler = null;
this._startPoint = null;
this._points = null;
this._size = 0;
...
AjaxBezierSample.MakeCurveBehavior.initializeBase(this, [element]);
};
AjaxBezierSample.MakeCurveBehavior.prototype = {
initialize : function() {
///
/// Initializes a MakeCurveBehavior instance.
///
AjaxBezierSample.MakeCurveBehavior.callBaseMethod(this, ‘initialize’);
this._mouseDownHandler = Function.createDelegate(this, this._onMouseDown);
this._mouseMoveHandler = Function.createDelegate(this, this._onMouseMove);
this._mouseUpHandler = Function.createDelegate(this, this._onMouseUp);
this._keyDownHandler = Function.createDelegate(this, this._onKeyDown);
this._doubleClickHandler = Function.createDelegate(this, this._onDoubleClick);
if (this.get_active()) {
this._wireHandlers();
}
this._propertyChangedHandler = Function.createDelegate(this, this._onPropertyChanged);
this.add_propertyChanged(this._propertyChangedHandler);
…
},
dispose : function() {
this._imgElt = null;
$clearHandlers(this.get_element());
AjaxBezierSample.MakeCurveBehavior.callBaseMethod(this, ‘dispose’);
},
_wireHandlers : function() {
$addHandler(this.get_element(), ‘mousedown’, this._mouseDownHandler);
$addHandler(this.get_element(), ‘mousemove’, this._mouseMoveHandler);
$addHandler(this.get_element(), ‘mouseup’, this._mouseUpHandler);
$addHandler(this.get_element(), ‘keydown’, this._keyDownHandler);
$addHandler(this.get_element(), ‘dblclick’, this._doubleClickHandler);
this.ensureFirefoxKeyEvents();
},
_unwireHandlers : function() {
$removeHandler(this.get_element(), ‘mousedown’, this._mouseDownHandler);
$removeHandler(this.get_element(), ‘mousemove’, this._mouseMoveHandler);
$removeHandler(this.get_element(), ‘mouseup’, this._mouseUpHandler);
$removeHandler(this.get_element(), ‘keydown’, this._keyDownHandler);
$removeHandler(this.get_element(), ‘dblclick’, this._doubleClickHandler);
},
_onPropertyChanged : function(sender, args) {
if (’active’ == args.get_propertyName()) {
if (this.get_active()) {
this._wireHandlers();
} else {
this._unwireHandlers();
}
}
},
Next, we need to handle the points and create the ghost curve. For this purpose, all the points (the passage points and the control points) are stored in an array in the following sequence : the passage point, the first control point, the second control point. We hence have three indices per point.
The code below illustrates this (for the sake of readability, the method updateCurveGhostPoints() that updates the ghost position is not shown. Please refer to the source code for more details). When the mouse is pressed, the addPoint() method is invoked to add a new passage point and two control points. The three points have initially the same coordinates until the next mouse drag. Then, when the mouse is dragged, the control points position is adjusted proportionally (in the react() method) :
_onMouseDown : function(e) {
if (e.button == Sys.UI.MouseButton.leftButton) {
if (this._points == null) {
this._points = [];
this._size = 0;
}
this._dragging = true;
var loc = Sys.UI.DomElement.getLocation(this.get_element());
var p = {'x':e.clientX - loc.x, 'y':e.clientY - loc.y };
// Add the new point
this._addPoint(p);
// ... and compute the control points
this._react(p);
// update ghost
if (this._size == 0) {
this.updateCurveGhostPoints();
} else {
this._points[this._size-1] = p;
this.addNewCurve();
}
this.updateMarkerPos(p);
this.onStartInteraction();
e.stopPropagation();
e.preventDefault();
}
},
_onMouseMove : function(e) {
if (this._size > 0 && this._dragging) {
var loc = Sys.UI.DomElement.getLocation(this.get_element());
var p = {'x':e.clientX - loc.x, 'y':e.clientY - loc.y };
if (this._size == 1) {
// If user is dragging the starting point, add the second point.
this._addPoint(p);
// and compute the control points
this._react(p);
} else {
// computes the control points position
this._react(p);
this._points[this._size-1] = p;
}
// update ghost
this.updateCurveGhostPoints();
e.stopPropagation();
e.preventDefault();
}
},
_addPoint : function(p) {
if (this._size > 1) {
this._points[(++this._size)-1] = p;
this._points[(++this._size)-1] = p;
this._points[(++this._size)-1] = p;
} else {
this._points[(++this._size)-1] = p; // start point
}
},
_react : function(p) {
if (this._size > 3) {
// Updates the position of the control point.
var prevp = this._size - 3; // previous left handle
var pivot = this._size - 2; // previous end point
this._points[prevp] = this.symmetric(this._points[pivot], p);
}
},
_onMouseUp : function(e) {
this._dragging = false;
},
_onDoubleClick : function(e) {
if (this._points != null) {
// Commit the action.
this.doCreateCurve();
this.onEndInteraction();
this.reset();
e.stopPropagation();
e.preventDefault();
}
},
_onKeyDown : function(e) {
if (this._points != null && e.keyCode == Sys.UI.Key.esc) {
// Cancel interaction
this.onEndInteraction();
this.reset();
e.stopPropagation();
e.preventDefault();
}
},
Commit the new curve to the server
Finally, the points defining the new curve are sent to the server-side part of our interactor to create the corresponding ILOG.Diagrammer.Graphic.BezierCurve object and add it to the view GraphicContainer. To do this, we create an asynchronous ASP.NET callback invoking the ViewBehavior.callServerAsync method passing the points array as parameter. At this time, the request has been processed but the image has not been updated to show the new content. In order to fix this, we override the ViewBehavior.onCallbackReceived method to refresh the view when the callback response is received :
doCreateCurve : function() {
var args = {};
if (this._size > 4) {
// get rid of the extra passage point coming from the double-click.
if (this._useVML)
args.points = this._points.slice(0, this._points.length-1-3);
else // under FF, we get one more mouse-down from the double-click
args.points = this._points.slice(0, this._points.length-1-2*3);
} else {
args.points = this._points.slice(0);
}
this.callServerAsync(args);
},
onCallbackReceived : function(args) {
this.updateView();
},
You can find the VS2008 project here: AjaxBezierSample VS2008 Project. Do not hesitate to look at the MakeCurveBehavior.js file to see how the the primitives points are updated.
The purpose of this implementation being a tutorial, many improvements are still to be done. For example, you could add properties to be able to customize the ghost rendering (the stroke color, the stroke style, etc.), an optional PostBack behavior (see ViewBehavior.doPostBack() method), additional editing capabilities (add/supress points or closed curves support).
As you can see, writing a new interactor for an AjaxDiagramView is essentially a client-side task. Fortunately, the Microsoft JavaScript Library and the ILOG.Diagrammer.Web.UI.ViewBehavior class provide a rich set of functionalities and services that make the JavaScript implementation easier and poweful. Also, the use of asynchronous callbacks to handle the interaction allows a very smooth user experience avoiding the annoying cost of a PostBack.







