But, as the saying goes - publish or perish - so here I go again but this time in a hopefully better protected environment.
This is the first article in what I hope will be a series on creating lightweight reusable code, and although I have no ambitions about making this a widespread toolkit, I hope it may be of use to someone.
It certainly is of use to me :)
Reusable Grid View - part 1
Did you ever write your data to a logfile to check them out? Maybe you wrote them to a string list and showed them in a Memo? Maybe you even stuffed them into a TStringGrid and found out the hard way how expensive that can be if you have a lot of data and how poorly the numeric data display as a string. You often end up having to tweak and re-tweak the formatting to make the dump readable.
After having to implent similar gridviews in three different tasks back to back and seeing more on the horizon, I decided that I wanted to make it easier to quickly put together a gridview for a specific set of data.
There are a lot of nice custom grid controls out there, but they all come at a price - either in the form of purchase, or in the form of having to invest time in writing glue to get your data in there, or even with cost of pure payload. Some grids are so function rich that they almost are a system in their own right.
Totally ignoring the not-invented-here syndrome warning lights, I decided to make my own little quick grid viewing kit.
- Make it ignorant to the underlying data structure
- Avoid having to adapt your data structure to this specific viewer
- Avoid duplicating data
- Make it easy to configure
- Make it easy to extend and modify
- The underlying data must have a known number of rows
- Each column will be have one type of content
- Make it reasonably stupid
Nice to haves: Well, maybe a few :)
Isolating the data and the viewer
In a way, the top three goals are in conflict. How can you access content if you don't know the format of the content? If you don't know the format, don't you have to convert it somehow, and don't you have to store that conversion? If you don't adapt the data to the viewer, how can you make a generic viewer?
Those that work with databases and web probably already know the answer: Base the solution on the Model View Controller pattern.
We need to find an effective way to encapsulate the data and present that encapsulation to the grid control. Let's briefly look at the options for encapsulating the data here.
- Inheritance - Not good - I don't want to force our data to be based on a type specific to my viewer
- Aggregation - I can make a class that knows both the dataset and the viewer
Aggregation allows me to write a base class that incorporate all the knowledge about the grid side of the problem, leaving only the need to implement a reasonably thin data access layer.
Since I don't want to duplicate content into the grid, I need to find a good way to retrieve what I need on demand and to massage it into the format I need to display it on the fly. The solution is to use something along the lines of a Visitor pattern. Since grid views typically have static columns - ie all fields in a column is of the same type - I will make the visitor look at the data from a column perspective, identifying which row it is visiting. This will be the column visitor (aka TGridViewColumn). I will create different type of grid view columns for different type of data. String, boolean, integer, float, date, and even an image one - which basically is just the integer visitor with a custom draw.
The Grid Controller
To avoid having to recreate the grid from scratch, I am going to attach myself to a TStringGrid. I am not going to inherit from it, just wrap it and inject myself where it counts. Why no inheritance? Well - I might want to move this code to a more capable grid at a later point in time. Also, by wrapping it - I can attach myself to a standard grid that is dropped in at design time without having to make a new component. This will be my grid controller (aka TGridViewController).
My key point for doing all this is to be able to display the data as best as I can. Hence, I will replace the TStringGrid cell drawing routine with my own. Firstly, I need a new string drawing routine. I'm going to add center and right justify, so that it will be easy to convert a number to a string, and then draw the string right justified. Fortunately, TStringGrid.OnDrawCell allows me to do inject my custom draw without severe tampering with TStringGrid.
I wasn't going to put in a lot of bells and whistles in my reusable grid view, but one thing that I thought would be useful was the abilty to control row color and column color. If I am viewing some sort of log - it would be nice if I could highlight a row with a problem, or a row matching some sort of highlight criteria. Again - I want to give the controller the same ability to tie together the underlying data and the color changes, so I will use the same visitor model for retrieving the customized color.
I am splitting my custom draw routine between the grid controller and the column visitor. This allow me to match the drawing customisation to the data access and conversion, both from a row as well as a column perspective. The column draw routine is actually split in a generic housekeeping outer draw routine, and a specific detail inner draw routine.
Initially I thought I would write descendant classes for each GridViewColumn per implementation. I.e. for each new view, I would write a class per column, inheriting the class from the appropriate column type (string, mumber etc). But then I realized that would mean quite a few extra lines of code to set up a grid.
So- how about using a pluggable visitor routine instead? That way I could actually implement the column data type column only once - and only add a single visitor routine in the custom GridViewController to retrive the actual value that is to be translated and drawn. The extra call overhead isn't really signficant since we are not typically talking several thousands of calls, but usually just a few hundred. Hence the GridViewColumn now has it's own property which hold the function used to retrieve the cell value from the GridViewController.
TGetTextMethod = function(const row:Integer):String of object;
TGetDoubleMethod = function(const row:Integer):Double of object;
That's all for the first part. More to come.