Wednesday, May 26, 2004

DataGrid Edits With Paging & Sorting

If you've used the ASP.NET DataGrid, then you've probably used its sorting capabilities. It's very easy: just declare the sort column name in the item template and set DataGrid.AllowSorting = true. Similarly, it's easy to turn auto-paging on by setting DataGrid.AllowPaging=true.

Things get sticky, however, when you start allowing edits within the DataGrid. After adding an EditItemTemplate to each TemplateColumn that you want to be editable, and setting the DataGrid.EditItemIndex, everything appears to be just fine. One problem is that paging during edit isn't a valuable capability for the risks (if a row on page 1 is in edit, and the user moves to page 2, what is the state of the edited row? Does the user expect that the edits have been persisted? What should happen when the user now want to edit an item on page 2?) Certainly there are cases in which paging during edits is valuable, but it generally is not worth the required coding, testing, etc., so it should be turned off during editing.

So, when entering edit mode, just set DataGrid.AllowPaging=false. Easy enough, right? Wrong! This introduces a very nasty bug. The bug occurs when the user attempts to edit a row on a page other than page 1. Your code merrily goes along setting the EditItemIndex to the value passed to your EditCommand (DataGridCommandEventArgs.Item.ItemIndex)
Your event handler would look something like this:


private void MyDataGrid_EditCommand(object source, DataGridCommandEventArgs e) {
    //...
    myDataGrid.EditItemIndex = e.Item.ItemIndex;
    myDataGrid.AllowSorting = false;
    myDataGrid.AllowPaging = false;
    BindGrid();
    //...
  }


The unfortunate effect, however, is that the item in edit will be the item at e.Item.ItemIndex of page 1! Oops!

The solution I use is to make the pagers invisible by:

    //...
    myDataGrid.PagerStyle.Visible = false;
    //...

Obviously, you need to turn visibility back on during the code for canceling or committing the edits.

As an aside, it is also important to disable sorting during edits. If EditItemIndex is 4, then the fifth item will be under edit regardless of the underlying data. So, when the underlying data changes position (as sorting would do), then the wrong item is editable. The easiest way to solve this is to disable sorting (see the AllowSorting property usage above). Otherwise, you'll have to update EditItemIndex after the sort. If paging is also involved, then you'll have even more headaches to deal with (e.g., the item under edit is on page 1; the user sorts; now the correct item to edit is on page 3. Your code has to get the user to the right grid page with the right item under edit. Not impossible, but....)


Tuesday, May 25, 2004

Better DataGrid insert method

After spending way too much time yesterday figuring out how to get a DataGrid footer's contents, this morning I found something that is much easier in the right situation.

For some reason Adding a New Record to a DataGrid from 4GuysFromRolla made its way into my Google search. Their solution is to use an Add button in the footer row, hook into the DataGrid's ItemCommand event, and redirect to an "insert into...." method when the command is the Add button.

In my current case, the Add button needs to be outside the DataGrid, so I can't try this solution now. However, I will be very interested to use it next time because it is a much, much easier mechanism for collecting the user's input in the footer controls. The ItemCommand event includes a DataGridCommandEventArgs parameter which provides easy access to the controls. For example:

   TextBox nameCtrl = e.Item.FindControl("nameTextBox")).
   string name = nameCtrl.Text;

Monday, May 24, 2004

Inserting data via a DataGrid footer row

(a.k.a.: How to retrieve footer row contents on postback)
(a.k.a.: DataGrid's Nasty Underbelly)

Adding a footer row to a DataGrid is a cake-walk. Getting to the footer's contents on postback is a whole different ball of wax!

Why should you want to get the contents? Because it provides a very elegant mechanism for inserting a new row into the DataGrid (and the DataSet behind it, etc.) Ok, so how can we make this work?

When debugging, you will find that DataGrid.Controls[0] is a DataGridTable. So you attempt to cast this in your code, but then you find that DataGridTable is not instantiable. What to do, what to do? If you can just get to the DataGridItem for the footer, then you can use FindControl to get each control in your footer. To cut to the chase, here is what you need to do:

  • Get a WebControls.Table object from the DataGrid
  • Use the Table to determine the index position of the footer row
  • Acquire the footer row as a DataGridItem
  • Use DataGridItem.FindControl to locate each control on the row, casting each appropriately

By way of an example:
   Table tbl = dataGrid.Controls[0];
   int idxFooter = tbl.Rows.Count - 1;
   DataGridItem gridItem = tbl.Rows[idxFooter] as DataGridItem;
   //Assuming a TextBox named UserName was defined in the footer template...
   TextBox name = gridItem.FindControl("UserName") as TextBox;

Now you can use the TextBox as usual! To insert the data to the back-end datastore (XML, RDBMS, etc.), I like to create a new row in the DataSet table and use a DataAdapter to update (using the InsertCommand in this case).

Wednesday, May 19, 2004

Crystal Reports -- Render vs. PreRender

Crystal (more specifically, Web.CrystalViewer) does some special stuff during its PreRender handler. When adding Parameters to the viewer, you need to do it before the control's PreRender. Otherwise, the rendered content will just be "Programming Error." I figured this out the hard way by creating a handler for Page.Render, and config'ing the Web.CrystalViewer within. After receiving the error message mentioned (and scratching my head for a while), I moved all the code to my own PreRender handler. By setting everything in PreRender, calling base.OnPreRender (to let the other controls handle PreRender, and not overriding Render, everything began to work.

The Love-Hate relationship continues...

Undoc'd exception on DateTime.Parse

The .NET Framework Class Library documentation for DateTime's Parse method identifies two exceptions that may occur: ArgumentNullException and FormatException. When using this method, however, I found that it also throws ArgumentOutOfRangeException for dates that have valid formats but are out of range. For example:

   DateTime.Parse( "1/1/20000" );    // oops! year = 20,000

will throw ArgumentOutOfRangeException.

Wednesday, May 12, 2004

Using Parameters With Crystal Reports

To use parameters with Crystal Reports, there seems to be two fundamental issues to deal with:
  • Create parameters on the report (.rpt)

  • Set parameters on the report viewer object before assigining the report.

For example, I have a report that needs to filter data based on a date range. So in the report I had to:
  1. Create the parameters, BeginDate & EndDate

  2. Using the Select Expert, apply the parameters to the selection criteria (this is actually very easy; once the parameters have been created, you can select them from the drop-downs in Select Expert).

  3. Drag and place the parameters onto the report. This step is not required, but I find that most reports need to represent how the underlying data was selected.

Now that the report has parameters, it must have some way of aquiring values for the parameters at run-time. This is the point at which I had the most trouble because I thought that the code should set the parameters on the report object at run-time. That doesn't work. It does work, however, if you add parameter values to the viewer object prior to handing the report to the viewer. The general method to follow is:
  1. Create one ParameterField object per parameter on the report.

  2. IMPORTANT: Set each ParameterField.ParameterFieldName property to the same name as the associated parameter in the report. If the names are not an exact match, then the report will not aquire the parameter value correctly.

  3. Add the each ParameterField to a ParameterFields object.

  4. Assign the ParameterFields object to the viewer's ParameterInfo property.

  5. Finally, assign the report to the viewer. (Actually the order of report and parameters assignment is not significant, but it is a little more logical)

Here's a simple C# code snippet from a web form's Page_Load event handler:


CrystalDecisions.Shared.ParameterField fld = new ParameterField();
CrystalDecisions.Shared.ParameterFields flds = new ParameterFields();

// Set begin date param
CrystalDecisions.Shared.ParameterDiscreteValue prmBeginDate =
new CrystalDecisions.Shared.ParameterDiscreteValue();
prmBeginDate.Value = new DateTime( 2004, 1, 2 );
// NOTE: the parameter name must match the report's parameter
// name exactly.
fld.ParameterFieldName = "BeginDate";
// Add the discrete param to the param field
fld.CurrentValues.Add( prmBeginDate );
// Add the param fld to the param fld collection
flds.Add( fld );

// Add the params to the viewer
// snippet info: crv is a class member declared as CrystalDecisions.Web.CrystalReportViewer
crv.ParameterFieldInfo = flds;

// Add the params to the viewer
crv.ParameterFieldInfo = flds;

// Now load the report
OverallEquipmentActivity2 rpt = new OverallEquipmentActivity2();
crv.ReportSource = rpt;


Just a Simple Development Log

The purpose of this blog to simple to help me remember all those little arcane settings, methods, etc. you have to deal with during software development. Frankly, the level of frustration generated by wrestling with Crystal Reports on my current project has driven me to create this blog.