The FormView Challenge
Mastering the FormView
control has the benefit of leveraging one of Microsoft's most powerful controls to enhance the user experience. Yet, there is sparse information describing and illustrating how to code a well behaved interface that leverages the FormView
control. I have yet to find a book that effectively explains its use. Nor have I found adequate tutorials on the many sites that I have searched.
I sympathize with users expressing frustration at many International firms where I have worked. They are using enterprise applications and attempting to modify records that rely solely upon the GridView
control. Often, the user must edit records that contain over fifty columns. That means that they must first locate the record, use the slider to find the correct column, edit the field, and pan back to the left side of the GridView
to update or insert a new record. Alternatively, the users are required to use the master/detail pattern for the record updates. That pattern can become quite confusing quickly. When the FormView
is used in combination with the MultiView
control, the user has a much friendlier experience. Better yet, the developer will use AJAX to enhance the smoothness of the application, but that is beyond the scope of this article.
This article focuses on exposing the tips-n-tricks that must be understood by the developer to work with the form view control. The idea here is to guide the developer in a direction such that the pit falls and quirks of the control are avoided, and to provide a basic example of a reliable design pattern that can be leveraged in future projects.
Business Problem
Management has observed that the users are operating at half their expected productivity because they are editing and inserting records composed of over fifty columns in a grid view. The users have difficulty navigating the page to find the correct column, make the update, and then they must return back to the beginning of the row to search for another record. The solution is to click an edit button on the left hand column, have the focus to the first field in a FormView
, edit the record, and submit the change. The addition of a new record will follow a similar process.
Finally, it is assumed that a search function is already present that users can utilize to navigate to the next record. So, that will be omitted in this project because it is not required. Only two columns will be displayed in the example to limit the amount of source code necessary to demonstrate the design pattern.
Illustrating the Use Case
The user opens an application to view the club listings, and clicks on the Edit button to modify a record.
In response to the edit request, the system displays the FormView
in Edit mode with the record populated. Note that the grid view record is not in Edit mode. Also, there is no need to navigate away from the page and deal with the complexities introduced by cross page posting patterns.
The user submits the change, and the system closes the form. It then lists the records with the updates.
Next, the user must add a record. The user clicks the Add-record link and the system responds by opening the form in Insert mode. The user fills out the club information and clicks the Submit link.
When the record is submitted, the system responds by closing the form and adding the record to the list of clubs. Optionally, the system could keep the form open for heads down data entry. I chose to close the form to illustrate how to manage the control in the switch structure for the developer's reference. Additional details will follow below.
Tips and Tricks
- When designing the form, use templates. The "
ItemTemplate
", regardless of whether it is used in read only mode, will cause the VS2008 designer to show the format of the form. I suggest using the same format in the edit, update, and insert item templates. - Put a
switch
statement in the "ItemCommand
" handler. I suggest generally ignoring all of the other form view event handlers because they tend to introduce quirks into the system design. It is better to use a simple control structure than to attempt intercepting the other events. I have found that using the form view control's other events only introduces complexity in the design and irritating quirks. - Always use a link or image button to put the form view control into the Add-record mode. This handles the case where no record will be displayed when there is an empty record. Otherwise, there will be challenges encountered when starting with an empty form view. An empty form view is not displayed, and the control is not instantiated when there is no record. That means the links will not be present.
- The
FormView
control expects an object that is a collection or enumeration. Thus, using the following technique will not work:
FormView1.ChangeMode(FormViewMode.Edit);
TextBox tb = (TextBox) FormView1.FindControl("ClubName");
tb.Text = "Club Name";
Instead, put the form into Edit
mode and bind a collection like an array list or data table to the data source. Then call the DataBind
method as follows:
FormView1.ChangeMode(FormViewMode.Edit);
FormView1.DataSource = al;
FormView1.DataBind();
Key Design Concepts
Managing the FormView
is done by handing the ItemCommand
event. A switch
structure is used to control the mode of the FormView
control within the ItemCommand
handler. The FormView
control has three modes that include insert, edit, and read only. The FormView
's ChangeMode()
method is used to switch the mode of the control.
The class Club
is used to contain records that are updated and new records that are inserted into the table.
Club |
Instance Variables |
ClubID | An integer used to hold the unique club's identification. |
ClubName | The name of the club. |
URL | The URL of the organization. |
The class ClubList
is a helper class that substitutes for a database table in this example. It is straightforward, and the key items are listed in the following table:
ClubList |
Instance Variables |
ClubList cl | The class uses the singleton design pattern. |
DataTable _dt | A data table is used to contain the club records. |
Methods |
BuildClubList() | Builds and populates a default club list for binding to the grid view when the application starts. |
AddClub(Club c) | Adds a club to the data table. The Club object is used to add a new record. |
UpdateClubByID(Club c) | Accepts the Club object and updates the record in the data table. |
Create the ClubList
class that builds a table for creating default data, adding, and updating club records. The records go into a data table. The class uses the singleton design pattern.
Default Form (ASP.NET) |
Event Handlers |
Page_Load | When the page loads for the first time, get the club list and bind it to the grid view. |
FormView1_ItemCommand | Whenever a form view ItemCommand event occurs, the switch structure uses e.CommandName to determine what event took place. The FormView is changed into the appropriate mode and the databind() function is called so that the update will take place. |
AddNewClub_Click | Click on the Add New Club link to handle the event to switch the form into Insert mode. |
gvClubList_RowEditing | Handles the edit click event message from the grid view. Here, the club record is retrieved from the data table and displayed in the FormView control. The edit event for the grid view is cancelled to prevent the control from entering the edit record mode. |
Methods |
UpdateClub() | Update a club record in the data table. |
AddClub() | Add a new club to the data table. |
Coding the Application
Step 1. Create the Club
class as follows:
public class Club
{
public Club()
{
}
private int _ClubID;
public int ClubID{
get { return _ClubID; }
set { _ClubID = value; }
}
private string _ClubName;
public string ClubName
{
get { return _ClubName; }
set { _ClubName = value; }
}
private string _URL;
public string URL
{
get { return _URL; }
set { _URL = value; }
}
}
Step 2. Create the ClubList
class that builds a table for creating default data, adding, and updating club records. The records go into a data table. The class uses the singleton design pattern.
public sealed class ClubList
{
private static readonly ClubList cl = new ClubList();
private static DataTable _dt;
private ClubList() { }
static public DataTable dt
{
get { return _dt; }
set { _dt = value; }
}
static public DataTable BuildClubList()
{
DataColumn col;
DataColumn col2;
DataColumn col3;
DataRow row;
if (!(dt == null))
{
return dt;
}
dt = new DataTable("ClubList");
col = new DataColumn();
col.DataType = Type.GetType("System.Int32");
col.ColumnName = "ClubID";
col.AutoIncrement = true;
col.AutoIncrementSeed = 1000;
col.AutoIncrementStep = 10;
col.ReadOnly = true;
col.Unique = true;
dt.Columns.Add(col);
DataColumn[] PrimaryKeyColumns = new DataColumn[1];
PrimaryKeyColumns[0] = dt.Columns["ClubID"];
dt.PrimaryKey = PrimaryKeyColumns;
col2 = new DataColumn();
col2.DataType = Type.GetType("System.String");
col2.ColumnName = "ClubName";
col2.AutoIncrement = false;
col2.Caption = "Club Name";
col2.ReadOnly = false;
col2.Unique = false;
dt.Columns.Add(col2);
col3 = new DataColumn();
col3.DataType = Type.GetType("System.String");
col3.ColumnName = "URL";
col3.AutoIncrement = false;
col3.Caption = "URL";
col3.ReadOnly = false;
col3.Unique = false;
dt.Columns.Add(col3);
row = dt.NewRow();
row["ClubName"] = "Joe's Rocketry";
row["URL"] = "www.joes.com";
dt.Rows.Add(row);
row = dt.NewRow();
row["ClubName"] = "American Rocketry";
row["URL"] = "www.ar.com";
dt.Rows.Add(row);
row = dt.NewRow();
row["ClubName"] = "NSSR Rockets";
row["URL"] = "www.nssr.com";
dt.Rows.Add(row);
return dt;
}
public static DataTable AddClub(Club c)
{
DataRow row;
row = dt.NewRow();
row["ClubName"] = c.ClubName;
row["URL"] = c.URL;
dt.Rows.Add(row);
row = dt.NewRow();
return dt;
}
public static Club GetClubByID(int ClubID)
{
Club c = new Club();
DataRow row = dt.Rows.Find(ClubID);
c.ClubID = (int)row["ClubID"];
c.ClubName = row["ClubName"].ToString();
c.URL = row["URL"].ToString();
return c;
}
public static void UpdateClubByID(Club c)
{
DataRow row = dt.Rows.Find(c.ClubID);
row.BeginEdit();
row["ClubName"] = c.ClubName;
row["URL"] = c.URL;
dt.AcceptChanges();
}
}
Step 3. Create your standard default page. Add the form and grid view controls. I like to organize the page using divisions, selectors from a cascading style sheet, and tables. The style sheet is included in the coding sample. The sample also includes the images used in this article.
<div id="wrapper">
<form id="form1" runat="server">
<div id="header">
<asp:Image ID="Image1" runat="server"
ImageUrl="~/images/ClubAdmin.jpg" />
</div>
<div id="maincontent">
<asp:FormView ID="FormView1"
runat="server" DataKeyNames="ClubID"
OnItemCommand="FormView1_ItemCommand"
BorderStyle="Outset"
BorderWidth="2px" CssClass="FormView">
<ItemTemplate>
<table>
<tr>
<td class="FormViewHeader">
Club Name:
</td>
<td>
<asp:Label ID="ClubName" runat="server"
Text='<%# Bind("ClubName") %>'></asp:Label>
</td>
</tr>
<tr>
<td class="FormViewHeader">
URL:
</td>
<td>
<asp:Label
ID="URL" runat="server"
Text='<%# Bind("URL") %>'></asp:Label>
</td>
</tr>
</table>
<asp:LinkButton ID="LinkButton1"
runat="server" CommandName="EditInfo"
Text="Edit">Edit</asp:LinkButton>
<asp:LinkButton ID="LinkButton10" runat="server"
CommandName="InsertInfo"
Text="New">Insert</asp:LinkButton>
</ItemTemplate>
<PagerSettings Mode="NextPreviousFirstLast" />
<EditItemTemplate>
<table>
<tr>
<td class="FormViewHeader">
Club Name:
</td>
<td>
<asp:TextBox
ID="ClubName" runat="server"
Text='<%# Bind("ClubName") %>'></asp:TextBox>
</td>
</tr>
<tr>
<td class="FormViewHeader">
URL:
</td>
<td>
<asp:TextBox ID="URL"
runat="server"
Text='<%# Bind("URL") %>'></asp:TextBox>
</td>
</tr>
</table>
<asp:LinkButton ID="LinkButton2"
runat="server" CommandName="UpdateInfo"
Text="UpdateInfo">Update</asp:LinkButton>
<asp:LinkButton ID="LinkButton3"
runat="server" CommandName="CancelUpdate"
Text="CancelInfo">Cancel</asp:LinkButton>
</EditItemTemplate>
<InsertItemTemplate>
<table>
<tr>
<td class="FormViewHeader">
Club Name:
</td>
<td>
<asp:TextBox ID="ClubName" runat="server"
Text='<%# Bind("ClubName") %>'></asp:TextBox>
</td>
</tr>
<tr>
<td class="FormViewHeader">
URL:
</td>
<td>
<asp:TextBox ID="URL" runat="server"
Text='<%# Bind("URL") %>'></asp:TextBox>
</td>
</tr>
</table>
<asp:LinkButton ID="LinkButton20" runat="server"
CommandName="SubmitInfo"
Text="SubmitInfo">Commit</asp:LinkButton>
<asp:LinkButton ID="LinkButton30"
runat="server"
CommandName="CancelInsert"
Text="CanceInsert">Cancel</asp:LinkButton>
</InsertItemTemplate>
</asp:FormView>
<br />
<br />
<hr />
<asp:LinkButton ID="AddNewClub" runat="server"
OnClick="AddNewClub_Click"
CssClass="LinkButton">
Add New Club</asp:LinkButton>
<asp:GridView ID="gvClubList"
runat="server" AutoGenerateColumns="False"
DataKeyNames="ClubID"
OnRowEditing="gvClubList_RowEditing"
AlternatingRowStyle-CssClass="AlternatingRowStyle">
<Columns>
<asp:CommandField ShowEditButton="True" />
<asp:TemplateField InsertVisible="False">
<HeaderTemplate>
<table width="400px">
<thead>
<td class="TableHeader" style="width: 40%">
Club Name
</td>
<td class="TableHeader">
URL
</td>
</thead>
</table>
</HeaderTemplate>
<AlternatingItemTemplate>
<table width="400px">
<tr>
<td style="width: 40%">
<asp:Label
ID="ClubName" runat="server"
Text='<%# Bind("ClubName") %>'></asp:Label>
</td>
<td>
<asp:Label ID="URL"
runat="server" Text='<%# Bind("URL") %>'></asp:Label>
</td>
</tr>
</table>
</AlternatingItemTemplate>
<ItemTemplate>
<table width="400px">
<tr>
<td style="width: 40%">
<asp:Label ID="ClubName" runat="server"
Text='<%# Bind("ClubName") %>'></asp:Label>
</td>
<td>
<asp:Label ID="URL" runat="server"
Text='<%# Bind("URL") %>'></asp:Label>
</td>
</tr>
</table>
</ItemTemplate>
</asp:TemplateField>
</Columns>
</asp:GridView>
<br />
</div>
</form>
</div>
Step 4. Add the logic to the code-behind for the default page. As mentioned, the key handlers are the FormView
control's ItemCommand
and the GridView
's RowUpdating
command. Take special note of how the edit is cancelled for the GridView
, and the FormView
is set into Edit
mode and populated with the record. The LinkButton
control's Click
event is used to put the form into Insert mode when the user chooses to add a new club to the list.
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if (!Page.IsPostBack)
{
gvClubList.DataSource = ClubList.BuildClubList();
gvClubList.DataBind();
FormView1.ChangeMode(FormViewMode.ReadOnly);
}
}
protected void FormView1_ItemCommand(object sender, FormViewCommandEventArgs e)
{
switch (e.CommandName)
{
case "UpdateInfo":
UpdateClub();
FormView1.ChangeMode(FormViewMode.ReadOnly);
gvClubList.DataSource = ClubList.dt;
gvClubList.DataBind();
break;
case "SubmitInfo":
AddClub();
FormView1.ChangeMode(FormViewMode.ReadOnly);
gvClubList.DataSource = ClubList.dt;
gvClubList.DataBind();
break;
case "CancelUpdate":
FormView1.ChangeMode(FormViewMode.ReadOnly);
break;
case "CancelInsert":
FormView1.ChangeMode(FormViewMode.ReadOnly);
break;
default:
break;
}
FormView1.DataBind();
}
protected void UpdateClub()
{
TextBox tb;
Club c = new Club();
c.ClubID = Int32.Parse(FormView1.DataKey.Value.ToString());
tb = (TextBox)FormView1.FindControl("ClubName");
c.ClubName = tb.Text;
tb = (TextBox)FormView1.FindControl("URL");
c.URL = tb.Text;
ClubList.UpdateClubByID(c);
}
protected void AddClub()
{
TextBox tb;
Club c = new Club();
tb = (TextBox)FormView1.FindControl("ClubName");
c.ClubName = tb.Text;
tb = (TextBox)FormView1.FindControl("URL");
c.URL = tb.Text;
if (c.ClubName != String.Empty)
{
ClubList.AddClub(c);
}
}
protected void AddNewClub_Click(object sender, EventArgs e)
{
FormView1.ChangeMode(FormViewMode.Insert);
}
protected void gvClubList_RowEditing(object sender, GridViewEditEventArgs e)
{
int ClubID;
string ClubName;
string URL;
Label lb;
Club c;
ArrayList al = new ArrayList();
ClubID = Int32.Parse(gvClubList.DataKeys[e.NewEditIndex].Value.ToString());
lb = (Label)gvClubList.Rows[e.NewEditIndex].FindControl("ClubName");
ClubName = lb.Text;
lb = (Label)gvClubList.Rows[e.NewEditIndex].FindControl("URL");
URL = lb.Text;
e.Cancel = true;
c = ClubList.GetClubByID(ClubID);
al.Add(c);
FormView1.ChangeMode(FormViewMode.Edit);
FormView1.DataSource = al;
FormView1.DataBind();
}
}
Conclusion
The FormView
control has proven to be one of the most powerful and useful controls for performing inserts, deletes, and updates related to managing records. It is a challenge to find good information that explains how to avoid the quirks present in this control. Once understood, it is a simple control to implement in any application. I use it often. Clearly, the user experience and efficiency is enhanced when the developer is able to effectively implement the control. I encourage developers to learn and leverage the pattern illustrated in this article.