Monday, July 16, 2007

Context Help Made Easy - Revisited

Some time ago I've read a great article Context Help Made Easy by Tom Clement. The article can be found here.

Tom's idea is great - rather than putting the HelpProvider on each of your application's forms, you keep the information about the topic mapping in a single XML file. There is also an additional message filter executing in the application context which traps F1 presses, reads the provided topic mapping and redirects the help provider to the specific topic.

The most interesing part of Tom's article is the idea of instrumenting the execution and providing a dialog form which is used to provide topic mapping for current context. It is then possible to train someone to use this feature to map user interface contexts to help topics - the mapping is provided while the application is actually running and not in the design time.

Below is the brief description of differences between Tom's and my implementation.

  • in Tom's implementation the Main routine of the application has to be augmented with the message filter injection. In my implementation - a custom component is used so that the only thing you have to do is to put the component on the main form of the appliaction
  • in Tom's implementation all user interface controls have to implement the mapping interface. I belive that this is not necessary - help topics can be mapped to a context defined by the FormClassName/Control1Name/Control2Name/Control3Name/..../ControlNName built directly from the runtime information (parent-child relation of controls and controls' names)
  • From the point of view of someone who actually maps controls to help topics the hierarchical view is much more natural than the flat ListView. In my implementation, topics which are not mapped are shown as red and topics already mapped - as black (please forgive localized labels, feel free to customize it)

  • in my implementation the XML file used to store the topic mapping, help.mapping, can be stored as a resource in any application module. If the file is found in the same directory as the main executable file of the application - it is used as a primary source of the topic mapping.
  • in Tom's implementation the ability to provide topic mapping is available always by pressing Ctrl+F1 instead of F1 which just shows the help topic. In my implementation you have to provide a special runtime commandline parameter, helpbuilder, to be able to use the map builder
  • the Control.FromHandle method used by Tom to retrieve the control instance from the window handle does not always work as expected. For example - when a ComboBox in DropDown mode is active then the message filter gets not the handle of the combo but rather the handle of the combo's internal textbox. The Control.FromHandle does not work then because the combo's internal textbox is not a control instantiated by the .NET. In my implementation when Control.FromHandle fails, the application tries to retrieve the control's parent control and repeats the Control.FromHandle. This correctly resolves the ComboBox (and other composite controls) issue.

In order to make your application compatible with my library you have to:

  • recompile the library from the provided source
  • put the HelpComponent on the main form of your application
  • run the application with helpprovider commandline parameter
  • use Ctrl+F1 to map help topics to controls. Remember about the hierarchy - you do not have to provide topics for all controls, it is sufficient to provide the topic for a container control and all its child controls inherit the mapping.
  • remember to provide a help file name (must be a *.chm file located beside the application main executable)
  • when the mapping is complete, include the help.mapping file stored beside the application main executable as an embedded resource in one of assemblies
  • remove the help.mapping file located beside the application main executable
  • rebuild the application

From now, the included resource is used as a primary help mapping provider. If you need to customize it further, repeat above process starting with the 3rd step (the included resource will be copied as a file beside the application main executable and used as a primary help mapping provider.

Download the source code here. You are free to use and modify it without any explicit permission.

Please leave a comment if you find this small library usable.

Edit: Please also take a look at the second part of this article: http://netpl.blogspot.com/2007/08/context-help-made-easy-reloaded.html

Tuesday, July 3, 2007

Tiny ASP.NET Ajax Framework Contest Continued...

Few days ago we've held a tiny contest of three ASP.NET Ajax frameworks. This time, three more contestants await to be compared:

These three above were chosen because the integration of an existing application is as simple as placing a single Ajax Manager on a web form - few other frameworks (and here) were rejected because they are either dead now, force you to use specific set of components or rely on writing client-side javascript code.

The result chart is available here:

  • ComfortASP.NET has a terrible requests-per-second ratio. It served only 35 (!) requests per second, while sending 6 and receiving 83 MB of data
  • Anthem.NET was able to process 133 request per second, sending 6 and receving 121 MB of data
  • Telerik's RAD Ajax did 292 requests per second, sending 7 and receiving 172 MB (!) of data

Gathering all results together, we had 2 non-Ajax contestants: pure Web.Forms application, Javascript version of it and 6 Ajax frameworks: Microsoft's ASP.NET AJAX, Ajaxium, VisualWebGUI, ComfortASP.NET, Anthem.NET and Telerik's RAD Ajax.

Regarding requests-per-second we have following results:

 

 

requests-per-second

reference

VWG

422

198,1220657

Javascript

400

187,7934272

Telerik

292

137,0892019

ASP.NET AJAX

222

104,2253521

Web.Forms

213

100

Anthem

133

62,44131455

Ajaxium

111

52,11267606

Comfort

35

16,43192488

 

Regarding number-of-bytes-sent we have following results:

 

 

sent data

reference

Javascript

1938800

24,23560589

VWG

2066800

25,83564589

Ajaxium

5029800

62,87407185

ASP.NET AJAX

5600600

70,00925023

Comfort

5880400

73,50683767

Anthem

6161400

77,01942549

Telerik

7293200

91,16727918

Web.Forms

7999800

100

 

Regarding number-of-bytes-received we have following results:

 

 

received data

reference

VWG

23348807

29,11700146

Javascript

37121000

46,29153905

Ajaxium

47756301

59,55423272

Web.Forms

80189600

100

Comfort

83742600

104,4307491

Anthem

121547800

151,575516

ASP.NET AJAX

129531800

161,5319194

Telerik

172294800

214,8592835

 

Few comments:

  • the overall winner is VWG, however the application had to be completely rewritten in order to run under this framework
  • the javascript version did not win (even in the bytes-received test!) and though this may seem surprising, it is caused by the script size
  • the Ajaxium, Anthem and Comfort reduce the requests-per-second ratio singificantly. The latter two are able to correctly handle dynamic, non-ajax content sent to the browser
  • regarding the requests-per-second, Telerik is significantly better than ASP.NET AJAX, however it downloads huge amout of data comparing to all other frameworks

Please feel free to study these resulsts and pick up your own winner. Thanks again to Krzysiek Owczarek for contributing to this contest.

Thursday, June 28, 2007

Tiny ASP.NET Ajax Framework Contest

 

Contest context

More than two years ago we've built a small web application used to collect and print documents required to participate in secondary school final examinations in Poland. The application is used quite extensively by people from all over the country.

The application, maturzysta.vulcan.pl, is located here.

From the web developer's point of view there is one interesting aspect of the main form of the application: there are about 30 dropdown lists with complicated logic between their selections: when you change the selection in one list, few others should be cleared or repopulated with context-bound data. There is also quite a lot of validation of various dependencies between user's selections.

The first version of the application was built as a server-side application: all SelectedIndexChanged events were auto-postbacked and processed on the application server. This caused a lot of trouble since the heavy form had to be sent to and from the server.

The year later we've completely redesigned the application, moving the interface logic to the javascript. The web traffic problem has been solved but two other appear. First of all - no one likes javascript, it's clumsy and error-prone. Then - we had to disable ASP.NET page validation mechanism because otherwise the page content modified on the client-side would be rejected on the server-side as potentially dangerous.

In the meantime, few promising AJAX frameworks appeared and we thought that it would be possible to go back to the initial version of the application but instead of expensive postbacks we could take the AJAX approach and replace postbacks with callbacks. This could solve all problems: reduce the traffic and still keep the application logic on the server-side.

Contest participants

Three AJAX frameworks were chosen for the contest:

The first and the third one were chosen because it's fairly straightforward to extend existing application with AJAX capabilities. The second one is perhaps one of the most promising AJAX frameworks out there, designed to fill the gap between Windows.Forms and Web.Forms programming.

In order to make the application compatible with participants, following steps have been taken:

  • in case of ASP.NET AJAX, the ScriptManager has been put on the form page and the whole page content has been put into the UpdatePanel. The concept of several small UpdatePanels with triggers between them has been rejected because of the complexity of the application logic and dependencies between controls.
  • in case of the Ajaxium framework, the Ajaxium initialization routine has been put into the OnBeginRequest event of the global application class as well as into the Page_Load method of the form.
  • in case of the VisualWebGUI framework, the application has been completely rewritten using the VWG library and components.

Compare the layout of the WebForms/AJAX application (save images to see them full size)

with the layout of the rewritten VWG version

 The judge

Microsoft Application Center Test has been choosen as the contest judgement tool. Tests were performed on the Windows XP machine serving as the application server. Exactly 200 sessions have been simulated for 5 concurrent users. Each session has been recorded as the complete user session - forms have been filled with data and dropdown selections have been made.

Each session ended with the application showing the print-ready document.

The results

The results are available here.

Observations:

  • both the Microsoft AJAX.NET and the Ajaxium frameworks are not capable of delivering dynamically created content to the web browser from within the AJAX callback [or at least we do not know how to do that]. This means that when the application produces, let say, XML data file and sends it to the application, instead of a typical "Save - Open - Cancel" prompt, you get ... nothing. Fortunately, this was not considered as a test case.
  • the original Web.Forms server-side application, numer (1) in the result chart, has been able to process 213 requests per second and ended with almost 8 MB of data sent to the server and 80 MB of data received from the server. These results has been considered as reference results.
  • Microsoft ASP.NET AJAX application, numer (2) in the result chart, has been able to process 222 requests per second with 5 MB of data sent to the server and 129 MB (!) of data received from the server.
  • The Ajaxium version, number (3) in the result chart, has been able to process only 111 request per second. It sent 5 MB and received 47 MB of data.
  • The VWG version, numer (4), did 422 requests per second with 2 MB sent and 23 MB received (!)
  • The javascript version, numer (5), did 400 requests per second, with 2 MB sent to the server and 37 MB received.

Results:

  • despite its simplicity, Ajaxium did unexpectably bad. We belive that the framework initialization takes just too much time (look at the "time to first byte") and this downgrades the performance.
  • Microsoft ASP.NET AJAX didn't really help regarding server-side resource consumption. It also sends large amount of data to the client - in our case this could be because large form has been put into single UpdatePanel. In overall it did a nice job of ajaxing the application but nothing else.
  • The javascript version was considered as an easy winner and, to our suprise, it sends quite a lot of data to the client. Well then, scripts are over 90 kB. Multiply it by 200 clients and you have the bandwitch consumption.
  • The VWG is an undoubt winner. It did a great job of serving the highest number of requests per second, 422, and is also a clear winner regarding the amount of data sent to and from the server. The only problem with VWG is that it is still in beta version and is not reliable for the time (it still has stability and compatibility problems, let's hope it will get better every other release).

What could be interesting is that the VWG version seems to be the slowest one from the end-user's point of view. It just feels sluggish inside the web browser. We belive that this is caused by the constant callbacks to the server and as you can see it does not slow down the processing pipeline at all - it is just slow inside your web browser.

I would like to thank my friend Krzysiek Owczarek for helping me to perform the tests.

 

Update 2007-07-15

Mark Krapovek, Ajaxium Support Manager, made a comment on the testing environment:

"The evaluation edition available freely on vendor's website DOES NOT SUPPORT CONCURRENT CONNECTIONS. That's just a limitation of the evaluation edition. Before testing the component for "200 sessions simulated for 5 concurrent users" you had to ask vendor for a version with all limitations removed.
So: if Ajaxium Evaluation Edition was able to serve more than 100 requests per second, being limited for only 1 connection at a time, it is the fastest component among all compared."

Mark, thank you for the information.

Tuesday, June 26, 2007

Wrapped InProcSessionStateStore

Although the .NET 2.0 Provider Model offers three built-in possibilities for persisting the session context information, there exists frameworks which limit the choice to the InPronSessionStateStore.

One of such frameworks is the Visual Web GUI Ajax framework designed to fill the gap between Windows.Forms and Web.Forms programming. The VWG framework looks very, very promising, however I feel that its development slows down a bit.

Nonetheless, the main idea of VWG is dumb client, allmighty server where the interface is rendered in the client's web browser and the application logic is executed completely on the server.

There are few limitations and one of them is the session state management - the session context collects much more data than the usual Web.Forms session does. Since VWG objects are not serializable, the SqlServer session state store cannot be used and what I immediately though about was: how much data is exactly stored in the session context?

To be able to inspect the session store it would be perfect to just override the InProcSessionStateStore. However, the class is internal. The idea below is to build a custom session state store which uses the InProcSessionStateStore internally but makes it possible to put custom logic into the session store-retrieve pipeline.

Two critical methods are GetItemExclusive and SetAndReleaseItemExclusive. This is where the custom code can be injected into the pipeline. The code can, for example, log the content of the session store or modify it somehow.

Can I then make my own custom serializer and store the session context in some persistent storage from the wrapped inproc store? you would ask.

Good question. I was not able to find a generic solution to the serialize non-serialized objects problem. The GeneralSerializableAdapter solution proposed here is probably a step in the right direction, however it does not serialize collections correctly (or when it does after you tweak it, it serializes far too much!) neither it is able to serialize non-serialized properties.

If you are able to write a generic serializer which would be able to serialize objects which are not marked as serialized (and do not contain parameterless constructors), please feel free to go a step further then I did and extend the inproc state store wrapper with the generic serializer.

Here is the code:

   1: /* Wiktor Zychla, 2007 */
   2: using System;
   3: using System.Data;
   4: using System.Configuration;
   5: using System.Web;
   6: using System.Web.Security;
   7: using System.Web.UI;
   8: using System.Web.UI.WebControls;
   9: using System.Web.UI.WebControls.WebParts;
  10: using System.Web.UI.HtmlControls;
  11: using System.Web.SessionState;
  12: using System.Web.Hosting;
  13: using System.Reflection;
  14: using System.Data.SqlClient;
  15: using System.Runtime.InteropServices;
  16: using System.Diagnostics;
  17: using System.IO;
  18: using System.Collections;
  19: using System.Runtime.Serialization.Formatters.Soap;
  20: using System.Xml.Serialization;
  21: using System.Runtime.Serialization.Formatters.Binary;
  22: using System.Runtime.Serialization;
  23:  
  24: public class WrappedInProcSessionStateProvider : SessionStateStoreProviderBase
  25: {
  26:     private object store;
  27:     private Type storeType;
  28:  
  29:     public override void Initialize( 
  30:         string name, 
  31:         System.Collections.Specialized.NameValueCollection config )
  32:     {
  33:         storeType = typeof( SessionStateStoreProviderBase )
  34:             .Assembly.GetType( "System.Web.SessionState.InProcSessionStateStore" );
  35:         store = Activator.CreateInstance( storeType );
  36:  
  37:         base.Initialize( name, config );
  38:     }
  39:  
  40:     public override SessionStateStoreData CreateNewStoreData( HttpContext context, int timeout )
  41:     {
  42:         if ( store != null )
  43:         {
  44:             return (SessionStateStoreData)
  45:                 storeType.InvokeMember(
  46:                 "CreateNewStoreData",
  47:                 System.Reflection.BindingFlags.InvokeMethod | 
  48:                 System.Reflection.BindingFlags.Instance | 
  49:                 System.Reflection.BindingFlags.Public,
  50:                 null, store, new object[] { context, timeout }
  51:             );
  52:         }
  53:         else
  54:             throw new Exception( "State not initialized" );
  55:     }
  56:  
  57:     public override void CreateUninitializedItem( 
  58:         HttpContext context, string id, int timeout )
  59:     {
  60:         if ( store != null )
  61:         {
  62:             storeType.InvokeMember(
  63:                 "CreateUninitializedItem",
  64:                 System.Reflection.BindingFlags.InvokeMethod | 
  65:                 System.Reflection.BindingFlags.Instance | 
  66:                 System.Reflection.BindingFlags.Public,
  67:                 null, store, new object[] { context, id, timeout }
  68:             );
  69:         }
  70:     }
  71:  
  72:     public override void Dispose()
  73:     {
  74:         if ( store != null )
  75:         {
  76:             storeType.InvokeMember(
  77:                 "Dispose",
  78:                 System.Reflection.BindingFlags.InvokeMethod | 
  79:                 System.Reflection.BindingFlags.Instance | 
  80:                 System.Reflection.BindingFlags.Public,
  81:                 null, store, null
  82:             );
  83:         }
  84:     }
  85:  
  86:     public override void EndRequest( HttpContext context )
  87:     {
  88:         if ( store != null )
  89:         {
  90:             storeType.InvokeMember(
  91:                 "EndRequest",
  92:                 System.Reflection.BindingFlags.InvokeMethod | 
  93:                 System.Reflection.BindingFlags.Instance | 
  94:                 System.Reflection.BindingFlags.Public,
  95:                 null, store, new object[] { context }
  96:             );
  97:         }
  98:     }
  99:  
 100:     public override SessionStateStoreData GetItem( 
 101:         HttpContext context, string id, out bool locked, 
 102:         out TimeSpan lockAge, out object lockId, out SessionStateActions actions )
 103:     {
 104:         if ( store != null )
 105:         {
 106:             object[] args = new object[] { context, id, null, null, null, null };
 107:  
 108:             SessionStateStoreData ret =
 109:                 (SessionStateStoreData)
 110:                 storeType.InvokeMember(
 111:                 "GetItem",
 112:                 System.Reflection.BindingFlags.InvokeMethod | 
 113:                 System.Reflection.BindingFlags.Instance | 
 114:                 System.Reflection.BindingFlags.Public,
 115:                 null, store, args
 116:             );
 117:  
 118:             locked  = (bool)args[2];
 119:             lockAge = (TimeSpan)args[3];
 120:             lockId  = args[4];
 121:             actions = (SessionStateActions)args[5];
 122:  
 123:             return ret;
 124:         }
 125:         else
 126:             throw new Exception( "State not initialized" );
 127:     }
 128:  
 129:     public override SessionStateStoreData GetItemExclusive( 
 130:         HttpContext context, string id, out bool locked, 
 131:         out TimeSpan lockAge, out object lockId, out SessionStateActions actions )
 132:     {
 133:         if ( store != null )
 134:         {
 135:             object[] args = new object[] { context, id, null, null, null, null };
 136:  
 137:             SessionStateStoreData ret = 
 138:                 (SessionStateStoreData)
 139:                 storeType.InvokeMember(
 140:                 "GetItemExclusive",
 141:                 System.Reflection.BindingFlags.InvokeMethod | 
 142:                 System.Reflection.BindingFlags.Instance | 
 143:                 System.Reflection.BindingFlags.Public,
 144:                 null, store, args
 145:             );
 146:  
 147:             locked  = (bool)args[2];
 148:             lockAge = (TimeSpan)args[3];
 149:             lockId  = args[4];
 150:             actions = (SessionStateActions)args[5];
 151:  
 152:             return ret;
 153:         }
 154:         else
 155:             throw new Exception( "State not initialized" );
 156:     }
 157:  
 158:     public override void InitializeRequest( HttpContext context )
 159:     {
 160:         if ( store != null )
 161:         {
 162:             storeType.InvokeMember(
 163:                 "InitializeRequest",
 164:                 System.Reflection.BindingFlags.InvokeMethod | 
 165:                 System.Reflection.BindingFlags.Instance | 
 166:                 System.Reflection.BindingFlags.Public,
 167:                 null, store, new object[] { context }
 168:             );
 169:         }
 170:     }
 171:  
 172:     public override void ReleaseItemExclusive( 
 173:         HttpContext context, string id, object lockId )
 174:     {
 175:         if ( store != null )
 176:         {
 177:             storeType.InvokeMember(
 178:                 "ReleaseItemExclusive",
 179:                 System.Reflection.BindingFlags.InvokeMethod | 
 180:                 System.Reflection.BindingFlags.Instance | 
 181:                 System.Reflection.BindingFlags.Public,
 182:                 null, store, new object[] { context, id, lockId }
 183:             );
 184:         }
 185:     }
 186:  
 187:     public override void RemoveItem( 
 188:         HttpContext context, string id, 
 189:         object lockId, SessionStateStoreData item )
 190:     {
 191:         if ( store != null )
 192:         {
 193:             storeType.InvokeMember(
 194:                 "RemoveItem",
 195:                 System.Reflection.BindingFlags.InvokeMethod | 
 196:                 System.Reflection.BindingFlags.Instance | 
 197:                 System.Reflection.BindingFlags.Public,
 198:                 null, store, new object[] { context, id, lockId, item }
 199:             );
 200:         }
 201:     }
 202:  
 203:     public override void ResetItemTimeout( HttpContext context, string id )
 204:     {
 205:         if ( store != null )
 206:         {
 207:             storeType.InvokeMember(
 208:                 "ResetItemTimeout",
 209:                 System.Reflection.BindingFlags.InvokeMethod | 
 210:                 System.Reflection.BindingFlags.Instance | 
 211:                 System.Reflection.BindingFlags.Public,
 212:                 null, store, new object[] { context, id }
 213:             );
 214:         }
 215:     }
 216:  
 217:     public override void SetAndReleaseItemExclusive( 
 218:         HttpContext context, string id, SessionStateStoreData item, 
 219:         object lockId, bool newItem )
 220:     {        
 221:         if ( store != null )
 222:         {
 223:             AnalyzeSessionContent( id, item );
 224:  
 225:             storeType.InvokeMember(
 226:                 "SetAndReleaseItemExclusive",
 227:                 System.Reflection.BindingFlags.InvokeMethod | 
 228:                 System.Reflection.BindingFlags.Instance | 
 229:                 System.Reflection.BindingFlags.Public,
 230:                 null, store, new object[] { context, id, item, lockId, newItem }
 231:             );
 232:         }
 233:     }
 234:  
 235:     private void AnalyzeSessionContent( string id, SessionStateStoreData item )
 236:     {
 237:         // item.Items
 238:     }
 239:  
 240:     public override bool SetItemExpireCallback( 
 241:         SessionStateItemExpireCallback expireCallback )
 242:     {
 243:         if ( store != null )
 244:         {
 245:             return (bool)
 246:                 storeType.InvokeMember(
 247:                 "SetItemExpireCallback",
 248:                 System.Reflection.BindingFlags.InvokeMethod | 
 249:                 System.Reflection.BindingFlags.Instance | 
 250:                 System.Reflection.BindingFlags.Public,
 251:                 null, store, new object[] { expireCallback }
 252:             );
 253:         }
 254:         else
 255:             throw new Exception( "State not initialized" );
 256:     }
 257: }
 258: