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.
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!