6

I have rigged up a technique to handle multiple subreports in an rdlc report, but as I have tried to make it generic and repeatable, I have instead had to take the model and tweak it slightly for each case.

For example, if I define an abstract interface, like such, I just cut and paste it from winform to winform as needed:

abstract class ISolutionStrategy
{
    public abstract void AlgorithmInterface(Int64 searchCriteria, SubreportProcessingEventArgs e);
}

First, I want to be able to bring this into each form by including an has-a object. I also want to encapsulate the behaviors of handling the dispatching by the delegate, and make the handling methods "generic" as well.

So, the design requirements are:

  • Create an object that can be included in a winform to handle multiple subreport processing
  • Instantiate and configure the object in the winform
  • Build the dispatch table or switch/case statement in the winform
  • Pass in all the methods to handle the specific requirments of that winform's report viewer

The GOAL is to make an object that can be tested standalone and made robust, and also to not have to cut and paste the wheel and do a bunch of manual tweaking for each new winform.

It seems to me that someone has found a better design out there than the one I currently have.

Create an object that can be included in a winform to handle multiple subreport processing

So far, I have a delegate in the local forms load event:

this.reportViewer1.LocalReport.SubreportProcessing += new SubreportProcessingEventHandler(LocalReport_SubreportProcessing);

which is handled by a switch statement in the *LocalReport_SubreportProcessing* method.

The body of the method contains a switch statement:

void LocalReport_SubreportProcessing(object sender, SubreportProcessingEventArgs e)

        {
            String commonSubreportKey = _commonSubreportKey;

            switch (e.ReportPath)
            {
                case "rptSubAlternateParts":
                    runSubAlternatePart(e, commonSubreportKey, new GetAlternateParts());
                    break;
                case "rptSubGetAssemblies":
                    runSubFailurePart(e, commonSubreportKey, new GetAssemblies());
                    break;
                case "rptSubGetAssemblies":
                    runSubGetGetEndItemLRMFailureInfo(e, commonSubreportKey, new GetEndItemLRMFailureInfo());
                    break;
                case "rptSubGetAssemblies":
                    runSubGetSubAssemblies(e, commonSubreportKey, new GetSubAssemblies());
                    break;
                default:
                    break;
            }
Aside:

In my opinion, the switch is mostly human readable compared to the alternative I considered. I considered using a hash with the report name as the key and the function call data as the value. However, I did not really know how to do it and I thought it would be harder for someone else to understand.


After that, a call is made to a function that rearranges the information passed from the function call in the switch statement:

    private static void runSubAlternatePart(SubreportProcessingEventArgs e1, String  commonReportKey, GetAlternatePart myAP)
            {
                myAP.AlgorithmInterface(commonReportKey, e1);
            }

This rearrangement is definitely code stuttering, but is a seemingly necessary intermediate to the Strategy pattern I am attempting to implement:

     abstract class IStrategy
        {
            public abstract void AlgorithmInterface(String searchParam, SubreportProcessingEventArgs e);

        }

Here is a concrete implementation of the Strategy for one of the reports:

class GetAlternatePart : IStrategy
{
private BLL.AlternatePartBLL ds = new BLL.AlternatePartBLL();


public override void AlgorithmInterface(String searchParam, SubreportProcessingEventArgs e)
              {

                    e.DataSources.Clear();
                    DataTable myDataTable = ds.GetAlternativePart(searchParam);
                    DataSet myDataSet = new DataSet();
                    myDataSet.Tables.Add(myDataTable);
                e.DataSources.Add(new ReportDataSource("BLL_AlternatePartBLL", myDataSet.Tables[0]));
            }

        }
    }

In any case, my desire is to not have to hand wire the same logic repeatedly between reports, as I have many reports with multiple subreports.

I would like a library quality way of using a class to dynamically create the middle parts where the stuttering occurs, and I would like to pass in an "anonymous" funciton which actually implements the detailed connecting of the subreport to its corresponding data source.

For a single report with subreports, or even a few one-off reports, what I am doing is ok, but how can it be made less manual, more robust and more testable?

My environment is Visual Studio 2008 with a target of .NET 3.5; there seems to be a difference in how abstract classes are declared and how they are compiled.

bentaisan
  • 1,056
  • 3
  • 12
  • 29
  • To absolutely minimize cut and paste, and to stay as DRY as possible, I would recommend a design goal of having less code in each Winform than you have specified. For example, do you need a dispatch table in each WinForm, or could you just name your methods to match e.ReportPath, and then invoke via Reflection? Also, your switch code is illegal--you have 3 case "rptSubGetAssemblies", which makes it a little more difficult to understand exactly what you are trying to do. Could you edit to correct the code? – J.T. Taylor Feb 13 '13 at 17:17
  • Another thought I have is that the need for human readability pales in comparison to making the code DRY. If you need to use nested generics and expression trees here to simplify implementation within each winform, then that is the right thing to do. – J.T. Taylor Feb 13 '13 at 17:18
  • Finally, when you write "Pass in all the methods to handle the specific requirements of that winform's report viewer", how different are these methods from one another? Do they all derive from the same base class? Do they have similar method signatures such that they can be represented by a common delegate? I ask this b/c I'm not sold on the need for Strategy pattern here, instead suspecting this could be implemented more simply with either generics and reflection (if your GetXxx methods are very similar to one another), or expression trees (in the more complex case that they are not). – J.T. Taylor Feb 13 '13 at 17:19
  • The subreports all use different business objects in a Business Logic Layer, and the parameters may be different. So, in order to process a subreport, the paramters and the BLL object are needed in order to create an instance of the populated data, which is then assigned to the subreports Binding source. So, rather than pack all of those steps into the CASEs, I made it so that one statement is needed for any given report. All of the subreports are triggered by one delegate, so this is all "dispatch" code. – bentaisan Feb 13 '13 at 22:01

1 Answers1

4

The solution I would suggest is a very simple refactoring to a base class, and it reduces the code you would need to write in each WinForm to two things: 1) the setting of the Report used for that form; and 2) the definition of how to get the report data for that form.

Assuming that each WinForm inherits from a base class called ReportForm, the code for each WinForm will look like this:

public partial class Form1 : ReportForm
{
    public Form1()
    {
        // Wire up the report used by the Visual Studio-designed report viewer to the base class
        base.WinFormReport = reportViewer1.LocalReport;

        InitializeComponent();
    }

    // The search parameters will be different for every winform, and will presumably
    //  come from some winform UI elements on that form, e.g., parentPartTextBox.Text
    protected override DataResult GetReportData(SubreportProcessingEventArgs e)
    {
        // Return the data result, which contains a data table and a label which will be
        //  passed to the report data source
        // You could use DataSet in DataResult instead of DataTable if needed
        switch (e.ReportPath)
        {
            case "rptSubAlternateParts":
                return new DataResult(
                    new BLL.AlternatePartBLL().GetAlternativePart(parentPartTextBox.Text)
                    , "BLL_AlternatePartBLL"
                );

            case "rptSubGetAssemblies":
                return new DataResult(
                    new BLL.SubAssemblyBLL().GetSubAssemblies(someOtherTextBox.Text)
                    , "BLL_SubAssemblyBLL"
                );

            default:
                throw new NotImplementedException(string.Format("Subreport {0} is not implemented", e.ReportPath));

        }
    }
                                .
                                .
                                .

The above code does these things:

1) Tells the base class (ReportForm) which Report has been used in the Form. You could refactor Report down to ReportForm as well if you like, but my approach allows you to still create and manipulate your ReportViewer and its Reports in Visual Studio. But if you are passing the Report programmatically and not in the designer, you might want to send Report from the derived WinForm classes into the base class.

2) Defines how the report will get all of its subreports' data. For that, we just need to return a DataTable and a label, as that is all that will eventually be required by the report data source. The code which binds the DataTable and label to the RDLC data source belongs in the base class (ReportForm), as that binding code will be common for all your WinForms.

Now, the ReportForm code should look as follows:

/// <summary>
/// Don't cut & paste into any Windows Forms, inherit the behavior you want from a base class
/// </summary>
public abstract class ReportForm : System.Windows.Forms.Form
{
    // I'm not sure exactly what this is used for, but I put it in base class in case there is some use for it here
    protected string _commonSubreportKey = "12345";

    // This will be the one line of code needed in each WinForm--providing the base class a reference
    //  to the report, so it has access to the SubreportProcessing event
    protected Report WinFormReport { get; set; }

    // Making this abstract requires each derived WinForm to implement GetReportData--foolproof!
    protected abstract DataResult GetReportData(SubreportProcessingEventArgs e);

    // Wire up the subreport_processing handler when any WinForm loads
    // You could override this in derived WinForms classes if you need different behavior for some WinForms,
    //  but I would bet this default behavior will serve well in most or all cases
    protected virtual void Form1_Load(object sender, EventArgs e)
    {
        Report.SubreportProcessing += new SubreportProcessingEventHandler(LocalReport_SubreportProcessing);
    }

    // When the Subreport processing event fires, handle it here
    // You could also override this method in a derived class if need be
    protected virtual void LocalReport_SubreportProcessing(object sender, SubreportProcessingEventArgs e)
    {
        // Get the data needed for the subreport
        DataResult dataResult = this.GetReportData(e);

        e.DataSources.Clear();
        e.DataSources.Add(new ReportDataSource(dataResult.Label, dataResult.Table));
    }
}

Notice that the ReportForm base class inherits from Form, and then all WinForms will inherit from ReportForm--that is the key to the whole design. Here's how this ReportForm base class works:

1) When the WinForm is instantiated, the base property WinFormReport is set, so the base object knows which Report is in use.

2) When the WinForm loads, the Form Load event is called on the base class since it is not defined in the derived class. On form load, the report's Subreport_Processing event is wired up.

3) When the user enters parameters and clicks something to create the report in the report viewer, eventually the subreports are instantiated by RDLC and the Subreport_Processing event fires multiple times, once for each subreport.

4) When the event fires, the base class event handler calls GetReportData(e), which will invoke the GetReportData method defined on the WinForm. Note that this method is abstract in the base class, so it cannot be defined on the base class, but must be defined in the derived class.

5) The GetReportData(e) method in the WinForm uses the dispatcher logic you initially indicated, to return a DataTable (could also be a DataSet if you need) and a text string to the base handler.

6) The base handler takes the DataTable/DataSet and the text string, feeds them to the report as the report data source, and there could also do whatever else is needed to display the report.

After much thought, I decided on recommending a fairly straightforward refactoring of common behavior into a base class, because I thought it would work given your requirements, and I didn't see where anything more complicated would be needed. I think you will find this approach very readable, have it absolutely minimize what is needed in each new WinForm, and above all, find it extremely extensible; that is, as you continue to develop the system, you will always ask "is this new code something that will need repeating in each WinForm, or is it common such that it should go into the base class?"

Feel free to add a comment if you have any questions or concerns about this approach, and I wish you the best of luck with it. I hope it is just what you are looking for!

J.T. Taylor
  • 4,147
  • 1
  • 23
  • 23
  • I like this answer. I am going to do a bit of testing, but it seems that I can use this pattern to solve this problem going forward with the WinForms reporting. I never thought of abstracting behavior to a base class; for some reason, the model I had of a base class is that it is designed first and changed rarely. Refactoring UP to a base class opened up my thinking in this regard.The answer gives a specific solution to this problem and it teaches an important refactoring concept. – bentaisan Feb 14 '13 at 14:39
  • I am changing some of the details, but this conceptually and practically covers the scope of my question, as well as the intangible requirements I put out. – bentaisan Feb 14 '13 at 15:04
  • Visual Studio (2008) is giving me two problems: (1) DataResult in the abstract class definition is not recognized in the LocalReport_SubreportProcessing method (2) If I inherit from ReportWinForm (my name for ReportForm), instead of Form, I cannot edit my form due to "Visual Studio cannot open a designer for the file because the class within it does not inherit from a class that can be visually designed". Even though ReportForm is derived from Form, it appears that some meta data is missing or something. – bentaisan Feb 14 '13 at 15:32
  • For 2) did you try http://msdn.microsoft.com/en-us/library/43fzdd42.aspx? Also, any way you could use VS2010--that's what I use and it works in my test app. – J.T. Taylor Feb 14 '13 at 16:38
  • For 1), are you getting error at compile time or run time? If compile time, what is the error? Local_SubreportProcessing and DataResult are both in the base class, there should be no issue with visibility. Do you want to post some more code? – J.T. Taylor Feb 14 '13 at 16:39
  • I am not sure what it means to move the code to the top and reload the designer. The declaration is as high up in the code as I can make it in legal C# syntax. Unfortunately, I am limited to Visual Studio 2008, but I have no idea why the abstract method can't be found. I cleaned and rebuild my solution and its still broke. I don't see anything wrong with your solution. – bentaisan Feb 14 '13 at 16:49
  • It sounds like you made a couple of modifications. Can you post to a new question, and respond to this thread with the link? It will really help me if I can see the exact code. Or, when you post to a new question it is likely someone else has seen this issue before as well and could jump right on it. – J.T. Taylor Feb 14 '13 at 16:52
  • http://stackoverflow.com/questions/14881412/how-can-a-base-class-with-abstract-methods-be-used-to-handle-delegate-calls – bentaisan Feb 14 '13 at 18:18
  • How can I get DataResult defined so that Visual Studio will recognize it? Would the command line compiler allow it to get past that stage? – bentaisan Feb 20 '13 at 18:32
  • I answered this in the other question, I believe it is because the designer cannot construct an abstract class. In this code, the base class does not have to be marked as abstract, you could just remove the abstract keyword from both base class and GetReportData method therein. – J.T. Taylor Feb 21 '13 at 00:06