That will be very cool and useful if you can show your user a ProgressBar on a web page which displays the actual progress of some long operations. Now let’s try to make it possible by using ASP.NET Atlas. This post can also show you some basic conceptions about extending Atlas client side controls. Also, the source code and demo can be downloaded here.
The basic ideas to implement this will be easy. Build an Atlas client side control and query a service to find how much we’ve done every tick. Then get the response and update the UI of progress bar. So in this demo, we separate the code into four parts:
Let’s go through the four steps.
Time Consuming Web Service
In the real world, a time consuming function may runs like following:
[WebMethod] public void TimeConsumingTask() { ConnectToDataBase(); GetSomeValueFromDataBase(); CopySomeFilesFromDisk(); GetARemoteFile(); }
Then we can insert some helpers to show how much the work has been done:
[WebMethod] public void TimeConsumingTask() { setProgress(0); ConnectToDataBase(); setProgress(10); GetSomeValueFromDataBase(); setProgress(40); CopySomeFilesFromDisk(); setProgress(50); GetARemoteFile(); setProgress(100); }
In this demo we just store the progress value in Cache and use Thread.Sleep() to delay the processing:
[WebMethod] public int StartTimeConsumingTask() { string processKey = this.Context.Request.UserHostAddress; string threadLockKey = "thread" + this.Context.Request.UserHostAddress; object threadLock = this.Context.Cache[threadLockKey]; if (threadLock == null) { threadLock = new object(); this.Context.Cache[threadLockKey] = threadLock; } // Only allow 1 running task per user. if (!Monitor.TryEnter(threadLock, 0)) return -1; DateTime startTime = DateTime.Now; // 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(70); } Monitor.Exit(threadLock); return (DateTime.Now - startTime).Seconds; }
GetProgress Web Service
This should be easy.We just get the progress value from Cache:
[WebMethod] public int GetProgress() { string processKey = this.Context.Request.UserHostAddress; object progress = this.Context.Cache[processKey]; if (progress != null) { return (int)progress; } return 0; }
Atlas ProgressBar control
Step 1: Derive from Sys.UI.Control
ProgressBar control should derive from the base Atlas control class, Sys.UI.Control and let’s make it a sealed class. The Sys.UI.Control base class contains some useful things, such as associating itself with an HTML element, which is the so called binding. Additionally, you should register your type to make it possible to instantiate it declaratively. Also register your class to let Atlas know it for further options, such as describing what the type is, etc.
Sys.UI.ProgressBar = function(associatedElement) { Sys.UI.ProgressBar.initializeBase(this, [associatedElement]); } Type.registerSealedClass('Sys.UI.ProgressBar', Sys.UI.Control); Sys.TypeDescriptor.addType('script','progressBar', Sys.UI.ProgressBar);
Step 2: Add private fields and the Setter/Getter
We have to add some configurable properties for our control. In this case, we have 3 properties:
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. For the service method property, we have following code:
var _serviceMethod; this.get_serviceMethod = function() { return _serviceMethod; } this.set_serviceMethod = function(value) { _serviceMethod = value; }
Step 3: Add a Timer to query the service on every tick
Here we include a Sys.Timer to query the service. Also define a delegate to represent the function that we want the timer to invoke on each tick. To get rid of the browser memory leak, we should make sure to finish the clean up when our control is disposing.
Still, notice that we prevent the control from querying the service multiple times when we are still waiting for a response.
var _timer = new Sys.Timer(); var _responsePending; var _tickHandler; var _obj = this; this.initialize = function() { Sys.UI.ProgressBar.callBaseMethod(this, 'initialize'); _tickHandler = Function.createDelegate(this, this._onTimerTick); _timer.tick.add(_tickHandler); this.set_progress(0); } this.dispose = function() { if (_timer) { _timer.tick.remove(_tickHandler); _tickHandler = null; _timer.dispose(); } _timer = null; associatedElement = null; _obj = null; Sys.UI.ProgressBar.callBaseMethod(this, 'dispose'); } this._onTimerTick = function(sender, eventArgs) { if (!_responsePending) { _responsePending = true; // Asynchronously call the service method. Sys.Net.ServiceMethod.invoke(_serviceURL, _serviceMethod, null, null, _onMethodComplete); } } function _onMethodComplete(result) { // Update the progress bar. _obj.set_progress(result); _responsePending = false; }
Step 4: Add control methods
We should be able to start/stop our progress bar. And since this control is an Atlas object, we want it be known by the Atlas framework by describing its methods in the descriptor.
this.getDescriptor = function() { var td = Sys.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); }
Oh… till now, the control’s done! Save it as ProgressBar.js.
ASP.NET Testing Page
Of course, the first step of building every Atlas page is adding a ScriptManager server control. In this case we refer to our ProgressBar control, time consuming web service and GetProgress web service. (The two web services are located in one file: TaskService.asmx)
<atlas:ScriptManager ID="ScriptManager1" runat="server" > <Scripts> <atlas:ScriptReference Path="ScriptLibrary/ProgressBar.js" ScriptName="Custom" /> </Scripts> <Services> <atlas:ServiceReference Path="TaskService.asmx" /> </Services> </atlas:ScriptManager>
Then styles and layouts:
<style type="text/css"> * { font-family: tahoma; } .progressBarContainer { border: 1px solid #000; width: 500px; height: 15px; } .progressBar { background-color: green; height: 15px; width: 0px; font-weight: bold; } </style> <div>Task Progress</div> <div class="progressBarContainer"> <div id="pb" class="progressBar"></div> </div> <input type="button" id="start" onclick="startTask();return false;" value="Start the Time Consuming Task!" /> <div id="output" ></div>
At last is the JavaScript event handler which makes the ProgressBar control ran.
<script type="text/javascript" language="javascript"> function startTask() { // new ProgressBar var pb = new Sys.UI.ProgressBar($('pb')); pb.set_interval(500); pb.set_serviceURL('TaskService.asmx'); pb.set_serviceMethod('GetProgress'); pb.initialize(); // start the task TaskService.StartTimeConsumingTask(onTaskCompleted); // start the ProgressBar pb.start(); } function onTaskCompleted(result) { // alert the time cost if (result != -1) $('output').innerHTML = 'Task completed in ' + result + ' seconds.'; } </script>
Screen Shots and Download
Great, everything’s done now! Let’s run it!
Not started:
Running:
Completed:
Source code available here.