Thursday, September 20, 2007

DevExpress XPO, DropDownList and Custom Formatting in DetailsView's ItemTemplate

It is a common scenario to put a DropDownList into GridView's or DetailsView's Item Template. Since most of the time we use object-oriented architecture, the DropDownList's data source is a business method which returns list of business entities.

Long ago, when we've faced this scenario for the first time, we had to solve the formatting issue - when binding a list to the DropDownList, you specify DataTextField and DataValueField:

   1: <asp:DropDownList 
   2:     ID="TheDropDownList" 
   3:     runat="server" 
   4:     DataSource="<%# DataProvider.BusinessObjects %>" 
   5:     DataTextField="Name" DataValueField="ID" /> 

Issues occur when you need the items to be formatted in a custom way.


The most convenient and coherent method of adding custom formatting is to add a dummy property to the business object class:



   1: public class BusinessObject : IFormattable {
   2:  
   3:   private string name;
   4:   public string Name { ...
   5:   }
   6:  
   7:   public BusinessObject This {
   8:     get { return this; }
   9:   }
  10:  
  11:   public string ToString( string format, IFormatProvider formatProvider )
  12:   {
  13:     switch ( format )
  14:     {
  15:       case "s" :
  16:         return ...;
  17:       default :
  18:         return this.Name;
  19:     }
  20:   }
  21: }

so that you can force custom formatting of your business objects inside the DropDownList like this (note the This value for the DataTextField property) :



   1: <asp:DropDownList
   2:   ID="TheDropDownList"
   3:   runat="server"
   4:   DataSource="<%# DataProvider.BusinessObjects %>"
   5:   DataTextField="This" DataValueField="ID"
   6:   DataTextFormatString="{0:s}" />

This worked like a charm for us, well until few days ago when we've tried the same on business objects persisted with DevExpress XPO



   1: public class BusinessObject : XPObject, IFormattable
   2: {
   3:     public BusinessObject This
   4:     {
   5:         get
   6:         {
   7:             return this;
   8:         }
   9:     }
  10:  
  11:     ...

and instead of custom formatting I got DevExpress.XPO.XPCollection(14) Count(1), DevExpress.XPO.XPCollection(15) Count(1) ... instead of custom formatted descriptions.


After over 2 hours the battle was won. Following trick



   1: public class BusinessObject : XPObject, IFormattable
   2: {
   3:     public IFormattable This
   4:     {
   5:         get
   6:         {
   7:             return this;
   8:         }
   9:     }
  10:  
  11:     ...

does the job.


It turns out that XPObject inherits from XPCustomObject which inherits from XPBaseObject which has the ToString method defined and this is where the problem lies. Only changing the signature of the This property gives expected results.


The solution is, however, far beyond any explanation I am able to come up with.

Wednesday, September 12, 2007

IE 7 search engine feature examine local files?

One of the most convenient features of Internet Explorer 7 is the possibility to redirect the phrase typed into the address bar to the default search engine (I prefer Google over Live! but it is quite easy to switch between different providers).

Suppose I type "c# pdf library" directly into the address bar. Instead of an error, Internet Explorer pops up the http://www.google.pl/search?hl=pl&q=c%23+pdf+library&lr= page and I can immediately browse search results.

I am used to this feature so much that yesterday I was quite surprised to see this instead of typical results page while searching for "Google Earth":

It seems that Internet Explorer first tries to match the phrase you enter into the address bar with names of some (which exactly?) local files, in this case the "Google Earth.lnk" located on my desktop.

Just a small inconvenience.

Tuesday, September 11, 2007

ASP.NET Web Application in Offline Mode Revisited

Probably most of You are aware of the app_offline.htm functionality. Basically if you put a html file named app_offline.htm in the root of a web application, the application goes to "offline" mode where all incoming requests are redirected to this file.

This seems great at first sight. You can temporarily disable the application while performing some maintenance. Specifically, you can delete all application files except the app_offline.htm and the application still works (even though the web.config is gone!) As long as the maintenance is completed you just remove or rename the app_offline.htm and the application is back from the void.

There is, however, one caveat and one drawback of this feature.

The caveat is that the app_offline.htm file must be longer that 512 bytes otherwise instead of its content you get ... 404 (can someone explain it?).

The drawback is that the offline mode affects everyone including you! Either anyone or noone can access the application. What would be nice, however, is to have the ability to temporarily redirect requests coming from outside but still be able to use the application locally.

The code below shows how to accomplish that requirement with really simple HTTP handler. To use it you have to:

  1. compile the source class
  2. add the handler section to the web.config file and put appoffline.htm (without the _ !) in the root of the application
   1: <httpHandlers>
   2:    <add verb="*" path="*.aspx" type="AppOffline.AppOfflineHandler, AppOffline" />
   3:  </httpHandlers>

 


Handler code:



   1: using System;
   2: using System.Collections.Generic;
   3: using System.Text;
   4: using System.Web;
   5: using System.IO;
   6: using System.Web.UI;
   7:  
   8: /* Wiktor Zychla, 2007 */
   9: namespace AppOffline
  10: {
  11:     public class AppOfflineHandler : IHttpHandler
  12:     {
  13:         string appOffline;
  14:         /// <summary>
  15:         /// appoffline.htm file content as singleton
  16:         /// </summary>
  17:         public string AppOffline
  18:         {
  19:             get
  20:             {
  21:                 if ( appOffline == null )
  22:                     appOffline = OfflineFileContent;
  23:  
  24:                 return appOffline;
  25:             }
  26:         }
  27:  
  28:         /// <summary>
  29:         /// Path to the appoffline.htm file
  30:         /// </summary>
  31:         public string OfflineFilePath
  32:         {
  33:             get
  34:             {
  35:                 return context.Server.MapPath( "~/appoffline.htm" );
  36:             }
  37:         }
  38:  
  39:         /// <summary>
  40:         /// appoffline.htm file content
  41:         /// </summary>
  42:         public string OfflineFileContent
  43:         {
  44:             get
  45:             {
  46:                 return File.ReadAllText( OfflineFilePath );
  47:             }
  48:         }
  49:  
  50:         /// <summary>
  51:         /// Is the application in the offline mode?
  52:         /// 
  53:         /// Yes - if the appoffline.htm exists
  54:         /// </summary>
  55:         public bool IsOffline
  56:         {
  57:             get
  58:             {
  59:                 return File.Exists( OfflineFilePath );
  60:             }
  61:         }
  62:  
  63:         HttpContext context;
  64:  
  65:         #region IHttpHandler Members
  66:         public bool IsReusable
  67:         {
  68:             get
  69:             {
  70:                 return true;
  71:             }
  72:         }
  73:  
  74:         public void ProcessRequest( HttpContext context )
  75:         {
  76:             this.context = context;
  77:  
  78:             // offline mode and remote request?
  79:             if ( !context.Request.IsLocal &&
  80:                  IsOffline
  81:                 )
  82:             {
  83:                 context.Response.Clear();
  84:                 context.Response.Write( AppOffline );
  85:  
  86:                 context.Response.End();
  87:             }
  88:             else
  89:                 // redirect to the default processing pipe
  90:                 PageParser.GetCompiledPageInstance( 
  91:                     context.Request.Path, 
  92:                     context.Request.PhysicalPath, 
  93:                     context ).ProcessRequest( context );             
  94:         }
  95:         #endregion
  96:     }
  97: }

This approach has its own drawback - it is not "maintenance" aware in a sense that when for example some crucial modules are gone from the /bin subfolder, the application will not load at all because of non-existent modules beeing referenced from within the web.config file.


The nice thing is that you can test the application locally but all remote requests are gracefully redirected to the appoffline.htm. I suggest then to use my feature side by side with the ASP.NET app_offline.htm.

Thursday, September 6, 2007

Query String Tampering

Query String Tampering is one of the easiest and most common Internet attacks. Each time a web application uses QueryString to pass sensitive information between pages, there is a risk that tampering is possible and would reveal otherwise unavaiable information.

Suppose that I am allowed to see the list of my order (567), and clicking on the application-generated link I am supposed to see the details.

http://the.address.com/details.aspx?orderid=567

 

What if I try to manually alter the parameter value and see an order from someone else?

http://the.address.com/details.aspx?orderid=568

 

If it works then we can say that the application is vulnerable to QST.

There are two possible solutions to this issue:

  1. sign query string parameters with additional signature. You can read about it here
  2. encrypt the whole query string so that there is only single encrypted parameter visible on the client-side. You can read about it here

 

From these two possibilities I prefer the latter since it seems to be slightly more ellegant. Custom implementation is easy and straightforward.

Wednesday, August 29, 2007

Context Help Made Easy - Reloaded

Yet another issue

A month ago I've written about context sensitive help. The article can be found here and please refer to it before further reading.

Although the concept works pretty well, there are some obvious drawbacks which apply also to the original concept of Help Providers. What is the drawback?

Well, you are allowed to assign one and only one help topic to a single control.

Suppose however that some controls are "data-polymorphic" in a sense that they store completely different data in different contexts. Three examples:

  • trees usually keep different node types at different levels of their hierarchies
  • lists, listviews etc. often show different types of objects when initialized in different contexts
  • some generic forms are initialized with different types of data but behave in the same way each time

Every time such situation occurs there is a problem with binding a proper help topic to such control. Ideally, since the context is determined by the data, you would like to have something like that in the application code:

   1: string ProvideHelpTopicForControl( Control TheControl )
   2: {
   3:   if ( TheControl == treeViewStructure )
   4:      return ProvideHelpTopicForTreeView( treeViewStructure );
   5:   if ( TheControl == lstListViewWithItems )
   6:      return ProvideHelpTopicForListView( lstListViewWithItems );
   7: }
   8:  
   9: ...
  10:  
  11: string ProvideHelpTopicForTreeView( TreeView Tree )
  12: {
  13:   if ( Tree.SelectedNode != null )
  14:   {
  15:     switch ( Tree.SelectedNode.Text )
  16:     ...
  17:  
  18:     or
  19:  
  20:     switch ( Tree.SelectedNode.GetType().Name )
  21:     ...
  22:   }
  23:   else
  24:     return someDefaultMapping;
  25: }

(the code is not complete but I hope you get the idea)


While such solution could be perfect for a small application with just few different forms, it could be extremely tedious to implement and maintain it in a complex code.


And of cource, such hardcoded mapping would completely ruin the whole idea of moving the topic mapping out of the application!


Solution - additional context binding


I've spent few hours on thinking of a solution which would just extend the existing idea with features powerful enough to handle such "data-polymorphic" controls.


What I finally came up with can be summarized as follows:



  • each topic mapping gets another set of so-called binding contexts (no relation to data-binding!)
  • each binding context is identified by its name and holds complete description of help topic (including HelpKeyword and HelpNavigator)
  • when a topic mapping is found for a control (but before the topic is shown to the user), the engine tries to locate a binding context which could be more precise for selected control than the parent topic. If such binding context is found - it is used to show the help topic instead of the parent mapping.

Binding contexts are build in a different way for different types of controls but it is still done outside of the application code. In my implementation I handle just few different types of controls and leave the default implementation for other controls (the context is retrieved from the control's text).



   1: public static string[] GetBindingContext( Control Control )
   2: {
   3:     /* obsługa szczególnych typów formantów */
   4:  
   5:     #region TreeView
   6:     if ( Control is ListView )
   7:     {
   8:         ListView l = Control as ListView;
   9:         if ( l.SelectedItems.Count > 0 &&
  10:              l.SelectedItems[0].Tag != null
  11:             )
  12:             return new string[] 
  13:             { string.Format( 
  14:                 "element {0}", 
  15:                 l.SelectedItems[0].Tag.GetType().Name ) };
  16:         else
  17:             return new string[] { string.Empty };
  18:     }
  19:  
  20:     if ( Control is TreeView )
  21:     {
  22:         TreeView t = Control as TreeView;
  23:         if ( t.SelectedNode != null )
  24:             return BuildBindingContext( t.SelectedNode );
  25:         else
  26:             return new string[] { string.Empty };
  27:     }
  28:     #endregion
  29:  
  30:     #region TabControl
  31:     if ( Control is TabControl )
  32:     {
  33:         TabControl tb = Control as TabControl;
  34:         if ( tb.SelectedTab != null )
  35:             return new string[] { tb.SelectedTab.Text };
  36:         else
  37:             return new string[] { string.Empty };
  38:     }
  39:     #endregion
  40:  
  41:     return new string[] { Control.Text };
  42: }
  43:  

So how it works?


Well, suppose the Topic is mapped for the TreeView and additional binding-contexts are defined. One named Context1 and mapped to Topic1 and Context2 mapped to Topic2.


Then, when F1 is pressed for any node except for Conext1 and Context2, the Topic is shown. However, when the node's text is Context1, Topic1 is shown and when the node's text is Context2, Topic2 is shown.


What's more interesting, the case of TreeView imposes that binding-contexts for controls are matched not only by the context name but, when there's no binding-context defined for selected node's text, the engine tries to find a context for selected node's parent, then for it's parent and so on.


This correctly handles a common scenario where a node name is fixed and set from within the code (e.g. Clients) but it's subnodes come from the database and thus their names are dynamic while the mapping expects a static, fixed set of names provided in a mapping file. In this scenario the same context will be assigned to Clients and all its subnodes (assuming that no specific binding context is assigned to subnodes).


There can be arbitrarily many of these additional binding-contexts for a control. 


As you can see in above code excerpt, the binding context for a ListView is determined by the item's Tag type but note also that this can be easily adopted to specific needs.


And how is the additional binding-context provided within the instrumentation mechanism?


Well, you just provide a topic mapping for the control as usual and then click "+" next to "Context" listbox.



The context's name is retrieved automatically from the application automatically and you just provide a topic mapping for it. You can also remove a context when it's not required anymore.


Conclusion


That's it. The new implementation just extends the former one which means that it should work correctly with help.mapping files generated with previous version of the library. When you compile the new source code, you would be able to extend the topic mapping with additional binding-contexts which will further refine the assignment of help topics to application's controls.


Feel free to use the code without any explicit permission. Leave a comment if you like this approach.