November 2003 - Posts
What's a collection? The dumb answer is that collections are objects that implement ICollection. A slightly better answer would be to say that collection is a group of instances of any given object type or struct. One customer is an instance, many customers is a collection.
There are a many generic built-in collections including ArrayList, SortedList, and HashTable, Stack and Queue, and then a bunch of specialized ones like ControlCollection, HttpSessionState, and DataGridItemCollection. Or you can create your own. For example, I now use Data Access Layer classes which implement collections instead of DataTables and DataSets to cache records in memory. CodeSmith has some great templates for automatically generating DAL Classes against tables.
Anyway, the nice thing about Cache, Session and Application is that you can store any sort of object in them, including a collection. Or a string array. A string array isn't a proper "collection" because it doesn't implement .Count(), but it works well for simple lists you might want to keep around. Let's say you wanted to keep a list alive for the lifespan of the Application:
// store
string[] myLanguages = new string[1];
myLanguages[0] = "English";
myLanguages[1] = "French";
Application("myLanguages")= myLanguages;
// retrieve
myLanguages = (String[])Application("myLanguages");
On the retrieve I just cast to the sort of object I'm pulling out et voila, regardez l'array. In the above example, you can can freely replace "Application" with "Session" if you want to pin this data to a single client session rather than the Application where is available to all users.
You could also replace "Application" with "Cache" but caches can disappear during a garbage collection so you need something a little different. Let's wrap it in a simple method:
public myNS.myClass.ClientCollection GetClientCollection(Boolean invalidate){
// Try retrieving the data from the cache
myNS.myClass.ClientCollection cClients = (myNS.myClass.ClientCollection)Cache("ClientList");
if ((cClients==null) || (invalidate)) {
// If it isn't in the cache or the caller wants fresh data then reload the data and cache it again.
cClients = myNS.myClass.LoadClientCollection();
Cache("ClientList") = cClients;
}
}
Again, the big difference with a cache is that it can be garbage collected, and unlike the Application or Session objects you can set up automatic rules to invalidate it when "something happens." Therefore when loading data from a cache you need to do the test for null (see above), and if the cache was invalidated or garbage collected you reload the data and stuff it back into the cache.
You can set either time dependencies (refresh every 15 minutes) or file dependencies (invalidate when a file, perhaps an .xml or .mdb, changes). Shoot, I thought I already blogged about how to invalidate a whole set of cached objects when a record is updated using a Key-based dependency, I guess that's next.
[2003-12-31: Posted a follow-up]
The other day I described how to check the values of individual validation controls on a page rather than the entire page. It worked fine, but it meant writing blocks of code that I found kinda klunky (see that post for an example).
Jon Galloway pointed me to an article that Enables/Disables the validation controls on a page which are bound by certain placeholder tags. The disadvantage is having to disable Client-side validation on some browsers. Jon said that it required a different naming convention, but it doesn't.
But, it got me thinking that if a naming convention were required -- say a certain prefix for each section -- then an even simpler solution would be possible. And it doesn't require disabling anything.
Here's how a better solution works:
private Boolean ValidateSection(String prefix) {
foreach (Object v in Page.Validators) {
// Ignore the "System.Web.UI.WebControls." part
String valType = v.GetType().ToString().Remove(0,26);
switch (valType) {
case ("RegularExpressionValidator"):
System.Web.UI.WebControls.RegularExpressionValidator v1 =
(System.Web.UI.WebControls.RegularExpressionValidator)v;
if ((v1.ID.StartsWith(prefix)) && (!v1.IsValid)) return false;
break;
[... other cases left out, get the download below ...]
default:
throw new Exception(
"Unknown validator type:",v.GetType().ToString() );
break;
}
}
return true;
}
The code runs through the Validation controls in a page, which are neatly found in one place: Page.Validators. For each, check the prefix against the one passed in and the validity of the control. One false invalidates the works.
Then instead of calling Page.IsValid or the lengthy block required in my earlier blog, you can use:
if (ValidateSection("Client")) {}
Sure it requires giving validation controls certain prefixes (“Client*” in the above example), but that's something I usually do per section anyway. Simple, eh? Enjoy!
Get the full ValidateSection method here.
This is a pretty simple block of code. There are other ways to export a CDF, this works for me.
<%@
page language="C#" enableviewstate="false" %><%@
import namespace="System.Data" %><%@
import namespace="System.Data.SqlClient" %><script runat=
"server">private void Page_Load(object sender, System.EventArgs e){
String strSelected = Request.QueryString["ID"]; DataSet dstClientList = MyNameSpace.Customers.GetCustomers(strSelected );
DataTable dt = dstClientList.Tables[0];
System.Text.StringBuilder sbHead = new System.Text.StringBuilder();
System.Text.StringBuilder sbBody = new System.Text.StringBuilder();
Object[] columns;
// Write the Column Names
foreach (DataColumn dc in dt.Columns) {
sbHead.Append(dc.ColumnName);
sbHead.Append(",");
}
if (sbHead.Length > 0) sbHead.Remove(0,1).Append("\r\n"); // Write the Data Rows
foreach (DataRow dr in dt.Rows) {
columns = dr.ItemArray;
foreach (Object column in columns) {
sbBody.Append(column.ToString());
sbBody.Append(",");
}
if (sbBody.Length > 0) sbBody.Remove(sbBody.Length - 1, 1)._ Append("\r\n");
}
Response.Clear ();
Response.Buffer = true;
Response.ContentType = "text/plain";
Response.AddHeader ("Content-Disposition", _
"inline;filename=cdf.txt");
this.EnableViewState = false;
Response.Write(sbHead.ToString());
Response.Write(sbBody.ToString());
Response.End ();
}
</script>
Get the above in a block that's easy to cut and paste (MSIE users should right-click and use Save Target As...), it also contains the improvements suggested in the comments. Enjoy!
I built a DataGrid with an EditTemplate to edit records and a FooterTemplate to Add records. I made the footer invisible and put a ButtonLink just below the grid to expand the footer. Then I made an if (IsValid) {}
section around my code to Update a record, and another around my code to Add a record.
The IsValid around the Update code didn't work. The IsValid around the Add code worked fine. This was because the Edit section simply didn't exist while Adding. However, though the Add section was invisible during an Edit, it still existed, and its controls were validated along with the Edit controls during an Update event.
The solution was to use control-level rather than page-level validation. IsValid by itself is equivalent to Page.IsValid. You can also do a FindControl and get the IsValid results of an individual validation control. This is the sort of C# code I used (if you get a crazy gap below, try widening your browser, worked for me):
public void dgCustomer_ItemCommand(Object sender, System.Web.UI.WebControls.DataGridCommandEventArgs e)
{
switch (e.CommandName)
{
case "cmdAddNewCustomer" :
RequiredFieldValidator valNewFirstNm = ((RequiredFieldValidator)e.Item.FindControl("valNewFirstNm"));
RequiredFieldValidator valNewLastNm = ((RequiredFieldValidator)e.Item.FindControl("valNewLastNm"));
RegularExpressionValidator valNewDoB = ((RegularExpressionValidator)e.Item.FindControl("valNewDoB"));
if (valNewFirstNm.IsValid && valNewLastNm.IsValid && valNewDoB.IsValid )
{
/* Code to add a new Customer */
}
break;
default:
break;
}
}
I applied similar code to both the Add and Edit sections even though it was only the Edit that caused trouble. That way if I add another form to the page (perhaps another editable grid), its validation controls won't interfere with these, provided I apply control-by-control validation to it as well.
If this becomes a frequent exercise, it would make sense to build a method that could parse through the children of a given control (like the Footer section or an embedded Panel), look for Validator controls and return their IsValid results ORed together. If anyone out there builds it first, send me a copy!
The two most frequently asked questions in I.T.:
1) How long will it take?
2) How much will it cost?
Jim McCarthy has it right by saying in essence, "anyone who says they knows in advance how long a software project will take is lying."
A good specification makes these easier to answer, but most outsourced I.T. projects, and even a few developed internally, do not allocate separate budgets for specification and implementation. Too often the process is to put out a Request-for-Proposals (RFP) with a vague description of the project goals, expecting complete specs in response. No wonder small I.T. shops have such a hard time making ends meet, most spend the bulk of their time architecting free specs.
This causes a massive amount of friction in the industry. A reason that large I.T. shops charge (what seem to be) disproportionately high amounts is that they have CFOs who look out for the bottom line. And the bottom line is that the cost of writing "free" specs must be recovered by the services that are billable.
Planning and Implementation are Separate Projects
Specification and coding are two separate projects that should be contracted separately. The specification should be as loosely coupled from the implementers as possible; meaning that a good spec should be possible to execute effectively by any reasonably skilled programmer or team.
The construction industry has it right, the architect is not the builder. The architect works with the builder, but they may be entirely different companies. When this happens in I.T. we'll have a mature industry.
More Posts