Building a Full-Featured Custom DataGrid Control
By Dimitrios Markatos | Published: 10 July 2005 | Reader Level: Intermediate
Download Source Code - 26KB
.NET, among its other amazing features, allows developers and programmers a rich platform with the capability to build custom controls deriving from already common base server controls, i.e. The Datagrid, the most commonly used and full-featured server control in .NET. One of the key components of .NET is OOP or Object-Oriented Programming, that focuses on creating reusable "black box" code enabling collective reuse throughout a website, significantly reducing development time when any changes have to be made affecting this one control. To gather a good grasp on this, take the time and read The Quick and Dirty .NET Guide to C#/VB Object-Oriented Programming.
In this article, we will examine and implement these very features and demonstrate how you can build your very own Datagrid control component, one that you will be able to customize, and more importantly reuse. Based on this ability, you will then end up with one powerful control that will have many implementations, from which you could learn about creating almost any other types of custom controls.
Now, some may question why would you attempt such a thing or why not simply use the common Datagrid? Well, good question! The quick and direct answer is reusability. Although requiring a lot more skill to create, the more all-inclusive answer would be custom controls promote lightly coded pages making for a lot less code to debug, offer greater flexibility, and more importantly, completely protected your code from anyone poking around.
Our example discussed here could easily be contained with the confines of a code-behind file, but this would usurp the latter aforementioned paradigms we have illustrated. With a custom control, you have a lot of power and control over what you render to the user or even restrict.
Having said that, this article will ultimately enable you to create a .NET page, incorporating a search form, with a pageable, and excitedly so a cacheable Datagrid custom control. Furthermore, learn various methods of interacting with your control, and learn how to assign and retrieve values from it as well!
Therefore, in this article we'll be learning how to:
- Create a .NET search page that will contain our custom control
- Learn how code-behind and compiling works
- Actually build our customizable Datagrid control class
- Demonstrate the ease of assigning and retrieving custom values to our control
- Enhance our control's behavior paging capabilities and the ability to displaying page and result numbers
- Demonstrate how to cache our Datagrid for added performance and scalability, all from within our control!
- Finally, show you how to enhance our Datagrid with four cool enhancements.
In turn, our result will be a page with no client side code or methods, only contain our code-behind reference and custom control DLL that we have created and compiled. After you finish this article, you will have amassed enough information in giving you the ability to create a custom server control class, and some great ideas to take what is here and expand even further. We are going to cover a lot of ground, but don't worry. In the end, you will end up having a number of solid techniques at your disposal for many various scenarios that may arise in the future.
Nevertheless, not to leave out any RAD VS.NET programmers, I have included this entire example as a C# VS.NET project solution, that includes the .aspx webform page containing our registered dll directive, and our C# code-behind that contains our custom datagrid methods that provide our UI page's interaction with our custom Datagrid control, and with the functionality we've set up when searching.
So with that said, let's get to it then.
Just so we know our objective, I will quickly illustrate what we expect our page results to look like once we are done. To peer ahead, after reading this article, you will be able to create a search form page utilizing a custom datagrid control that includes paging, results display, and most importantly caching, that will look like this:

All from a Datagrid server control tag that looks like this:
<DMGrid:myCustDG ID="MyDataGrid" runat="server" autoCols="true" bgClr="Red" frClr="White" />
Pretty awesome! Incidentally, I had written an article covering data caching, and demonstrated a Cached Datagrid utilizing almost identical techniques discussed here. Be sure to read .NET Data Caching, Custom ASP.NET Datagrid Paging With Exact Count and Drilldown Datagrid Searching with ASP.NET for all the methods and techniques discussed here.
Therefore, before reading this article, take the time to download the source code, compile it and see it in action. See the results, then do a few custom modifications, recompile it, then view the result of your own modifications. By doing so, you should have no problem grasping the advanced techniques discussed and presented here, or with the how in creating your own customizations.
The manner in which this example is demonstrated (as a full search page) is just the way I happen to do it. There's a lot going on here, which is the reason why I want you to have a run through the code, and get a good idea of what's going on. This way, our objective in examining how easy a custom datagrid control can be created, and how easy your own customizations can be applied, is accomplished. So with all this in mind, let's begin by looking at everything taking place, from the top.
Once you peer into our main .NET page, you'll immediately appreciate the awesome power of .NET's code behind technology by noticing that there's no visible client-side code! All you're seeing is a few standard server side controls and our main custom Datagrid control tag. All of the main code responsible for our application is hidden away in our code-behind code, implemented through our @Page and @Register directives.
Remember the old days of ASP? How many lines of code would your page be typically filled with if doing the same thing? Lots and lots, right? All jumbled together and messy. However in .NET, we instead have a total of 25 lines of code (which includes line breaks and stuff)! Now that's the amazing thing about .NET!
Now when our example is ran, it'll utilize JavaScript to do all sort of things, like focus the cursor in our main search box right on Page_Load. We also have some JavaScript attached to our search button, our reset button that clears our search criteria and our results, and finally a function that will always reset focus back to our main textbox after each search.
This type of JavaScript of course may seem cut and dry; well not quite. As we'll soon discover, that is simply is not quite the case, nor so cut and dry as I'll explain. So before we get into actually creating our control, we'll first examine some issues found with our textbox form field, and its interaction with our custom Datagrid control within .NET.
We all know that the process of submitting text from a textbox to a form is pretty straightforward. We type our text, then either hit return or press the submit button; this is the typical scenario. However, here lies the problem that I was just referring to, and really needs correcting. There is an issue with .NET's Textbox server control, that if the user decides to press the enter key from within the textbox containing the same criteria, the form will not postback (as of v1.1 of the .NET Framework, this does not seem to have been resolved)! Physically pressing the submit button is the only way around this. Moreover, the Textbox's OnTextChanged method or the AutoPostBack property can't help us either, unless again you modify the text in some way. The result being we lose our initial Result No. X to X stats; they simply reset to 0. Our previously fired event ceased to reinitialize!
At any rate, not be out done, with a little dash of JavaScript, our issue quickly becomes a non-issue. Bear in mind the code below is formatted to standard JS specifications. After my explanation of the code's function I'll show you how it is implemented in our code-behind scheme.
So, our first step to resolving this is to add an attribute to our Textbox control that'll trigger the JavaScript we need to call, once the user hits return. We do this on Page_Load, like so:
srchTxt.Attributes.Add ("onkeydown", "getButton();");
Here we use our Control's Attribute.Add method that programmatically inserts our JavaScript into the Textbox control's AttributeCollection object. After this has taken place, the above issue if replicated won't occur. The client-side JavaScript (compatible with both IE and Netscape) executes the getButton() function, which simply grabs the users keystrokes, and handles the event. In turn, calling the submitText() function once it detects the Enter Key was pressed, and clicks our form's Submit button for us. Here is the JavaScript:
<script language="JavaScript">
var isIE;
if (navigator.appName == "Microsoft Internet Explorer"){isIE = true;}
function getButton()
{
//Capture Netscape events
if(!isIE) {
document.captureEvents (Event.KEYDOWN);
}
document.onkeydown = submitText
}
function submitText(evt){
var theButtonPressed;
if (isIE){
theButtonPressed = window.event.keyCode;
}else{
theButtonPressed = evt.which;
}
if (theButtonPressed == 13) {
if(isIE){
with(event){
cancelBubble = true;
returnValue = false;
}
}
document.getElementById ("Search").click();
}
}
</script>
Now getting back to our JavaScript code's location, it is all hidden away in our datagrid.aspx.cs code-behind file. Implementing this is done through another magnificent .NET method known as RegisterClientScriptBlock. This method allows us to register client-side script blocks in our page from behind the scenes.
The JavaScript above was discussed in clearer detail simply to show what is being done. Adding this code to our code-behind file is done like so, similar to the bare-bones example listed below:
StringBuilder jsScript = new StringBuilder();
string nl = Environment.NewLine;
jsScript.Append ("<script language=JavaScript>" + nl);
jsScript.Append ("function Hello() { " + nl);
jsScript.Append (" document.write ('Hello'); " + nl);
jsScript.Append ("}" + nl);
jsScript.Append ("</script>" + nl);
RegisterClientScriptBlock("clientScript", jsScript.ToString());
//Close our StringBuilder Object
jsScript = null;
Above we utilized the StringBuilder class to concatenate and format our JavaScript code, and add a nl for our line breaks, using the Environment Class's NewLine member. We then call our RegisterClientScriptBlock method which will add our code after our page's <form runat= server> element, and finally close our StringBuilder object by setting it to null. Once you run the application and view the page's source code, our client-block code will look exactly like the standard JavaScript code we discussed.
Now returning back, one added note regarding the submitText() JavaScript function. IE's event bubbling has to further be disabled, alongside the value returned by our event, so it won't interfere with any other elements higher up on the page, nor allow multiple actions to execute either. Other server controls in our page are there to trigger events that'll interact with our custom Datagrid control. We notice that both the search and reset button's utilize the onClick method for their events, and needless to say, are completely immune to the aforementioned JS behavior. Without complaint, they submit the form and execute the Rebind_Grid method.
<asp:textbox id="srchTxt" runat="server" />
<asp:button text="Search" onClick="Rebind_Grid" runat="server" />
<asp:button text="Reset" onClick="Reset_Grid" runat="server" />
As an added note, our Rebind_Grid Method is also an important method in that it further demonstrates component-property assignment. This method is responsible for sending to our component our SQL query command and binding our datagrid by calling our custom control classes GetDataGrid() method. Finally, in our page we have our Stats Label server control that displays our Datagrid data/count statistics, and again all emanating from our complied control class. How all this occurs is going to be demonstrated later on.
<asp:label id="Stats" runat="server" />
But now that we've covered our initial .aspx page, and any known problems, we can now dig to our control's internal workings and everything pertaining to it.
As mentioned earlier, below we have our custom control tag responsible for our Datagrid:
<DMGrid:myCustDG ID="MyDataGrid" runat="server"
autoCols="true" bgClr="Red" frClr="White" />
Now, by looking at the above Datagrid control tag you'll certainly notice some unrecognizable and uncommon properties that do not come from a standard out-of-the-box Datagrid. Well, that's the amazing thing. By virtue of building your own custom control, you can produce the same data results as shown in the image above from something as insignificant looking as the aforementioned control tag. This simple example should easily inspire you to create even other types of controls as well.
Before we dive right into creating out control, keep in mind that it is not mandatory to include any properties at all. All it could really contain is "<DMGrid:myCustDG ID="MyDataGrid" runat="server" />," and you could still get the exact same results as shown before, as long as you pre-assigned these in your code. I thought I'd demonstrate assigning properties both from without and within.
Now where do we begin? Ah yes, the class. When beginning to undertake the task of creating a custom control, you need to keep it mind that this is an ordinary object-oriented class. A class is like an upscale function or method that encapsulates any number of properties and functions, all within a unique name or namespace.
The control's C# code residing in our source file is a typical public class with a constructor (the main function called when the newly created object gets initialized). The full-fledged control (assembly) we'll create inherits all the common features of a Datagrid control from its properties and methods to its event handling.
Custom class creation in .NET, as in our article, implements this skeletal layout, or three typical steps:
1) Namespace setup and import (optional)
2) Variable declarations
3) Class Construction
// we define our aliases representing any namespaces we'll be using
namespace CustDataGrid
{
// We import all the namespaces we'll require for our control
using System;
using System.Data;
// Create our main class and our inherited control
public class myCustDG : DataGrid
{ //Inherit all the features of a Datagrid base class
// Declare any variables here
private int _pageSz;
//... Our main constructor and all required methods are placed here
// Then close everything off
} // End Class
} //End Namespace
In summary, there is not much to it as far as creating a class in .NET. Ours will contain all the functionality we need, and within this you will learn how to take away from it or add to it to your liking, depending on what you would like to accomplish in particular.
Having said that, within this class component we end up doing the following:
- Set up the variables and properties that we'll be retrieving and assigning from our page, thus customizing our control
- Access the database and return our cached data results, circumventing any initial Datagrid limitations that are right out of the box and were mentioned in my previously listed articles.
- Implement our paging
- Write out our data statistics to our page control
All of our control's code resides in our C# class source file we named mycustDG.cs, that is listed at the end of this article. Here, as aforementioned, we will be defining our Datagrid's custom constructor, wherein we'll setup all our Datagrid properties, set up our events and bind our Datarid. In saving overburdening the reader from an exhaustive play by play of all the properties, I will explain all key concepts used and their context. Once you grasp the concepts all else will be quite easy to follow.
Recall back to our custom control server tag where you noticed some new custom properties. Let's veer into how we did this, and how we can create custom properties in our class and assign them values from our page.
Utilizing properties are similar to variables, but since properties have accessors, they specify the statements to execute in order to read or write their values. For example, in our Datagrid control tag I have a property pageSz. Of course, you can figure this out to be PageSize, which is indeed a standard datagrid base property. However, I simply created this custom name just so I can demonstrate how easy it is in doing so, and in how to assign properties to your control, from an external source. As in our initial layout example, we'll now add our properties to our public GetDataGrid() datagrid constructor method.
//We set our private internal variable
private int _pageSz;
//Our property that will retrieve the value from our external
//datagrid control tag and assign it to our datagrid method
public int pageSz
{
get { return _pageSz; }
set { _pageSz = value; }
}
//Our DataGrid Constructor
public void GetDataGrid()
{
PageSize = pageSz; //pulled from our property above
}
Here, the Set accessor is assigning the value to our _pageSz variable, and the Get accessor is returning it. Moreover, our pageSz being assigned from our page's custom server control, sets the value to our datagrid property.
Since our control tag property pageSz = "5", that is what our datagrid, when rendered, will display. The rest as you will see in the source code simply includes any properties you want to hard-coded or customize as I have in like manner.
Also within our main GetDataGrid method, we call our two event handlers that are in charge of our paging and the event handler that will enable us the detailed data statistics we want, like the page number, how many records found, etc, as we'll soon see.
Finally, an added note regarding our GetDataGrid() method, is that it's ability to retrieve data rest solely on another method. This other method further is responsible for binding our grid with cached data as well, and I will explain all this as we press on. So with all said let's start off light by dealing with the paging.
To Page or Not to Page
Regarding our paging, as you'll notice has not much to it. Within our GetDataGrid() method we call the PageChanged method to handle our paging.
PageIndexChanged += new DataGridPageChangedEventHandler (PageChanged);
The actual PageChanged method is the commonly used .NET method. It assigns the Datagrid's CurrentPageIndex to correspond with the event argument's NewPageIndex property, and then rebinds our Datagrid. So far so good, I hope. Next we'll examine how we gather our Datagrid's start and end count for each page of data.
You do the math
We mentioned earlier that within our main GetDataGrid() method, we have two event handlers. One for paging, that we just finished discussing, and two, the method that will allow us to figure out the start and end count for each Datagrid page we're on.
ItemCreated += new DataGridItemEventHandler (GridCreated);
ItemCreated simply means that every time the DataGrid control is created, when paging or sorting, whenever, our event handler above get 's called, and in turn calls our method below and we then display our, i.e. "Result No. 16 to 20." statistics.
private void GridCreated(Object sender, DataGridItemEventArgs e)
{
//Declare the ItemType
ListItemType elemType = e.Item.ItemType;
if (elemType == ListItemType.Item
|| elemType == ListItemType.AlternatingItem)
{
//A DataGrid Item represents an item (row) in the DataGrid control
//e.Item is the table row where the command is raised
DataGridItem dgRow = (DataGridItem) e.Item;
//Gets the index number of the DataGridItem
//object from the bound data source
int rowCntIncr = dgRow.DataSetIndex + 1;
//Get the start and end index count from datagrid
StartCnt = rowCntIncr-dgRow.ItemIndex;
EndCnt = rowCntIncr;
}
}
The ListItemType is what represents the different data-bound indexed items found in our Datagrid. What we essentially do is check for and references every row created and get a sequential count. The variables responsible for pulling this data are StartCnt and EndCnt. All this corresponds to all this data displayed to the user with our onPreRender method.
Now onto the piece d' resistance, our data caching. Within our datagrid class, we include our method GetDataGrid() which set's up all we've discussed - our grid's properties, event handlers, etc. Now as I mentioned before, I nevertheless, in this example, assigned my Datagrid's datasource to another method.
As shown, within our method below we create our connections, get our Dataset and return our results so our DataSource may be bound.
using System.Web.SessionState; // For HttpSessionState
// Our main constructor
public void GetDataGrid() {
...
//Gets bound from DataSet method below
DataSource = GridDataCache();
...
}
Now the GridDataCache method once called will return the data to which our DataSource gets assigned, and as we'll now explore can be either standard data or cached data.
private DataSet GridCachedData()
{
// Data Caching - Session API
DataSet dgCache = (DataSet) HttpContext.Current.Session ["dgCache"];
// Data Caching - Web Cache API
//DataSet dgCache = (DataSet) HttpContext.Current.Cache.Get ("dgCache");
//Check first to see if our cache already exists
if (dgCache == null)
{
// Get data from database and create Dataset
// ...
// Insert Dataset into Session Object
HttpContext.Current.Session.Add ("dgCache" ,objDS);
// Alternatively, insert Dataset into Data Cache
//HttpContext.Current.Cache.Insert ("dgCache",
// objDS, null, DateTime.Now.AddMinutes(10), TimeSpan.Zero);
return objDS;
}
else
{
//Since cache now exists, return cached Dataset
return dgCache;
}
}
OK, let's examine what's taking place here. Aside from commonplace database opening and connecting, we utilize data caching that we've set up accordingly. These techniques are straightforward and I would advise you reading both Drilldown Datagrid Searching with ASP.NET and .NET Data Caching for an exact methodology to what we're doing here. However, since we are implementing caching within our component, the common caching methods when working with the web cache API discussed in my articles won't work in this case. Why? When implementing caching inside of a component, by its very nature, you'll need the find the cache object through the current HTTP request via the HttpContext.Cache Class property. The common System.Web.Caching object cannot accommodate us in this fashion.
Notice how I list two ways of doing the same thing, one is utilizing the Session API and the other the Cache API. Both have their advantages and disadvantages, that were amply discussed in my Drilldown Datagrid article above. Therefore, after reading my prior articles, you'll find the techniques to be the same, save using HttpContext instead of what I demonstrated there, and the case for either caching method.
Moreover, also notice that with each newly created search, we always need to clear any prior cached data. Therefore, within our .NET page you'll notice that our server-side methods do this for us.
HttpContext.Current.Session.Remove ("dgCache");
In this instance, we remove any prior values kept in session state.
Note: Later on, you can test this new caching/paging implementation by commenting out the return dgCache, recompile it and then start paging. Hmm, no page 2. That's because there is no data. Another test is in our page I added a reset button that will remove this cache from memory and reset our Datagrid. Commenting this out, and re-searching will bring you back the previously cached results!
Well, keep this in mind as we'll get to compiling our class and importing it into our page. Before this, we'll touch on the last of our classes methods. This is a nice one, which is responsible for writing to our label control all our statistics, as we briefly noted at the beginning.
Displaying Our Results
Again, on our page we have all our pertinent web server controls, but only one of those is responsible for displaying our data statistics that emanate from within our DLL. So how does this all happen? We accomplish this in our component with a wonderful little method called OnPreRender, that sends to our Stats Label server control (that we use more or less as a placeholder, which it literally can be as well) - "Your search for X found X records / Results No. 16 to 20, Page 4 of 6, etc." But prior to sending our statistics to our page's server control, we 1) check to see it even exists (otherwise no stats will display) and 2) display our results only upon Page.IsPostback. Anyway here it is:
protected override void OnPreRender(EventArgs e)
{
Control StatsCtrl = Page.FindControl ("Stats");
if (StatsCtrl != null)
{
if (Page.IsPostBack)
{
if (RcdCount == 0)
{
StatsCtrl.Controls.Add (new LiteralControl(
"<font size=2><b>No Records Found</b></font>"));
Visible = false;
}
else
{
StatsCtrl.Controls.Add (new LiteralControl(
"<font size=2><b>Your search for <font color=red> " +
HttpContext.Current.Request["srchTxt"] +
" </font>found " + RcdCount + " records / " + PageCount +
" pages<BR>Results No. " + StartCnt + " to " + EndCnt +
" / Page " + (CurrentPageIndex+1) + " of " + PageCount +
"</b></font>"));
}
}
}
Now what's this again? Well, this method overrides the Control.OnPreRender method that typically occurs when the server control is about to render to the page, and before any viewstate content is saved. We use this opportunity within our Datagrid class to gather all our stats in one swell foop, and send them to our Stats server control on our main page. How is this done? We declare our Stats control as a Control (StatsCtrl) to which we use the FindControl method to locate it on our page, and add the new LiteralControl object to the located server control's Control.Controls property.
The e EventArgs parameter contains the event data handled by our method. Even still, when the overriding occurs, it writes out the datagrid paging statistics drawn from within the very class this method resides, to our Stats label server control before rendering any content to the user and before view state is enabled, consequentially paralleling our datagrid paging events. Additionally since we need to also know what criteria the user entered, we can obtain this information using HttpContext.Current.Request ["srchTxt"] which is the HttpRequest object for our current HTTP request.
Try this quick test to see the order you render your controls. Add trace="true" to your @Page Directive and in our OnPreRender method add - HttpContext.Current.Trace.Write ("Here I Am") and then recompile; and you'll see that this indeed overrides OnPreRender.
I hope I haven't lost you so far. Moving on, since we pull into our page values from our compiled class as demonstrated earlier, assigning values to your custom Datagrid control works in the same exact way. So to set the pageSz, for instance, to 10, on Page_Load perhaps, you can write - MyDataGrid.pageSz = 10; - and voila, the datagrid will display 10 records per page.
Incidentally, within your page, you would access the datagrid using its ID. However, since your compiled class is a Datagrid control itself, any values assigned internally, all that is needed is the the datagrid property alone.
Till now, most of the key elements in our DLL have been grappled with. Now we finalize our application by compiling it and then importing it into our page thus rounding off everything full circle.
Anytime you create a class of any kind that requires implementation within your page it first needs to be compiled, and placed into your applications bin, or wwwroot\inetpub\bin folder. Since our classes language is C#, we use the .NET C# compiler from a command line like so, or typed into a .bat batch file (which is included in this article's source code as makedg.bat):
csc /t:library /out:bin\mycustDG.dll mycustDG.cs /optimize
This tells our compiler that we are going to create a DLL named mycustdg using mycustDG.cs as the source file. /t imports all pertinent namespaces used in our control and /optimize optimizes our DLL for more efficiency.
Note: By default, .NET v1.1 may not have added the compiler's path to your environment variables. Therefore running this command line utility may not work as listed. To remedy this, add "%SystemRoot%\Microsoft.NET\Framework\v1.1.4322" to your System's Path Environment Variables, via Control Panel > System > Advanced > Environment Variables > System Variables > Path. Edit this variable, and append the aforementioned value to it. This way you could now execute any of the compilers anywhere without having to specify any full paths.
Now, once we've compiled our assembly, we import it at the top of our page using the @Register directive, like so, in conjunction to our custom Datagrid control tag above:
<%@ Register TagPrefix="DMGrid" Namespace="CustDataGrid" Assembly="myCustDG" %>
and when we setup our custom Datagrid we get the results that were shown in the image at the beginning of this article, and that's it!
As for VS.NET users, I have included the entire example as a VS.NET project in C# for download. All you need to do is create a new C# Web Application Project, compile the C# source file as a dll as mentioned, and reference it into to your project solution via Add New Reference.
To make it even easier for you, simply download the VS.NET Project code, as it contains everything I just mentioned. Now extract it into a folder named CustomDatagrid within wwwroot/inetpub, and simply double click the CustomDatagrid.sln file and voila.
The complete C# standard .NET page code:
<%@ Page language="C#" Inherits="CustDG.WebForm" Src="datagrid.aspx.cs"
Debug="False" Explicit="True" Strict="True" Buffer="True"
Trace="False" EnableSessionState="True" %>
<%@ Register TagPrefix="DMGrid" Namespace="CustDataGrid" Assembly="myCustDG" %>
<HTML>
<BODY onLoad="txtFocus();">
<BR>
<h3>Cached Custom Datagrid Control Class</h3>
<form runat="server">
<br>
<asp:textbox id="srchTxt" runat="server" />
<asp:button text="Search" id="Search"
OnTextChanged="Rebind_Grid" AutoPostBack="True" runat="server" />
<asp:button text="Reset" name="Reset"
onClick="Reset_Grid" runat="server" />
<BR><BR>
<asp:label id="Stats" runat="server" />
<BR><BR>
<DMGrid:myCustDG ID="MyDataGrid" runat="server"
autoCols="true" bgClr="Red" frClr="White" />
</form>
</BODY>
</HTML>
And for all you VS.NET programmers, here is the VS C#.NET Project Solution - the .aspx page
<%@ Page language="C#" Inherits="CustDG.WebForm" Codebehind="datagrid.aspx.cs"
Debug="False" Explicit="True" Strict="True" Buffer="True"
Trace="False" EnableSessionState="True" %>
<%@ Register TagPrefix="DMGrid" Namespace="CustDataGrid" Assembly="myCustDG" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<HTML>
<HEAD>
<title>Custom Datagrid Control</title>
<%-- datagrid.aspx.cs contains our methods that interact with and
control our Datagrid's functionality on this page --%>
<meta content="Microsoft Visual Studio .NET 7.1" name="GENERATOR">
<meta content="C#" name="CODE_LANGUAGE">
<meta content="JavaScript" name="vs_defaultClientScript">
</HEAD>
<body onload="txtFocus();" MS_POSITIONING="GridLayout">
<BR>
<h3>Cached Custom Datagrid Control Class</h3>
<form id="Form1" method="post" runat="server">
<br>
<asp:textbox id="srchTxt" runat="server" />
<asp:button id="Search"
onclick="Rebind_Grid" runat="server" text="Search" />
<asp:button id="Reset"
onclick="Reset_Grid" runat="server" text="Reset" />
<BR><BR>
<asp:label id="Stats" runat="server" />
<BR><BR>
<DMGrid:myCustDG ID="MyDataGrid" runat="server"
autoCols="true" bgClr="Red" frClr="White" />
</form>
</body>
</HTML>
Below now I'll list the Code-Behind code applicable to both standard ASP.NET and Visual C#.NET applications. The only difference between the two is in the way each platform references the code-behind file. Both reference this file within their respective @Page Directives, however our standard C# page needs to reference this code with the "Src= file location" attribute, whereas VS.NET uses "Codebehind= file location" attribute.
//datagrid.aspx.cs Code-Behind Page
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
//Import mycustDG dll namespace so this code-behind page
//can access our custom Datagrid control on the main page.
using CustDataGrid;
namespace CustDG
{
public class WebForm : System.Web.UI.Page //Inherit Page Class
{
protected System.Web.UI.WebControls.TextBox srchTxt;
protected System.Web.UI.WebControls.Button Search;
protected System.Web.UI.WebControls.Button Button1;
protected System.Web.UI.WebControls.Label Stats;
// Declare myDatagrid as myCustDG from
// referenced dll to allow code-behind
// Datagrid interaction with main Page Datagrid control properties
public myCustDG MyDataGrid;
public void Page_Load (Object Sender, EventArgs E)
{
//Implement Client Side JavaScript code
StringBuilder jsScript = new StringBuilder();
string nl = Environment.NewLine;
jsScript.Append ("<script language=JavaScript>" + nl);
jsScript.Append ("<!--" + nl);
jsScript.Append ("var isIE; " + nl);
jsScript.Append ("if(navigator.appName == \"Microsoft ");
jsScript.Append ("Internet Explorer\"){isIE = true;} " + nl);
jsScript.Append (" " + nl);
jsScript.Append ("function getButton() " + nl);
jsScript.Append ("{ " + nl);
jsScript.Append (" //Capture Netscape events " + nl);
jsScript.Append (" if(!isIE) { " + nl);
jsScript.Append (" document.captureEvents(Event.KEYDOWN); " + nl);
jsScript.Append (" } " + nl);
jsScript.Append (" document.onkeydown = submitText" + nl);
jsScript.Append ("} \n" + nl);
jsScript.Append ("function submitText(evt){ \n" + nl);
jsScript.Append (" var theButtonPressed; \n" + nl);
jsScript.Append (" if (isIE){ " + nl);
jsScript.Append (" theButtonPressed = window.event.keyCode; " + nl);
jsScript.Append (" }else{ " + nl);
jsScript.Append (" theButtonPressed = evt.which; " + nl);
jsScript.Append (" } \n" + nl);
jsScript.Append (" if (theButtonPressed == 13) { \n" + nl);
jsScript.Append (" if(isIE){ " + nl);
jsScript.Append (" with(event){ " + nl);
jsScript.Append (" cancelBubble = true; " + nl);
jsScript.Append (" returnValue = false; " + nl);
jsScript.Append (" } " + nl);
jsScript.Append (" } \n" + nl);
jsScript.Append ("document.getElementById(\"Search\").click(); " + nl);
jsScript.Append (" } " + nl);
jsScript.Append ("} \n" + nl);
jsScript.Append ("function txtFocus(){ \n" + nl);
jsScript.Append (" with(document.getElementById(\"srchTxt\")){ " + nl);
jsScript.Append (" focus(); " + nl);
jsScript.Append (" select(); " + nl);
jsScript.Append (" } \n" + nl);
jsScript.Append ("} \n" + nl);
jsScript.Append ("//--> " + nl);
jsScript.Append ("</script>" + nl);
//Allows our .NET page to add client-side script blocks when
//page loads, instead of the conventional HTML JS tags.
RegisterClientScriptBlock("clientScript", jsScript.ToString());
//Close our StringBuilder Object
jsScript = null;
srchTxt.Attributes.Add("onkeydown", "getButton();");
Response.BufferOutput = true;
if (Page.IsPostBack)
{
//Set up our database connection string<
MyDataGrid.strConn=
"server=(local);uid=sa;pwd=;database=Northwind;";
MyDataGrid.Visible = true;
MyDataGrid.autoCols = true;
GridAction ();
}
//Response.Flush();
}
public void Rebind_Grid (Object Sender, EventArgs E)
{
//Remove session value to create new one before rebinding
HttpContext.Current.Session.Remove("dgCache");
//Remove cache to create new one before rebinding
//HttpContext.Current.Cache.Remove("dgCache");
Stats.Visible = true;
MyDataGrid.CurrentPageIndex = 0;
GridAction ();
}
public void GridAction ()
{
MyDataGrid.sqlQuery = "SELECT SupplierID As No, CompanyName, " +
"ContactName, Country FROM Suppliers where CompanyName like '%"
+ srchTxt.Text + "%' or ContactName like '%" + srchTxt.Text +
"%' or Country like '%" + srchTxt.Text +
"%' Order by CompanyName asc";
MyDataGrid.GetDataGrid(); //Rebind Grid
}
public void Reset_Grid (Object Sender, EventArgs E)
{
//Remove session value to create new one before rebinding
HttpContext.Current.Session.RemoveAll();
//Remove cache to create new one before rebinding
//HttpContext.Current.Cache.Remove("dgCache");
srchTxt.Text = "";
Stats.Visible = false;
MyDataGrid.CurrentPageIndex = 0;
MyDataGrid.Visible = false;
}
} // End Class
} //End Namespace
And the complete C# class source code for our compiled myCustDG dll used in our @Register Directive and referenced in our VS.NET project:
//C# - mycustDG.cs - Written by Dimitrios Markatos, 2003
using System.Reflection; //For assembly information attributes
[assembly: AssemblyTitle("Custom DataGrid Control")]
[assembly: AssemblyDescription(
"Pageable, Cacheable Datagrid Custom Control Class")]
[assembly: AssemblyCopyright("Dimitrios Markatos [dmarko1@aol.com] - 2003")]
namespace CustDataGrid
{
/// <summary>
/// Custom Datagrid Control Class
/// Author: Dimitrios Markatos - dmarko1@aol.com
/// Date: 5/22/2003
/// </summary>
using System;
using System.Data;
using System.Data.SqlClient;
using System.Diagnostics; //For Tracing
using System.Drawing; // For Datagrid Colors
using System.Web; // For HttpContext
using System.Web.SessionState; // For HttpSessionState
using System.Web.UI; //For Literal Controls
using System.Web.UI.WebControls; // For Datagrid
public class myCustDG : DataGrid
{ //Inherits the DataGrid Class
// Internal private variables
protected DataGrid MyDataGrid;
protected DataSet objDS, dgCache;
protected SqlConnection objConnect;
protected SqlDataAdapter objDataAdapter;
private bool _autoCols;
private string _strConn, _sqlQuery;
private Color _bgClr, _frClr;
private int RcdCount, StartCnt, EndCnt; //_pageSz,
int _bWidth = 0;
//Allow external access to variables
public string strConn
{
get { return _strConn; }
set { _strConn = value; }
}
public bool autoCols
{
get { return _autoCols; }
set { _autoCols = value; }
}
public string sqlQuery
{
get { return _sqlQuery; }
set { _sqlQuery = value; }
}
public int bWidth
{
get { return _bWidth; }
set { _bWidth = value; }
}
public Color bgClr
{
get { return _bgClr; }
set { _bgClr = value; }
}
public Color frClr
{
get { return _frClr; }
set { _frClr = value; }
}
protected override void OnPreRender(EventArgs e)
{
Control StatsCtrl = Page.FindControl("Stats");
if (StatsCtrl != null)
{
if (Page.IsPostBack)
{
if (RcdCount == 0)
{
StatsCtrl.Controls.Add(new LiteralControl(
"<font size=2><b>No Records Found</b></font>"));
Visible = false;
}
else
{
StatsCtrl.Controls.Add(new LiteralControl(
"<font size=2><b>Your search for <font color=red> "
+ HttpContext.Current.Request ["srchTxt"] +
" </font>found " + RcdCount + " records / " +
PageCount + " pages<BR>Results No. " + StartCnt +
" to " + EndCnt + " / Page " + (CurrentPageIndex+1)
+ " of " + PageCount + "</b></font>"));
Visible = true;
}
}
}
}
public void GetDataGrid()
{
PageSize = 5;
AutoGenerateColumns = autoCols;
AllowPaging = true;
GridLines = GridLines.None;
BorderWidth = bWidth;
BorderColor = Color.Black;
ShowFooter = true;
CellPadding = 3;
CellSpacing = 0;
Font.Name = "Verdana, Helvetica";
Font.Bold = true;
Font.Size = FontUnit.XXSmall;
Width = 500;
// Header Setting
HeaderStyle.Font.Bold = true;
HeaderStyle.BackColor = bgClr;
HeaderStyle.ForeColor = frClr;
HeaderStyle.HorizontalAlign = HorizontalAlign.Center;
// Pagerstyle settings
PagerStyle.Mode = PagerMode.NumericPages;
PagerStyle.BackColor = Color.Gainsboro;
PagerStyle.PageButtonCount = 10;
PagerStyle.Font.Size = FontUnit.XSmall;
PagerStyle.HorizontalAlign = HorizontalAlign.Right;
DataSource = GridCachedData();
try
{
DataBind();
}
catch
{
CurrentPageIndex = 0; //to catch any errors
}
PageIndexChanged +=
new DataGridPageChangedEventHandler(PageChanged);
ItemCreated += new DataGridItemEventHandler (GridCreated);
//Tracing inside a component
// HttpContext.Current.Trace.Write ("Datagrid");
}
private DataSet GridCachedData()
{
DataSet dgCache = (DataSet) HttpContext.Current.Session["dgCache"];
if (dgCache == null)
{
SqlConnection objConnect = new SqlConnection(strConn);
SqlDataAdapter objDataAdapter =
new SqlDataAdapter (sqlQuery.ToString(), objConnect);
DataSet objDS = new DataSet();
//Create DataTable
objDataAdapter.Fill (objDS);
HttpContext.Current.Session.Add ("dgCache" ,objDS);
//HttpContext.Current.Cache.Insert("dgCache", objDS, null,
DateTime.Now.AddMinutes(10), TimeSpan.Zero); // Caching way
RcdCount = objDS.Tables[0].Rows.Count;
return objDS;
}
else
{
RcdCount = dgCache.Tables[0].Rows.Count;
return dgCache;
}
}
//Paging
protected void PageChanged(Object sender,
DataGridPageChangedEventArgs e)
{
CurrentPageIndex = e.NewPageIndex;
GetDataGrid();
}
private void GridCreated(Object sender, DataGridItemEventArgs e)
{
//Declare the ItemType
ListItemType elemType = e.Item.ItemType;
if (elemType == ListItemType.Item
|| elemType == ListItemType.AlternatingItem)
{
//A DataGrid Item represents
//an item (row) in the DataGrid control
//e.Item is the table row where the command is raised
DataGridItem dgRow = (DataGridItem) e.Item;
//Gets the index number of the DataGridItem
//object from the bound data source
int rowCntIncr = dgRow.DataSetIndex + 1;
//Get the start and end index count from datagrid
//declare as typed variables first then convert
StartCnt = rowCntIncr-dgRow.ItemIndex;
EndCnt = rowCntIncr;
}
}
} // End Class
} // End NameSpace
The final section below is compiled from forum questions that were posted to me and that I answered on DNJ. I also include additional code to further enhance the Datagrid's functionality in this article.
The four added topics are:
- How to customize the Datagrid pager
- Adding a Custom Datagrid ItemTemplate
- Formatting Column Output in the DataGrid
- Adding an Incremental Count Column To DataGrid
How to customize the Datagrid pager
For example, to get your Datagrid pager to display paging like "<Prev 12 13 14 15 Next>", you'll need to replace the start and end paging ellipses "..." with < Prev and Next >. How's that done? By enumerating through the paging link button controls, determine the index position for the ellipses and then replacing them.
Here's how:
Here is the entire GridCreated method. Replace the original one found in the article, with the following:
private void GridCreated (Object sender, DataGridItemEventArgs e)
{
//Declare the ItemType
ListItemType elemType = e.Item.ItemType;
if (elemType == ListItemType.Item
|| elemType == ListItemType.AlternatingItem)
{
//A DataGrid Item represents an item (row) in the DataGrid control
//e.Item is the table row where the command is raised
DataGridItem dgRow = (DataGridItem) e.Item;
//Gets the index number of the DataGridItem
//object from the bound data source
int rowCntIncr = dgRow.DataSetIndex + 1;
//Get the start and end index count from DataGrid
//declare as typed variables first then convert
StartCnt = rowCntIncr-dgRow.ItemIndex;
EndCnt = rowCntIncr;
}
else if (elemType == ListItemType.Pager)
{
TableCell Pager = (TableCell) e.Item.Controls[0];
//Additional Pager Text
TableCell dgPagerText = new TableCell();
dgPagerText.Width = Unit.Percentage(25);
dgPagerText.ID = "dgPagerText";
dgPagerText.HorizontalAlign = HorizontalAlign.Left;
Label InfoText = new Label();
InfoText.ID = "InfoText";
dgPagerText.Controls.Add(InfoText);
//Add Table Cell to Pager
e.Item.Controls.AddAt(0, dgPagerText);
//Add custom Previous/Next paging section
//Loop indexed by 2 through datagrid paging
//numbers controls jumping over the spaces
for (int i = 0; i <= Pager.Controls.Count; i+=2)
{
object pgNumbers = Pager.Controls[i];
int bCnt = PagerStyle.PageButtonCount;
int endPagingIndex = Pager.Controls.Count-1;
if (pgNumbers.GetType().Name == "DataGridLinkButton")
{
LinkButton lb = (LinkButton) pgNumbers;
//Deal only with ellipses
if (lb.Text == "...")
{
//Find index position of paging ellipses
if (i == 0)
{
lb.Text = "< Prev";
}
else if (i == endPagingIndex)
{
lb.Text = "Next >";
}
}
}
}//End Loop
}
}
Formatting Column Output in the DataGrid
To begin, you'll first want to disable AutoGenerateColumns by setting it to false, by doing so set your query to "Select * From ..." etc. Next, the following code would go into the classes GetDataGrid() method, right before the DataSource = GridCachedData(); line.
The code below creates custom BoundColumns that permit formatting. In this example we formatted our SupplierID as currency just to show how it's done, as in your question:
//Programmatic Custom BoundColumns
BoundColumn SupplierID = new BoundColumn();
BoundColumn CompanyName = new BoundColumn();
BoundColumn ContactName = new BoundColumn();
BoundColumn Country = new BoundColumn();
SupplierID.HeaderText="No.";
SupplierID.DataField="SupplierID";
SupplierID.DataFormatString = "{0:c}";
CompanyName.HeaderText="CompanyName";
CompanyName.DataField="CompanyName";
ContactName.HeaderText="ContactName";
ContactName.DataField="ContactName";
Country.HeaderText="Country";
Country.DataField="Country";
//Clear the DataGrid first
Columns.Clear();
//Add them to our Datagrid
Columns.Add(SupplierID);
Columns.Add(CompanyName);
Columns.Add(ContactName);
Columns.Add(Country);
Adding a Custom Datagrid ItemTemplate
I'll also demonstrate a couple of ways to programmatically add a custom ItemTemplate to your datagrid.
Normally, to get an ItemTemplate with your Datagrid, you'd do something like this:
<asp:TemplateColumn>
<HeaderTemplate>Country</HeaderTemplate>
<ItemTemplate>
<asp:Label Text='<%# DataBinder.Eval(Container.DataItem, "Country") %>'
runat="server"/>
</ItemTemplate>
</asp:TemplateColumn>
To do the same thing in our custom Datagrid control is easy. Here now we'll modify the "Country" datafield column in this case, programmatically.
Setting up an ItemTemplate is easy, as shown before in our BoundColumn thread, like so:
TemplateColumn Temp = new TemplateColumn();
Next, you add whatever TemplateColumn class members you wish. In this case, we'll add a header text:
Temp.HeaderText= "Country";
and then add it to your datagrid as in our previous DG formatting thread, in any order:
Columns.Add (Temp);
Now this works fine, except you won't get any data. Here are two ways to get data.
Method one is a quick and easy. To get something from this you can load an external server control:
Temp.ItemTemplate = Page.LoadTemplate("control.ascx");
This of course could contain pretty much whatever, within reason of course. The other method in getting data into your custom ItemTemplate is by implementing ITemplate. The class to do this is:
public class custDGTemplate : ITemplate
{
private string fieldname;
public custDGTemplate (string itemcolumname)
{
fieldname = itemcolumname;
}
public void InstantiateIn (Control container)
{
LiteralControl lc = new LiteralControl();
lc.DataBinding += new EventHandler(this.OnDataBinding);
container.Controls.Add (lc);
}
public void OnDataBinding (object sender, EventArgs e)
{
LiteralControl lc = (LiteralControl) sender;
DataGridItem container = (DataGridItem) lc.NamingContainer;
lc.Text = ((DataRowView) container.DataItem) [fieldname].ToString();
}
}
Add this class anywhere within the mycustDG.cs source code file. Next, add the line below to your other custom ItemTemplate's members and pass the database field name to the to your custDGTemplate class, like so:
Temp.ItemTemplate = new custDGTemplate ("Country");
Adding an Incremental Count Column To DataGrid
I've got a cool little code snippet that you can add to the Datagrid to give it an new column with an incremental count. This first newly added column will have the count, with row 1 being 1, row 2 being 2, and the count goes on.
It's the same principle laid out in my myCustDG.cs class file's GridCreated method. So to implement this, you'll need to do two things:
1) In accordance to the previous two forum threads I added on creating custom templates, follow those examples and programmatically add another BoundColumn to the DataGrid so it can accommodate the incremental counting, like so:
BoundColumn Count = new BoundColumn();
Then add the header text:
Count.HeaderText = "No.";
Then add it to the DataGrid with the rest, just like in the other forum threads:
Columns.Add(Count);
2) Since we're already getting a count of how many records are found in our GridCreated method by showing us in our results – Results No. x to x, and now that we have just created a new column, all we need to do is write this number value to that column, by adding the two lines below to the GridCreated method, within the ListItemType.Item / ListItemType.AlternatingItem conditional after EndCnt = rowCntIncr; and before the ListItemType.Pager check:
//Add new Literal Control to write our Incremental Count from rowCntIncr
LiteralControl numberIncr = new LiteralControl(rowCntIncr.ToString());
//Write out to the first column the value count
dgRow.Cells[0].Controls.Add (numberIncr);
That's it, a quick and easy way to get an incremental count for your DataGrid.
So in retrospect, you've ended up with a full-featured custom Datagrid control that you can reuse! Well, I hope that wasn't too overwhelming. We covered quite a bit of ground, but in the end, we ended up with some powerful implementations, that have a great number of possibilities. Creating your own custom controls has you doing less work by putting into place a good code/layout separation schema.
In any event, the methodologies examined here can easily be fitted to other well-know server controls in creating other new and very customizable controls. I hope that you ended up learning more of the profound possibilities and options .NET offers the next-generation of developers.
Until Next Time. Happy .NETing</>
About Dimitrios Markatos
Dimitrios, or Jimmy as his friends call him, is an independent .NET architect who specializes in Microsoft Technologies for creating high-performance and scalable data-driven enterprise Web and desktop applications. Till now Jimmy has authored nearly two dozen .NET articles, published on Dot Net Junkies, 4 Guys From Rolla, Sitepoint, Developer Fusion, MSDN Academic Alliance, Developers.NET, and The Official Microsoft ASP.NET Site, covering various advanced and unique techniques on .NET.