Tuesday, March 29, 2011

Master-Detail with the GridView, DetailsView and jQuery's ThickBox

Master-Detail with the GridView, DetailsView and jQuery's ThickBox: "

And so I continue on messing around with jQuery and looking into ways I can use it in my WebForms applications.  Something I sheepishly admitted to in my last post was that I don't want to use 3 client side libraries {ASP.NET AJAX, AjaxControlToolkit, jQuery}, yet I never really took the time to see what the size of the core jQuery library is.  And for that matter I haven't really looked at what scripts some of my favorite AjaxControlToolkit controls are pulling down either.  I also assumed there is a bunch of overlap between jQuery and ASP.NET AJAX, but I haven't looked into that either. 

So I thought I would check some of this out and along the way rebuild my Master-Detail with the GridView, DetailView and ModalPopup Controls and replace the ModalPopup with jQuery's ThickBox.  I found it pretty interesting - read on to see how it went.

Live Demo | Download

ThickBox Demo

What I Was Looking For

While building this sample I decided I would keep my eye out for the following things ...

  1. Size Comparison (minified, but not gzipped) of AjaxControlToolkit's ModalPopup versus jQuery's ThickBox
  2. Installation / Configuration 
  3. Programming Model / API 
  4. ASP.NET AJAX Integration 

 

jQuery's ThickBox

If you are not familiar with jQuery's ThickBox = here the 2 sentence description taken from the ThickBox home page ...

ThickBox is a webpage UI dialog widget written in JavaScript on top of the jQuery library.  Its function is to show a single image, multiple images, inline content, iframed content, or a content server through AJAX in a hybrid model

Size Comparison

This is what I was most interested in.  I get a lot of comments to my AjaxControlToolkit posts that comment on how large the client side footprint of the Toolkit is.  And I honestly don't really know how to respond - mostly because I don't have a baseline that I can compare it to.  And jQuery's ThickBox and the Toolkit's ModalPopup is not exactly like comparing apples to apples, but I think it is close enough to get a feeling for things.

So here are the numbers ...

 FileSize (Compressed)
ThickBox  
 jQuery.js55KB
 ThickBox.js/ThickBox.css10KB
Total 65KB
ModalPopup  
 Common.js28KB
 BaseScripts.js11KB
 RoundedCornersBehavior.js27KB
 Timer.js1KB
 DropShadowBehavior.js7KB
 DynamicPopulateBehavior.js6KB
 DragDropScripts.js23KB
 DynamicPopulateBehavior.js6KB
 Floatingehavior.js4KB
 ModalPopupBehavior.js18KB
Total 131KB

 

The ModalPopup and its supporting scripts is just over twice the size of the jQuery plus ThickBox combo.  Interesting.  If you are curious about where I got the numbers from - I used the file system to get the jQuery/ThickBox numbers and Reflector to look at the sizes of the Toolkit's supporting JavaScript files (they are embedded resources). 

image

I should mention though that although both the ModalPopup and ThickBox JavaScript files are both minified - this wasn't done with the same tool.  And to my novice eye, it looks like the one jQuery uses does a better job.  So you should keep this in mind when interpreting these numbers.  Also, this is without gzipping - so these numbers will both be smaller after this is applied as well.

 

ThickBox Installation and Configuration

If you want to use the ThickBox, you have to do the following things ...

1.  Download the latest version of jQuery and add a script reference to your page

2.  Download thickbox.js, thickbox-compressed.js, thickbox.css and the loadingAnimation.gif graphic and place them in your web site.  I included both the compressed and non-compressed versions of thickbox just in-case I wanted to debug any issues I ran into. 

After steps #1 and #2, your site should look something like this (the highlighted are the jQuery/ThickBox components) ...

image

And your page will include the following JavaScript and CSS references.

image

3.  Next, you need to update the thickbox-compressed.js and thickbox.js files to include the location of the loading graphic you would like to display while the ThickBox is loading.  The ThickBox component displays an animated gif while the ThickBox is being displayed.  By default the component assumes the location of this graphic is images/loadingAnimation.gif.  So if this isn't the location on your web server you will have to manually update the thickbox.js and thickbox-compressed,js files and update this variable.  In my app, I am storing the graphic at _assets/img/loading.gif so I updated the thickbox JavaScript files to use this path instead of the default one.

 

   1: //  change this ...
   2: var tb_pathToImage = "images/loadingAnimation.gif";
   3:  
   4: //  to this ...
   5: var tb_pathToImage = "_assets/img/loading.gif";

 

 

Programming Model

The ThickBox programming model works as follows ...

You add an A or INPUT to your page and adorn it with the thickbox CSS class.  Then, like magic, when the element is clicked the ThickBox is displayed.  Here is a real simple example that shows an image in a ThickBox - when the A is clicked the nested IMG element is displayed in a ThickBox.

   1: <a href="images/single.jpg" title="add a caption to title attribute / or leave blank" class="thickbox">
   2:     <img src="images/single_t.jpg" alt="Single Image"/>
   3: </a>

 

The ThickBox also supports other scenarios too - like displaying some in-line content from the page inside the ThickBox (like a hidden DIV or something) as well as showing an IFRAME in the ThickBox.  These other scenarios require some additional parameters - and the ThickBox uses some of the existing INPUT and A attributes to specify these values ...

  • title - you can use the A or INPUT's title attribute to specify a title for the ThickBox
  • A.href and INPUT.alt - the value of this attribute is parsed into some of the parameters that control how the ThickBox behaves

For example, here is how you would use the ThickBox to display some hidden content on your page...

  • The href attribute of the A element contains the encoded parameters
  • #TB_inline tells the ThickBox that the content to be displayed exists some where on the current page
  • height/width specify the height and width of the ThickBox
  • inlineId is the ID of the element that will be displayed
  • modal specifies that the ThickBox will be displayed modally
   1: <a href="#TB_inline?height=155&width=300&inlineId=hiddenModalContent&modal=true" class="thickbox">Show hidden modal content.</a>

 

And here is a very simple example of an anchor that when clicked displays some hidden content.

   1: <a href="#TB_inline?height=50&width=300&inlineId=hiddenModalContent" title="Simple Demo" class="thickbox">Show hidden content.</a>
   2: <div id="hiddenModalContent" style="display:none;">
   3:     <div style="text-align:center;">Hello ThickBox!</div>
   4: </div>

 

And here is what it looks like.  In this sample, the dialog isn't modal so it can be closed via the Close or pressing the Escape key or just by clicking any where else on the page.

image

 

ASP.NET AJAX Integration

And now of the real question - how well does the ThickBox work with my WebForms apps?  Well, I tried porting over my previous sample of using the ModalPopup to edit rows in a GridView.  Here is how it went ...

  • I started down the path of using the ThickBox to display some hidden inline content, but that didn't work out to well.  The ThickBox takes the contents of the inline content and moves the DOM elements out to an immediate child of the document's BODY element.  That doesn't work too well when used with WebForms because I need everything contained within the FORM element for my page to work properly. 

Check it out - below is what the DOM looks like.  See how the FORM element and the TB_window DIV are siblings - that isn't going to work. 

image

But this isn't exactly a show stopper either - I can still move my detail content over to a separate page and have the ThickBox load it up using an IFRAME.  So that's were I went next.

  • I updated the markup for my main GridView and changed the last column to render an A tag with all of the parameters I need to tell the ThickBox to load up a new page in an IFRAME.  Remembering how the ThickBox parses the arguments from the anchor's href and title attributes, I used a databinding expression to build out the values for these attributes. 

When the dialog shows I want to include the Customer name in the title, so I am using the ContactName property to build the title value.  I also want to show the Detail.aspx page passing the current rows ID value as an argument.  So I again use a databinding expression to set the href attribute to the value I want.

   1: <asp:GridView ID="gvCustomers" runat="server" DataSourceID="odsCustomerList" CssClass="datagrid" GridLines="None" AutoGenerateColumns="false">
   2:     <Columns>
   3:         <asp:BoundField DataField="ID" HeaderText="ID" ReadOnly="true" />
   4:         <asp:BoundField DataField="CompanyName" HeaderText="Company" ReadOnly="true" />
   5:         <asp:BoundField DataField="ContactName" HeaderText="Name" ReadOnly="true" />
   6:         <asp:BoundField DataField="ContactTitle" HeaderText="Title" ReadOnly="true" />                
   7:         <asp:BoundField DataField="Address" HeaderText="Address" ReadOnly="true" />                
   8:         <asp:BoundField DataField="City" HeaderText="City" ReadOnly="true" />                
   9:         <asp:TemplateField>
  10:             <ItemTemplate>
  11:                 <a id="btnShowPopup" runat="server" class="thickbox" 
  12:                     title='<%# Eval("ContactName", "Details for {0}") %>' 
  13:                     href='<%# Eval("ID", "Detail.aspx?ID={0}&TB_iframe=true&height=220&width=400") %>'>Edit</a>                
  14:             </ItemTemplate>
  15:         </asp:TemplateField>                        
  16:     </Columns>                    
  17: </asp:GridView>
  • Next, I created the Detail.aspx page that displays the detailed information for a given Customer.  This page requires a single parameter, ID, that passed via the querystring.  This page displays the Customer detail information in a DetailsView and allows you to edit the values.  The page has a single Save button that when clicked ensures the page is valid, saves the changes back to the data store, and closes the ThickBox window.

The interesting part here is that the Detail page is responsible for closing itself after the user presses the Save button.  So to accomplish this, I need to register a small script that will run after the Save has completed that will close the ThickBox.  To get this bit of work done, I first created a JavaScript function on the main page called updated that closes the ThickBox window using the ThickBox's tb_remove function.  Next, I need to let the main grid know that it needs to refresh itself (otherwise you wouldn't see any of the changes you just made).  I handle this by setting up a hidden Refresh button that when clicked causes the main grid to refresh itself.

Here is what the updated function looks like ... 

   1: function updated() {
   2:     //  close the popup
   3:     tb_remove();
   4:     
   5:     //  refresh the update panel so we can view the changes  
   6:     $('#<%= this.btnRefreshCustomers.ClientID %>').click();      
   7: }

 

And here is what the Detail page's Save event handler looks like ...

   1: protected void BtnSave_Click(object sender, EventArgs args)
   2: {
   3:     if (this.Page.IsValid)
   4:     {
   5:         //  move the data back to the data object
   6:         this.dvCustomerDetail.UpdateItem(false);
   7:         
   8:         //  register the script to close the popup
   9:         this.Page.ClientScript.RegisterStartupScript(typeof(detail_aspx), "closeThickBox", "self.parent.updated();", true);
  10:     }
  11: } 

 

  • And I actually need make one more modification.  My customer grid is contained within an UpdatePanel.  And even though this grid doesn't sorting or paging, the example as-is wouldn't work too well after the user triggered an event that causes the UpdatePanel to refresh itself.  The reason why is because the ThickBox works by fetching all of the A and INPUT's that contain the thickbox class - and this selection occurs just after the page is initially loaded.  And it doesn't happen again after that.  So we need to add a special piece of logic that will reapply the ThickBox to the A elements that are contained within our UpdatePanel.  And here is the bit of JavaScript that I am using to do this ...
   1: function pageLoad(sender, args) {
   2:     if(args.get_isPartialLoad()){
   3:         //  reapply the thick box stuff
   4:         tb_init('a.thickbox');
   5:     }
   6: }

 

This is again scanning the complete page for anchor elements with the thickbox class and adding the ThickBox behavior to them.  This bit of logic could be optimized because ...

  • We don't need to scan the complete page again, just inside any UpdatePanels that were refreshed.  This is where it might be useful to use a Sys.Component to auto-magically keep track of this stuff for us.  We could easily hook into some of the ASP.NET client side page events and reapply the ThickBox stuff to any elements that contained within an UpdatePanel.  This is pretty similar to the approach I used here to maintain scroll position across partial post-backs.    

 

Conclusion

So, after all of this, here are a few things I found interesting.

  • When I saw all of the scripts the ModalPopup required I couldn't help think it might be interesting if there was a way for the control to only render the scripts it needed to function based on the features you are currently using.  That would be useful.
  • It took me a while to learn the ThickBox's API.  Encoding the parameters into the href is crafty, but it also makes the features hard to discover without documentation.  The Toolkit has a very consistent API.
  • None of the jQuery stuff is going to have a server side API - so when using jQuery from my WebForms I am going to be injecting quite a bit more JavaScript from the page's codebehind than I am used to.
  • I didn't like how the ThickBox forces me to separate my detail widget into a separate page.  But the reality is that most of the jQuery stuff is not written with WebForms in mind.  So running into this stuff might be the norm when trying to use these scripts.

Also, keep in mind this is just one sample.  In my mind, this doesn't tell me to replace the Toolkit with jQuery or vice versa. 

 

That's it.  Enjoy!   

"

No comments: