Tuesday, November 20, 2007

Loose coupling of embedded DetailsViews

I often embed a DetailsView inside yet another DetailsView. Typical scenario - there is a "Person DetailsView" and "Address DetailsView". The former one has one TemplateField with the latter embedded.

Since I always work with objects, I also use ObjectDataSources. The ObjectDataSource will provide a business object as the data source for the control if and only if the underlying data provider provides the correct business object.

Usually I just add three sections to the ObjectDataSource - SelectParameters, UpdateParamteters and DeleteParameters each one providing exactly one parameter, the ID of the underlying object.

In case of one DetailsView embedded inside another one this does not work - the first DetailsView consumes the ID of the person supplied by the Select Parameter but the embedded DetailsView should use the Person's ID_ADDRESS value as the ID.

To overcome this difficulty I've just implemented a simple solution where one of the Page's controls has to implement following interface:

   1: namespace Vulcan.Uczniowie.UILayer
   2: {
   3:     public interface IParamValueProvider
   4:     {
   5:         object ProvideValue( string ParamName );
   6:     }
   7: }

and the SelectParameter just retrieves the value supplied by the control:



   1: namespace Vulcan.Uczniowie.UIHelpers
   2: {
   3:     [DefaultProperty( "ParamName" )]
   4:     public class ParamValueProviderParameter : 
   5:        QueryStringParameter
   6:     {
   7:         private string paramName;
   8:         public string ParamName
   9:         {
  10:             get
  11:             {
  12:                 return paramName;
  13:             }
  14:             set
  15:             {
  16:                 paramName = value;
  17:             }
  18:         }
  19:  
  20:         protected override object Evaluate( 
  21:             System.Web.HttpContext context, 
  22:             System.Web.UI.Control control )
  23:         {
  24:             if ( context != null &&
  25:                  context.Handler is Page
  26:                 )
  27:             {
  28:                 IParamValueProvider Provider = 
  29:                     FindParamValueProvider( context.Handler as Page );
  30:                 if ( Provider != null )
  31:                     return Provider.ProvideValue( ParamName );
  32:  
  33:                 return null;
  34:             }
  35:  
  36:             return null;
  37:         }
  38:  
  39:         private IParamValueProvider FindParamValueProvider( Control Control )
  40:         {
  41:             if ( Control != null )
  42:                 foreach ( Control Child in Control.Controls )
  43:                 {
  44:                     if ( Child is IParamValueProvider )
  45:                         return (IParamValueProvider)Child;
  46:  
  47:                     IParamValueProvider RecursiveRet = FindParamValueProvider( Child );
  48:                     if ( RecursiveRet != null )
  49:                         return RecursiveRet;
  50:                 }
  51:  
  52:             return null;
  53:         }
  54:     }
  55: }

The top level Details View just implements the interface:



   1: #region IParamValueProvider Members
   2:  
   3:    // implemented in the top level DetailsView
   4:    public object ProvideValue( string ParamName )
   5:    {
   6:        switch ( ParamName )
   7:        {
   8:            // somehow provide ID_ADDRESS for the embedded DetailsView
   9:            case "ID_ADDRESS" :
  10:            if ( ParameterHelper.GetParameterValue( 
  11:                   this.DataSource.SelectParameters["ID"] ) != null )
  12:            {
  13:               int id = Convert.ToInt32( 
  14:                 ParameterHelper.GetParameterValue( 
  15:                   this.DataSource.SelectParameters["ID"] ) );
  16:               Person Item = Person.Retrieve( Global.Model, id );
  17:  
  18:               return Item.ID_ADDRESS;
  19:            }
  20:            break;
  21:        }
  22:  
  23:        return null;
  24:    }
  25:  
  26: #endregion

and the embedded one defines the SelectParameter as:



   1: <SelectParameters>
   2:     <vlc:ParamValueProviderParameter Name="ID" ParamName="ID_ADDRESS" />
   3: </SelectParameters>

This way we have a perfect loose-coupling between the top level and embedded views - both communicate using the interface IParamValueProvider.


To complete the example I have to explain and provide code for the ParameterHelper.GetParameterValue method.


You see, the Select/Update/Delete parameters are directly available only inside the ObjectDataSource's Selecting/Inserting/Deleting event handlers! If you are to retrieve the current value of a parameter outside one of these handlers you will learn that there's no CurrentValue property on the Parameter class. Or rather, there is but it's private!


Therefore the ParameterHelper aims to retrieve the value so that it is available from any context, not only from within one of handlers I've mentioned.



   1: namespace Vulcan.Application.UIHelpers
   2: {
   3:     public class ParameterHelper
   4:     {
   5:         public static object GetParameterValue( Parameter Parameter )
   6:         {
   7:             return Parameter.GetType().InvokeMember(
   8:                 "ParameterValue",
   9:                 System.Reflection.BindingFlags.GetProperty | 
  10:                 System.Reflection.BindingFlags.NonPublic | 
  11:                 System.Reflection.BindingFlags.Instance,
  12:                 null, Parameter, null );
  13:         }
  14:     }
  15: }

No comments: