Solving the Everyday Reporting Issues
With any line-of-business application at some point reports are likely to present developers with a problem. Good friend and longtime ComponentOne user, Dom Sinclair, told me about how he solves the common reporting issues developers face. Read below to discover his solution.
The trouble with reports is that end users like them. They like them a lot. After a while it doesn't take long for them to start sending little e-mails back to you , the developer, running along the lines of 'we like this report a lot, but it would be really nice if we had something similar that does such and such.'
Now unless you're a genius and can predict every single variant of each report that your end users might want (along with their attendant customizations) you are always going to find yourself being asked to develop new reports. Often as not the requests are actually quite reasonable and would serve to enhance the value of your product as a whole.
So the question now becomes: how can I provide a means of updating my reports without having to do any serious work within the UI that's displaying them? Over the years my own personal preference has been to have a centralized reports form, and I've gone this way for two reasons. Firstly, it avoids the need to have to build multiple report forms for different sections of an application (and the attendant nightmare of updating them), and secondly, it has allowed me to structure my reports in such a way that the end user can find what they want much more easily. As new report requests are made and implemented, updating the report viewer to reflect these changes requires no real work on my part.
This approach requires that you keep your report definition files in a separate sub directory of your main application folder, and that in turn you break up your report definition files into logical groups and save them in carefully named group files, which in turn end up in a carefully laid out directory structure.
Initially when the user opens up their reports they see a top level view.
Which can be expanded,
And expanded,
And when they select a report to view and possibly print they get a handy visual notification in the list of which report they selected. In this case the Sea Fish Levy Report.
So how do we do this?
We begin by creating our reports (obviously) and then saving them in a well defined file structure. The one that corresponds to what you see above is illustrated below.
Here we see the main application directory with a Reports sub directory. In turn that is further subdivided into different sections, which can in turn be subdivided (ad nauseam). Within each directory is ONE report definition file carefully named to reflect it's intended directory location and appended to that is the word 'Reports'.
With that done we need to create our report form. Let's start with a new windows form to which we'll add a split container.
In Panel1 we'll add a TreeView (and set its dock property to fill) and in Panel2 we'll do the same with a CPrintPreviewControl. We'll also add a C1Report control and an ImageList control.
In the properties box for the CPrintPreviewControl provide a short meaningful name, such as ppc, and the C1Report I'll call clr.
And as far as the basic form layout goes that will suffice.
Now for the code that makes it work. Open up the forms code view and replace what there is by default with the following:
Public Class Form1
Private rptDef As String
Private rptFile As String
Private ReadOnly Property RootDir() As String
Get
Return My.Application.Info.AssemblyName.ToString & "
Reports"
End Get
End Property
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles MyBase.Load
SetUpTreeView()
End Sub
Private Sub SetUpTreeView()
'This will create the tree node in the TreeView control
TreeView1.ImageList = ImageList1
'Add this as a root node
'this takes the form of (root node, the text to be displayed, the image to be displayed, the selected image to be displayed)
Dim lroot As TreeNode = TreeView1.Nodes.Add(RootDir, RootDir, 0,
0)
lroot.Tag = Application.StartupPath & "\\Reports"
RetrieveFolderListForTreeView(lroot.Tag.ToString,
TreeView1.Nodes(0))
End Sub
Private Sub RetrieveFolderListForTreeView(ByVal dir As String, ByVal parentNode As TreeNode)
Dim lfolder As String
Try
'Add folders to treeview
Dim lfolders() As String = IO.Directory.GetDirectories(dir)
If lfolders.Length 0 Then
Dim lfolderNode As TreeNode
Dim lfolderName As String
For Each lfolder In lfolders
lfolderName = IO.Path.GetFileName(lfolder)
lfolderNode = parentNode.Nodes.Add(lfolderName, lfolderName, 0, 0)
lfolderNode.Tag = lfolder
'for each folder that we find we want a list of reports in the
'report definition xml file that it contains
'and then we need to check to see if there are any sub folders in this folder by calling this routine again
PopulateReportView(lfolder, lfolderNode)
RetrieveFolderListForTreeView(lfolder, lfolderNode)
Next
End If
Catch lex As UnauthorizedAccessException
parentNode.Nodes.Add("Access Denied")
End Try
End Sub
Private Sub PopulateReportView(ByVal folder As String, ByVal parentNode As TreeNode)
'this is where we extract the information from the reports .xml definition file
'any sub report has a suffix added to its name of "Sub"
'this bit effectively finds them and ensures that they do not appear in the tree
'as we do not want to call up sub reports on their own
'Finally the report node tag now needs to be set to contain all of the information required to direct
'the c1 report component to the specific file.
rptFile = String.Format("{0}\\{1} Reports.xml", folder,
parentNode.Text)
rptDef = rptfile
Dim lreports() As String = clr.GetReportInfo(rptDef)
Dim lreport As String
If lreports.Length 0 Then
Dim lreportnode As TreeNode = Nothing
For Each lreport In lreports
If lreport.Contains("Sub") Then
Else
lreportnode = parentNode.Nodes.Add(lreport, lreport, 2, 3)
lreportnode.Tag = rptDef
End If
Next
End If
End Sub
'The following two methods control the image displayed in the tree view as the user selects reports
Private Sub TreeView1_AfterCollapse1(ByVal sender As Object, ByVal e As System.Windows.Forms.TreeViewEventArgs) Handles TreeView1.AfterCollapse
e.Node.ImageIndex = 0
End Sub
Private Sub TreeView1_AfterExpand1(ByVal sender As Object, ByVal e As System.Windows.Forms.TreeViewEventArgs) Handles TreeView1.AfterExpand
e.Node.ImageIndex = 1
End Sub
Private Sub TreeView1_DoubleClick(ByVal sender As Object, ByVal e As
System.EventArgs) Handles TreeView1.DoubleClick
RenderReport()
End Sub
Private Sub RenderReport()
'first of all clear any existing report and then get the report con string to use
ppc.Document = Nothing
Dim lrptpath As String = TreeView1.SelectedNode.Tag.ToString
Try
clr.Load(lrptpath, TreeView1.SelectedNode.Text)
Catch lex As Exception
If TypeOf (lex) Is System.UnauthorizedAccessException Then
Dim lmsg As String = "Please select a report as opposed to the directory in which they are situated"
MessageBox.Show(lmsg, "No Report Selected", MessageBoxButtons.OK, MessageBoxIcon.Error)
ppc.Document = Nothing
Return
Else
MessageBox.Show(lex.Message.ToString)
End If
End Try
End Sub
End Class
Conclusion
There are a couple of points to note. The first is to do with the way that I chose to handle sub reports (which obviously you don't want to display). Whatever approach you take to this obviously has a bearing on your naming structure for the reports. There is little or no serious error checking in the code samples. You would need to add this.
Finally, to test this during development you will need a reports directory structure similar to the finished article in your debug bin.
Now when your end users need fresh reports you just ship them a revised reports directory. If you have the self extractor version of winzip you can even automate the process of installation for your end users.
Hopefully this will provide a few of you who use C1 reports with a different approach to what is often a perennial problem.