Welcome to Pete Brown's 10rem.net

First time here? If you are a developer or are interested in Microsoft tools and technology, please consider subscribing to the latest posts.

You may also be interested in my blog archives, the articles section, or some of my lab projects such as the C64 emulator written in Silverlight.

(hide this)

Creating a Simple Report Writer in Silverlight 4

Pete Brown - 09 May 2010

Over the weekend, I was working on the Printing chapter of my Silverlight 4 Book. As part of that chapter, I decided to build a simple report writer. While there's a lot of other useful stuff in the chapter (buy my book! <g>) I felt the report writer could be very useful to release into the wild.

Silverlight 4 Printing API

The book chapter goes into great detail on the printing API. I won't duplicate that here. However, you can find additional information on MSDN.

Simple Report Writer Goals

The goal of the report writer is to create a simple way to print reports in Silverlight. A non-goal was to create a report designer or similar. I'll leave that to you all and to the vendors.

To be a useful example, the report writer needs to support:

  • Page Header and Page Footer
  • Page numbering, including total page count
  • Automatic pagination
  • Rows of arbitrary size
  • Report footer that may include totals, counts, etc.

I met all those goals. There's certainly more that can be done (see end of this post), but it works as a proof-of-concept and basic report writer.

image

Printing Mechanics

In order to support flexible layout, total page count and (eventually) grouping, I decided to pre-build the report before feeding it to the Silverlight printing system. The pre-build fills a List of UI Elements, one UI Element per page. The page itself is made up of three main areas, the PageHeader, the Items area, and the Page Footer. Each are colored differently in the screen shot below.

image

(I used the XPS Printer to create the on-screen version. Saves paper)

Templates

There are four template properties (each of type DataTemplate) in play in the image above:

  • PageHeaderTemplate
  • PageFooterTemplate
  • ItemTemplate
  • ReportFooterTemplate

Each of those is specified in Xaml, by the person creating the report. Those are then used when creating each page. For example, here are the templates shown in this page:

<local:Report x:Name="Report" Title="Current Customer List">
    <local:Report.PageHeaderTemplate>
        <DataTemplate>
            <Grid Margin="1 1 1 20">
                <Rectangle Stroke="Black" />

                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto" />
                        <RowDefinition Height="Auto" />
                    </Grid.RowDefinitions>

                    <TextBlock Text="{Binding Title}"
                                Grid.Row="1"
                                FontSize="16"
                                FontWeight="Bold"
                                Margin="5"
                                HorizontalAlignment="Left"
                                VerticalAlignment="Top" />

                    <TextBlock Text="{Binding CurrentPageNumber, StringFormat='Page {0}'}"
                                Grid.Row="1"
                                Margin="5"
                                HorizontalAlignment="Right"
                                VerticalAlignment="Top" />

                </Grid>

            </Grid>
        </DataTemplate>
    </local:Report.PageHeaderTemplate>

    <local:Report.ItemTemplate>
        <DataTemplate>
            <Grid HorizontalAlignment="Stretch" Margin="5 0 0 20">
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto" />
                    <RowDefinition Height="Auto" />
                </Grid.RowDefinitions>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="200" />
                    <ColumnDefinition Width="150" />
                    <ColumnDefinition Width="150" />
                </Grid.ColumnDefinitions>

                <StackPanel Grid.Column="0"
                            Orientation="Horizontal">
                    <TextBlock Text="{Binding LastName}"
                                FontWeight="Bold" />
                    <TextBlock Text=", "
                                FontWeight="Bold" />
                    <TextBlock Text="{Binding FirstName}"
                                FontWeight="Bold" />
                </StackPanel>

                <TextBlock Grid.Column="1"
                            Text="{Binding LastOrderDate, StringFormat='{}{0:D}'}"
                            TextAlignment="Left" />

                <TextBlock Grid.Column="2"
                            Text="{Binding TotalBusinessToDate, StringFormat='{}{0:C}'}"
                            TextAlignment="Right" />

                <TextBlock Grid.Column="3"
                            Text="{Binding OutstandingBalance, StringFormat='{}{0:C}'}"
                            TextAlignment="Right"/>
                        
                <StackPanel Grid.Row="1" Orientation="Horizontal" Grid.ColumnSpan="4">
                    <TextBlock Text="{Binding StreetAddress}" />
                    <TextBlock Text=" " />
                    <TextBlock Text="{Binding City}" />
                    <TextBlock Text=", " />
                    <TextBlock Text="{Binding State}" />
                </StackPanel>
            </Grid>
        </DataTemplate>
    </local:Report.ItemTemplate>

    <local:Report.PageFooterTemplate>
        <DataTemplate>
            <Grid Margin="1 20 1 1">
                <Rectangle Stroke="Black" />
                <StackPanel Orientation="Horizontal"
                            HorizontalAlignment="Right"
                            VerticalAlignment="Center"
                            Margin="5">
                    <TextBlock Text="{Binding CurrentPageNumber, StringFormat='Page {0}'}"/>
                    <TextBlock Text="{Binding TotalPageCount, StringFormat=' of {0}'}" />

                </StackPanel>

            </Grid>
        </DataTemplate>
    </local:Report.PageFooterTemplate>

    <local:Report.ReportFooterTemplate>
        <DataTemplate>
            <Grid Margin="1 20 1 1">
                <Rectangle Stroke="Black" />

                <Grid>
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="200" />
                        <ColumnDefinition Width="200" />
                        <ColumnDefinition Width="150" />
                        <ColumnDefinition Width="150" />
                    </Grid.ColumnDefinitions>

                    <TextBlock Grid.Column="1"
                                Text="{Binding Count, StringFormat='{}{0} customers'}"
                                TextAlignment="Left" />

                    <TextBlock Grid.Column="2"
                                Text="{Binding TotalSales, StringFormat='{}{0:C}'}"
                                TextAlignment="Right" />

                    <TextBlock Grid.Column="3"
                                Text="{Binding TotalOutstandingBalance, StringFormat='{}{0:C}'}"
                                TextAlignment="Right" />
                </Grid>
            </Grid>
        </DataTemplate>
    </local:Report.ReportFooterTemplate>


</local:Report>

Template Dependency Properties

The templates are all standard Xaml. You can see that I use regular old DataBinding just like you would if the report were actually a listbox or a DataGrid or something. The templates are defined on the report object as Dependency Properties. Here's an example of one:

public DataTemplate ItemTemplate
{
    get { return (DataTemplate)GetValue(ItemTemplateProperty); }
    set { SetValue(ItemTemplateProperty, value); }
}

public static readonly DependencyProperty ItemTemplateProperty =
    DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(Report), new PropertyMetadata(null));

 

Building the Page

To prepare a page, I do the following:

  • Create the page header from the PageHeaderTemplate
  • Measure() the header to update the DesiredSize
  • Create the page footer from the PageFooterTemplate
  • Measure() the footer to update the DesiredSize
  • Create the items panel (a StackPanel)
  • Calculate the available room for items by taking the page area (from the printing system) and subtracting the header and footer desired sizes.
  • Add the page to the internal collection of pages

Here's the code

private Grid GetNewPage(Size printableArea, out StackPanel itemsPanel, out Size itemsPanelMaxSize)
{
    CurrentPageNumber++;
            
    // pagePanel is the root element for this page
    Grid pagePanel = new Grid();

    RowDefinition headerRow = new RowDefinition();
    headerRow.Height = GridLength.Auto;

    RowDefinition itemsRow = new RowDefinition();
    itemsRow.Height = new GridLength(1, GridUnitType.Star);

    RowDefinition footerRow = new RowDefinition();
    footerRow.Height = GridLength.Auto;

    pagePanel.RowDefinitions.Add(headerRow);
    pagePanel.RowDefinitions.Add(itemsRow);
    pagePanel.RowDefinitions.Add(footerRow);

    // Create the Page Header
    FrameworkElement header = PageHeaderTemplate.LoadContent() as FrameworkElement;
    header.DataContext = this;
    Grid.SetRow(header, 0);
    pagePanel.Children.Add(header);
    header.Measure(new Size(printableArea.Width, printableArea.Height));


    // Create body / itemsPanel to fit in between
    itemsPanel = new StackPanel();
    itemsPanel.Orientation = Orientation.Vertical;
    itemsPanel.HorizontalAlignment = HorizontalAlignment.Stretch;
    itemsPanel.VerticalAlignment = VerticalAlignment.Top;
    Grid.SetRow(itemsPanel, 1);
    pagePanel.Children.Add(itemsPanel);


    // create the Page footer
    FrameworkElement footer = PageFooterTemplate.LoadContent() as FrameworkElement;
    footer.DataContext = this;
    Grid.SetRow(footer, 2);
    pagePanel.Children.Add(footer);
    footer.Measure(new Size(printableArea.Width, printableArea.Height));

    // Calculate how large the items panel can be
    itemsPanelMaxSize = new Size(printableArea.Width, printableArea.Height - footer.DesiredSize.Height - header.DesiredSize.Height);

    _pageTrees.Add(pagePanel);

    return pagePanel;
}

Building the Report

That builds a page, but doesn't handle any actual data. That function is inside the BuildReport method. BuildReport loops through all the data, adds rows while they fit, and calls GetNewPage() when it needs a new page.

private void BuildReport(Size printableArea)
{
    _pageTrees.Clear();
    CurrentPageNumber = 0;

    // event letting everyone know we're building the report
    OnBeginBuildReport(EventArgs.Empty);

    // if no data, exit
    if (ItemsSource == null)
        return;

    IEnumerable reportItems = ItemsSource;
    IEnumerator reportItemsEnumerator = reportItems.GetEnumerator();

    reportItemsEnumerator.Reset(); // required for > 1 print from same data

    // ready our panel variables for creation
    StackPanel itemsPanel = null;
    Grid pagePanel = null;
    Size itemsPanelMaxSize = new Size();

    // create first page
    pagePanel = GetNewPage(printableArea, out itemsPanel, out itemsPanelMaxSize);

    while (reportItemsEnumerator.MoveNext())
    {
        // notify that we're about to build an item
        PrintingEventArgs args = new PrintingEventArgs();
        args.DataContext = reportItemsEnumerator.Current;
        OnBeginBuildReportItem(args);

        // create row. Set data context.
        FrameworkElement row = ItemTemplate.LoadContent() as FrameworkElement;
        row.DataContext = args.DataContext;
        row.Measure(printableArea);

        // create a new page if we're out of room here
        if (row.DesiredSize.Height + itemsPanel.DesiredSize.Height > itemsPanelMaxSize.Height)
        {
            pagePanel = GetNewPage(printableArea, out itemsPanel, out itemsPanelMaxSize);
        }

        // add the row to the items panel, and then re-measure
        itemsPanel.Children.Add(row);
        itemsPanel.Measure(printableArea);
    }

    // create report footer
    PrintingEventArgs reportFooterEventArgs = new PrintingEventArgs();
    reportFooterEventArgs.DataContext = this;
    OnBeginBuildReportFooter(reportFooterEventArgs);

    FrameworkElement reportFooter = ReportFooterTemplate.LoadContent() as FrameworkElement;
    if (reportFooter != null)
    {
        reportFooter.DataContext = reportFooterEventArgs.DataContext;
        reportFooter.Measure(printableArea);

        // fit the footer into the report
        if (reportFooter.DesiredSize.Height + itemsPanel.DesiredSize.Height > itemsPanelMaxSize.Height)
        {
            pagePanel = GetNewPage(printableArea, out itemsPanel, out itemsPanelMaxSize);
        }

        itemsPanel.Children.Add(reportFooter);
    }

    // we're done!
    OnEndBuildReport(EventArgs.Empty);

}

Printing the Report

Note all the Measure() calls in there. Those are expensive, but necessary to figure out how much room we have left.

Finally, the integration with the Silverlight printing engine:

public void Print()
{
    CurrentPageNumber = 0;

    int pageIndex = 0;

    _printDocument.PrintPage += (s, e) =>
        {
            if (pageIndex == 0)
            {
                BuildReport(e.PrintableArea);
                TotalPageCount = _pageTrees.Count;
                CurrentPageNumber = 0;
            }

            if (_pageTrees.Count > 0)
            {
                CurrentPageNumber++;
                e.PageVisual = _pageTrees[pageIndex];
            }

            e.HasMorePages = pageIndex < _pageTrees.Count - 1;

            pageIndex++;
        };


    _printDocument.BeginPrint += (s, e) =>
        {
            OnBeginPrint(EventArgs.Empty);
        };

    _printDocument.EndPrint += (s, e) =>
        {
            OnEndPrint(EventArgs.Empty);
        };

    _printDocument.Print(Title);
}

In the Print method, I build the report when we start. I need to do that here so I have the PrintableArea from the PrintPage arguments. I then loop through the pages we built. Note that you have to update the CurrentPage number here as well. In the original BuildReport method, I incremented it to ensure everything sizes correctly. Here, I increment it again as the databound values are re-evaluated and need to be correct.

In the original code, I was not pre-building pages. I would go through similar steps, but assign them to the PageVisual before I added the rows. You must add the visual tree containing your page elements only after it is fully populated. If you add it early and simply add items to the tree after you've already assigned it, you'll get strange results, like this:

image

In that example, I added the header and footer ahead of time, but then assigned the tree to e.PageVisual, and then added additional elements to it. I cut off the right and bottom for the screen shot, but you can see that all the data for the report ended up stacked in the content area. Not good.

Using the Report

The majority of the work is in the Xaml templates. Once you have those done, the report is easy to use:

private void PrintReport_Click(object sender, RoutedEventArgs e)
{
    CustomerService service = new CustomerService();
    service.LoadCustomers(50);

    CustomerTotals totals = new CustomerTotals();

    Report.ItemsSource = service.Customers;

    // total rows as they are printed
    Report.BeginBuildReportItem += (s, ea) =>
        {
            Customer customer = ea.DataContext as Customer;

            totals.Count++;
            totals.TotalOutstandingBalance += customer.OutstandingBalance;
            totals.TotalSales += customer.TotalBusinessToDate;
        };

    // use the totals in the report footer
    Report.BeginBuildReportFooter += (s, ea) =>
        {
            ea.DataContext = totals;
        };

    Report.Print();
}

The only unusual bit there is how I handle totals. Since I don't make any assumptions about the data, I need to provide some way for you to tally items as they are printed, and then use those numbers in the report footer. I do that by raising an event for each item as it is added, and by then raising an event for the report footer. The report footer event allows you to replace the DataContext for the footer with something of your own: in this case, a class containing calculated totals.

Conclusion

You can take this code and build on it, or snag ideas from it. See CodePlex request below should you be interested in contributing back. Some nice enhancements would be:

  • Offer Even and Odd Item templates. If those are provided, don't use the ItemTemplate, instead use the appropriate even/odd template based on the current row.
  • Subreports and grouping. This gets trickier when you consider that groups can span pages, but is doable.
  • Support for images. Because the pages are not part of a real visual tree until printing time, Images do not work. Images are resolved (downloaded/loaded) once they are added to the true visual tree. The solution here may be to force everything through a preview process.
  • Print preview. Since we have all the trees for each page, it should be reasonable to create a print-preview window that allows paging through them (perhaps an items control or similar will work here). Will require exposing the pages collection outside of the report, or having the print-preview functionality inside the Report class itself.
  • Report Building off the UI thread. Not sure if this is possible; I haven't investigated it yet.
  • Report Progress Reporting. Raise event when each page is actually printed, so a progress bar can be made.

I'm considering putting the full project code up on CodePlex so anyone can browse it and add to it. Is that interesting/useful to you? Also, if you're interested in enhancing this project to make a real report writer, let me know. As I mentioned, this will be included in my book as well, with more details on the printing system.

     
posted by Pete Brown on Sunday, May 9, 2010
filed under:      

36 comments for “Creating a Simple Report Writer in Silverlight 4”

  1. Stonesays:
    Hi,

    great post! ...and yes: it would be very usefull if you decide to share your project.
    I think report (+printing) functionality is a "must have" in a modern application if you build a Silverlight-App for business reasons.

    One question about your upcoming Silverlight-book: There is a chapter called "The ViewModel pattern, testing, and debugging ". Does this mean that this chapter is about MVVM ?

    With best regards,

    Stone
  2. Jacek Psays:

    I'm trying to use Your Code but I'm really on my very begining with silverlight and I have really basic problems.
    For that moment I even have problem with the firs line
    <local:Report x:Name="Report" Title="Current Customer List">
    Apparently I don't have the type "local:Report" so I'm missing some reference. but which exacly?

    I belive that going further with Your code I will finde myself in this situation multiple times, so if it's possible could You post the working project with this code? I belive that this will save a loot of time to many pepople.

  3. Evesays:
    I'm very interested on this job because it can opens the road towards the understanding and the development of good printing solutions.
    I hope that you decide to share this project soon.

    Best regards and many thanks,
    Eve
  4. Petesays:
    Thanks all.

    This will be up on codeplex in the next couple weeks. I'm going to return from Redmond and merge what I did with what David Poll put together and then put it all out for you to grab.

    Pete
  5. Aloksays:
    Hi

    I downloaded this demo from http://silverlightreporting.codeplex.com/
    when i am running it, it is not showing data in report control.
    I debugged it also data is coming in completed event but not showing in reportcontrol

    please help what is wrong with it

    is there any insatallation required for this

    thanks
    alok sengar
  6. Amarsays:
    Hi,
    The code works fine. But i am getting an ArgumentOutOfRange Exception in Report.cs when i click the PrintReport button second time( i mean without refreshing the output page).Please help me to fix this issue.
  7. Pradeepsays:
    HI,
    i m not getting the output when i clicked the LoadData button..please check and send me the correct code..
    plaese if u know how to generate silverlight reports for the data retrieved from database send me the code to mail-id as early as possible..
    I m waiting for ur response..

    Thanking U....
  8. Nk54says:
    Pete, your blog is becoming one of my favorite !

    I discover it with your netduino project which was as interesting as this one !

    I was using David Poll library to print report (with preview etc.) it was very good ! But the library is about 66ko (SLaB.Printing.Controls.dll 36ko ; SLaB.Utilities.dll 11ko ; SLaB.Utilities.Xaml.dll 19ko) and that's too much for me.

    Thank you a lot !
  9. raymond says:
    Pete, Your code as been life saver im new to Silverlight and wpf had modify the reports everything works fine but why is the file size so large when you print?? 25 page report almost 3 gigs.

    thanks
    Raymond
    P.s. Im planning on buying your book this week :)
  10. Jaroslavassays:
    I was download this version http://silverlightreporting.codeplex.com/

    But I also not getting the output when i clicked the LoadData button.
    Maybe someone can tell me what is wrong
  11. Jerrysays:
    I am getting the same problem with printing twice. It seems like the lamda expression to handle the PrintPage event is added twice (multiple times) to the event log and then is called when there aren't any more pages to print. It needs to be removed from the list of event handlers when the report is finished.

    Any code updates available?
  12. Petesays:
    I was going to wait to update this until SL5 was in the wild with the new vector printing support, but I'll take a look at it fsoon (after MIX11)

    Is this happening only when the report is < 1 page, > 1 page, something else?

    Pete
  13. jyosays:
    thanks for this use full article...... How to start this application???? is it silverlight business application,....... in which type of application we have build this report??
  14. Rashmisays:
    Hi Pete ,

    I am a big fan of your articles and also your book , "SilverLight 4 IN Action".
    A Very Big Thanks To You !

    Currently , I have trouble working on the Report Viewer mentioned in the article , I am unable to find the local : Report mentioned here .I also looked through the Code Plex Website , could the local which you have referenced in the article above be referring to xmlns:local="clr-namespace:System.Windows.Printing.Reporting;assembly=Silverlight.Reporting" .

    And I would want to know if this works with Silverlight 4 ?

    Thanks a Lot ..
  15. Briansays:
    How do I get the row.measure to account for a changing Itemscontrol? When I print, and the items control fills up, it ends up not printing multiple pages, and thus looks odd.

    If I don't have the Itemscontrol on the report, it will print multiple pages correctly if it has need too.

    I tested it with a lot of data with Itemscontrol in the report, and my suspissions were confirmed when it printed multiple pages, but was missing some items because the Itemscontrol pushed them off the page.

    Thanks for this tool, and just let me know if there is anything I can do to help with these types of reports. :)

Comment on this Post

Remember me