Localization (multi language) of a RDLC report with Microsoft ReportViewer

In the previous post I wrote how to use the Microsoft ReportViewer with a report and a subreport in a WPF application. In this post I’am going to describe how you can localize a RDLC report because out of the box there is no support for localization. With localization I mean that all the labels, table headers, etc are being translated depending on a language setting in the application.

Because a RDLC report is just an ordinary XML file we execute the following steps to translate a report:

  1. load the report in a XML Document object
  2. find all the nodes with a specific attribute
  3. the value of this attribute is the actually name of a term in a resource file (*.resx)
  4. replace the value of this node with value retrieved from the resource file

I use the Visual Studio solution of my previous post about the ReportViewer as a starting point.

The report TextBox control has a property called ValueLocID. This property we use to store the name of a translated term which is stored in a resource file.

In the XML of the RDLC report the TextBox control property ValueLocID is stored in the LocID attribute of a Value element. The value of this attribute we are going to use to translate and replace the innerText of the Value element.

<Textbox Name="Textbox1">
  <CanGrow>true</CanGrow>
  <KeepTogether>true</KeepTogether>
  <Paragraphs>
    <Paragraph>
      <TextRuns>
        <TextRun>
          <Value rd:LocID="Customer">Customer:</Value>
          <Style>
            <FontWeight>Bold</FontWeight>
          </Style>
        </TextRun>
      </TextRuns>
      <Style />
    </Paragraph>
  </Paragraphs>
</Textbox>

Before the report is loaded into the ReportViewer we are going to translate the report with a helper class. In line 16 we load the report definition as a Stream object. This Stream object is the input for the TranslateReport method of the RdlcReportHelper class. The same steps are done for the subreport. In line 11 we set the current UI culture to Dutch (Netherlands) so all the TextBox controls which have a value set for the ValueLocID property are translated into Dutch.

void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
    // get the customers from the OData service
    IEnumerable<Customer> customers = Repository.GetCustomers();
    // get a reference to the WinForms ReportViewer control
    Microsoft.Reporting.WinForms.ReportViewer reportViewer = (Microsoft.Reporting.WinForms.ReportViewer)windowsFormsHost.Child;
    // subscribe to SubreportProcessing event for passing the related Orders data
    reportViewer.LocalReport.SubreportProcessing += new SubreportProcessingEventHandler(LocalReport_SubreportProcessing);
        
    // Change the CurrentUICulture to Dutch (Netherlands) 
    Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("nl-NL");
        
    // the report is included as an embedded resource in the assembly
    // load the report as a stream from the assembly   
    Assembly assembly = Assembly.GetExecutingAssembly();             
    Stream reportStream = assembly.GetManifestResourceStream("Wpf.ReportViewer.CustomerReport.rdlc");
    // translate the report
    reportStream = RdlcReportHelper.TranslateReport(reportStream);
    // load the report
    reportViewer.LocalReport.LoadReportDefinition(reportStream);
        
    Stream subreportStream = assembly.GetManifestResourceStream("Wpf.ReportViewer.OrdersReport.rdlc");
    // translate the subreport
    subreportStream = RdlcReportHelper.TranslateReport(subreportStream);
    // load the subreport
    reportViewer.LocalReport.LoadSubreportDefinition("Wpf.ReportViewer.OrdersReport.rdlc", subreportStream);
        
    // the CustomerReport has one dataset which must be filled
    reportViewer.LocalReport.DataSources.Add(new ReportDataSource("CustomerDataset", customers));
    // now let the ReportViewer render the report
    reportViewer.RefreshReport();
}

In the TranslateReport method of the RdlcReportHelper class we create a XDocument object from the Stream object. Then we iterate through all the Value elements. If a Value element has a LocID attribute we trying to find the translated term in a resource file using the value of the attribute.

public static class RdlcReportHelper
{
    public static Stream TranslateReport(Stream reportStream)
    {
        XDocument reportXml = XDocument.Load(reportStream);

        foreach (var element in reportXml.Descendants(XName.Get("Value", @"http://schemas.microsoft.com/sqlserver/reporting/2008/01/reportdefinition")))
        {
            XAttribute attribute = element.Attribute(XName.Get("LocID", @"http://schemas.microsoft.com/SQLServer/reporting/reportdesigner"));

            if (attribute != null)
            {
                string translatedValue = Resources.ReportResources.ResourceManager.GetString(attribute.Value);
                element.Value = string.IsNullOrEmpty(translatedValue) ? element.Value : translatedValue;
            }
        }

        Stream ms = new MemoryStream();
        reportXml.Save(ms, SaveOptions.OmitDuplicateNamespaces);
        ms.Position = 0;

        return ms;
    }
}

The Visual Studio 2010 solution can be downloaded from here

Advertisements

Showing a report with a subreport in the ReportViewer control which has a OData service as datasource in a WPF application

The Microsoft ReportViewer control is a powerfull control for embedding reports in a .Net application. There are two versions of this control: a WinForms version and a WebForms (Asp.Net) version. Unfortunately there is no native WPF version available at this moment but it is possible to use this control in a WPF application as explained later in this post.

The ReportViewer control has a number of advantages:
– is a freely redistributable component
– is able to show RDLC (Report Definition Language Client-side) format reports when the control is in local mode
– is able to show RDL ((Report Definition Language) format reports from a SQL Service Reporting Service (SSRS) when the control is in remote mode
– can export reports to Excel, PDF and Word (and additional formats when running in remote mode) without having Excel, Adobe Reader or Word installed
– supports print preview

In this post I’am going to show how to:
– use the ReportViewer control in a WPF application
– use an OData service as a datasource for the report
– use a report with a sub report

Using the ReportViewer control in a WPF application

As mentioned before the ReportViewer control is not available as a native WPF control but only as a WinForms and WebForms control. With the WPF WindowsFormsHost control is possible to host WinForms controls in a WPF application so also the WinForms ReportViewer control.

To use the WindowsFormsHost control together with the WinForms ReportViewer control you must add references to the following assemblies:
– WindowsFormsIntegration
– System.Windows.Forms
– Microsoft.ReportViewer.WinForms

After adding the references you can use the ReportViewer control in XAML.

<Window x:Class="Wpf.ReportViewer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:windowsFormsIntegration="clr-namespace:System.Windows.Forms.Integration;assembly=WindowsFormsIntegration"
        xmlns:winForms="clr-namespace:Microsoft.Reporting.WinForms;assembly=Microsoft.ReportViewer.WinForms"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <windowsFormsIntegration:WindowsFormsHost x:Name="windowsFormsHost">
            <winForms:ReportViewer>                
            </winForms:ReportViewer>       
        </windowsFormsIntegration:WindowsFormsHost>    
    </Grid>
</Window>

Using an OData service as datasource

The RDLC reports can use many different sources as datasource. In this post I’am using the public available Northwind OData service as datasource. To use this service add a service reference in a Visual Studio project (in my case a WPF application) to http://services.odata.org/Northwind/Northwind.svc.To retrieve data from this datasource I created a Repository class which is responsible for communicating with the service.

public static class Repository
{
    public static IEnumerable<Customer> GetCustomers()
    {
        return NorthwindEntitiesDataService.Customers;
    }

    public static IEnumerable<Order> GetOrdersForCustomer(string customerId)
    {
        return NorthwindEntitiesDataService.Orders.Where(o => o.CustomerID == customerId).Take(5);
    }

    private static NorthwindEntities _NorthwindEntitiesDataService;

    private static NorthwindEntities NorthwindEntitiesDataService
    {
        get
        {
            if (_NorthwindEntitiesDataService == null)
            {
                _NorthwindEntitiesDataService = new NorthwindEntities(new Uri(@"http://services.odata.org/Northwind/Northwind.svc"));
            }

            return _NorthwindEntitiesDataService;
        }
    }
}

This Repository class will be used later in this post.

Report with a subreport

We will use two reports: a (main) report which shows customers (CustomerReport.rdlc) and a subreport (OrdersReport.rdlc) which shows the related orders. The CustomerReport uses the Customer dataset from the service reference as datasource and the OrdersReport will use the Customer (Orders) dataset as datasource.

In the subreport a parameter (CustomerIdParameter) is defined which is used for passing the customer Id from the CustomerReport to the OrdersReport (subreport) so the related orders can be retrieved and shown.

This parameter has to be filled in the CustomerReport. Therefore we have to specify in the properties of the subreport in the CustomerReport which field from the customer dataset is used for filling this parameter.

The reports are now ready to show the data. The last thing we have to do is telling the ReportViewer control to show this report. We already placed the ReportViewer control in a XAML file. In the code behind of the XAML file we have to add some code.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.Loaded += new RoutedEventHandler(MainWindow_Loaded);
    }

    void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        // get the customers from the OData service
        IEnumerable<Customer> customers = Repository.GetCustomers();
        // get a reference to the WinForms ReportViewer control
        Microsoft.Reporting.WinForms.ReportViewer reportViewer = (Microsoft.Reporting.WinForms.ReportViewer)windowsFormsHost.Child;
        // subscribe to SubreportProcessing event for passing the related Orders data
        reportViewer.LocalReport.SubreportProcessing += new SubreportProcessingEventHandler(LocalReport_SubreportProcessing);
        // the report is included as an embedded resource in the assembly
        // use the full name
        reportViewer.LocalReport.ReportEmbeddedResource = "Wpf.ReportViewer.CustomerReport.rdlc";
        // the CustomerReport has one dataset which must be filled
        reportViewer.LocalReport.DataSources.Add(new ReportDataSource("CustomerDataset", customers));
        // now let the ReportViewer render the report
        reportViewer.RefreshReport();
    }

    void LocalReport_SubreportProcessing(object sender, SubreportProcessingEventArgs e)
    {
        // our subreport has only 1 parameter
        string customerId = e.Parameters["CustomerIdParameter"].Values[0];
        // our subreport has only 1 dataset 
        string dataSourceName = e.DataSourceNames[0];
        // get the orders for the current customer
        IEnumerable<Order> orders = Repository.GetOrdersForCustomer(customerId);

        e.DataSources.Add(new ReportDataSource(dataSourceName, orders));            
    }
}

Because we are using a subreport we have to subscribe to the SubreportProcessing event of the ReportViewer. Each time a customer is rendered in the CustomerReport this event is fired. In the event arguments the value of the CustomerIdParameter is passed which we will use to retrieve the related orders from the OData service. After retrieving the orders data we fill the dataset for the subreport.

The Visual Studio 2010 solution can be downloaded from here