Strongly-typed Telerik reports
I absolutely loathe magic strings, especially if all they’re used for is pointing to a member on a type. Using strings to reference a type member is ridiculously brittle, as any change in the source type member, rename or remove, will not cause a compile-time error, but a runtime error. This is unacceptable.
The reporting tools I’ve used (SSRS, Crystal Reports, Telerik, Dundas) all assume that you’d like to report straight off a database. If not a database, then a DataSet. For many types of reports, this works fine, as we’d create a separate reporting database, views, and in some cases, an SSAS cube. But in other cases, all the report is trying to show is a nice, printable PDF version of the domain model. It’s in these cases that reporting tools fall flat.
For these non-aggregate reports, we like to create a flattened view of our domain model, optimized for viewing in a report, using AutoMapper. Fortunately, our reporting tool of choice (Telerik Reports) lets us use objects as data sources. We can do things like:
report.DataSource = Mapper.Map<Customer, CustomerOrderReportModel>(customer);
Where CustomerOrderReportModel is a flattened version of our Customer (plus their orders, etc.). When it comes to configuring a text box value to be bound from the CustomerOrderReportModel data source, you’ll find some fun code:
this.txtCustomerName.Value = "=Fields.Name";
Ugh. A magic string to point to a type member, a property on a class. Which means that renaming the member will kill my report, and that’s something that’s not only difficult to remember to do, but you’ll have to rely on text searches or running all your reports to ensure you didn’t break anything. Again, this is unacceptable. But we have a trick up our sleeve – strongly-typed reflection with expressions.
Putting expressions into the mix
The first thing I had to find was where I could configure the expressions. Since reports are very designer heavy (as they should be), I found that any trick I used still had to work with the designer. At the very least, it couldn’t break the designer.
In the Telerik case, the best I could do was to override a method used during report generation, but not viewing. This is the OnNeedDataSource method for Telerik, it’s not called during designing. I tried to put all of the expression code into the normal designer code location (InitializeComponent), but this broke the designer.
To create expressions, it’s a lot easier of the type you’re creating expressions for is already part of some generic context. For example, I could inherit from a base generic class, and my generic type could be inferred easily. It’s not much fun having to specify the generic argument for every field in a report, so I’d like to have that specified just once. In my case, I created a marker interface:
public interface IReport<TReportModel> { }
I had to use a marker interface instead of a base class as the base class broke the designer. Next, an extension method to generate the string Telerik needs for reports from an expression I’d rather use. I can’t change Telerik to use expressions, but I can at least generate what it needs from an expression:
public static class ReportExtentions { public static string Field<TReportModel>(this IReport<TReportModel> report, Expression<Func<TReportModel, object>> expression) { return "=Fields." + UINameHelper.BuildNameFrom(expression); } public static string PartialField<TReportModel>(this IReport<TReportModel> report, Expression<Func<TReportModel, object>> expression) { return "Fields." + UINameHelper.BuildNameFrom(expression); } }
That Expression<Func<TReportModel, object» piece should start to look awfully familiar for anyone doing strongly-typed reflection. The UINameHelper is just a utility class used throughout our system to output expression strings from real expressions. Next, I just need to mark my report as an IReport, and configure the textboxes through expressions:
public partial class TestReport : Report, IReport<CustomerOrderReportModel> { public TestReport() { InitializeComponent(); } public TestReport(CustomerOrderReportModel model) : this() { DataSource = model; } protected override void OnNeedDataSource(object sender, System.EventArgs e) { base.OnNeedDataSource(sender, e); txtCustomerName.Value = this.Field(m => m.Name); } }
Although I have to use “this.” to get the extension method to show up, it’s a lot easier than having to specify the generic argument every single time. The “Field” method merely generates the correct configuration that Telerik needs, and Telerik is none the wiser.
With strongly-typed reflection in my reports, I’m able to eliminate the magic strings that are the source of so many runtime bugs we often don’t catch until production. Magic strings are maintainability grenades, and I like to squash them wherever they show up.