Every now and then, I need to add a log reader to an ASP.NET application, and it is a pain when the log gets large, because if I let the
GridView
do the paging and attach it to the database table, then it reads all the records from the table and throws away those it doesn't need for the current page. This quickly gets slow...
Or, I have to write code in the codebehind which handles the paging for me, and then filter the records from the DB myself. I have to provide a page number, first, next, previous and last buttons, and make them work too. I still have to format the data that comes from the DB, since my
enum
values are just integers as far as the DB is concerned, and I may not want all fields displayed, or displayed in the default format.
It's quite simple to make the
GridView
do all the work for you, with a small amount of (largely) generic code.
For example, my Log entry DB record holds:
iD : int
loggedAt : DateTime
message : string
cause : enum, stored as int
infoType : enum, stored as int
additionalInfo : string or byte[] (depending on infoType)
What I actually want to display is a page of twenty log entries, with shorter "hints" as to the message and additional information:
10236 06/01/2011 10:13:47 EmailSystem To:xxx@xxx.com b... String System.Net.Mail.SmtpFailedR...
10237 06/01/2011 16:29:48 Unknown System.Web.HttpUnhandledExc... String mon.DbDataAdapter.Fill(Data...
10238 06/01/2011 16:32:31 Unknown System.Web.HttpUnhandledExc... None No additional info
You can do all this pretty easily by tying an
ObjectDataSource
to your gridview, and providing a custom class to do the
ObjectDataSource
work.
GridView HTML
<asp:GridView ID="gridViewPagedLog"
runat="server"
AllowPaging="True"
AllowSorting="True"
CellPadding="4"
DataSourceID="dataLogViewer"
ForeColor="#333333"
GridLines="None"
PageSize="20"
>
<FooterStyle BackColor="#1C5E55" Font-Bold="True" ForeColor="White" />
<RowStyle BackColor="#E3EAEB" />
<EditRowStyle BackColor="#7C6F57" />
<SelectedRowStyle BackColor="#C5BBAF" Font-Bold="True" ForeColor="#333333" />
<PagerStyle BackColor="#666666" ForeColor="White" HorizontalAlign="Center" />
<HeaderStyle BackColor="#1C5E55" Font-Bold="True" ForeColor="White" />
<AlternatingRowStyle BackColor="White" />
</asp:GridView>
<asp:ObjectDataSource ID="dataLogViewer"
runat="server"
EnablePaging="true"
TypeName="LogViewer"
SelectMethod="ShowPagedLogs"
SelectCountMethod="GetRowCount"
StartRowIndexParameterName="startRow"
MaximumRowsParameterName="pageSize"
SortParameterName="SortColumns"
>
</asp:ObjectDataSource>
Ignoring the obvious, let's look at these:
AllowPaging="True" : Causes the GridView to do paging
AllowSorting="True" : Causes sorting to be permitted (we need this)
DataSourceID="dataLogViewer" : Names the ObjectDataSource for thr Gridview
PageSize="20" : Sets the rows per page - defaults to 10
EnablePaging="true" : Causes the data to be paged
TypeName="LogViewer" : Names the custom class to fetch the data
SelectMethod="ShowPagedLogs" : Names the method in the custom class to get the page of data
SelectCountMethod="GetRowCount" : Names the method in the custom class to get the total rows count
StartRowIndexParameterName="startRow" : Names the parameter passed as the index to the page method in the custom class
MaximumRowsParameterName="pageSize" : Names the parameter passed as the size to the page method in the custom class
SortParameterName="SortColumns" : Names the parameter passed as the column to sort on to the page method in the custom class
You can ignore a lot of this stuff here, basically it requires you to provide a couple of methods in your custom class:
public DataSet ShowPagedLogs(int startRow, int pageSize, string sortColumns)
public int GetRowCount()
Provide those, and you are done.
Create a new class in your project - it should go into the "
App_code
" folder. (If you don't have one, VS will prompt and create it for you.) Call it
LogViewer
(or match with the "TypeName" in the ObjectDataSource definition):
using System;
using System.Data;
using System.Configuration;
using System.Data.SqlClient;
using SMDAL;
using SMWebSiteUtils;
public class LogViewer
{
#region Constants
private const string dbConName = "LoggingDatabase";
private const string sqlPagedFront = "SELECT * FROM (SELECT ";
private const string sqlPagedRowNumberPrefix = ", ROW_NUMBER() OVER (ORDER BY ";
private const string sqlPagedRowNumberSuffix = ") AS ResultSetRowNumber FROM ";
private const string sqlPagedEndFormat = ") AS PagedResults WHERE ResultSetRowNumber > {0} AND ResultSetRowNumber <= {1}";
#endregion
#region Property bases
private string sortColumns = string.Empty;
#endregion
#region Properties
public string SortColumns
{
get { return sortColumns; }
set { sortColumns = value; }
}
#endregion
#region Constructors
public LogViewer()
{
}
#endregion
#region Public Methods
public DataSet ShowPagedLogs(int startRow, int pageSize, string sortColumns)
{
if (!String.IsNullOrEmpty(sortColumns))
{
SortColumns = sortColumns;
}
else
{
SortColumns = "loggedAt";
}
string dbCon = ConfigurationManager.ConnectionStrings[dbConName].ConnectionString;
DataSet ds = new DataSet("Log entries");
DataTable dt = ds.Tables.Add("Logs");
dt.Columns.Add("iD");
dt.Columns.Add("Logged at");
dt.Columns.Add("Cause");
dt.Columns.Add("Message");
dt.Columns.Add("AI type");
dt.Columns.Add("Additional Information");
try
{
using (SqlConnection con = new SqlConnection(dbCon))
{
con.Open();
using (SqlCommand com = new SqlCommand(SqlPage("*", SortColumns, "SMAdmin.Log", "", startRow / pageSize, pageSize), con))
{
SqlDataReader r = com.ExecuteReader();
while (r.Read())
{
int id = Convert.ToInt32(r["iD"]);
DateTime t = (DateTime) r["loggedAt"];
string m = (string) r["message"];
AdditionalInfoType at = (AdditionalInfoType) Convert.ToInt32(r["infoType"]);
ErrorLogCause c = (ErrorLogCause) Convert.ToInt32(r["cause"]);
byte[] add = (byte[]) r["additionalInfo"];
string additionalInfo;
switch (at)
{
case AdditionalInfoType.None:
additionalInfo = "No additional info";
break;
case AdditionalInfoType.ByteArray:
additionalInfo = Summarise(add);
break;
case AdditionalInfoType.String:
additionalInfo = System.Text.Encoding.ASCII.GetString(add);
break;
case AdditionalInfoType.Unknown:
additionalInfo = "Unknown additional info";
break;
default:
additionalInfo = "Error: Unknown type of additional info";
break;
}
dt.Rows.Add(id, t, c.ToString(), Summarise(m), at.ToString(), Summarise(additionalInfo));
}
}
}
}
catch
{
}
return ds;
}
public int GetRowCount()
{
string dbCon = ConfigurationManager.ConnectionStrings[dbConName].ConnectionString;
using (SqlConnection conn = new SqlConnection(dbCon))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand("SELECT COUNT(*) FROM SMAdmin.Log", conn))
{
return (int) cmd.ExecuteScalar();
}
}
}
#endregion
#region Public static methods
public static string Summarise(byte[] ab)
{
return Summarise(ab, 16, 1, false);
}
public static string Summarise(byte[] ab, int bytesPerLine, int lines, bool showOffset)
{
int count = bytesPerLine * lines;
string formatShowOffset = "X" + GetMaxHexDigits(count).ToString();
StringBuilder sb = new StringBuilder(count * 3 + 2);
int bytes = Math.Min(count, ab.Length);
int current = 0;
string linePrefix = "";
for (int i = 0; i < lines; i++)
{
sb.Append(linePrefix);
linePrefix = "\n";
if (showOffset)
{
sb.Append(current.ToString(formatShowOffset) + ": ");
}
for (int j = 0; j < bytesPerLine; j++)
{
sb.Append(ab[current++].ToString("X2") + " ");
}
}
return sb.ToString();
}
public static string Summarise(string s)
{
return Summarise(s, 30);
}
public static string Summarise(string s, int maxLength)
{
if (s.Length <= maxLength)
{
return s;
}
return s.Substring(0, maxLength - 3) + "...";
}
public static string SqlPage(string fields, string orderField, string table, string additionalClauses, int pageNo, int linesPerPage)
{
fields = fields.Trim(", ".ToCharArray());
int messageLengthEstimate = sqlPagedFront.Length +
fields.Length +
sqlPagedRowNumberPrefix.Length +
orderField.Length +
sqlPagedRowNumberSuffix.Length +
table.Length +
1 +
additionalClauses.Length +
sqlPagedEndFormat.Length +
(2 * 10);
StringBuilder sb = new StringBuilder(messageLengthEstimate);
int pageStart = pageNo * linesPerPage;
sb.Append(sqlPagedFront);
sb.Append(fields);
sb.Append(sqlPagedRowNumberPrefix);
sb.Append(orderField);
sb.Append(sqlPagedRowNumberSuffix);
sb.Append(table);
sb.Append(" ");
sb.Append(additionalClauses);
sb.Append(string.Format(sqlPagedEndFormat, pageStart, pageStart + linesPerPage));
return sb.ToString();
}
#endregion
}
The paging itself works by encapsulating a regular SQL
SELECT
statement and including a function to number the rows. It then only returns the rows which are within range.
For example: if the SQL statement to select the fields from the Grid view was:
SELECT iD, loggedAt, message, cause, infoType, additionalInfo FROM SMAdmin.Log WHERE cause=0
then after encapsulation it would become:
SELECT * FROM (SELECT iD,
loggedAt,
message,
cause,
infoType,
additionalInfo,
ROW_NUMBER() OVER (ORDER BY loggedAt) AS ResultSetRowNumber
FROM SMAdmin.Log WHERE cause=0) AS PagedResults
WHERE ResultSetRowNumber > 20 AND ResultSetRowNumber <= 40
What does this do?
The original
Select
clause is changed to add
ROW_NUMBER() OVER (ORDER BY loggedAt) AS ResultSetRowNumber
which adds a field to the resulted results: the ordinal number of the row in the dataset, in order of log date.
The returned fields are then processed through a second
SELECT
where only the relevant rows are returned.
The beauty of this is that all the unwanted data stays in the database server, only the wanted rows are returned each time. This is MUCH faster than selecting the rows from the complete table, especially when the table size gets large.