Advanced .NET: Storing ViewState in a database

Posted by Adrian Sat, 27 Jun 2009 21:06:00 GMT

There are several ways that you can store viewstate in a database (rather than in a hidden field on the page), but there are very few examples of how to actually do it on the web. Maybe that’s because there aren’t many genuine reasons for changing the default behaviour. You certainly should think twice before changing such a fundamental element of your web applications. This article shows you how to do it if you need to.

Note that this article was originally written in 2007, and has not been tested in recent versions of .NET. Please let me know how you get on using the comments form at the bottom of this page if you use it. Thanks.

There are several ways that you can store viewstate in a database (rather than in a hidden field on the page), but there are very few examples of how to actually do it on the web. Maybe that’s because there aren’t many genuine reasons for changing the default behaviour. You certainly should think twice before changing such a fundamental element of your web applications.

Before ASP.NET 2.0, there was really just one way of achieving what we are about to do; You had to overload two methods in the Page class – LoadPageStateFromPersistenceMedium and SavePageStateToPersistenceMedium. In these methods you would use the exotically named and largely undocumented LosFormatter. This way still works, and you can find examples on Google.

With ASP.NET 2.0 Microsoft introduced a new class, PageStatePersister. By extending this class, we can create our own ViewState persisters. Two such classes already ship – the HiddenFieldPageStatePersister (classic behaviour), and SessionPageStatePersister (stored the same data in Session).

We’re going to create a DatabasePageStatePersister that will store our serialized data in a SQL Server database.

You should be aware that the code I will give you here works, but it is thoroughly untested in the real world.

Our Database

You will need a database, naturally. In this database you will need the following objects.

You will need a table:

CREATE TABLE [dbo].[ViewStateStore]
(
    [guid] [char](128) NOT NULL,
    [viewstatedata] [text] NOT NULL
)

The table should have an index on the guid so that we don’t slow down too much as the table grows:

CREATE NONCLUSTERED INDEX [IDX_Main] ON [dbo].[ViewStateStore] 
(
    [guid] ASC
)

You will also need two stored procuedres:

CREATE PROCEDURE dbo.GetViewState 
(
@guid char(128)
) AS

SET NOCOUNT ON;

SELECT viewstatedata
    FROM ViewStateStore
        WHERE guid = @guid;
CREATE PROCEDURE dbo.StoreViewState 
(
@guid char(128),
@ViewStateData text
) AS

SET NOCOUNT ON;

IF (EXISTS(SELECT 1 FROM ViewStateStore WHERE guid = @guid)) BEGIN

    UPDATE ViewStateStore SET
        viewstatedata = @ViewStateData
        WHERE guid = @guid

END ELSE BEGIN

    INSERT INTO ViewStateStore (guid, viewstatedata)
        VALUES (@guid, @ViewStateData);

END

You must also make sure that you have a database login that can access the database and execute the two procedures.

The Code

We must create a class that extends PageStatePersister.

We will need to provide a constructor taking one parameter (the Page that is creating it), a Load() method and a Save() method. Load and Save are responsible for taking the StateBag and turning it in to a serialized string (and vice-versa).

Because each page’s viewstate is seperate, we must somehow tie the serialized string to the page. For Microsoft’s HiddenElement approach, it isn’t a problem, because the ViewState is intrinsicly tied to the page. For our model, we must ensure that each page has it’s own ViewState that doesn’t interfere with other pages that the user might open (for example, the user is looking at a page that opens a pop-up window—the pop-up window must not interfere with our stored viewstate for the original page).

My solution is to create a GUID in the Page class that is creating the Persister. We shall see this in a second. First, our persister class:

public class DatabasePageStatePersister : PageStatePersister
{
    string _GUID = "";

    public DatabasePageStatePersister(Page p, string GUID) : base(p)
    {
        _GUID = GUID;
    }

    public override void Load()
    {
        string ViewStateData = GetViewState(_GUID);

        this.ViewState = this.StateFormatter.Deserialize(ViewStateData);
    }

    public override void Save()
    {
        string ViewStateData = this.StateFormatter.Serialize(this.ViewState);

        StoreViewState(_GUID, ViewStateData);
    }

    // Database functions ...

}

Our constructor takes a Page and a guid (as a string). The base class constructor will make some associations for us, so we must be careful to call it.

Save() and Load() use the standard helper class StateFormatter to do the hardwork. All we need to do is get/set the values in the database.

Here’s the database code – it should go where the comment is in the extract above.

private SqlConnection _c = new SqlConnection("SERVER=.;DATABASE=ViewState;UID=ViewStateDemo;PWD=ViewStateDemo;");

private void StoreViewState(string guid, string data)
{
    _c.Open();

    try
    {
        SqlCommand cmd = new SqlCommand("StoreViewState", _c);
        cmd.CommandType = CommandType.StoredProcedure;

        cmd.Parameters.Add(new SqlParameter("guid", SqlDbType.Char, 128));
        cmd.Parameters["guid"].Value = guid;

        cmd.Parameters.Add(new SqlParameter("ViewStateData", SqlDbType.Text));
        cmd.Parameters["ViewStateData"].Value = data;

        cmd.ExecuteNonQuery();
    }
    finally
    {
        _c.Close();
    }
}

private string GetViewState(string guid)
{
    string Data = "";

    _c.Open();

    try
    {
        SqlCommand cmd = new SqlCommand("GetViewState", _c);
        cmd.CommandType = CommandType.StoredProcedure;

        cmd.Parameters.Add(new SqlParameter("guid", SqlDbType.Char, 128));
        cmd.Parameters["guid"].Value = guid;

        SqlDataReader r = cmd.ExecuteReader();

        if (r.Read())
        {
            Data = r["viewstatedata"].ToString();
        }
    }
    finally
    {
        _c.Close();
    }

    return Data;
}

Nothing exciting there.

Inside our Page

In the code behind for your page, we must wire up the Persister class that we just created by overloading the property PageStatePersister. Add this code to the body of your code-behind class:

private PageStatePersister _PageStatePersister;

protected override System.Web.UI.PageStatePersister PageStatePersister
{
    get
    {
        if (_PageStatePersister == null)
        {
            string guid = "";

            if (Request["__DATABASE_VIEWSTATE"] == null)
            {
                Guid g = Guid.NewGuid();
                guid = g.ToString();
            }
            else
            {
                guid = Request["__DATABASE_VIEWSTATE"].ToString();
            }

            _PageStatePersister = new DatabasePageStatePersister(this, guid);

            Literal l = new Literal();
            l.Text = "<div><input type=\"hidden\" name=\"__DATABASE_VIEWSTATE\" value=\"" + guid + "\" /></div>";
            this.Form.Controls.Add(l);
        }

        return _PageStatePersister;
    }
}

As you can see, we are changing the behaviour of the page so that the first time that anything tries to access the PageStatePersister variable we will jump in and create an instance of our own class. We also generate a GUID (always guaranteed to be unique) and store this in a hidden element at the end of our form. This is how we link the page request to the data that we stored in the database.

I strongly recommend that the code we created just now for your Page class be placed in its own class (that inherits from System.Web.UI.Page), and that your own webpages inherit from your new class.


About

We are a small British company that produces business-oriented software and solutions. These articles are a product of our daily work - information that we think might be useful to share. We hope you find them useful.

Our Software

These are some of our products. Several are open source, some are web-based and others are proprietary:

Categories

Archives

Syndicate

ml> ._trackPageview(); } catch(err) {} ml> l> pageTracker._trackPageview(); } catch(err) {} ml> ._trackPageview(); } catch(err) {} ml> l>