Efficient Server-Side View State Persistence, Two Dot DOH!!


So, I made a post yesterday about the way we're keeping our view state persisted on the server.

I had been feeling pretty good about the methods that I came up with and the results of not only our tests, but the real world tests that we've seen with this code "out in the wild" per se...  I started to do a lot of reading.  When I say a lot, I mean I fired up google, started at the top and went through every "view state" related link I could find to see if there was something I was missing or hadn't thought of.

On the fine art of being humbled by those smarter than you...

Scott Hanselman has been known for putting together some view state hacks over the years.  He has even written a book (well, part of a book) that has had some of his view state hacks it in.  If you read through all of his blog posts relating to view state (and I have) you begin to get the subtle impression (that is, subtle like a jack hammer outside your bedroom window at 4am) that he is against keeping view state separated from the page.  His post title "Moving ViewState to the Session Object and more Wrongheadedness" on 2/28/2004 really grabbed my attention.  More specifically the paragraph where he said:

If you store ViewState in the Session object in this way, you are assuming the user will access only one page at a time, and you may confused other pages in their attempt to load values from Bogus ViewState.  More importantly, what happens if the user opens new browser window, and starts accessing DIFFERENT pages but sharing the same session.  Well, you get the idea.

But I SWEAR it worked on my machine!!

I know that I had tested the situation Scott had outlined above before pushing my code live.. I know that there was no way that it could fall prey to the edge situation that he mentions... Then the waves of self doubt start to roll in.

So I fired up the exact same sample application that I provided as a download and started running some tests.

  1. Single Browser (IE7) - No problems at all.  Everything works as expected
  2. Single Browser (FF3) - No problem at all.  Everything works as expected
  3. Two Browsers (IE7 / FF3) - No problem at all.  Everything works as expected. 
  4. Two Browsers (IE7 / IE7) - No problem at all.  Everything works as expected.

At that point, I started feeling pretty good about myself... thoughts of "Oh, he wrote that four and a half years ago.. things have changed since then." and "I knew I had tested this.. i r so smart!"...

And then the darkness came...

Scott had said something else in his post that I read and re-read over and over again (the bold is my emphasis):

It pulls the ViewState from the request NameValueCollection (including the Form collection, etc).  Each 'instance' of a page has it's own ViewState.

Then it hit me.... I fired up my test again with IE, created some view state with changed data... then, instead of opening up another IE window... I opened up my test page in another IE Tab.. Low and behold, the second tab was stomping all over the first tab's view state data.  Welcome to the black parade.

Get defines the start of a new page instance

After a brief period of huddling in the corner balled up in the fetal position crying over my own insignificance, I went back and started thinking about how we know when a page is a new instance or not.  The answer is actually really clear.  Every time a HTTP Get verb is called on a page, that page is spun up in a new instance.  with ASP.Net, all page interaction is handled through the POST verb (why it's called Post Back after all).  So all I have to do is key the persisted view state, not only to the user's session id and requested page path, but to an identifiable page instance when the Get was first called.  Once I establish that unique key based on those three factors, then I need to re-use that key and persisted cache file for every subsequent page post.

Realizing that, it's just a matter of modifying the Save() method of the CachePageStatePersister to only generate a new key and cache file if and only if the requested page is not in a post back state.  Otherwise, it should pull the key from the pages hidden field, and cache file path from the Page.Cache..

   1: public override void Save()
   2: {
   3:     if (ViewState != null || ControlState != null)
   4:     {
   5:         if (Page.Session == null)
   6:             throw new InvalidOperationException("Session is required for CachePageStatePersister (SessionID -> Key)");
   7:  
   8:         string vsKey;
   9:         string cacheFile;
  10:  
  11:         if (!Page.IsPostBack) // create a unique cache file and key based on this user's session and page instance (time)
  12:         {
  13:             string sessionId = Page.Session.SessionID;
  14:             string pageUrl = Page.Request.Path;
  15:             vsKey = string.Format("{0}{1}_{2}_{3}", VSPREFIX, pageUrl, sessionId, DateTime.Now.Ticks);
  16:  
  17:             string cachePath = Page.MapPath(CACHEFOLDER);
  18:             if (!Directory.Exists(cachePath))
  19:                 Directory.CreateDirectory(cachePath);
  20:             cacheFile = Path.Combine(cachePath, BuildFileName());
  21:         }
  22:         else    // get our vs key from the page, re use it, and the cache file (pulled from page.cache)
  23:         {
  24:             vsKey = Page.Request.Form[VSKEY];
  25:             if (string.IsNullOrEmpty(vsKey)) throw new ViewStateException();
  26:             cacheFile = Page.Cache[vsKey] as string;
  27:             if (string.IsNullOrEmpty(cacheFile)) throw new ViewStateException();
  28:         }
  29:  
  30:         IStateFormatter frmt = StateFormatter;
  31:         string state = frmt.Serialize(new Pair(ViewState, ControlState));
  32:         using (StreamWriter sw = File.CreateText(cacheFile))
  33:             sw.Write(state);
  34:  
  35:         Page.Cache.Add(vsKey, cacheFile, null, DateTime.Now.AddMinutes(Page.Session.Timeout),
  36:                        Cache.NoSlidingExpiration, CacheItemPriority.Low, ViewStateCacheRemoveCallback);
  37:         Page.ClientScript.RegisterHiddenField(VSKEY, vsKey);
  38:     }
  39: }

 

So, with this small change in the Save() method, our server side view state persister truly supports accurate retrieval of a pages view state, no matter what the user is doing on their end with whichever browser configuration they may be running.

Download Sample Project (10.3k)

author: Jason Monroe | posted @ Wednesday, August 06, 2008 11:59 AM | Feedback (0)

Efficient Server-Side View State Persistence


It's all about the user experience...

[Update: There is a small bug with this posting. I've made a follow up post here to explain the problem and the solution as well]

We all know that the quickest way to kill a web site is to have all of your users disappear.  After all, a web site without users is pretty pointless.  We are constantly striving to bring fresh new content and the latest and greatest "WOW" factor to our sites in order to not only garner new users, but to keep the users we already have coming back.

Users *HATE* slow sites.  This is common knowledge, and when Steve put together his 5 truths about web users, he made this #2 on his list:

2. Users have more than a LAN between their browser and your servers.

I am in the middle of building the latest and greatest. The main page has 15 uncompressed javascript files, 10 background gifs, a couple of extra css files, and sends almost half a meg of data to render the page. It takes about a second for all of this to show up in my browser.

Of course, my browser is sitting on the server, and I've got endless RAM and multiple processors.

When I put this on a test server and hit it from home through DSL, the cloud, and our shared pipe at work - a second becomes over 10 seconds. Long enough that I check ESPN for any new football news.

Having a slow app is like building a car with doors that don't open. People won't buy a car if they have to crawl through the window to get in.

Put your app on an external server and hit it with consumer-grade net access.

Enter View State, Killer of User Experience

View State is a mechanism employed by ASP.Net web pages to persist the state of the page itself, individual controls, objects and data that are housed on that particular ASP.Net web page.  View State is a double edged sword in that using it properly allows the developer to build full and robust web applications that seem to overcome the stateless nature of the web.  Scott Mitchell has written an excellent article on view state some years ago that is published on MSDN.  I won't go into the detail on view state that he does (since he's already done an excellent job), but I will mention a few things:

  1. To much of a good thing... By default, every control that you add to an ASP.Net web page has view state enabled.  Every button, label, grid, drop down.. everything.  That means every control on your page makes the size of the view state grow.
  2. View State is persisted in the HTML Markup.  That's right boys and girls.  Every control on your page that makes use of view state, making that view state bigger.. makes your overall page size bigger as well.
  3. View State travels both ways.  Since view state is used by ASP.Net, and ASP.Net only exists on the server, then that view state that was sent down to the browser, has to be sent BACK to the server for it to be used.  Yes, that includes partial page updates with AJAX as well.
  4. View State is ONLY used on the server.  I know I said this in #3, but it bears repeating here... the only place that view state is EVER used, is on the server, by the ASP.Net runtime...

Even the most responsible developers can be plagued by a large view state...

As the complexity of our web applications grow, more and more controls get added to the screen, more and more data needs to be persisted... view state grows.  There's no way to avoid it.  Even if we are diligent in our efforts in only making sure that those things that need view state are the only ones with it turned on, view state can grow to several kilobytes in size.  It's not uncommon to have view state sizes that run into the 50k to 100k range on custom intranet/extranet applications.

Couple this with the fact that view has to be sent down to the browser, and back up to the server on every post back and asynchronous post back done via AJAX, we can see how just the sheer size of the web page causes our overall application performance to tank.  The thing is, ever growing view state is not a new problem.  Over the years, many people (most much smarter than I am) have come up with methods to combat the size. Scott Hanselman put together a blog post on how to compress the view state before it's sent to the browser as had several other authors. Another solution often sought after is moving the view state from the top of the page to the bottom of the page.  Moving the view state enables the browser to start rendering the HTML content right away instead of waiting for this massive hidden field content to stream down.  However, that still doesn't negate the fact that the view state has to travel back up to the server.

No, to solve this view state problem once and for all, I'd like to redirect your attention to rule #4...

View State is *ONLY* used on the server!!!

It seems logical to me, that if view state is only used on the server, then we should keep it on the server and not make it travel down the wire to the client browser, and back up to the server... every... single... time.

History

I stumbled upon a Code Project article entitled Keep ASP.Net ViewState out of ASPX Page for Performance Improvement which took it's inspiration from an article posted by Peter Bromberg.  The basic premise of the article is to override the System.Web.UI.Page SavePageStateToPersistenceMedium and LoadPageStateFromPersistenceMedium methods.  The code samples show how to take the view state and save them in either the Page.Cache or in Session.. or some combination there of.  In developing our solution, I opted to persist the view state into cache.  I was extremely excited to see that my average page size went from 70k in size to just over 20k.  Feeling good about the initial tests, I rolled the branch into our code based and published the code to our training server to put a slightly heaver load on the code for testing.  After about 3 hours of use during a training class, the server came crashing to it's knees.

Sever resources are not infinite.

What had happened, was now, instead of the 30k to 60k view state being sent back and forth to the client, it was living happily in Page.Cache.  Of course, Page.Cache is an in-memory caching device.  So for 50 users, over the course of 3 hours.. the ASP.Net cache grew to the point that it choked out the rest of the memory available to the ASP.Net worker process. 

Not wanting to give up on the concept (I still believed that it was the best way to handle our large view state problem and keep the user experience optimal), I went "back to the drawing board" and came up with another solution to persist the view state on the server, while still being mindful of the finite server resources.

Rethinking the design

Requirements:

  1. The view state needs to be persisted on the server
  2. The view state persistence mechanism needs to be identified by a specific user session
  3. The persisted view state artifact must not be allowed to remain forever
  4. The persisted view state should be able to be enabled and disabled on a page by page bases
  5. Different persistence mechanism's should be able to be used.
  6. Page development and structure should not be modified.

Control Adapters to the Rescue

Since the ASP.Net Page object, at it's core is a control (granted.. it is *THE* control.. but a control non-the-less) we're able to modify it's behavior with a simple control adapter.  Typically, when speaking about control adapters, the development community at large thinks about CSS Control Adapters.

MSDN defines control adapters thusly:

Control adapters are components that override certain Control class methods and events in its execution lifecycle to allow browser or markup-specific handling. The .NET Framework maps a single derived control adapter to a Control object for each client request.

I had discovered this rather excellent (although brief) article by Robert Boedigheimer on using Server Side View state in ASP.Net using the SessionPageStatePersister.  I knew then that this was the road I wanted to go down... although using the SessionPageStatePersister would run into the same problems with finite memory resources.. so that as a brush stroke solution wouldn't do...

My solution actually contains two parts.  The first part is the control adapter PageStateAdapter, and the second is the custom persistence mechanism CachePageStatePersister.

PageStateAdapter

To solve my requirements #4 and #5, I decided to go with a simple attribute scheme.  Where basically, the developer can decide to use a different view state persistence scheme simply by putting an attribute at the top of the pages class definition.  The attribute class and supporting enum are both defined in the PageStateAdapter class

   1: public enum StateStorageTypes { Default, Cache, Session, InPage }
   2:  
   3: [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
   4: public class PageViewStateStorageAttribute : Attribute
   5: {
   6:     private readonly StateStorageTypes storageType = StateStorageTypes.Default;
   7:  
   8:     public PageViewStateStorageAttribute(StateStorageTypes stateStorageType)
   9:     {
  10:         storageType = stateStorageType;
  11:     }
  12:  
  13:     internal StateStorageTypes StorageType
  14:     {
  15:         get { return storageType; }
  16:     }
  17: }

 

Now all the PageStateAdapter has to do is implement the virtual method GetStatePersister

   1: public override PageStatePersister GetStatePersister()
   2: {
   3:     PageViewStateStorageAttribute psa =
   4:         Attribute.GetCustomAttribute(Page.GetType(), typeof(PageViewStateStorageAttribute), true) as PageViewStateStorageAttribute ??
   5:         new PageViewStateStorageAttribute(StateStorageTypes.Default);
   6:  
   7:     PageStatePersister psp;
   8:     switch (psa.StorageType)
   9:     {
  10:         case StateStorageTypes.Session:
  11:             psp = new SessionPageStatePersister(Page);
  12:             break;
  13:         case StateStorageTypes.InPage:
  14:             psp = new HiddenFieldPageStatePersister(Page);
  15:             break;
  16:         default:
  17:             psp = new CachePageStatePersister(Page);
  18:             break;
  19:     }
  20:     return psp;
  21: }

 

If a developer wishes to override the (now) default view state persistence method of CachePageStatePersister, they can do so by applying a simple attribute to the page class declaration

   1: [PageStateAdapter.PageViewStateStorage(PageStateAdapter.StateStorageTypes.InPage)]
   2: public partial class ViewStateInPage : System.Web.UI.Page
   3: {
   4: }

 

CachePageStatePersister

The CachePageStatePersister inherits from PageStatePersister.  So all it has to do is implement the two virtual methods Load() and Save().

   1: public override void Save()
   2: {
   3:     if (ViewState != null || ControlState != null)
   4:     {
   5:         if (Page.Session == null)
   6:             throw new InvalidOperationException("Session is required for CachePageStatePersister (SessionID -> Key)");
   7:  
   8:         IStateFormatter frmt = StateFormatter;
   9:         string state = frmt.Serialize(new Pair(ViewState, ControlState));
  10:         string sessionId = Page.Session.SessionID;
  11:         string pageUrl = Page.Request.Path;
  12:         string vsKey = string.Format("{0}{1}_{2}", VSPREFIX, pageUrl, sessionId);
  13:  
  14:         string cachePath = Page.MapPath(CACHEFOLDER);
  15:         if (!Directory.Exists(cachePath))
  16:             Directory.CreateDirectory(cachePath);
  17:  
  18:         string cacheFile = Path.Combine(cachePath, BuildFileName());
  19:  
  20:         using (StreamWriter sw = File.CreateText(cacheFile))
  21:             sw.Write(state);
  22:  
  23:         Page.Cache.Add(vsKey, cacheFile, null, DateTime.Now.AddMinutes(Page.Session.Timeout),
  24:                        Cache.NoSlidingExpiration, CacheItemPriority.Low, ViewStateCacheRemoveCallback);
  25:         Page.ClientScript.RegisterHiddenField(VSKEY, vsKey);
  26:     }
  27: }

 

In our save method, you can see we're doing a number of things.. first is generating a unique view state key based on the page that's being requested and the users session id.  Then we're serializing BOTH the view state and control state to a physical file (in this case, we're storing the file in the ~/App_Data/Cache directory).  After the file is created, we store the view state key, and the path to the file in the Page.Cache, and save the view state key to a hidden field to the page.

So we are mindful of finite server resources by only storing the file path in cache, we are mindful of download page size by only storing the unique key in the page (instead of the entire view state) and by using the Page.Cache object to tie everything together we're giving our persisted view state files a life time.  Notice on the last bit of the Page.Cache.Add() method, we're defining ViewStateCacheRemoveCallback as our call back method when an item is removed from cache.

   1: public static void ViewStateCacheRemoveCallback(string key, object value, CacheItemRemovedReason reason)
   2: {
   3:     string cacheFile = value as string;
   4:     if (!string.IsNullOrEmpty(cacheFile))
   5:         if (File.Exists(cacheFile))
   6:             File.Delete(cacheFile);
   7: }

 

When the Cache makes the call back, it passes on what that cache object contained, which we know is the file path to the persisted view state object.  All we have to do when we receive the call back, is to delete the physical file.

The Load method basically works in reverse of the save method..

   1: public override void Load()
   2: {
   3:     if (!Page.IsPostBack) return;   // We don't want to load up anything if this is an inital request
   4:  
   5:     string vsKey = Page.Request.Form[VSKEY];
   6:  
   7:     // Sanity Checks
   8:     if (string.IsNullOrEmpty(vsKey)) throw new ViewStateException();
   9:     if (!vsKey.StartsWith(VSPREFIX)) throw new ViewStateException();
  10:  
  11:     IStateFormatter frmt = StateFormatter;
  12:     string state = string.Empty;
  13:  
  14:     string fileName = Page.Cache[vsKey] as string;
  15:     if (!string.IsNullOrEmpty(fileName))
  16:         if (File.Exists(fileName))
  17:             using (StreamReader sr = File.OpenText(fileName))
  18:                 state = sr.ReadToEnd();
  19:  
  20:     if (string.IsNullOrEmpty(state)) return;
  21:  
  22:     Pair statePair = frmt.Deserialize(state) as Pair;
  23:  
  24:     if (statePair == null) return;
  25:  
  26:     ViewState = statePair.First;
  27:     ControlState = statePair.Second;
  28: }

 

Some points of interest here are... if the page is not working as a post back, we don't want to load our view state from persistence.  This solves the problem of a user loading stale data.  The Load() method gets it's view state key from the page, then gets the path to the persisted view state file from the Page.Cache using that view state key.  The file is then read, de-serialized into a Pair object, then ViewState and ControlState are loaded from the Pair object.

The Browsers File

The last point of order is to wire up the PageStateAdapter for use with a simple .browser file located in App_Browsers:

   1: <browsers>
   2:   <browser refID="Default">
   3:     <controlAdapters>
   4:       <adapter controlType="System.Web.UI.Page" adapterType="PageStateAdapter" />
   5:     </controlAdapters>
   6:   </browser>
   7: </browsers>

 

Conclusion

View state, while a powerful tool, can very quickly become the demise of your users in experiencing your web site to the fullest.  By making use of built-in ASP.Net technologies like Cache and Control Adapters, we can effectively and accurately persist view state on the server instead of streaming it down to the user.  Using the outline PageStateAdapter has enabled our developers to make full use of our existing ASP.Net skill set, including our heavy use of ASP.Net AJAX, without having to re-design our entire page framework; it is truly a drop in solution.  Our PageStateAdapter method could be very easily modified to persist the view state files to a common location for a multi-homed web environment, and the Page.Cache replaced with a common mechanism in that same environment.  We've been using this technique in our training environment under both real world load and extreme test load with very impressive results.

In our code base, we've also added to the global Application_Start and Application_End events to delete all the .cache files that might be present.  This takes care of any artifacts that might be around when the server reboots, or that might have been missed in the cache remove call back.

Download the sample project (11k)

author: Jason Monroe | posted @ Tuesday, August 05, 2008 5:38 PM | Feedback (0)

Performing IT Acrobatics...


So, I want to be more proactive in keeping my blog up to date.  Something that I've been meaning to do for some time now.  The problem is, at least for me.. Blogging is somewhat cumbersome.  Ya see, I like using Windows Live Writer to edit and publish my blog.  I find it easy to use, and the plug-ins available to it for various things (I use Code Snippit and Znagit (a snag-it wrapper)).

The problem is... I can only use WLW on my home computer and my laptop.  Ya see, my primary workstation is a Dell Precision (duel Xeon procs).  I have a total of 8 gigs of ram, and in order to use all that precious ram, I'm running Windows XP x64. 

WLW doesn't support XP x64.

So, because I don't have a convenient way to work up a blog post while I'm sitting down at my primary workstation, blog posts don't happen.

Recently, I've had to spin up a 32bit XP environment using Virtual PC for another project that we've got going on.. so it hit me, run WLW inside the VM.  And that's where we're at today.  Though, the odd thing is, I'm using SubText for my blogging platform (When I started BlogEngine.Net was just an idea) and with WLW inside the VM, I'm unable to pull down my blog theme like I can with it on my laptop or home machine.

*sigh*

So tell me, what kind of acrobatics and hoop jumping do you have to go through to get things done?

author: Jason Monroe | posted @ Tuesday, August 05, 2008 4:02 PM | Feedback (0)

BlogEngine.NET 1.4.5 released


I usually don't like to repost other people posts.. but this one has some merit and is worth mentioning.

Mads & company have done EXCELLENT work with the BlogEngine.Net and they deserve our congrats and thanks.

If YOU are looking for a blogging platform, then you would be amiss if you did not take a look at BE.

BlogEngine.NET 1.4.5 released

author: Jason Monroe | posted @ Friday, August 01, 2008 8:59 AM | Feedback (0)

Messin' with majax - mutexCheckBoxes


So I've been playing around a bit with a little JavaScript plug-in library extension set for ASP.Net Ajax that Matt Berseth came up with that he's dubbed 'majax'When last we left off, I was trying to make the check box group extension of his library to work with ASP.Net Checkboxes as well as regular HTML Checkboxes.  All in all, I think it was a fairly successful effort.

As with all good things, necessity is the mother of invention.  Our core application at the day job has a GridView that displays some records.  Now these records have a set of options associated with them, but the problem is, only one record can have one of the options available to them at a time.  So, if record A, has option 1 chosen, then records B, C...n can not have option 1 selected.

The developer who wrote this particular page (a few years ago), satisfied the rules checking by posting the page back with every... single... checkbox onclick event in the gridview.  The page would work itself through all of the rows in the grid, find the check box that was just checked, and uncheck all of the other checkboxes in that column.  User experience goes right out the window with that one.

So I decided to see if I could throw majax at the problem and see if anything suck ...

Download Sample App | Live Demo

Let's get this party started!

I won't get into the mark up of the ASPX page to deeply in the post, but what I did was drop a GridView on the page and setup a series of TemplateFields in the columns markup.  Then in the ItemTemplate for the columns, I dropped in some checkboxes.  As with my previous post and Matt's Demo, I added the needed ScriptReference's to the ScriptManager on the page:

<asp:ScriptManager ID="ScriptManager1" runat="server">
    <Scripts>
        <asp:ScriptReference Assembly="AjaxControlToolkit" Name="AjaxControlToolkit.ExtenderBase.BaseScripts.js" />
        <asp:ScriptReference Assembly="AjaxControlToolkit" Name="AjaxControlToolkit.Common.Common.js" />
        <asp:ScriptReference Assembly="AjaxControlToolkit" Name="AjaxControlToolkit.MutuallyExclusiveCheckBox.MutuallyExclusiveCheckBoxBehavior.js" />
        <asp:ScriptReference Path="~/_assets/js/majax.js" />
        <asp:ScriptReference Path="~/_assets/js/majax.mutexCheckBox.js" />
    </Scripts>
</asp:ScriptManager>

 

I created a VERY simple class to act as a dummy data holder, so that I could bind the gridview to a generic List<>.

   1: public class MutexCheckboxSampleData
   2: {
   3:     public string SomeText1 { get; set; }
   4:     public string SomeText2 { get; set; }
   5:     public bool Option1 { get; set; }
   6:     public bool Option2 { get; set; }
   7:     public bool Option3 { get; set; }
   8:     public bool Option4 { get; set; }
   9: }

 

The actual creating of the data and binding it to the gridview happens with a helper BindGridView() method and a static GetSampleData() method.  I like to cheat a little bit when it comes to creating random strings of data and make a quick call to Path.GetRandomFileName().

   1: private void BindGridView()
   2: {
   3:     gvMutexCheckBoxes.DataSource = GetSampleData();
   4:     gvMutexCheckBoxes.DataBind();
   5: }
   6:  
   7: private static List<MutexCheckboxSampleData> GetSampleData()
   8: {
   9:     List<MutexCheckboxSampleData> retValue = new List<MutexCheckboxSampleData>();
  10:     // Create our first one as a default with all options on
  11:     retValue.Add(new MutexCheckboxSampleData
  12:     {
  13:         SomeText1 = Path.GetRandomFileName(),
  14:         SomeText2 = Path.GetRandomFileName(),
  15:         Option1 = true,
  16:         Option2 = true,
  17:         Option3 = true,
  18:         Option4 = true
  19:     });
  20:     // and create some more records with out the options..
  21:     for (int i = 0; i < 5; i++)
  22:         retValue.Add(new MutexCheckboxSampleData { SomeText1 = Path.GetRandomFileName(), SomeText2 = Path.GetRandomFileName() });
  23:  
  24:     return retValue;
  25: }

 

Now, this is where things can get a little dicey and hard to follow.  Since we don't know at design time, how many checkboxes we're going to have in the mutually exclusive group, or what their id's will even be... we need to have a way to keep track of them.  What I did was to create a List<string> as a data holder for each option column that's going to be displayed in the grid: