Domain driven reports: adding custom code

In my last post I described how to create reports based on a domain model. Which led to many an interesting reaction. In this post I will explore some of my further explorations based on those comments as well on my personal requirements. In this post I will sketch a way to give reports a distinct place in the overall architecture of an application, how to separate the report from its presentation on a winform or a webform and how to add custom, domain based, code to a report.

Reporting is a service

In an application reporting is one of the services. As an implementation I have added a reporting class library to the solution. This class library contains the embedded report, the code to use the report in a report viewer and the report’s custom code.

Arep1

This project has a reference to the domain model and contains many of the pieces of my first reporting story:

  • The DataSources based on the domain entities Invoice and InvoiceLine.
  • An interface IReport describing how the report will be exposed to a report viewer
  • A base class ReportBase implementing shared code
  • The report definition Invoice.rdlc
  • A class Invoicing which implements IReport as well as some custom code.

The IReport interface describes what a report should expose

public interface IReport

{

    string ReportDataSourceName { get; }

    Stream ReportDefinition { get;  }

    List<string> ReportAssemblyNames { get; }

}

It contains

  • The name of the datasource we met in the previous post. 
  • The report definition as a stream. In the previous post the report was embedded in the same assembly as the report viewer, in this scenario the report is embedded in another assembly and can be streamed from there
  • A list of assembly names. These are needed by the viewer when we are going to add custom code to the report

The ReportBase class provides a base implementation

public abstract class ReportBase : IReport

{

    protected List<string> _reportAssemblyNames

        = new List<string> {Assembly.GetExecutingAssembly().GetName().ToString() };

 

    public List<string> ReportAssemblyNames

    {

        get { return _reportAssemblyNames; }

    }

 

    public Stream ReportDefinition

    {

        get { return Assembly.GetExecutingAssembly().GetManifestResourceStream(ReportName); }

    }

 

    protected abstract string ReportName {get;}

 

    public abstract string ReportDataSourceName { get;}

 

}

It provides

  • The base list of assemblynames, already containing the full name of the Reporting assembly
  • A stream providing the report definition
  • Stubs for the report and datasource name

Building the invoicing report

On the ReportBase class the Invoicing class is based.

using System.Collections.Generic;

using Gekko.Administratie.DomainModel;

using Gekko.Administratie.Services.Reporting.Base;

 

namespace Gekko.Administratie.Services.Reporting

{

    public class Invoicing : ReportBase

    {

        public Invoicing()

        {

            _reportAssemblyNames.Add(“DomainModel, Version=1.0.0.0, Culture=neutral, PublicKeyToken=7610f12aa6ca9a54″);

            _reportAssemblyNames.Add(

                “nHibernateHelpers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=ac06ce2921f125f7″);

        }

 

        protected override string ReportName

        {

            get { return “Gekko.Administratie.Services.Reporting.Invoice.rdlc”; }

        }

 

        public override string ReportDataSourceName

        {

            get { return “Gekko_Administratie_DomainModel_Invoice”; }

        }

 

        public static string SayBoe()

        {

            return “Just say boo…”;

        }

 

        public decimal RunningTotal;

 

        public decimal MyLineTotal(object lines)

        {

            var theLines = lines as IList<InvoiceLine>;

            decimal result = 0;

            if (theLines != null)

                foreach (var line in theLines)

                {

                    result += line.LineTotal;

                }

            RunningTotal += result;

            return result;

        }

    }

}

Notice that the class is using the domain model. In the constructor is adds the names of assemblies used to the list and provides the name of the report and it’s datasource.

It has two kind of custom members. There is the static SayBoe method, which just return a string. The instance member MyLineTotal is far more interesting. As a parameter is expects an IList of InvoiceLines. In which the LineTotal property is calculated in the domainmodel. The running total of all lines is summed in RunningTotal, another exposed member. It would be really nice to use these custom members in our report.

To use the custom code in the report requires some fiddling. All assemblies used should be added to the reference list of the report. To be trusted the assemblies should be signed. Notice the PublicKeyToken.

Arep2

By adding the Invoicing class to the classes list the report will create an instance of that class when rendering the report. The instance will be available as MyInvoices.

Now the members of the invoicing class are avaliable in the report. The static members are referenced through the class name, this is now a valid reporting expression:

=Gekko.Administratie.Services.Reporting.Invoicing.SayBoe()

The fully qualified class name with the static method.

The instance members are reached through the Code prefix

=Code.MyInvoices.MyLineTotal(Fields!Lines.Value)       

Note that the MyLineTotal method is passed the value of the Lines field. This is a list of InvoiceLines. In the report itself this property is not usable. In the method the parameter is typed as object, the report does not know how to handle an IList<InvoiceLine> and would throw an exception when meeting one. But in the method’s implementation the passed in Lines can be cast to a typed IList and be used as intended.

public decimal MyLineTotal(object lines)

{

    var theLines = (IList<InvoiceLine>)lines;

    decimal result = 0;

    foreach (var line in theLines)

        result += line.LineTotal;

    RunningTotal += result;

    return result;

}

Now that is really nice, I do have full domain specific functionality inside my report.

The runningtotal is accessible through a straightforward reporting expression

=Code.MyInvoices.RunningTotal

Just like any normal property.

Displaying the report

With the report designed it’s time to display it. The .NET framework has two report viewers, one for winforms and one for asp.net. These reportviewers can work with server-side (SQL Reporting Services) reports and with clientside reports. The programming models for the winform and the webform viewer  look identical but share, as far as I have discovered, no usable root. I will repeat the code for both variations

On a windows form.

The project references the reporting project. The form contains a report viewer of which no properties are set.

private void Form1_Load(object sender, EventArgs e)

{

    var repo = new InvoiceRepository();

    var invoices = new Invoicing();

    MakeReport(reportViewer1.LocalReport, invoices, repo.ListPrintableInvoices());

    reportViewer1.RefreshReport();

}

The repoistory is opened, an invoicing object is created, the MakeReport helper method ties everything together after which the report can be displayed.

static void MakeReport(LocalReport report, IReport reportSettings, object data)

{

    report.ExecuteReportInCurrentAppDomain(Assembly.GetExecutingAssembly().Evidence);

    foreach (var assemblyName in reportSettings.ReportAssemblyNames)

        report.AddTrustedCodeModuleInCurrentAppDomain(assemblyName);

    report.LoadReportDefinition(reportSettings.ReportDefinition);

    report.DataSources.Add(new ReportDataSource(reportSettings.ReportDataSourceName, data));

}

A report is just some running .net code. The first line sets the context. In case you omit this the reportviewer will not load the expression evaluator and the report will be blank. The second line explicitly sets a list of assemblies which the report is allowed to load. Note again that these assemblies have to be signed, else they will not be trusted. The third line loads the report definition in the viewer. And the last one binds the data to the report.

On a web form

The asp.net reportviewer only works in a classical asp.net application as it requires the viewstate. In case you are working in an mvc application it is no real problem to add a webform to the (otherwise) mvc web-app.

The markup for the reportviewer is simple

<%@ Page Language=”C#” AutoEventWireup=”true” CodeBehind=”Faktuur.aspx.cs” Inherits=”GekkoAdministratie.Faktuur” %>

 

<%@ Register Assembly=”Microsoft.ReportViewer.WebForms, Version=9.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a”

    Namespace=”Microsoft.Reporting.WebForms” TagPrefix=”rsweb” %>

 

<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN” “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>

 

<html xmlns=”http://www.w3.org/1999/xhtml” >

<head runat=”server”>

    <title></title>

</head>

<body>

    <form id=”form1″ runat=”server”>

        <rsweb:ReportViewer ID=”ReportViewer1″ runat=”server” Width=”100%” Height=”600px”>

        </rsweb:ReportViewer>

    </form>

</body>

</html>

(Yes drag and drop works fastest :)

The code to set up the report looks completely the same as for the windows form

protected void Page_Load(object sender, EventArgs e)

{

    var repo = new InvoiceRepository();

    var invoices = new Invoicing();

    MakeReport(ReportViewer1.LocalReport, invoices, repo.ListPrintableInvoices());

}

 

static void MakeReport(LocalReport report, IReport reportSettings, object data)

{

    report.ExecuteReportInCurrentAppDomain(Assembly.GetExecutingAssembly().Evidence);

    foreach (var assemblyName in reportSettings.ReportAssemblyNames)

        report.AddTrustedCodeModuleInCurrentAppDomain(assemblyName);

    report.LoadReportDefinition(reportSettings.ReportDefinition);

    report.DataSources.Add(new ReportDataSource(reportSettings.ReportDataSourceName, data));

}

The only difference is that this LocalReport type is from another namespace as the LocalReport type in winforms.

You don’t have to (and can not) issue a RefreshReport, having set up the report it will be rendered to the viewer just fine.

Wrapping up

This is more or less it. It’s still a sketch but it does display the idea. Some things to watch:

Loading an assembly in a report can give you a hard time in Visual Studio. When building the assembly all expressions in the report are compiled to. After adding custom members, you need a successful build before you can use those custom members in report expressions. And when things really stop working you need an occasional restart of Visual Studio. Quite essential were the tips I found in this great post by, all about nothing, Gerhard Stephan. Without the post-build event described there nothing will even build.

In my scenario I am doing something somewhat strange (and perhaps even silly). The assembly I am loading in the report is the same assembly in which the report is embedded. My main intent was to keep all parts of the report, from the content to the viewer related stuff, together. I’m open for any other view on this.

But for now I’m going to pack our tent and let everything sink deeper in on the camping site. France here we come.

This entry was posted in Data. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • http://ibub.hu/ John

    3 years old post but was big help for me. Thanx Peter!