Autumn is always welcomed as the season of great events in developers world. Autumn of 2010 shed light on the future of C# and VB.NET. Asynchrony support in C# 5.0 is what widely discussed nowadays. I would like to make a contribution as well. Let's look at some real-world scenario that makes use of ActiveReports and analyze how the new C# 5.0 features can help in the implementation.
The problem
Imagine that you build a WPF application. The UI includes the window with single button "Open Annual Report as PDF". If a user clicks on this button, then of course the annual report which is built by using ActiveReports is exported to PDF and the resulting PDF file is opened in the default PDF viewer that is installed on the system. Here is the XAML, I removed the attributes that don't matter for the discussion:
<Window>
<Grid>
<Button x:Name="btnExport" Click="btnExport_Click" >
Open Annual Report as PDF</Button>
</Grid>
</Window>
Here is the code that performs exportation:
private string ExportReportToPdf(string rpxPath)
{
var arReport = new ActiveReport();
var reader = new XmlTextReader(rpxPath);
arReport.LoadLayout(reader);
arReport.Run(false);
var pdfExport = new PdfExport();
var outFileName = System.IO.Path.GetTempFileName() + ".pdf";
pdfExport.Export(arReport.Document, outFileName);
return outFileName;
}
And the button click handler:
private void btnExport_Click(object sender, RoutedEventArgs e)
{
string pdfFileName = ExportReportToPdf(@"C:\AnnualReport.rpx");
Process.Start(pdfFileName);
}
This code has the obvious drawback. If the annual report is huge, then its running and export may take a long time. Let's say that the report is SO huge that it takes 1 minute to run and export to PDF(it's hard to imagine though because ActiveReports is super-fast!). Our ExportReportToPdf works in the UI thread and it does not return the control to the caller. It means that UI is not responsive while ExportReportToPdf does its job. This is not acceptable at all. The UI should be responsive. Moreover, it would be nice to allow end-user to cancel the operation and provide some kind of progress report. Indeed .NET framework has all the ammunition that is needed to solve these problems. For example BackgroundWorker usage example is very similar to what I described above. However, MS realized that using the techniques such as BackgroundWorker is painful - they make use of the background thread, they require quite convoluted code for the cancellation and progress reporting support, they require extra work for updating UI safely and so on. Let's see how we can make UI responsive, provide cancellation and progress reporting support by using the new C# 5.0 features.
All you need is Await and Async
Now let's rewrite the button click handler by using the new C# 5.0 async and await features. Async and Await is not magic. It's the syntax sugar for continuation passing style implementation. There are nice blogs that describe how async and await work in details. This post just shows how they can be used in conjunction with the real-world scenario that makes use of ActiveReports. So, here is the async version of the button's click handler:
async private void btnExportClickAsync(object sender, RoutedEventArgs e)
{
var task = TaskEx.Run(() => ExportReportToPdf(@"c:\AnnualReport.rpx"));
await task;
Process.Start(task.Result);
}
And now the UI is responsive while the annual report is run and exported! Doesn't it look much easier to implement than, for example, Asynchronous Design Pattern?
What about cancellation?
Now let's add the support for cancellation of report running. In C# 5.0 the long-running task, such as report running or exportation should take care about cancellation. The task is provided with the object that reports whether the cancellation happened. The cancellation may be forced by the timeout or by the explicit invocation of Cancel method of the special object. For example let's add Cancel button to our UI and implement the cancellation support in our long-running task. Here is the updated XAML:
<Window Title="Pending..." >
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Grid.Row="0" x:Name="btnExport" Click="btnExportClickAsync">
Export report to PDF</Button>
<Button Grid.Column="1" Grid.Row="0" x:Name="btnCancel" Click="btnCancel_Click"
IsEnabled="false">Cancel</Button>
</Grid>
</Window>
So, we've added the window title that will indicate the state of the task. It may be "pending", "in progress", "done", "canceled". Those are not standard statuses but rather human-friendly statuses names which I use for the sake of clarity. Also, Cancel button was added and it is disabled when the application starts. There is no anything to cancel before task is run, isn't it?
Now let's look at the updated code of the Export button click handler:
private CancellationTokenSource cancelationObject;
async private void btnExportClickAsync(object sender, RoutedEventArgs e)
{
cancelationObject = new CancellationTokenSource();
try
{
this.Title = "In progress...";
btnExport.IsEnabled = false;
btnCancel.IsEnabled = true;
var task = TaskEx.Run(() => ExportReportToPdf(@"C:\AnnualReport.rpx"));
await task;
this.Title = "Done";
Process.Start(task.Result);
}
catch (OperationCanceledException)
{
this.Title = "Canceled";
}
finally
{
cancelationObject = null;
btnExport.IsEnabled = true;
btnCancel.IsEnabled = false;
}
}
So, we initialize and pass the "cancellation object" in our long-running task(through the class field) and catch the OperationCanceledException. If it is thrown, then the task was canceled. But who performs the actual cancellation? As I mentioned, In C# 5.0 the long-running task, such as report running or exportation should take care about cancellation. The idea: let's intercept the PageStart event of ActiveReport object and cancel the report running if it is needed. We need to update the code of ExportReportToPdf method:
private string ExportReportToPdf(string rpxPath)
{
var arReport = new ActiveReport();
var reader = new XmlTextReader(rpxPath);
arReport.LoadLayout(reader);
arReport.PageStart += (s, a) => cancelationObject.Token.ThrowIfCancellationRequested();
// the rest of the code
}
The last think to get cancellation working. Remember about Cancel button? Let's make it do its job!:
private void btnCancel_Click(object sender, RoutedEventArgs e)
{
cancelationObject.Cancel();
}
So, the entire picture of cancellation support is: The UI is responsive while the report running and exportation does its job. The user may click Cancel button and it tells "cancel the task!" to the special object. The report running intercepts the new page start event and tells "throw the cancellation exception if you told to do that!" to the special object. The task running catches the "cancellation exception" and updates the UI accordingly. The only drawback here is we can't cancel the task when the exportation starts(PdfExport.Export method is called). We only can cancel the task while the report is run. Unfortunately Export method does not raise any events that we can intercept. Also, the report running can be canceled only when the new page starts processing. Note though that the API I use lives since from ActiveReports 1.0 version(which was born in past century) but we talk about C# 5.0.
Progress reporting
Let's add the last gorgeous feature in our application. We will keep the user informed about the status of the task. The idea is very similar to the cancellation mechanism: we have the special Progress object, we report to that object in the handler of PageStart event of the report, we intercept the event of that object, we get the progress report in the handler, we update the UI with the progress report. All those actions happen in UI thread, we don't need to care about the safe UI updates. Let's look at the code(I removed the parts that do not matter for progress reporting:
private EventProgress<int> progress = new EventProgress<int>();
private string ExportReportToPdf(string rpxPath)
{
var arReport = new ActiveReport();
var reader = new XmlTextReader(rpxPath);
arReport.LoadLayout(reader);
int pageCount = 0;
arReport.PageStart += (s, a) =>
{
cancelationObject.Token.ThrowIfCancellationRequested();
(progress as IProgress<int>).Report(pageCount++);
};
// the rest of the code
}
async private void btnExportClickAsync(object sender, RoutedEventArgs e)
{
cancelationObject = new CancellationTokenSource();
try
{
this.Title = "In process...";
btnExport.IsEnabled = false;
btnCancel.IsEnabled = true;
var task = TaskEx.Run(() => ExportReportToPdf(@"C:\AnnualReport.rpx"));
progress.ProgressChanged += (s, args) =>
{
this.Title = "Pages done: " + args.Value.ToString();
};
await task;
// the rest of the code
}
VoilĂ ! We've made the UI responsive, added cancellation and progress reporting support for our long-running report task by using the new C# 5.0 features. And they look very comfortable to use so far. The code is clean and very readable(at least I believe in that :-))
Next time
Now let's imagine that we want to merge two huge reports and export the resulted document in PDF. How we can start two reports at the same time and have the UI responsive and await until both of them are completed? C# 5.0 may help too! Look for the Next episode of AsyncActiveReporting in 1 week :)
No comments:
Post a Comment