I want to inject an interface into my Report instance, using my IOC container.
This interface will be used by the report to get data,
ex:
IQueryHandler<GetMyReportQuery, IEnumerable<MyReportData>> _queryHandler = <injected instance>.
IEnumerable<MyReportData> data = _queryHandler.Handle(new GetMyReportQuery {
param1 = <param1 enterred by user>,
param2 = <param2 enterred by user>,
});
I've seen somewhere suggested to use a custom report resolver, and instantiate an InstanceReportSource within, but this is no use as parameters are not available in the custom report resolver.
I've seen also suggested to use an ObjectDataSource, but it doesn't work as it needs a parameter less constructor to instantiate the business object, which prevent me from injecting my dependencies in the constructor.
So I'm a bit clueless how are we supposed inject our dependencies with Telerik reporting.
Please advise, thanks.
9 Answers, 1 is accepted
Here is the solution I've used, hope it can help others.
In my report file:
- I add a second ctor to be used by my IOCContainer
- The default parameterless ctor is still used by the designer
- Use the NeedDataSource event to execute my query and bind the result to the processingReport.DataSource. Here I have access to the parameters using the processingReport.
- OPTIONAL: In the designer, I bind the report to an ObjectDataSource, which is binding to the GetPreviewData() method. This is used to show dummy data when previewing.
public
partial
class
F0201Report : Telerik.Reporting.Report, IReport<F0201Model>
{
private
readonly
IReportQueryHandler<F0201Query, F0201Model> _queryHandler;
// Called by designer
public
F0201Report()
{
InitializeComponent();
this
.ReportParameters.ForEach(x => x.Visible =
true
);
}
public
F0201Report(IReportQueryHandler<F0201Query, F0201Model> queryHandler)
{
InitializeComponent();
this
.ReportParameters.ForEach(x => x.Visible =
false
);
_queryHandler = queryHandler;
this
.DataSource =
null
;
// will be set in NeedDataSource event
}
private
void
F0201Report_NeedDataSource(
object
sender, System.EventArgs e)
{
var processingReport = (Telerik.Reporting.Processing.Report)sender;
var model = _queryHandler.Handle(
new
F0201Query()
{
RequestNumber = processingReport.Parameters[
"requestNumber"
].Value?.ToString()
});
processingReport.DataSource = model.Data;
}
/// <summary>
/// Called by the ObjectDataSource used in Designer for preview
/// </summary>
public
IEnumerable<F0201Dto> GetPreviewData(
string
requestNumber =
null
)
{
var list =
new
List<F0201Dto>()
{
new
F0201Dto() {RequestNumber =
"1"
},
new
F0201Dto() {RequestNumber =
"2"
},
new
F0201Dto() {RequestNumber =
"3"
}
};
if
(requestNumber !=
null
)
{
list = list.Where(x => x.RequestNumber == requestNumber).ToList();
}
return
list;
}
}
Next I created a Custom ReportResolver which use my container to get the Report instance, and return an InstanceReportSource.
public
class
ReportResolver : IReportResolver
{
private
readonly
IIocManager _iocManager;
public
ReportResolver()
{
_iocManager = IocManager.Instance;
}
public
ReportSource Resolve(
string
reportId)
{
var reportType = Type.GetType(reportId);
if
(reportType ==
null
)
throw
new
UserFriendlyException($
"Could not find a corresponding report for type '{reportId}'."
);
var report = (Report)_iocManager.IocContainer.Resolve(reportType);
var reportSource =
new
InstanceReportSource { ReportDocument = report };
return
reportSource;
}
Thank you for sharing your solution.
Please note that the resolver is called on registering a viewer, on getting information about the requested report's ReportParameters collection and on creating the document in the requested format (HTML, PDF, etc.) - REST Service Resolver Usage.
On each call you have to return the same report instance.
In general, reports are strongly typed and you need to provide exact data schema to be used by the report. I can suggest you to create a data service that returns the data and loads it in a specific business object that can be used via ObjectDataSource e.g. Connecting Telerik Reporting to OData feeds. You can make the type injection on creating the data service to know which type( business object) to be used as data source.
Other approach is to use only the data items' NeedDataSource events and a generic data access layer that handles internally which type to be used. Then you can use the default parameterless constructor of the report.
Regards,
Stef
Telerik by Progress
[quote]Please note that the resolver is called on registering a viewer, on getting information about the requested report's ReportParameters collection and on creating the document in the requested format (HTML, PDF, etc.) - REST Service Resolver Usage.
On each call you have to return the same report instance.[/quote]
Those are 3 different web requests, right ? How should I correlate them to make sure to use the same Report instance ?
What are the consequences of using a different report instance, like in my solution ?
[quote]In general, reports are strongly typed and you need to provide exact data schema to be used by the report. I can suggest you to create a data service that returns the data and loads it in a specific business object that can be used via ObjectDataSource e.g. Connecting Telerik Reporting to OData feeds. You can make the type injection on creating the data service to know which type( business object) to be used as data source.[/quote]
I'm not clear on this one. I am using strongly type binding. And no, I don't want to add OData into the mix.
[quote]Other approach is to use only the data items' NeedDataSource events and a generic data access layer that handles internally which type to be used. Then you can use the default parameterless constructor of the report.[/quote]
No I can't, the queryHandler have dependencies to be injected. The queryHandler need to be resolved by the container.
Each request will send the string set as reportViewer.ReportSource.Report to the service's Resolver.Resolve method. You can add custom information like id or else letting you know what the expected result is. If you return a different report instance during these calls, there will be bubbled an error in the viewer with description what broke the report processing.
The idea of the example with the OData feed is that you can have the queryHandler in a data access layer that returns the final data object. Then you need an instance of the data access layer and the method that returns the ready data object. The reporting engine will create an instance of the class specified by the ObjectDataSource.Datasource (the data access layer) and it will execute the method specified by the ObjectDataSource.DataMember property (the method returning the final data object)).
Regards,
Stef
Telerik by Progress
[quote]
Each request will send the string set as reportViewer.ReportSource.Report to the service's Resolver.Resolve method. You can add custom information like id or else letting you know what the expected result is. If you return a different report instance during these calls, there will be bubbled an error in the viewer with description what broke the report processing.
[/quote]
Sorry, I am so confused by your answer. The reportviewer makes 3 differents web requests, that is not possible for the backend to return the same report instance each time. Multiple users could request the same report with different parameters. The requests could even go to a different server in a web farms scenario.
The ReportResolver I published is working fine, I get no such error you describe.
I'm still clueless what you recommend to change in my ReportResolver.
[quote]
The reporting engine will create an instance of the class specified by the ObjectDataSource.Datasource (the data access layer) and it will execute the method specified by the ObjectDataSource.DataMember property (the method returning the final data object)).
[/quote]
There seems to be a misunderstanding on dependency injection... my data access layer class has constructor dependencies that need to be satisfied, thus it cannot be set in ObjectDataSource.Datasource since that require parameter-less constructor.
We follow the composition root pattern (http://blog.ploeh.dk/2011/07/28/CompositionRoot/)
I don't want to use Service Locator anti-pattern.
The idea is that reports are not designed to use Dependency Injection pattern, nor data source components are.
You can create custom constructor in the report and use it to instantiate the report per your needs. To use other than the default report's constructor, you need a custom resolver for the Reporting REST service. Each viewer requests reports via string message that is received by the service's resolver (in the Resolve method). Each viewer can send customized information in the string, which can include a generated by you viewer ID letting you recognize which viewer sends the request and what report instance should be returned by the resolver.
You can check the above information by adding a custom resolver in the Reporting REST Service and debugging the resolver's Resolve method and tracing the Network traffic (sent requests). You will notice the viewer sends requests including an id we generate per viewer, that lets know the service whose requests are handled. From that point, it is important the service to resolve the received string information to the same report for the current client.
Regards,
Stef
Telerik by Progress
And you point is ... ?
All this is done in the resolver I posted, right ?
When you replied to my original post, I was under the impression you were suggesting some correction... but still I have no idea what they are.
The idea was to notify you how and when the Reporting REST Service calls its ReportResolver, and that it is required to return the same report instance as a response to all calls.
Also I wanted to let you know of other data-binding approaches, where the dependency injection pointing the exact type of data you will use can happen in another data access layer, where you can use the strongly typed data object filled with data via ObjectDataSource. Illustrated with the OData feed example.
Regards,
Stef
Telerik by Progress