|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
IntroductionThis article presents a custom server control representing a database query, with support for input parameters and the ability to target other user interface controls for output. Techniques for customizing the processing of control markup are demonstrated, including the use of a custom BackgroundA few months ago, I wrote the article ActionTags, describing an approach for developing server controls that encapsulate functionality separate from a user interface. This was done as a means of exploring the potential for creating libraries that make ASP.NET functional for non-programmers. I included examples of actions that are associated to the events of other user interface controls through a mutual <aq:Query runat="server" connection="SqlCon" requeryOnPostback="false"
queryType="Text"
enabled="true" >
<sql>
Select CustomerID, OrderDate, Field3, Field4, ColorCode
From MyTable
Where OrderDate > @orderDate
And CustomerID = @id
</sql>
<parameter name="@orderDate" valueFrom="GET" valueFromId="orderDate" />
<parameter name="@id" valueFrom="Control" valueFromId="txtCustomerID"
valueFromProperty="Text"/>
<outputTo target="myDataGrid">
<outputFieldTo target="txtField3" field="Field3" />
<outputFieldTo target="lblOrderDate"
field="OrderDate" format="dd mmm yyyy" />
<outputFieldTo target="lblOrderDate"
field="ColorCode"
outputProperty="BackColor" />
</aq:Query>
The The inner content of the
Ironically, this syntax won't fit the ActionTag model I originally developed. I wanted more flexible options for outputting complete multiple-row result sets, or field values from single-row result sets (the original Custom ParsingThe .NET Framework offers several ways to customize the processing of control markup. The If a class is marked
Though the <myPrefix:Parameter runat="server".../>
should become simply: <Parameter ... />
Neither Subclassing ControlBuilderDevelopers can subclass
For the Query tag, we are interested in controlling how an inner tag is matched to an object. For that we can override the public class QueryControlBuilder : ControlBuilder
{
public override Type GetChildControlType(String tagName,
IDictionary attributes)
{
// if the child tag is <Sql>, return a SqlContent object type
if (String.Compare(tagName, "sql", true) == 0)
return typeof(SqlContent);
// if the child tag is <Parameter>,
// return a Parameter object type
if (String.Compare(tagName, "parameter", true) == 0)
return typeof(Parameter);
// if the child tag is <OutputTo>,
// return an OutputTo object type
if (String.Compare(tagName, "outputto", true) == 0)
return typeof(OutputTo);
// if the child tag is <OutputFieldTo>,
// return an OutputFieldTo object type
if (String.Compare(tagName, "outputfieldto", true) == 0)
return typeof(OutputFieldTo);
// if any other tag name is used, throw an exception
throw new Exception(
"The <Query> tag should only contain <Sql>, " +
"<Parameter>, <OutputTo>, and <OutputFieldTo> tags.");
}
}
This [
ControlBuilder(typeof(QueryControlBuilder))
]
public class Query : Control
{
. . .
private string _sql = "";
private ArrayList _parameters = new ArrayList();
private ArrayList _outputs = new ArrayList();
private ArrayList _outputFields = new ArrayList();
. . .
protected override void AddParsedSubObject(Object obj)
{
if (obj is SqlContent)
_sql = ((SqlContent)obj).Text;
if (obj is Parameter)
_parameters.Add((Parameter) obj);
if (obj is OutputTo)
_outputs.Add((OutputTo) obj);
if (obj is OutputFieldTo)
_outputFields.Add((OutputFieldTo) obj);
}
. . .
}
The public class SqlContent : Literal
{ }
The inner text is treated as literal content this way, but conveniently distinguished as having come from a Helper ClassesThe Query control uses two helper classes: ConnectionUtilityThe <?xml version="1.0"?>
<Connections>
<Connection id="SampleSqlServer"
class="System.Data.SqlClient.SqlConnection"
adapter="System.Data.SqlClient.SqlDataAdapter"
connectionString="Data source=(local);Initial Catalog=mydb;Integrated
Security=SSPI;"
/>
<Connection id="SampleMdb"
class="System.Data.OleDb.OleDbConnection"
adapter="System.Data.OleDb.OleDbDataAdapter"
connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data
Source=c:\inetpub\wwwroot\Query\test.mdb;User Id=admin;Password="
/>
<Connection id="SampleOracle"
assembly="System.Data.OracleClient"
class="System.Data.OracleClient.OracleConnection"
adapter="System.Data.OracleClient.OracleDataAdapter"
connectionString="Data Source=oracle1; User Id=myUser;Password=myPass"
/>
</Connections>
By declaring the provider-specific objects through the ControlUtilityThe
Input ParametersThe Query control uses custom // potential places to look for parameter values
public enum ParameterValueFrom
{
Control, // locate parameter value from a server control
Get, // locate parameter value from the queryString
Post, // locate parameter value from a form submission
Any // locate parameter value in Control, Get, or Post
// in that order
}
Properties of the public class Parameter : Control, IAttributeAccessor
{
private string _name;
private string _value;
private ParameterValueFrom _valueFrom = ParameterValueFrom.Any;
private string _valueId;
private string _valueFromProperty;
private string _default;
// Name property - maps to the IDataParameter.ParameterName property
public string Name {
get {return _name;}
set {_name = value;}
}
// Value (optional) - used to supply a literal value
// for the parameter; if not supplied, then the
// ValueFrom, ValueFromId, and ValueFromProperty
// properties are used to determine the parameter value
public string Value {
get {return _value;}
set {_value = value;}
}
// ValueFrom - specifies where the parameter value
// comes from (Control, Get, Post, or Any)
public ParameterValueFrom ValueFrom
{
get {return _valueFrom;}
set {_valueFrom = value;}
}
// ValueFromId - the id of the server control,
// queryString field, or form submittal field
// from which the parameter value should be obtained
public string ValueFromId {
get {return _valueId;}
set {_valueId = value;}
}
// ValueFromProperty (optional) - if used, specifies
// the property of a server control from which the
// parameter value should be obtained; if not used,
// then logic based on the control type is applied
// instead
public string ValueFromProperty {
get {return _valueFromProperty;}
set {_valueFromProperty = value;}
}
// Default - specifies a value that should be used
// if the parameter value is not supplied where
// ValueFrom says it should be (or is supplied as
// a blank string)
public string Default {
get {return _default;}
set {_default= value;}
}
. . .
}
Using IAttributeAccessorTo allow for database-specific flexibility, the custom <parameter name="pLast" valueFrom="Any" valueFromId="lastName"
oracleType="NVarChar" size="128" />
The public class Parameter : Control, IAttributeAccessor
{
. . .
private StringDictionary _otherAttributes = new StringDictionary();
. . .
// Attributes - maintains a list of other attributes
// that may have been supplied in the <Parameter> tag
public StringDictionary Attributes
{
get {return _otherAttributes;}
}
// SetAttribute - implemented as part of the
// IAttributeAccessor contract
public void SetAttribute(string key, string value)
{
// locally store the attribute name and value
_otherAttributes[key] = value;
}
// GetAttribute - implemented as part of the
// IAttributeAccessor contract
public string GetAttribute(string key)
{
// retrieve the attribute value
return _otherAttributes[key];
}
. . .
}
As each Query control uses a public class Parameter : Control, IAttributeAccessor
{
. . .
public string DetermineValue(Control relativeTo, HttpContext context)
{
// is there a Value property? if so, use it as a literal
if (_value != null && _value != "")
{
return this.Value;
}
// if a value is not supplied, then we'll
// look at either the Request.Form,
// Request.QueryString, or a server control for a value;
// inspect the ValueFrom property;
// should we try a server control's value?
if (ValueFrom == ParameterValueFrom.Control
|| ValueFrom == ParameterValueFrom.Any)
{
string s;
// if ValueFromProperty is supplied,
// look for the value using it
if (_valueFromProperty != null & _valueFromProperty != "")
s = ControlUtility.GetControlValue(relativeTo,
_valueId, _valueFromProperty);
else
// if not, then look for the value
// depending on the type of control
s = ControlUtility.GetControlValue(relativeTo, _valueId);
if (s != null && s != "")
return s;
}
// should we try the query string?
if (ValueFrom == ParameterValueFrom.Get
|| ValueFrom == ParameterValueFrom.Any)
{
if (context.Request.QueryString[ValueFromId] != null)
return context.Request.QueryString[ValueFromId];
}
// should we try a form post?
if (ValueFrom == ParameterValueFrom.Post
|| ValueFrom == ParameterValueFrom.Any)
{
if (context.Request.Form[ValueFromId] != null)
return context.Request.Form[ValueFromId];
}
// if still no value, use the default (if available)
if (_default != null) return _default;
// still no value, return null;
return null;
}
}
Executing the QueryBy default, the query will execute in the . . .
public class Query : Control {
private string _queryEvent = "PreRender";
. . .
public string QueryEvent
{
get {return _queryEvent;}
set {_queryEvent = value;}
}
. . .
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
// if the triggering event is Init, execute the query now
if (String.Compare(_queryEvent, "Init", true) == 0)
ExecuteQuery(this, e);
// otherwise, set up an event handler to execute the query later
else
{
BindingFlags bf = BindingFlags.Instance | BindingFlags.Public;
Type t = this.GetType();
EventInfo ei = t.GetEvent(_queryEvent, bf);
if (ei == null)
throw new Exception(
string.Format(
"The <Query> control has no '{0}' event",
_queryEvent)
);
ei.AddEventHandler(this,
new System.EventHandler(ExecuteQuery));
}
}
private void ExecuteQuery(Object o, EventArgs e)
{
. . .
}
}
We'll now dissect the private void ExecuteQuery(Object o, EventArgs e)
{
if (_enabled && (_requeryOnPostback || !this.Page.IsPostBack) )
{
// attempt to get the application's Connections file
string connectionsFile =
ConfigurationSettings.AppSettings["ConnectionsFile"];
if (connectionsFile == null || connectionsFile == "")
throw new Exception("The <appSettings> element of the "
+ "Web.Config file must contain an entry for"
+ "'ConnectionsFile'");
// ensure the Connection property has been specified
if (_connection == null || _connection == "")
throw new Exception("The 'connection' attribute of "
+ "the <Query> tag must be specified.");
// use the ConnectionUtility class
// to establish a database connection
ConnectionUtility cu = new ConnectionUtility(connectionsFile,
_connection);
. . .
}
}
With the connection established, the private void ExecuteQuery(Object o, EventArgs e)
{
if (_enabled && (_requeryOnPostback || !this.Page.IsPostBack) )
{
. . .
try
{
// get a database-specific command object from the
// ConnectionUtility helper class
IDbCommand cmd = cu.GetCommandObject(_sql);
// loop through the Parameters collection
foreach (Parameter p in _parameters)
{
// Determine the parameter's value
string sValue = p.DetermineValue(this, this.Context);
// Apply the parameter using the ConnectionUtility
// helper class
cu.AddParameter(cmd, p.Name, sValue, p.Attributes);
}
// execute the query and retrieve the resulting DataSet
DataSet ds = cu.GetDataSet(cmd);
. . .
}
catch (Exception ex)
{
throw ex;
}
finally
{
cu.Dispose();
}
}
}
As the page was parsed, any private void ExecuteQuery(Object o, EventArgs e)
{
if (_enabled && (_requeryOnPostback || !this.Page.IsPostBack) )
{
. . .
try
{
. . .
// for every OutputTo item we have,
// bind the query results
// to it; these controls must support
// the DataSource property
// or an exception is thrown
foreach (OutputTo output in _outputs)
{
Control c = ControlUtility.LocateControl(
this, output.Target);
if (c == null)
throw new Exception(
string.Format(
"Cannot find <OutputTo> target with id='{0}'",
output.Target)
);
// got the control; now attempt to bind the results
// using Reflection objects
Type t = c.GetType();
PropertyInfo pi = t.GetProperty("DataSource");
pi.SetValue(c, ds, null);
c.DataBind();
// for convenience, if we're binding to
// a dropdownlist
// control, select its top item
if (c is DropDownList)
((DropDownList) c).SelectedIndex = 0;
}
}
. . .
}
}
Similarly, private void ExecuteQuery(Object o, EventArgs e)
{
if (_enabled && (_requeryOnPostback || !this.Page.IsPostBack) )
{
. . .
try
{
. . .
// for every OutputFieldTo item we have,
// bind a single row field
// to it;
if (ds.Tables[0].Rows.Count > 0)
foreach (OutputFieldTo output in _outputFields)
{
// use the ControlUtility helper
// class to locate the
// target control
Control c = ControlUtility.LocateControl(this,
output.Target);
if (c == null)
throw new Exception(
string.Format(
"Cannot find <OutputTo> target with id='{0}'",
output.Target)
);
// got the target control;
// now attempt to set the field value;
if (output.OutputProperty != null
&& output.OutputProperty != "")
// if a specific output property
// is specified, use it
ControlUtility.SetControlValue(this,
output.Target,
ds.Tables[0].Rows[0][output.Field],
output.Format,
output.OutputProperty);
else
// if not, attempt to determine the property
// based on the control type
ControlUtility.SetControlValue(this,
output.Target,
ds.Tables[0].Rows[0][output.Field],
output.Format);
}
}
. . .
}
}
About the ExamplesThe examples assume the location of the connections file to be c:\inetpub\wwwroot\query\connections.config. If this is different on your machine, make sure to modify the "ConnectionsFile" value in the Web.Config file. Sample databases are in the form of Access 2000 .mdb files; Categories.aspxShows a single query with no parameters, targeting a Products.aspxShows two queries: one targeting a ProductsByCategory.aspxShows two queries; the first accepts a category ID supplied through the querystring and targets a Calendar.aspxThis example borrows the DataCalendar control from a previous article, and offers a data-driven event display calendar, with no programming code required EventDetail.aspxThis page is called to provide the detail for a clicked event from Calendar.aspx, accepting the event ID through the querystring. Several Samples.txtThis text file shows additional examples of database-specific parameter syntax within the SummaryThe ASP.NET custom Query control presented here provides data querying ability encapsulated in a declarative tag. It serves as an example of custom tag parsing through an associated custom
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||