Click here to Skip to main content
Click here to Skip to main content
Go to top

Manage ViewState Using ASP.NET 2.0 Provider Architecture

, 5 Jun 2007
Rate this:
Please Sign up or sign in to vote.
Serverside ViewState management using custom providers based on ASP.NET 2.0 provider pattern architecture.

Introduction

This article shows an implementation of a couple of custom viewstate providers using the ASP.NET 2.0 Provider architecture and the Provider Model design pattern. To get more information regarding the ASP.NET 2.0 Provider Model, please read Introduction to the Provider Model.

Background

About half a year ago, I was working on a project that required moderately large datasets fetched from the database and displayed in a DataGrid. Going to the database on every postback seemed like a waste of resources, therefore, I decided to store the data in the viewstate. And, that's when the page load times started taking longer and longer. I started researching alternative solutions for optimizing the viewstate, and found a moderate amount of information and examples online. Nevertheless, I have not been able to find a complete solution that would let me easily manage and extend the functionality of the viewstate without having to make significant changes to the website.

I have looked at a number of interesting and useful articles that provided different takes on the implementation of viewstate management, but none had a complete and easy to integrate solution.

The code presented below was inspired by or partially taken from the following articles:

Using the code

I will concentrate on describing just the viewstate provider code, without going into details on the provider pattern design. For a detailed explanation of the pattern design and architecture, please see the references listed above.

Writing the providers

First, we need an abstract class representing the viewstate and exposing the functions actual providers will implement. ViewStateProvider is derived from ProviderBase, and will be the base class for all concrete viewstate provider classes. It will have two functions that must be implemented by the derived classes: LoadPageState and SavePageState.

''' <summary>
''' Defines the contract that ASP.NET implements to provide viewstate
''' services using custom viewstate providers.
''' </summary>
Public MustInherit Class ViewStateProvider
    Inherits System.Configuration.Provider.ProviderBase


    Public MustOverride Property ApplicationName() As String

''' <summary>
''' The hidden field variable name where
''' our viewstate reference key is stored on a page.
''' </summary>
Public MustOverride Property ViewStateKeyName() As String

    ''' -----------------------------------------------------------------------------
    ''' <summary>
    ''' Loads any saved view-state of the current page from virtually any
    ''' storage medium other than a hidden field
    ''' </summary>
    ''' <param name="pControl">System.Web.UI.Page</param>
    ''' <returns>The saved view state</returns>
    ''' -----------------------------------------------------------------------------
    Public MustOverride Function LoadPageState(ByVal pControl As Control) As Object

    ''' -----------------------------------------------------------------------------
    ''' <summary>
    ''' Saves any view-state information of the page to virtually any
    ''' storage medium other than a hidden field
    ''' </summary>
    ''' <param name="pControl">System.Web.UI.Page</param>
    ''' <param name="viewState">A System.Object
    '''       in which to store the view-state information</param>
    ''' -----------------------------------------------------------------------------
    Public MustOverride Sub SavePageState(ByVal pControl As Control, _
           ByVal viewState As Object)

End Class

Next, we need an actual provider class. I have three concrete provider classes in the example:

  • SessionViewStateProvider
  • Viewstate is being maintained as a session variable.

  • CompressionViewStateProvider
  • Viewstate is being compressed using ICSharpCode.SharpZipLib and written to a page.

  • SqlViewStateProvider
  • Viewstate is maintained using a modified ASPState database. The ViewState is written to the database similar to an OutOfProc Session.

Let's look at one of them - SessionViewStateProvider.

Note: This provider requires a SQL script attached with the code to be executed on the database where the viewstate will be stored. Proper SQL connection and impersonation must be set up in Web.config. All of the SQL script code is taken from Adam Weigert's SqlViewState - The Path to Better ViewState Storage. Other providers in the example do not require any additional setup, except registration in a Web.config (explained here).

''' <summary>
''' Manages storage of viewstate object for an ASP.NET
''' application in a SQL Server database.
''' </summary>
''' <remarks><para><pre>
''' RevisionHistory:
''' --------------------------------------------------------------------------------
''' Date        Name            Description
''' --------------------------------------------------------------------------------
''' 10/11/2006    Oleg Sobol        Initial Creation
''' inspiration taken from
''' http://weblogs.asp.net/adweigert/archive/2004/03/09/
'''          sqlviewstate-the-path-to-better-viewstate-storage.aspx
''' 5/11/2007    Oleg Sobol      Added ViewStateKeyName and DEFAULT_TIMEOUT
''' </pre></para></remarks>
Public Class SqlViewStateProvider
    Inherits System.Web.UI.ViewStateProvider

	Public Const DEFAULT_TIMEOUT As Integer = 25

	Private _applicationName As String
	Private _connectionString As String
	Private _connectionStringName As String
	Private _timeout As TimeSpan
	Private _enableViewStateMac As Boolean
	Private _lockLoad As New Object
	Private _lockSave As New Object
	Private _viewStateKeyName As String = "__VIEWSTATE_KEY"

    Public Overloads Overrides Property ApplicationName() As String
        Get
            Return _applicationName
        End Get
        Set(ByVal value As String)
            _applicationName = value
        End Set
    End Property

    Public Property ConnectionStringName() As String
        Get
            Return _connectionStringName
        End Get
        Set(ByVal value As String)
            _connectionStringName = value
        End Set
    End Property

    Public Property Timeout() As TimeSpan
        Get
            Return _timeout
        End Get
        Set(ByVal value As TimeSpan)
            _timeout = value
        End Set
    End Property

    Public Property EnableViewStateMac() As Boolean
        Get
            Return _enableViewStateMac
        End Get
        Set(ByVal value As Boolean)
            _enableViewStateMac = value
        End Set
    End Property

    Public Overrides Property ViewStateKeyName() As String
        Get
        Return _viewStateKeyName
        End Get
        Set(ByVal value As String)
        _viewStateKeyName = value
        End Set
    End Property

    Public Overloads Overrides Sub <a name="init">Initialize</a>(ByVal name As String, _
                     ByVal config As NameValueCollection)
        ' Verify that config isn't null
        If config Is Nothing Then Throw New ArgumentNullException("config")
 
	   	' Assign the provider a default name if it doesn't have one
	        If String.IsNullOrEmpty(name) Then name = "SqlViewStateProvider"

	    ' Add a default "description" attribute
	    ' to config if the attribute doesn't exist or is empty
	    If String.IsNullOrEmpty(config("description")) Then
	        config.Remove("description")
	        config.Add("description", "SQL viewstate provider")
	    End If

	    ' Call the base class's Initialize method
	    MyBase.Initialize(name, config)

	    ' Initialize _applicationName
	    _applicationName = config("applicationName")
	    If String.IsNullOrEmpty(_applicationName) Then _applicationName = "/"
	    config.Remove("applicationName")

	    Dim connect As String = config("connectionStringName")
	    If String.IsNullOrEmpty(connect) Then
	        Throw New ViewStateProviderException(_
	              "Empty or missing connectionStringName")
	    End If
	    config.Remove("connectionStringName")
	    If WebConfigurationManager.ConnectionStrings(connect) Is Nothing Then
	        Throw New ViewStateProviderException("Missing connection string")
	    End If
	    _connectionString = _
	      WebConfigurationManager.ConnectionStrings(connect).ConnectionString
	    If String.IsNullOrEmpty(_connectionString) Then
	        Throw New ViewStateProviderException("Empty connection string")
	    End If

	    Dim timeout As String = config("timeout")
	    If String.IsNullOrEmpty(timeout) OrElse Not IsNumeric(timeout) Then
	        _timeout = TimeSpan.FromMinutes(_
	            ViewStateProvidersConfig.DefaultViewStateTimeout)
	    Else
	        _timeout = TimeSpan.FromMinutes(CInt(timeout))
	    End If
	    config.Remove("timeout")

	    Dim enableViewStateMac As String = config("enableViewStateMac")
	    Try
	        _enableViewStateMac = CBool(enableViewStateMac)
	    Catch ex As Exception
	        _enableViewStateMac = False
	    End Try
	    config.Remove("enableViewStateMac")

	    ' Throw an exception if unrecognized attributes remain
	    If config.Count >

SessionViewStateProvider's Initialize method expects to find a configuration attribute named ConnectionStringName identifying a connection string in the <connectionStrings> configuration section. The connection string is used by the LoadPageState and SavePageState methods to load/save the viewstate string from/to the database.

Other properties include:

  • ApplicationName which gets populated from the applicationName attribute.
  • ViewStateKeyName property will contain the name of the hidden field where the reference to the viewstate will be kept on the page. This reference cannot be kept in the default __VIEWSTATE field, because it gets overwritten by the ASP.NET rendering engine.
  • Timeout property which sets the time in minutes for how long the database should keep the current viewstate record. A SqlServer job runs every so often and deletes all the expired viewstate records from the table.
  • SavePageState serializes the viewState object passed into it using System.Web.UI.LosFormatter. Creates a GUID to serve as a unique identifier, and saves the record in a database. The newly generated GUID is then saved in a hidden field variable of a page.
  • LoadPageState retrieves the GUID from the hidden variable and gets the viewstate from the database.

Web Application configuration

The viewstate provider requires the following to be added to the Web.config of the Web Application:

<configuration>
    <configSections>
        <sectionGroup name="system.web">
            <section name="viewstate" 
               type="System.Web.UI.ViewStateSection, CustomProviders"
               restartOnExternalChanges="true" 
               allowDefinition="MachineToApplication" />
        </sectionGroup>
    </configSections>
    <connectionStrings>
        <add name="ViewStateConnectionString" 
          connectionString="Server=(local);Database=ASPState;
                            Integrated Security=True;" />
    </connectionStrings>
    <system.web>
        <viewstate defaultProvider="CompressionViewStateProvider" 
                  enabled="true">
            <providers>
                <add name="SqlViewStateProvider" 
                   type="System.Web.Configuration.Providers.SqlViewStateProvider,
                         CustomViewStateProviders" 
                   connectionStringName="ViewStateConnectionString" 
                   timeout="35" />
                <add name="SessionViewStateProvider" 
                   type="System.Web.Configuration.Providers.SessionViewStateProvider, 
                         CustomViewStateProviders" 
                   numberOfPagesInMemory="10" />
                <add name="CompressionViewStateProvider" 
                  type="System.Web.Configuration.Providers.CompressionViewStateProvider, 
                         CustomViewStateProviders" />
            </providers>
        </viewstate>
    <system.web>
</configuration>

Explanation of the infrastructure for the configuration

Since the viewstate is not a stock configuration section, a custom configuration section must be written that derives from System.Configuration.ConfigurationSection. The following is a System.Web.UI.ViewStateSection class that exposes two properties: Providers and DefaultProvider. The <ConfigurationProperty> attributes map ViewStateSection properties to <viewstate> attributes in a configuration file. As a result, the DefaultProvider property will get its value from the <viewstate> element's defaultProvider attribute, if present, and so on.

''' <summary>
''' Maps to a <viewstate> section in a configuration file
''' </summary>
Public Class ViewStateSection
    Inherits ConfigurationSection


    <ConfigurationProperty("providers")> _
    Public ReadOnly Property Providers() As ProviderSettingsCollection
        Get
            Return CType(MyBase.Item("providers"), ProviderSettingsCollection)
        End Get
    End Property

    <ConfigurationProperty("defaultProvider"), DefaultSettingValue("")> _
    Public Property DefaultProvider() As String
        Get
            Return CStr(MyBase.Item("defaultProvider"))
        End Get
        Set(ByVal value As String)
            MyBase.Item("defaultProvider") = value
        End Set
    End Property

End Class

Below is the way our custom section is registered with ASP.NET to be inside the system.web section of a configuration file. Note: type="System.Web.UI.ViewStateSection, CustomProviders" has the namespace, class, and assembly name in which the class is located.

<configSections>
    <sectionGroup name="system.web">
        <section name="viewstate" 
           type="System.Web.UI.ViewStateSection, CustomProviders"
           restartOnExternalChanges="true" 
           allowDefinition="MachineToApplication" />
    </sectionGroup>
</configSections>

Loading and initializing viewstate providers

At last, we need a class that will load and manage all viewstate providers registered in Web.config. The ViewStateManager class will be used in the actual ASPX pages to load/save the viewstate. It will contain a collection of all the providers registered in the Web.config, with one of them set as the default provider.

''' <summary>
''' Manages all viewstate manipulations.
''' </summary>
Public Class ViewStateManager

    Private Shared _provider As ViewStateProvider = Nothing
    Private Shared _providers As ViewStateProviderCollection = Nothing
    Private Shared _lock As New Object
    Private Shared _enabled As Boolean
    Private Shared _enabledSet As Boolean

    ''' <summary>
    ''' Default provider set in web.config
    ''' </summary>
    Public Shared ReadOnly Property Provider() As ViewStateProvider
        Get
            Return _provider
        End Get
    End Property

    Public Shared ReadOnly Property Providers() As ViewStateProviderCollection
        Get
           Return _providers
        End Get
    End Property

    Public Shared ReadOnly Property Enabled() As Boolean
        Get
            If Not _enabledSet Then
                _enabled = GetViewStateSection().Enabled
                _enabledSet = True
            End If
            Return _enabled
       End Get
    End Property

    Shared Sub New()
        Call LoadProviders()
    End Sub

    Public Shared Function LoadPageState(ByVal pControl As Control) As Object
        ' Make sure a provider is loaded
        Call LoadProviders()
        ' Delegate to the provider
        Return _provider.LoadPageState(pControl)
    End Function

    Public Shared Sub SavePageState(ByVal pControl As Control, _
                                    ByVal viewState As Object)
        ' Make sure a provider is loaded
         Call LoadProviders()
        ' Delegate to the provider
         _provider.SavePageState(pControl, viewState)
    End Sub

    Private Shared Sub LoadProviders()
        ' Avoid claiming lock if providers are already loaded
        If _provider Is Nothing Then
            SyncLock _lock
                ' Do this again to make sure _provider is still null
                If _provider Is Nothing Then
                    ' Get a reference to the <viewstate> section
                    Dim section As ViewStateSection = GetViewStateSection()
                    ' Is custom viewstate management enabled
                    If section IsNot Nothing Then _enabled = section.Enabled
                    _enabledSet = True

                    If _enabled Then
                        ' Load registered providers and
                        ' point _provider to the default provider
                        _providers = New ViewStateProviderCollection
                        ProvidersHelper.InstantiateProviders(section.Providers, _
                                        _providers, GetType(ViewStateProvider))
                        _provider = _providers(section.DefaultProvider)
                        If _provider Is Nothing Then
                            Throw New ViewStateProviderException(_
                               "Unable to load default ViewStateProvider")
                        End If
                    End If
                End If
            End SyncLock
        End If
    End Sub

    Private Shared Function GetViewStateSection() As ViewStateSection
        Return CType(WebConfigurationManager.GetSection(_
               "system.web/viewstate"), ViewStateSection)
    End Function

End Class

Inside LoadProviders, the ProvidersHelper.InstantiateProviders subroutine loops through the viewstate/providers section under system.web in Web.Config and loads the registered providers. It looks at the following to load a provider:

<add name="SqlViewStateProvider" 
  type="System.Web.Configuration.Providers.SqlViewStateProvider,
        CustomViewStateProviders" 
  connectionStringName="ViewStateConnectionString" timeout="35" />

Using ViewStateManager

Now we need to overload the functions exposed by the Page class to use our viewstate manager to save/load the viewstate. In the example, this is done by deriving a PageTemplate class that inherits from System.Web.UI.Page, and making all ASPX pages inherit from PageTemplate.

PageTemplate has the following exposed besides the overridden viewstate functions:

  • ServerSideViewState - set to false to use a regular page viewstate. This property can be set in Web.config or in the page's Init method.
  • SetServerSideViewStateProvider - sets the provider to be used on a page. This is a hack I used to enable using different providers on a per page basis.

Note: A common practice is to use the defaultProvider property in the Web.config to set the provider for the whole application. Using SetServerSideViewStateProvider could get messy fast, and is not a best practice in my opinion.

Below is the proper code for the overloaded page. The code in the example will differ slightly to show different implementations on a per page basis.

Protected Overloads Overrides Function LoadPageStateFromPersistenceMedium() As Object
    If _serverSideViewState AndAlso ViewStateManager.Enabled Then
        Return ViewStateManager.LoadPageState(Me)
    Else
        Return MyBase.LoadPageStateFromPersistenceMedium
        ' regular client viewState
    End If
End Function

Protected Overloads Overrides Sub SavePageStateToPersistenceMedium(ByVal viewState As Object)
    If _serverSideViewState AndAlso ViewStateManager.Enabled Then
        ViewStateManager.SavePageState(Me, viewState)
    Else
        MyBase.SavePageStateToPersistenceMedium(viewState)
        ' regular client viewState
    End If
End Sub

Anthem.Net integration

Anthem.Net is, in my opinion, the best AJAX library out to date. And, it would be a shame if it couldn't be used in conjunction with the CustomViewState implementation. To read more on Anthem.Net, please see: Introduction to Anthem.NET.

Note: This solution will always use the default viewstate provider set in web.config. Setting the provider on a per page basis using the SetServerSideViewStateProvider property will be ignored.

Because Anthem is managing outputting the ViewState to the page separately from ASP.NET, we need to make Anthem write our hidden variable with the ViewStateKeyName to the page if it exists. Retrieval and loading of viewstate will be handled by ASP.NET as the normal page lifecycle is started by Anthem during a callback.

To integrate ViewStateManager, we need to make the following modifications in the Anthem project:

  • Add CustomProviders.dll as a reference to the Anthem project in order to get access to ViewStateManager.
  • Add the following function in Manager.cs to retrieve the viewstate key from the page's markup after the callback.
  • private string GetServerSideViewState(string html)
    {
        if (ViewStateManager.Enabled) {
            return GetHiddenInputValue(html, "<input type=\"hidden\" name=\"" +
            ViewStateManager.Provider.ViewStateKeyName + "\" id=\"" +
            ViewStateManager.Provider.ViewStateKeyName + "\" value=\"");
        }
        else {
            return null;
        }
    }
  • Create an overloaded WriteValueAndError function in Manager.cs, which will add values including an updated serverSideViewState variable to the response string to be written back to the page.
  • /// <summary>
    /// Adds ServerSideViewState support
    /// </summary>
    private static void WriteValueAndError(
    	StringBuilder sb,
    	object val,
    	string error,
    	string viewState,
    	string viewStateEncrypted,
    	string serverSideViewState,
    	string serverSideViewStateKey,
    	string eventValidation,
    	Hashtable controls,
    	string[] scripts)
    {
        sb.Append("{\"value\":");
        WriteValue(sb, val);
        sb.Append(",\"error\":");
        WriteValue(sb, error);
        if (viewState != null)
        {
            sb.Append(",\"viewState\":");
            WriteValue(sb, viewState);
        }
        if (viewStateEncrypted != null)
        {
            sb.Append(",\"viewStateEncrypted\":");
            WriteValue(sb, viewStateEncrypted);
        }
        if (serverSideViewState != null)
        {
            sb.Append(",\"serverSideViewState\":");
            WriteValue(sb, serverSideViewState);
        }
        if (serverSideViewStateKey != null)
        {
            sb.Append(",\"serverSideViewStateKey\":");
            WriteValue(sb, serverSideViewStateKey);
        }
        if (eventValidation != null)
        {
            sb.Append(",\"eventValidation\":");
            WriteValue(sb, eventValidation);
        }
        if (controls != null && controls.Count > 0)
        {
            sb.Append(",\"controls\":{");
            foreach (DictionaryEntry control in controls)
            {
                sb.Append("\"" + control.Key + "\":");
                WriteValue(sb, control.Value);
                sb.Append(",");
            }
            --sb.Length;
            sb.Append("}");
        }
        if (scripts != null && scripts.Length > 0)
        {
            sb.Append(",\"pagescript\":[");
            foreach (string script in scripts)
            {
                WriteValue(sb, script);
                sb.Append(",");
            }
            --sb.Length;
            sb.Append("]");
        }
        if (GetManager()._clientSideEvalScripts.Count > 0)
        {
            sb.Append(",\"script\":[");
            foreach (string script in GetManager()._clientSideEvalScripts)
            {
                WriteValue(sb, script);
                sb.Append(",");
            }
            --sb.Length;
            sb.Append("]");
        }
        sb.Append("}");
    }
  • Next, we need to modify the WriteResult(Stream stream, MemoryStream htmlBuffer) function in Manager.cs to integrate CustomViewState. This function sends the result string back to the page. Modifications are shown in bold:
  • internal void WriteResult(Stream stream, MemoryStream htmlBuffer)
    {
        string viewState = null;
        string serverSideViewState = null;
        string viewStateEncrypted = null;
        string eventValidation = null;
        Hashtable controls = null;
        string[] scripts = null;
        if (_updatePage)
        {
            string html = 
              HttpContext.Current.Response.ContentEncoding.GetString(
              htmlBuffer.GetBuffer());
            viewState = GetViewState(html);
            serverSideViewState = GetServerSideViewState(html);
    #if V2
            viewStateEncrypted = GetViewStateEncrypted(html);
            eventValidation = GetEventValidation(html);
    #endif
            controls = GetControls(html);
            foreach (object o in _targets.Values)
            {
                Control c = o as Control;
                if (c != null && !c.Visible)
                {
                    if (c.ID != null && controls.ContainsKey(c.ID))
                        controls[c.ID] = "";
                }
            }
    
            scripts = GetScripts(html);
        }
        StringBuilder sb = new StringBuilder();
        try
        {
            // If the serverSideViewState is null,
            // that means CustomViewState is turned off on the page,
            // and thus will be ignored inside WriteValueAndError.
            // But we will double check for it here anyways.
            if (ViewStateManager.Enabled && serverSideViewState != null) {
                WriteValueAndError(sb, _value, _error, viewState, viewStateEncrypted,
                    serverSideViewState, ViewStateManager.Provider.ViewStateKeyName, 
                    eventValidation, controls, scripts);
            }
            else {
                WriteValueAndError(sb, _value, _error, viewState, viewStateEncrypted, 
                                   eventValidation, controls, scripts);
            }
        }
        catch (Exception ex)
        {
            // If an exception was thrown while formatting the
            // result value, we need to discard whatever was
            // written and start over with nothing but the error
            // message.
            sb.Length = 0;
            WriteValueAndError(sb, null, ex.Message, null, null, null, null, null);
        }
    
        // If an IOFrame was used to make this callback,
        // then wrap the response in a <textarea> element
        // so the iframe will not mess with the text of the JSON object.
        string response = sb.ToString();
        if (string.Compare(HttpContext.Current.Request["Anthem_IOFrame"], "true", true) == 0)
        {
            response = "<textarea id=\"response\">" + 
                       response + "</textarea>";
        }
    
        byte[] buffer = HttpContext.Current.Response.ContentEncoding.GetBytes(response);
        stream.Write(buffer, 0, buffer.Length);
    }
  • Finally, we need to modify Anthem.js to update the serverSideViewState hidden input value on a page. Modifications are shown in bold:
  • function Anthem_UpdatePage(result) {
        var form = Anthem_GetForm();
        if (result.viewState) {
            Anthem_SetHiddenInputValue(form, "__VIEWSTATE", result.viewState);
        }
        
        if (result.serverSideViewState && result.serverSideViewStateKey) {
            Anthem_SetHiddenInputValue(form, result.serverSideViewStateKey, 
                                       result.serverSideViewState);
        }
        if (result.viewStateEncrypted) {
            Anthem_SetHiddenInputValue(form, "__VIEWSTATEENCRYPTED", 
                                       result.viewStateEncrypted);
        }
        if (result.eventValidation) {
            Anthem_SetHiddenInputValue(form, "__EVENTVALIDATION", 
                                       result.eventValidation);
        }
        if (result.controls) {
            for (var controlID in result.controls) {
                var containerID = "Anthem_" + 
                    controlID.split("$").join("_") + "__";
                var control = document.getElementById(containerID);
                if (control) {
                    control.innerHTML = result.controls[controlID];
                    if (result.controls[controlID] == "") {
                        control.style.display = "none";
                    } else {
                        control.style.display = "";
                    }
                }
            }
        }
        if (result.pagescript) {
            Anthem_LoadPageScript(result, 0);
        }
    }

That's it. The server-side viewstate makes Anthem noticeably more responsive. Check it out for yourself!

Points of interest

This code has been written but, unfortunately, never been tested in a real life application. Very interested to see SqlViewStateProvider and other providers get used in a large scale application with a high usage. Any ideas or other provider (better) implementations? All feedback is most welcome.

History

  • June 5, 2007
    • Removed the ViewStateProvidersConfig class.
    • Added the Enabled property to the Viewstate config section.
    • Added Anthem integration, and a sample page using the Anthem datagrid.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Oleg Sobol
Web Developer
United States United States
Oleg Sobol has been developing web applications in ASP.NET using VB.NET for the past three years. Currently working for a leading online higher education provider, developing new online initiatives and supporting multiple education platforms.

Comments and Discussions

 
Questionnice article PinmemberMember 108861212-Jan-10 20:30 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140916.1 | Last Updated 5 Jun 2007
Article Copyright 2007 by Oleg Sobol
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid