Archive for June, 2008

June 27, 2008: 9:50 am: Dan.Net, Programming

About 18 months ago we decided to make the jump from the standard Visual Studio Windows Forms toolset to Infragistics’ NetAdvantage.

The decision was motivated primarily by frustration with the limitations of the DataGridView.  We use grids all over the place in our application, and it is important to users that they be able to organize the data by grouping it, filtering it and reordering the columns.  It seemed a waste of time to build our own extensions to DataGridView to provide these capabilities when there were so many 3rd party tools that had already done that work.  After trying out a few different packages, I decided that the Infragistics toolset had the best combination of features, price and learning curve.

Of course, the downside to using any 3rd party tools is the dreaded “lock-in” — as you build code around those tools it becomes harder and harder to drop the tool if you no longer like it.  Fortunately we haven’t had much reason to regret the decision so far. NetAdvantage has proven to be quite stable, and the support that Infragistics offers through their Knowledge Base and, in particular, their Forums means that we haven’t been stuck for long on any programming problems.

There are, however, a number of areas where the best way of implementing a feature using NetAdvantage isn’t obvious.  So, I thought it might be helpful to fellow Infragistics users to publish a series of articles sharing some coding tips.

One caveat: we haven’t kept up with Infragistics’ quarterly updates to their product, so these tips are based on  NetAdvantage 2007 Version 1.  It’s possible that workarounds are not needed with more recent versions, though the other tips are probably still valid:

Row Filters

NetAdvantage includes a relatively sophisticated runtime GUI to allow users to enter values or formulas for filtering a column.  The options even include regular expressions, as shown in the screenshot, though admittedly most users (and, sadly, many developers) have no idea how to make use of them.  

All of these same filters are also available to your code.  For example, to provide the user with the ability to toggle between a simple view and a detailed view of the data, it is often easiest (and fastest) to load all of the details into the grid, then use a filter to hide all of the detail rows when the user selects the “simple view” option.  If there isn’t a column which distinguishes the detail rows that you want to filter out, just add one to the underlying List — your code can filter on columns that aren’t even visible in the grid!

Here’s a generalized method for setting a filter which displays only rows which contain specific values in a specific column:

public static void Infragistics_FilterSetValues(UltraGridBand band, string strColumnName,
object[] strValues)
{
band.ColumnFilters[strColumnName].ClearFilterConditions(); // clear any previous filter
band.ColumnFilters[strColumnName].LogicalOperator = FilterLogicalOperator.Or;
for (int i = 0; i < strValues.Length; i++)
{
band.ColumnFilters[strColumnName].FilterConditions.Add( FilterComparisionOperator.Equals,
strValues[i]);
}
}

To prevent the user from changing the filter on a particular column:

band.Columns["StrMarketCode"].AllowRowFiltering = DefaultableBoolean.False;

To prevent the user from changing the filter on any column of the grid:

band.Override.AllowRowFiltering = DefaultableBoolean.False;

If you allow users to filter the rows themselves, then you may need to know when they’ve changed the filters so that you can adjust any related data on the screen.  For example, if displaying some summary statistics in a separate grid you’ll need to update the summary when the user filters out some data .

A good event to use is AfterRowFilterChanged.  You can call the following method to get the new filters for the column:

public static void Infragistics_FilterGetValues(UltraGridBand band,
string strColumnName, out FilterComparisionOperator[] filterOperators, out object[] filterValues)
{
FilterConditionsCollection filters = band.ColumnFilters[strColumnName].FilterConditions;
filterOperators = new FilterComparisionOperator[filters.Count];
filterValues = new object[filters.Count];
for (int i=0; i < filters.Count; i++)
{
filterOperators[i] = filters[i].ComparisionOperator;
filterValues[i] = filters[i].CompareValue;
}
}

Turning off unwanted grid features

The first time you drag an Infragistics UltraGrid onto a window and fire it up, you’ll be surprised to find that users can:

  • see every column in the underlying Data Source, including child Lists or Tables (which appear as drill-down rows in the grid)
  • split the grid into regions (similar to Excel’s Split feature)
  • prevent selected columns from scrolling (similar to Excel’s Freeze feature)

These features are all enabled by default.  You can easily use the Designer GUI to turn them off, but in order to enforce a consistent set of rules for all our grids I found it preferable to write some common code that sets some global defaults:

//default is to not allow user to split into multiple sections
grid.DisplayLayout.MaxColScrollRegions = 1;
// don’t allow multiple row scroll regions either
grid.DisplayLayout.MaxRowScrollRegions = 1;
// set number of bands (drill down levels)
grid.DisplayLayout.MaxBandDepth = intNumBands;

if (intNumBands == 1)
grid.DisplayLayout.ViewStyle = ViewStyle.SingleBand;
else
// only display the expansion indicator when child bands exist
grid.DisplayLayout.Override.ExpansionIndicator = ShowExpansionIndicator.CheckOnDisplay;

// scroll grid contents immediately by default
grid.DisplayLayout.ScrollStyle = ScrollStyle.Immediate;
// change background of advanced filter cells that contain values (to catch the user’s eye)
grid.DisplayLayout.Override.FilterCellAppearanceActive.BackColor = Color.PaleGoldenrod;

// set band-level settings for each band
for (int intBandCounter = 0; intBandCounter < intNumBands; intBandCounter++)
{
// support multiple column sorting by default
grid.DisplayLayout.Bands[intBandCounter].Override.HeaderClickAction = HeaderClickAction.SortMulti;

// prevent push-pin from appearing in header
grid.DisplayLayout.Bands[intBandCounter].Override.FixedHeaderIndicator = FixedHeaderIndicator.None;

// don’t use Row Layout as default — otherwise can’t set visible position of columns
grid.DisplayLayout.Bands[intBandCounter].UseRowLayout = false;
}

Saving Grid Settings

With so many grid customizations available to the user, it’s important to save the settings so that the user doesn’t have to redo the customizations each time they run the application. Fortunately, Infragistics provides methods for serializing and deserializing all of the grid settings using XML.  We use the .Net AppSettings class to save the XML strings in a configuration file.  (Incidentally, Infragistics provides most of its grid features in the selection list of its combo box too, and this same code can be used to save settings for an UltraCombo control).

The following code saves the settings — we do this automatically in the Form_Closing event. (Note that I embed the application version number and the user’s language setting at the beginning — the calling code may need to make some adjustments if these settings are loaded by a later version or in a different language).

MemoryStream stream = new MemoryStream();
ourGrid.DisplayLayout.SaveAsXml(stream);
clsGlobal.Infragistics_SaveSettings(“ourGridLayout”, stream);




public static void Infragistics_SaveSettings(string strComponentID, MemoryStream stream)
{


byte[] bytLayout = stream.ToArray();
ASCIIEncoding ascEncoding = new ASCIIEncoding();
//string strSettings = ascEncoding.GetString(bytLayout);
//add a prefix with the product version and current language
string strPrefix = “!!” + Application.ProductVersion + “, ” +


clsGlobal.AppSettings[strComponentID] = Array_Concatenate(bytPrefix, bytLayout);

}

In the Form_Load event, we call the following method for each of the components.  Note that I use the version and language settings that I stored in the configuration file to warn the calling code if either has changed.  This is particularly important for the language, since all of the header captions and other displayable text are embedded in the settings string.  If the user has switched to another language (unlikely for real users, but common for our salespeople), the calling code should update all of this text with the correct language.

public static bool Infragistics_LoadSettings(string strComponentID, out MemoryStream stream,
out bool blnSettingsOK, out bool blnCaptionsOK)
{
// out parms must be initialized
blnSettingsOK = false;
blnCaptionsOK = false;
// load saved grid layouts
byte[] bytLayout = new byte[0];try
{
bytLayout = (byte[])clsGlobal.AppSettings[strComponentID];
}
catch (System.Configuration.SettingsPropertyNotFoundException)
{
Debug_WriteToLog(“Infragistics_LoadSettings – property not found: ” + strComponentID);
stream = new MemoryStream();
return false;
// ignore property not found — it won’t exist the first time the program is run
}if (!Object_IsNullOrBlank(bytLayout))
{
int intOffset = Array.IndexOf(bytLayout, Byte.Parse(“254″));
if (intOffset < 0)
{
// shouldn’t happen – not a valid layout
stream = new MemoryStream();
return false;
}

// strip off the prefix
ASCIIEncoding ascEncoding = new ASCIIEncoding();
string strPrefix = ascEncoding.GetString(bytLayout, 0, intOffset);
if (strPrefix.StartsWith(“!!”))
{
int intEndOffset = strPrefix.IndexOf(“!!”, 2);
if (intEndOffset > 0)
{
strPrefix = strPrefix.Substring(2, intEndOffset – 2);
string[] strParts = Array_FromDelimitedList(strPrefix);
if (strParts.Length >= 1)
{
blnSettingsOK = (strParts[0] == Application.ProductVersion);
}
if (blnSettingsOK & (strParts.Length >= 2))
{
blnCaptionsOK = (strParts[1] == NLS.ThisLanguage);
}
}
}

stream = new MemoryStream(bytLayout, intOffset + 1, bytLayout.Length – intOffset – 1);
return true;
}
else
{
stream = new MemoryStream();
return false;
}

}

Incidentally, don’t forget that users may make a change to the grid settings that they don’t want to keep (and they may do it inadvertently without noticing at the time), so be sure to give users a way to revert to the default settings for your application.

June 2, 2008: 5:15 pm: Dan.Net, IBM Universe (U2), Programming

My last post described various attempts to improve the response time of a large SELECT statement from a very large table in IBM Universe. I’m pleased to report that we finally found the trick, though it turned out to having nothing to do with .Net and very little with UniOLEDB.

As mentioned at the end of my last post, my effort to optimize the SELECT statement by changing the .Net code had topped out around the 5 minute mark, well short of the 45-second response time that a query written in Universe Basic could achieve. I couldn’t see any way that .Net and UniOLEDB could replicate the technique used by the Universe Basic code, which involved building a native List structure from the keys, using a native Universe Basic command to extract the matching records from the large table, then running the SELECT on just the extracted records.

Fortunately my .Net code didn’t have to replicate this technical trickery; it could just stand back and let the Universe Basic code do the heavy lifting. Thanks to the fact that Universe Basic subroutines can return resultsets to .Net, I simply passed my SELECT statement to our Universe Basic code, let it do its thing, and processed the results.

OleDbCommand cmdTest = new OleDbCommand(”CALL GET.RECORDSET(‘SELECT yada yada yada’)″, conn);
cmdTest.CommandType = CommandType.Text;
dataAdapter = new OleDbDataAdapter(cmdTest);
dataAdapter .Fill(dataSet, “Table1″);

From the point of view of the .Net application this change is totally transparent — the contents of the resultset are exactly the same as when my code submitted its own SELECT statement. The Universe Basic subroutine had to be modified to extract the table name and key list from the SELECT statement, and it has since worked flawlessly in our testing, without stumbling over any rows or column types.

This turned out to be quite a good overall solution: virtually no .Net code changes, about half-a-day of coding for the Universe Basic programmer, and since the .Net code still uses SQL we maintained close compatibility with other databases. If we were to replace Universe with a relational database, I would just have to change a couple of lines of code in the .Net application.

The performance improvement when reading large amounts of data from the very large table was dramatic: from a previous best of 310 seconds down to just 45. Performance is slightly poorer than before on smaller SELECTs, but not enough to be noticeable to the user.

I’m a little disappointed that we couldn’t find a solution that only used .Net code. We already had to replace our INSERTs, UPDATEs and DELETEs with Universe-specific code as described in this post, and now the last facade of seamless SQL support has fallen. However, it was probably naive to think that we could transparently run SQL statements on a non-relational database like Universe. When in Rome, do as the Romans do. An appropriate axiom, considering that the Universe database is almost ancient history!