Friday, May 27, 2005

 

How to Generate PDF Files (For Free)

This solution is for Windows. Getting this to all work together isn't easy, but it can be very rewarding.

Do the following, in this order:
  1. Install GhostScript
  2. Install GSView
  3. Download and unzip the Adobe PPD files
  4. Install Adobe Universal PostScript Windows Printer Driver and point it to the ppd file from step 3.
  5. Install RedMon and follow the instructions to configure it for GhostScript.
Then you can just print to the new printer you created from any program in order to create a PDF file.
 

Hiding Columns in a DataGrid

I needed to be able to let the user hide and unhide columns from the DataGrid control. This was pretty easy, as I just added a LinkButton to each of the HeaderTemplates:
<SUP><asp:linkbutton runat="server" CommandArgument="3"
CommandName="HideColumn">x</asp:linkbutton></SUP>
where the CommandArgument is set to be the column's index (starting at 0). Then inside the MyDataGrid_ItemCommand function, I just set MyDataGrid.Columns(e.CommandArgument).Visible = False when e.CommandName = "HideColumn".

To allow someone to show the column again, I just created a DropDownList of all of the hidden columns, which can be found by looping through MyDataGrid.Columns and adding a column to the list if .Visible = False. Since I not only provided a HeaderTemplate for each column, but also specified the HeaderText, I can use the .HeaderText property in the loop to add the name of the column to the DropDownList.
 

Maintaining Control Values in DataGrid Header

The previous post described how I have a DataSet which the DataGrid binds to in order to display the data. The problem with this approach is that each time the .DataBind() method is called, the DataGrid recreates all of the controls from scratch. If controls are placed in the header or footer, then those controls as well are replaced, despite the fact that they are not databound. In order to get around this problem, I needed to save the values of all of the controls before the .DataBind() call and then restore those values after the .DataBind().

Saving the value of a TextBox inside the header is a pain and the code is really ugly:
MyTextBoxText = CType(MyDataGrid.Controls(0).Controls(0).FindControl("MyTextBox"), TextBox).Text
Doing this for each textbox leads to some very inelegant code. Instead of doing this, I desired a general function that would save the value for anything that it finds. I ended up with the following function:
Private Sub SaveControls(ByRef ctrl As Control)
Dim con As Control
'Loop through every control inside the one given
For Each con In ctrl.Controls
If con.ID <> "" Then
If TypeOf con Is TextBox Then
ViewState("Saved" & con.ID) = CType(con, TextBox).Text
ElseIf TypeOf con Is CheckBox Then
ViewState("Saved" & con.ID) = CType(con, CheckBox).Checked
ElseIf TypeOf con Is Panel Then
ViewState("Saved" & con.ID) = CType(con, Panel).Visible
ElseIf TypeOf con Is BaseValidator Then
ViewState("Saved" & con.ID) = CType(con, BaseValidator).IsValid
End If
End If
SaveControls(con) 'Now look inside this control for more controls
Next
End Sub

This recursive function takes a control and looks for useful controls inside all of the children of the control. It saves the important values of each of the TextBox, CheckBox, Panel, and BaseValidator controls so that they can be restored later. Only the controls of those types with a specified ID will be saved. I call this function as follows
SaveControls(MyDataGrid.Controls(0).Controls(0))
to save the values of each of the important controls in the header of the DataGrid. (See the previous posts for how to do the footer).

Then I wrote a function called LoadControls which is the exact same except the assignment statements go the other way. To use this effectively, I do things in the following order:
  1. SaveControls (for header, footer, and edit row)
  2. DataBind
  3. LoadControls (for header, footer, and edit row)
Thus, any values stored in any header, footer, or edit row controls will be maintained, despite the fact that the DataGrid was rebound.

I had it set up so that not only were there TextBox controls in the header and footer, but each of them also had a validation control associated with them. If the validation control (server side) rejected the input, then the IsValid was set to False, as one should expect. However, I had the problem that the ValidationSummary control did not show up on the page. There were errors, but the errors were not visible. I finally tracked down the problem to the fact that the validators were reset to their original state during the .DataBind() method. So, I had to include the BaseValidator class in my SaveControls and LoadControls functions in order to remember if each one was valid or not. Then because the page was rendered with the validators in the correct state, everything worked out and the ValidationSummary control knew which messages to display.
 

Maintaining Data with a DataGrid

In my program, I need to have a DataGrid display some data from the database and then let someone enter in the data or modify or delete the existing data. However, I don't want the changes to be made to the actual database until the user has completed all of their edits. Thus, each change in the DataGrid needs to be made to some local copy of the data, rather than the actual database. The problem is that the DataGrid control does not maintain its state over roundtrips to the server.

To solve this I just created a variable of the type DataSet. Initially, I read the information from the database into the DataSet variable and then bind the DataGrid to the DataSet. This makes the information show up in the DataGrid. Well, the DataSet is just a variable on the page and will be destroyed with the page. Thus, I have to save the DataSet variable in the ViewState of the page.

In Page_Load:
  • If IsPostBack is false (first page load), then go set up the DataSet either by grabbing actual data or just the schema from the actual database.
  • If IsPostBack is true, then I do MyDataSet = CType(ViewState("MyDataSet"), DataSet)
In Page_PreRender:
  1. Assume that all modifications to the MyDataSet variable have been made according to the OnClick or other events.
  2. Bind the data to the DataGrid: MyDataGrid.DataSource = MyDataSet and then MyDataGrid.DataBind()
  3. ViewState("MyDataSet") = MyDataSet
The problem with this approach is that the data is stored twice in the page sent to the user: once to display it in the table, and once encoded in the ViewState variable. Oh well.

Thursday, May 26, 2005

 

Finding Controls in a DataGrid

One neat thing to do when working with an ASP.NET DataGrid control is to put some controls (TextBoxes, Links, CheckBoxes, Validators, etc.) in the header and footer of the DataGrid. It is easy to add these things to the grid using a TemplateColumn. However, finding these controls is difficult in the code. Even if you add a control and give it a unique name that does not match the name in any data bounded row or anywhere else on your page, ASP.NET cannot provide the automatic hookup to the controls like it can with a normal TextBox or other control. This is probably because the control lives inside of the DataGrid and is not visible to the Page as a whole.

The secret to finding these controls is knowing where to look. For controls inside a bounded row (like when a row is in edit mode), it is easy to find the appropriate DataGridItem and then use the FindControl method. The problem is that the header and footer are not contained in the DataGrid's DataGridItemCollection. Instead, I have to look inside the rows of the table that the DataGrid uses to display the information. Basically, the method is this:
MyDataGrid.Controls(0).Controls(row).Controls(col).FindControl("MyTextBox")
This looks in the col-th column of the row-th row of the 0-th table of the DataGrid to find the textbox named MyTextBox. Remember that all indexing starts at 0. So, for the row, 0 means the header row, 1 through MyDataGrid.Items.Count are the data bounded rows, and MyDataGrid.Items.Count+1 is the footer.

It seems, however, that you do not actually need to specify the column. Just finding a control inside the row will work. So, to get the control named "MyTextBox" out of the header, use the following:
MyDataGrid.Controls(0).Controls(0).FindControl("MyTextBox")
And to get the control named "MyTextBox" out of the footer, use the following:
MyDataGrid.Control(0).Controls(MyDataGrid.Items.Count+1).FindControl("MyTextBox")

 

TextBox Validating Number of Decimal Places

So one of the requirements that I had was to validate that my TextBox controls in ASP.NET accept only positive numbers with 3 or fewer decimal places. It is easy to use one of the CompareValidator controls to find out if a given entry is an Integer or a Double, as that is well documented. However, neither of these validates for 3 or less decimals; only for 0 or unlimited decimals. So, I just used a RegularExpressionValidator with the following regular expression:
-?((\d*\.\d{0,3})|(\d+))
After that was created, the requirements for the program changed and what was needed was to allow the user to enter in any number of decimal places they wanted, but then the number entered had to be trimmed to have a maximum of only 0 or 3 decimal places, depending on the TextBox. Of course other TextBox controls accept plain text and do not need to be trimmed.

I despise code that uses large Select Case statements or large amounts of If statements, so I wanted a solution that was dynamic. This was the easiest thing that I could think of:
  1. To any TextBox that needs its value trimmed, add DecimalPlaces="3" (or however many you need) in the asp:TextBox tag for any textbox that you need trimmed.
  2. In the code, check for each TextBox if myTextBox.Attributes("DecimalPlaces") <> "".
Now I ran into the problem of automatically finding all of the textboxes so that I can do the trimming. I did this with a search through all the controls on a page or some smaller container on your page. The following recursive code does this:
   Private Sub TrimTextBoxNumbers(ByVal ctrl As Control)
Dim con As Control
Dim txt As TextBox
Dim value As Double
Dim decplaces As Integer
'loop through each child control
For Each con In ctrl.Controls
'check if this control is a textbox
If TypeOf con Is TextBox Then
txt = CType(con, TextBox)
'check if this textbox specifies a number of decimal places
If txt.Attributes("DecimalPlaces") <> "" Then
'try to trim to the given number of decimal places
Try
decplaces = CInt(txt.Attributes("DecimalPlaces"))
Dim reg As Regex = New Regex("-?((\d*\.\d{0," & decplaces & "})|(\d+))")
Dim m As Match = reg.Match(txt.Text)
If m.Success Then
txt.Text = m.Value
End If
Catch 'just abort if there is an error trimming
End Try
End If
End If
TrimTextBoxNumbers(con)
Next
End Sub
I just call it by something resembling this: TrimTextBoxNumbers(Page). Notice that by using a regular expression instead of converting to a decimal, shifting, flooring and shifting, this maintains the zeros at the right to indicate significant digits. That is ".2504" will be changed to ".250" and not ".25" if there are 3 decimal places required.

Wednesday, May 25, 2005

 

Automatic numbering of the columns in a DataGrid

For my ASP.NET DataGrid problem that I mentioned earlier, I will start off by saying how I got the rows to automatically number themselves. I know this is posted lots of places elsewhere, but I'll put it here quickly anyway.

I just added a column to the DataGrid that looks like this:
  <asp:TemplateColumn HeaderText="No.">
<ItemTemplate>
<asp:Label runat="server" Text='<%# DataBinder.Eval(Container, "ItemIndex")+1 %>'>
</asp:Label>
</ItemTemplate>
<FooterTemplate>
<asp:Label runat="server" Text='<%# MyDataGrid.Items.Count+1 %>'>
</asp:Label>
</FooterTemplate>
<EditItemTemplate>
<asp:Label runat="server" Text='<%# DataBinder.Eval(Container, "ItemIndex")+1 %>'>
</asp:Label>
</EditItemTemplate>
</asp:TemplateColumn>

 

A complicated DataGrid for ASP.NET (VB)

I've spent this past week working on a project which involved making a complicated DataGrid control. I will try to make a few posts about what I did, but basically, I needed the following features:
  • A textbox to always display in the header of some columns so that the user can enter their own column title.
  • Not only must one be able to edit and delete a row, but one must be able to add a row using text boxes.
  • The changes to the data (new rows, updated rows, deleted rows) must not actually be made in the database until all of the edits are complete.
  • The rows should automatically number themselves.
  • Any column can be hidden or unhidden, whenever the user wants. Values store in a hidden column should not be destroyed.
  • And for the most difficult requirement: in each column header, there must be a checkbox which (when checked) makes 2 additional textboxes appear in the header where one can enter minimum and maximum tolerances for the values stored in the column.
  • When a row is added or updated, the values in each column must be checked against these tolerances to make sure they are valid. They also must be formatted correctly as either an integer or a number with 3 or less decimal places (depending on the column).
  • All of the values entered should be validated using the built in ASP.NET validation controls so that a single message can summarize all of the errors.

This page is powered by Blogger. Isn't yours?