Building an 'AJAX' ProgressBar in Atlas

Someone on the Atlas forums was interested in an Atlas progress bar. So, I went ahead and wrote a basic, client-side Atlas progress bar. If you want, you can also download the source.

So, what is involved to build such a control? Not a hell'u'valot, really.

Step 1: Derive from Web.UI.Control

First of all, you will want to derive from the base Atlas control class, Web.UI.Control. It contains some plumbing for you, such as associating itself with an HTML element. Additionally, you should register your type to make it possible to instantiate it declaratively. In this case, we will register our type with the namespace 'script' and the tagname 'progressBar'. You should also register your class, so Atlas can do its thing, such as describing what the base type is, etc.

Web.UI.ProgressBar = function(associatedElement) {
Web.UI.ProgressBar.initializeBase(this, [associatedElement]);
}
Type.registerSealedClass('Web.UI.ProgressBar', Web.UI.Control);
Web.TypeDescriptor.addType('script','progressBar', Web.UI.ProgressBar);

Step 2: Make it configurable

Secondly, you will want to add some properties to let a user configure the control. In our case, we will add properties like an interval, the url to the service, and a method that we will call on this service to get the progress. Properties have to follow an exact naming convention: the getter of the property should be a function prefixed with 'get_', and the setter should be prefixed with 'set_' and expect 1 parameter. Additionally, we should add these properties to our control's descriptor. Please see step 4 on how this should be done.

// Define a 'private field'.
          
var _serviceURL;

this.get_serviceURL = function() {
return _serviceURL;
}

this.set_serviceURL = function(value) {
    _serviceURL = value;
}

Step 3: Add a timer that will query the service on every tick

Since we want to query the service n milliseconds, we could just re-use the Web.Timer internally. We should define a delegate to represent the function that we want the timer to invoke on each tick. To be safe, we should make sure we clean up after ourselves when our control is disposed.
Please note that we prevent our control from querying the service multiple times when we are still waiting for a response.

var _responsePending;
var _timer;
var _tickHandler;

this.initialize = function() {
    Web.UI.ProgressBar.callBaseMethod(this, 'initialize');

    _tickHandler = Function.createDelegate(this, this._onTimerTick);

	// Add our event handler to the tick event of the timer.
    _timer.tick.add(_tickHandler);

	this.set_progress(0);
}

this.dispose = function() {
	if (_timer) {
		// Remove the reference of the timer's tick event to our event handler.
        _timer.tick.remove(_tickHandler);
        _tickHandler = null;
        _timer.dispose();
    }

	// Just to be safe we should explicitly set this to null to make 
	// sure we aren't still referencing the timer.
    _timer = null;

    Web.UI.ProgressBar.callBaseMethod(this, 'dispose');
}

this._onTimerTick = function(sender, eventArgs) {
	if (!_responsePending) {
        _responsePending = true;

	// Asynchronously call the service method. Pass a reference to 
	// this control as the context, so we can use that in our 
	// '_onMethodComplete' callback function.
        Web.Net.ServiceMethodRequest.callMethod(_serviceURL, _serviceMethod, 
			null, _onMethodComplete, null, null, this);
    }
}

function _onMethodComplete(result, response, context) {
	// Get a reference to the control.
	var behavior = context;

    // Update the progress bar.
    behavior.set_progress(result);
    _responsePending = false;
}

Step 4: Add a few methods to control our progress bar

Finally, we should add a few methods to start/stop our progress bar. All we need to do is add a start/stop function that enables/disables the timer. Since we want these to be callable by other objects, we should describe these methods in our control's descriptor.

this.getDescriptor = function() {
var td = Web.UI.ProgressBar.callBaseMethod(this, 'getDescriptor');
    td.addProperty('interval', Number);
    td.addProperty('progress', Number);
    td.addProperty('serviceURL', String);
    td.addProperty('serviceMethod', String);
    td.addMethod('start');
    td.addMethod('stop');
return td;
}

this.start = function() {
    _timer.set_enabled(true);
}

this.stop = function() {
    _timer.set_enabled(false);
}

Step 5: Test our control

Now we're done with our control, we should be able to test it. Once you added a reference to your new script control's file, you should be good to go and use it. Since our progress bar needs to be started explicitly, and since we will want to simulate a task, we should add a few more controls to put together a proper demo.

[...]
<divclass="progressBarContainer">
<!-- This div is associated with our progress bar control. -->
<divid="pb1"class="progressBar"></div>
</div>
[...]

<scripttype="text/xml-script">
<pagexmlns:script="http://schemas.microsoft.com/xml-script/2005">
	<components>
		<!-- This component can be invoked to actually start a 
						 simulated time-consuming task. -->
		<serviceMethodid="taskService1"url="TaskService.asmx"
		methodName="StartTask1"/>
		
		<!-- Our progress bar's id should refer to a valid associated 
						 HTML element. -->
		<progressBarid="pb1"interval="500"serviceURL="TaskService.asmx"
		serviceMethod="GetProgressTask1"/>
		
		<!-- Our button should both start the task and the progress bar. -->
		<buttonid="start1">
			<click>
				<invokeMethodtarget="taskService1"method="invoke"/>
				<invokeMethodtarget="pb1"method="start"/>
			</click>
		</button>
	</components>
<references>
	<addsrc="ScriptLibrary/AtlasUI.js"/>
	<addsrc="ScriptLibrary/AtlasControls.js"/>
	<!-- Add a reference to our control. -->
	<addsrc="ScriptLibrary/ProgressBar.js"/>
</references>
</page>
</script>

As you can see in this code, we invoke a method 'StartTask1' in our service to start the task, and we invoke 'GetProgressTask1' to get the progress for this task. It is really up to you on how you implement these. For example, if you had a file upload scenario, you could implement 'GetProgressTask1' by checking how many bytes were uploaded already and how big the file is.
Since I don't really want you to upload stuff to my server, I have decided to simulate a time-consuming task instead. This task pretty much sleeps and registers progress every 100/200 milliseconds. The GetProgressTask1 method just returns the registered progress.

[WebMethod]
publicint GetProgressTask1()
{
	// Get the registered progress for the user's running task.
	string processKey = this.Context.Request.UserHostAddress + "1";
	object savedState = this.Context.Cache[processKey];
	if (savedState != null)
	{
		return (int)savedState;
	}
	return 0;
}

[WebMethod]
publicvoid StartTask1()
{
	string processKey = this.Context.Request.UserHostAddress + "1";
	
	// Create a lock object to prevent 1 user from running task 1 
	// multiple times.
	string threadLockKey = "thread1" + this.Context.Request.UserHostAddress;
	object threadLock = this.Context.Cache[threadLockKey];
	if (threadLock == null)
	{
		threadLock = newobject();
		this.Context.Cache[threadLockKey] = threadLock;
	}

	// Only allow 1 running task per user.
	if (!Monitor.TryEnter(threadLock, 0))
	return;

	// Simulate a time-consuming task.
	for (int i = 1; i <= 100; i++)
    {
		// Update the progress for this task.
		this.Context.Cache[processKey] = i;
        Thread.Sleep(100);
    }

	// The task is done. Release the lock.
    Monitor.Exit(threadLock);
}

Hopefully this gives you an idea of what is involved to create a basic client-side Atlas control.

You might also like...

Comments

Wilco Bauwer For more information go to my website at http://www.wilcob.com.

Contribute

Why not write for us? Or you could submit an event or a user group in your area. Alternatively just tell us what you think!

Our tools

We've got automatic conversion tools to convert C# to VB.NET, VB.NET to C#. Also you can compress javascript and compress css and generate sql connection strings.

“Weeks of coding can save you hours of planning.”