My thoughts…

A mix of many topics.

UpdatePanel + Back/Forward Browser Navigation

Posted by Rebecca Chernoff on May 11, 2008

Incorporating AJAX into a website is something that web developers have latched onto.  Certainly it can provide a better user experience, but that can come at the loss of features the end-user is familiar with.  This past week, I encountered this very scenario.  The details are pretty basic:  On my page I have a search bar at the top that allows the user to specify a couple of search parameters.  These parameters get fed into a select sql query, and the results are shown in a GridView.  The GridView has paging and sorting enabled, and in the previous classic asp version, the users have become accustomed to using the browser navigation buttons to move between pages and searches viewed.  The GridView is within an UpdatePanel that is triggered by the Button in the search panel.

If you have ever done any work with UpdatePanels, you will realize where I am going with this (or you could just read the title of the post…).  Once the GridView is in the UpdatePanel, the browser doesn’t see it as a traditional postback and no entry is added to the browser’s history stack.  I can imagine how many phone calls I would receive:  “I was on page 5 of the results, and when I hit the back button, it took me back to the home page…”  Not something I want to have to deal with.  Surely there must be a way to use UpdatePanels and still use the browser’s history stack.

ASP.NET 3.5 Extensions Preview

There are a few different pieces to the 3.5 Extensions Preview.  One of these pieces provides a new System.Web.Extensions.dll, version 3.6.0.0 that gives the ScriptManager the ability to manage history for us.  The installer for the Extensions Preview will place the new versions of the included dlls into the GAC.  Once this is done, everything is ready to take advantage of this new feature in just a few short steps.

  1. Create a new ASP.NET 3.5 Extensions Web Site.  If you want to integrate the navigation functionality into an existing website, you will need to modify the web.config file to reference the 3.6.0.0 dlls.  Create a new Extensions Web Site and use that to update your existing website. 
  2. Set the EnableHistory property on the ScriptManager to True.
  3. Add history entries to the ScriptManager when the page state should be saved.
  4. Handle the Navigate event on the ScriptManager to load the page to the previous state.

That’s it – simple!

Configuring the ScriptManager

There are two properties of interest here.  ScriptManager.EnableHistory should clearly be set to True.  The other property of interest is ScriptManager.EnableStateHash.  When this property is set to False, the state will be stored as plaintext at the end of the url (after any querystring parameters).  Setting EnableStateHash will hash the state to protect the data and prevent tampering.

Adding History Entries

In the scenario mentioned above, I want to save the state of the page whenever a column is sorted or the user selects a new page index.  In the Paged and Sorted event handlers for the GridView I will call my AddHistoryPoint helper function.  The code is pretty simple, so I will let the code speak for itself.  Last post was in C#, so this one is in VB.NET.

Protected Sub AddHistoryEntry()
    Dim sm As ScriptManager = ScriptManager.GetCurrent(Me.Page)
    If sm.IsInAsyncPostBack AndAlso Not sm.IsNavigating Then
        Dim state As New NameValueCollection()
        state("field1") = field1.Text.Trim()
        state.Add("sort", gvResults.SortString)
        state.Add("page", gvResults.CurrentPage)
        ScriptManager.GetCurrent(Me.Page).AddHistoryPoint(state, "Custom Title")
    End If
End Sub

A couple of things should be called to attention here.  First, notice that the state is only stored if two conditions are met.  The first is to avoid duplicates.  If the page is not in an async postback, that means it is in a regular postback and that the browser will already have an entry in the history stack.  The second is to ensure that an infinite loop doesn’t start.  When a saved state is requested, it must navigate to that state.  If that involves changing the page index, that means that the Paging event will be fired.  As this is when the new state is stored, an infinite loop would eventually result in a stack overflow.  Once those two conditions are met, the new state can be saved.  If you are just saving a single field, you do not need to

Navigating to a Saved State

When the back or forward button is used in the browser, the ScriptManager will pick up that action and fire the Navigate event.

Protected Sub ScriptManager1_Navigate(ByVal sender As Object, ByVal e As System.Web.UI.HistoryEventArgs)
    field1.Text = e.State("field1")
    gvResults.SortString = e.State("sort")
    gvResults.CurrentPage = e.State("page")
 
    ' Check if this is a blank entry form
    If Not e.State.HasKeys() Then
        pnlResults.Visible = False
        upResults.Update()
    Else
        BindData(sender, e)
    End If
 
    upSearch.Update()
 
End Sub
 

So once again, this is pretty simple code.  The first 3 lines restore the state of the parameters of my page.  I need to update the textbox to reflect the data used in the GridView.  If the state is empty, i.e., if the initial page load is requested again, I hide the results as they are originally.  If there is a state to bind, then I bind the data.  My UpdatePanel is set to UpdateMode=”Conditional”, so updating that is the last piece of the puzzle.

 

That wraps up the post, thanks for reading! 😛

Posted in ASP.NET, ASP.NET AJAX | 12 Comments »

Using a Primary Key with ASP.NET Data Controls

Posted by Rebecca Chernoff on April 27, 2008

It is common in ASP.NET to be working with data.  Using data on the page immediately brings up several concerns to be aware of.  One issue many people are aware of is how to have access to the data that you, as the developer, need – without letting the user have access to the data.  The main example of this is the primary key of your data.  If there is a GridView displaying a list of users for a community site, the users should not be able to see the primary key for anyone.  However, if a row is edited, having the primary key of that user is vital.  Having access to the primary key is extremely important in this scenario.  There are a couple different ways to have access to fields in code-behind that are not visible to the user.  These methods are the topic for this article.

Let’s just get straight to the code now, shall we?  If you want to follow along, create a new website in Visual Studio 2008.  Then, added an xml file to the App_Data folder called Holidays.xml.  This xml contains all the federal holidays that the U.S. Government recognizes for the year 2009.

<?xml version="1.0" encoding="utf-8" ?>
<holidays>
	<holiday>
		<id>1</id>
		<name>New Year's Day</name>
		<date>01/01/2009</date>
	</holiday>
	<holiday>
		<id>2</id>
		<name>Martin Luther King, Jr. Day</name>
		<date>01/19/2009</date>
	</holiday>
	<holiday>
		<id>3</id>
		<name>Washington's Birthday</name>
		<date>02/16/2009</date>
	</holiday>
	<holiday>
		<id>4</id>
		<name>Good Friday</name>
		<date>04/20/2009</date>
	</holiday>
	<holiday>
		<id>5</id>
		<name>Memorial Day</name>
		<date>05/25/2009</date>
	</holiday>
	<holiday>
		<id>6</id>
		<name>Independance Day (observed)</name>
		<date>07/03/2009</date>
	</holiday>
	<holiday>
		<id>7</id>
		<name>Labor Day</name>
		<date>09/07/2009</date>
	</holiday>
	<holiday>
		<id>8</id>
		<name>Thanksgiving Day</name>
		<date>11/26/2009</date>
	</holiday>
	<holiday>
		<id>9</id>
		<name>Christmas</name>
		<date>12/25/2009</date>
	</holiday>
</holidays>

In order to display the holidays, I am using a very basic GridView. I am not using a DataSource control, rather I’ve chosen to bind the GridView to a collection in code-behind.  I’ve also set up a label to display information about the currently selected row.

<asp:Label runat="server" ID="lblSelected"
    Text="Please select a holiday." />

<asp:GridView runat="server" ID="gvHolidays"
    AutoGenerateColumns="False"
    AutoGenerateSelectButton="True"
    OnSelectedIndexChanging="gvHolidays_SelectedIndexChanging">
    <Columns>
        <asp:BoundField HeaderText="Name"
            DataField="Name" SortExpression="Name" />
        <asp:BoundField HeaderText="Day"
            DataField="Day" SortExpression="Day" />
        <asp:BoundField DataField="Date" HeaderText="Date"
            SortExpression="Date" />
    </Columns>
</asp:GridView>

In the code-behind, I pull the holidays out of the xml file and stuff them into the GridView.  I am using a couple of new features in .net 3.5.  If you don’t understand the syntax, don’t worry.  Trust me that the holidays from my xml file are being bound to the grid.  You can bind the grid however you want.

One important piece here is that my holidays object does include the Id field.  There is no column for the Id field in the GridView, because as the topic of this article dictates, the user cannot have access to the primary key data. For now though, this is just the setup. The good stuff comes in a minute.

protected void Page_Load(object sender, EventArgs e) {
    gvHolidays.SelectedIndexChanging += new GridViewSelectEventHandler(gvHolidays_SelectedIndexChanging);
    if (!IsPostBack) {
        XDocument xDoc = XDocument.Load(HttpContext.Current.Server.MapPath("~/App_Data/Holidays.xml"));
        var holidays = from h in xDoc.Descendants("holiday")
                       orderby h.Element("date").Value
                       select new { Id = Int32.Parse(h.Element("id").Value), Name = h.Element("name").Value, Day = h.Element("day").Value, Date = DateTime.Parse(h.Element("date").Value) };
        gvHolidays.DataSource = holidays;
        gvHolidays.DataBind();
    }
}

void gvHolidays_SelectedIndexChanging(object sender, GridViewSelectEventArgs e) {
    int rowIndex = e.NewSelectedIndex;
    lblSelected.Text = "You selected RowIndex " + rowIndex;
}

At this point, we have a GridView that looks like the following:  When a row is selected, the GridViewSelectEventArgs provides us with the index of the row that was selected in the NewSelectedIndex property.

Basic GridView

This is all fine and dandy, but now that the border of the puzzle is put together, it is time to get to the mess that is the rest of the puzzle.  My DataSource includes the Id field, but this isn’t something that the user should have access to.  This field needs to be available server-side, but should not be readable on the client-side.  We have 3 options for accomplishing this.

Try #1:  BoundField with Visible=”False”

This option might be what you try first.  Don’t!  It won’t work.  When a BoundField is not going to be rendered (i.e., when Visible=”False”), the value does not make a round-trip between the client and the server.  This makes it impossible to get the value of our primary key from the server when we react to a Selecting, Updating, or Deleting event.

Try #2:  TemplateField with Visible=”False”

After hearing that a BoundField will not work, it might seem that a TemplateField won’t work either.  In fact, it will work.  A TemplateField is really just the controls you put inside the template.  This means that the controls themselves will handle the Visible property.  If a Label s put iinside a TemplateField, then it will provide the primary key.  There is one caveat I should mention for this method.  If you are adding multiple TemplateFields, while it won’t increase the size of your html markup, it will increase the size of your ViewState.

<asp:TemplateField Visible="false">
    <ItemTemplate>
        <asp:Label runat="server" ID="lblId"
            Text='<%# Eval("Id") %>' />
    </ItemTemplate>
</asp:TemplateField>

Once the TemplateField is in the GridView, pulling out the primary key is a trivial matter.

protected void gvHolidays_SelectedIndexChanging(object sender, GridViewSelectEventArgs e) {
    int rowIndex = e.NewSelectedIndex;
    int primaryKey = Int32.Parse(((Label)gvHolidays.Rows[rowIndex].FindControl("lblId")).Text);
    lblSelected.Text = "You selected RowIndex " + rowIndex + " which has a primary key of " + primaryKey;
}

Doing it this way gets a little messy because the return object from FindControl must be cast into a Label. Then, the Text of the Label must be parsed into an int. Not what I would call clean. There is an easier way…

Try #3:  Using DataKeyNames and DataKeys properties

I’d like to offer the final method, which is the one that I recommend.  The data controls in ASP.NET 2.0+ have a DataKeyNames property.  When this property is used, a DataKeys collection will be created that contains the values specified as a DataKeyName. Let’s update our GridView to use the DataKeyNames property.

<asp:GridView runat="server" ID="gvHolidays"
    AutoGenerateColumns="False"
    AutoGenerateSelectButton="True"
    OnSelectedIndexChanging="gvHolidays_SelectedIndexChanging"
    DataKeyNames="Id"
    <Columns>
        <asp:BoundField HeaderText="Name"
            DataField="Name" SortExpression="Name" />
        <asp:BoundField DataField="Date" HeaderText="Date"
            SortExpression="Date"
            DataFormatString="{0:D}" />
    </Columns>
</asp:GridView>

Notice that I have set DataKeyNames=”Id”. In my code-behind, the DataKeys collection will now exist.

protected void gvHolidays_SelectedIndexChanging(object sender, GridViewSelectEventArgs e) {
    int rowIndex = e.NewSelectedIndex;
    int primaryKey = (int)gvHolidays.DataKeys[rowIndex].Values["Id"];
    lblSelected.Text = "You selected RowIndex " + rowIndex + " which has a primary key of " + primaryKey;
}

Simple, right?! Since the DataKeyNames property only contains one field, it is ok to use gvHolidays.DataKeys[rowIndex].Value. It is good practice to not use this shortcut because if the code is changed later to put another field into the DataKeys collection, the existing code needs to be changed. Yes, multiple fields can be put into the DataKeyNames property – comma separate them. A caveat though: don’t put too many fields into the DataKeys collection because it will increase the weight of the page.

It is extremely important to recognize that both of these methods have issues you must be aware of if you are using Updating and Deleting in your GridView. Read this article!!! Really these issues only come into play if you have ViewState turned off. Even if you are not currently doing this, read this article. Maybe down the road when you are, you will remember this article. 😛

Posted in ASP.NET | 16 Comments »

Churned Ajax Toolkit

Posted by Rebecca Chernoff on April 24, 2008

It has been quite a while since I have had the time to work on a personal project of this (*crosses fingers*) magnitude.  I am excited to present my newest project:  the Churned Ajax Toolkit!  This toolkit will grow into a library of controls that serve as a supplement to the Ajax Control Toolkit.  The inclusion of controls will largely be driven by the community.  The initial release was posted yesterday so head on over to the project page and check it out!  A live demo is also available.

This initial release contains a single control:  TextEditExtender.  This control extends a textbox, but displays it as a label when the textbox does not have focus.  This in-place editing provides a more friendly user experience.  The control is customizable by setting various css properties.  You can view the documentation on the Discussions page at the project page.

The release contains 3 files:

  • ChurnedAjaxToolkit_1.0.0.0:  The source code, demo website, and the dll.
  • ChurnedAjaxToolkit_1.0.0.0_DLL:  The dll only.
  • ChurnedAjaxToolkit_1.0.0.0_DemoSite:  The demo website only.

Enjoy!

Posted in ChurnedAjaxToolkit | 1 Comment »