Skip to main content Skip to footer

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.

ComponentOne Product Manager Greg Lutz

Greg Lutz

comments powered by Disqus