In <my last post/> I explained how you might add a search bar to your TaskList. I'm going to follow up with another sample that demonstrates how to build a search bar that selects tasks by date. To make selecting a date for my sample easy to do, I've decided to use <The Coolest DHTML / JavaScript Calendar/> that I could find.
The first thing I’m going to do is add the following ASP.NET controls to the form tag within my web page.
<div style="margin: 6px 0;">
<asp:dropdownlist id="columnsDropDownList" runat="server" />
<asp:dropdownlist id="commandsDropDownList" runat="server" />
<asp:textbox id="date" runat="server" />
<script type="text/javascript">
Calendar.setup({
inputField : "date", // id of the input field
ifFormat : "%m/%d/%Y", // format of the input field
showsTime : false,
singleClick : true
});
</script>
<asp:button id="searchButton" runat="server" text="Search" />
</div>
I am going to use the DropDownList controls to display the columns
and operators the user selects to match the date they pick from the popup calendar associated with the TextBox control. The Button control is used to kick off the search.
To get the Calendar control to work, you need to add the following lines to the head tag.
<script type="text/javascript" src="calendar/calendar.js"></script>
<script type="text/javascript" src="calendar/calendar-setup.js"></script>
<script type="text/javascript" src="calendar/lang/calendar-en.js"></script>
<link rel="stylesheet" type="text/css" media="all" href="calendar/skins/aqua/theme.css" />
Next, I'm going to add a GridView control to display the results of
my search.
<asp:gridview id="gridView" autogeneratecolumns="False"
emptydatatext="Sorry, no matching records were found." runat="server">
<columns>
<asp:boundfield datafield="ProcessName" headertext="Process" readonly="True"/>
<asp:boundfield datafield="TaskName" headertext="Task" readonly="True"/>
<asp:boundfield datafield="ResponsibleName" headertext="Responsible" readonly="True"/>
<asp:boundfield datafield="TaskStatus" headertext="Status" readonly="True"/>
<asp:boundfield datafield="TaskStartDate" headertext="Start Date"
htmlencode="False" dataformatstring="{0:d}" readonly="True"/>
<asp:boundfield datafield="TaskDueDate" headertext="Due Date"
htmlencode="False" dataformatstring="{0:d}" readonly="True"/>
</columns>
</asp:gridview>
With the controls in place, it's time to start adding some code. The first thing I'm going to do is build a DateColumnDecorator class that implements the IDateColumn interface.
using System;
using Teamplate.BLL.Data;
public class DateColumnDecorator : IDateColumn
{
private readonly string displayName;
private readonly IDateColumn column;
public DateColumnDecorator(string displayName, IDateColumn column)
{
this.displayName = displayName;
this.column = column;
}
public override string ToString()
{
return displayName;
}
public string ColumnName
{
get { return column.ColumnName; }
}
public ICriteria GreaterThan(DateTime value)
{
return column.GreaterThan(value);
}
// more methods
}
The primary purpose of the decorator class is to wrap another IDateColumn instance in order to override the ToString method. By default the ToString method returns the value of the ColumnName property. Although useful, this is not what I want to display to the user in the DropDownList control. All of the other methods on the decorator simply delegate to the IDateColumn instance being wrapped.
The DateColumnDecorator is put to work in the Columns property as seen below.
private IList<IDateColumn> columns;
private IList<IDateColumn> Columns
{
get
{
if (columns == null)
{
columns = new List<IDateColumn>();
columns.Add(new DateColumnDecorator("Due Date", TaskList.TaskDueDate));
columns.Add(new DateColumnDecorator("Start Date", TaskList.TaskStartDate));
}
return columns;
}
}
My second DropDownList control allows the user to select an operator that describes how they want to compare against the selected date. The goal here is to translate several unique method signatures into one general purpose signature. The easiest way I could think of to do this was to introduce a command pattern.
I'm going to start with the following abstract class whose primary responsibility is to handle the display name functionality.
using System;
using Teamplate.BLL.Data;
public abstract class DateCommand
{
private readonly string displayName;
public DateCommand(string displayName)
{
this.displayName = displayName;
}
public override string ToString()
{
return displayName;
}
public abstract ICriteria Execute(IDateColumn column, DateTime value);
}
For each operator that I want to appear in the DropDownList control, I am going to create a class that inherits from the abstract DateCommand class. Now all I need to do is override the Execute method and call the corresponding method on the IDateColumn instance being passed in to the Execute method.
using System;
using Teamplate.BLL.Data;
public class GreaterThan : DateCommand
{
public GreaterThan() : base(">")
{
}
public override ICriteria Execute(IDateColumn column, DateTime value)
{
return column.GreaterThan(value);
}
}
This well-known, although often overlooked, technique is called double-dispatch.
using System;
using Teamplate.BLL.Data;
public class GreaterThanOrEqualTo : DateCommand
{
public GreaterThanOrEqualTo() : base(">=")
{
}
public override ICriteria Execute(IDateColumn column, DateTime value)
{
return column.GreaterThanOrEqualTo(value);
}
}
The command objects adapt each call on a unique method signature of the IDateColumn instance into a general purpose method called Execute. When I add a Commands property to my code behind, all of my command objects are added to a list of type DateCommand. Nothing like a little polymorphism to spice things up.
private IList<DateCommand> commands;
private IList<DateCommand> Commands
{
get
{
if (commands == null)
{
commands = new List<DateCommand>();
commands.Add(new GreaterThan());
commands.Add(new GreaterThanOrEqualTo());
commands.Add(new EqualTo());
commands.Add(new LessThanOrEqualTo());
commands.Add(new LessThan());
}
return commands;
}
}
With everything in its place, it's now time to hookup our event handlers, load the DropDownList controls and of course obtain a new session token.
private string sessionToken;
private int userId;
protected void Page_Load(object sender, EventArgs e)
{
BSession bSession = new BSession();
bSession.Connect(string.Empty, string.Empty, string.Empty);
sessionToken = bSession.GetToken();
userId = bSession.GetUserId();
LoadControls();
HookupEventHandlers();
}
private void HookupEventHandlers()
{
searchButton.Click += delegate { FindMatchingTaskRecords(); };
}
private void LoadControls()
{
if (!IsPostBack)
{
columnsDropDownList.DataSource = Columns;
columnsDropDownList.DataBind();
commandsDropDownList.DataSource = Commands;
commandsDropDownList.DataBind();
}
}
And finally, it's time to bring the new BQueryRuntime API into play. Pay attention to the SearchCriteria property as that's where all of the magic takes place.
private ICriteria SearchCriteria
{
get
{
IDateColumn selectedColumn = Columns[columnsDropDownList.SelectedIndex];
DateCommand selectedCommand = Commands[commandsDropDownList.SelectedIndex];
return selectedCommand.Execute(selectedColumn, DateTime.Parse(date.Text));
}
}
private void FindMatchingTaskRecords()
{
BQueryRuntime runtime = new BQueryRuntime();
runtime.SetSessionToken(sessionToken);
IQuery query = runtime.CreateQuery(QueryType.TaskList);
query.AddCriteria(
TaskList.TaskStatus.MatchAny(TaskStatus.Ready | TaskStatus.Waiting)
.And(TaskList.ResponsibleId.EqualTo(userId))
.And(SearchCriteria));
gridView.DataSource = runtime.ExecuteDataSet(query);
gridView.DataBind();
}
That's it. Feel free to download the complete project <here/>. If you like what you see, or even if you don't, I'm open to comments.