65.9K
CodeProject 正在变化。 阅读更多。
Home

高级报表查看器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.79/5 (30投票s)

2009年4月8日

CPOL

6分钟阅读

viewsIcon

510141

downloadIcon

13905

本文展示了如何扩展 Visual Studio 2008 中附带的 ReportViewer 控件。最重要的扩展是增加了导出到 Microsoft Word 的功能

引言

本文展示了如何扩展 Visual Studio 2008 中附带的 ReportViewer 控件。最重要的扩展是增加了导出到 Microsoft Word 的功能。

AdvancedReportViewer.png

文章亮点

  • 集成到 WPF 应用程序
  • 对象数据源使用
  • 导出到 Microsoft Word
  • 本地化
  • 自定义

背景

在选择报表引擎时,我选择了 Microsoft Reporting Services。它基于开放的 RDL 格式,并且已经足够成熟。Studio 中附带的 ReportViewer 可以免费使用(无需 SQL Server 许可证)。它也不需要任何与 SQL Server 的连接。并且它可以导出到 Microsoft Excel 和 Adobe PDF。

随着时间的推移,出现了一个新需求 - 导出到 Microsoft Word。SQL Server 2008 中包含的以及将包含在 Visual Studio 2010 中的新版本 ReportViewer 可以做到这一点。而 Visual Studio 2008 SP1 中附带的版本则不能。但我们现在就需要这个导出功能。

导出到 Microsoft Word 的方法

最简单的解决方案是 - 导出到 *.pdf,然后再从 *.pdf 导出到 *.doc。但由于我的报表是俄语的,所以无法做到。编码方面存在一些问题,导致生成的 *.doc 无法阅读。当然,有一些第三方 RDL 渲染器可以导出到 Microsoft Word。但它们不是免费的。经过一番研究,一个想法浮现在脑海中

  • 可以启用导出到 HTML
  • 如果你将 *.html 保存为 *.doc,Microsoft Word 会打开它

导出到 HTML 功能包含在本地 ReportViewer 的代码中,但它是关闭的。我找到了一篇文章如何重新启用它(参见链接部分)。生成的 *.do? 并不是真正的 Microsoft Word 文档。但它可以用 Microsoft Word 编辑,看起来像 *.doc。如果这还不够,可以使用 Microsoft Word COM Interop 库并以原生的 *.doc 格式保存。这种方法的另一个问题是,每页都不会有页眉和页脚。

注意:你可能想知道为什么我不能直接使用 SSRS 2008 中的新 ReportViewer。答案是 - 那里没有本地 ReportViewer 控件。只有服务器端的。我经历了艰难的尝试才明白。下载了 SQL Server 2008 Reporting Services Report Builder 2.0。搜索了 ReportViewer DLL 并尝试实例化它。出现了异常 - CreateLocalReport 方法未实现。

集成到 WPF 应用程序

我们需要一种与控件通信的简单方法。要在 XAML 中使用它,我们需要两个依赖属性

  • RDLC 报表的源(RDLC 代表客户端 RDL)
  • 报表的数据源
<controls:ReportViewerUserControl
    EmbeddedReport="AdvancedReportViewer.Reports.SampleReport.rdlc"
    DataSource="{StaticResource samplePerson}" />

其中 DataSource 是包含报表数据的对象,EmbeddedReport 是一个 string,它指示在哪里搜索 RDLC 报表。

EmbeddedReport 很简单。它设置 reportViewer.LocalReport.ReportEmbeddedResource 属性。RDLC 报表作为嵌入资源添加到项目中。因此,要使用它们,必须指定完全限定路径 - 默认 namespace.reports directory.report 文件名。

对象数据源使用

DataSource 比较棘手。ReportViewer 期望数据采用特殊格式。

  • 它只适用于 IListSource (DataTables) 和 IEnumerable (Collections)
  • 数据源的名称必须与其类型匹配
  • 必须使用 ReportDataSource 包装器

经过一段时间的反复试验,代码终于写好了

private static ReportDataSource CreateReportDataSource( object originalDataObject )
{
    string name = originalDataObject.GetType( ).ToReportName( );
    object value = originalDataObject;

    // DataTable
    if( originalDataObject is IListSource )
    {
    }
    // Collection
    if( originalDataObject is IEnumerable )
    {
        name = GetCollectionElementType( originalDataObject ).ToReportName( );
    }
    // Just an object
    else
    {
        value = new ArrayList { originalDataObject };
    }

    Debug.Assert( !string.IsNullOrEmpty( name ), 
		"Data source's name must be defined " );
    Debug.Assert( value != null, "Data source must be defined" );
    return new ReportDataSource( name, value );
}

其中 Type.ToReportName( )

public static string ToReportName( this Type type )
{
    var isTypedDataTable =
        type.IsNested &&
        type.BaseType.FullName.StartsWith( "System.Data.TypedTableBase" );

    if( isTypedDataTable )
    {
        // in:  Some.Namespace.CategoryDataSet+CategoryDataTable
        // out: CategoryDataSet_Category
        var match = Regex.Match( type.FullName, @"^.+\.(\w+\+\w+)DataTable$" );
        return match.Groups[ 1 ].Value.Replace( "+", "_" );
    }
    else
    {
        // in:  Some.Namespace.TypeName
        // out: Some_Namespace_TypeName
        return type.FullName.Replace( ".", "_" );
    }
}

使用显示单个对象数据的报表可能看起来很奇怪。但这实际上是我项目的主要用例。无论如何,代码足够灵活,可以接受集合、DataSetDataTable 作为数据。

导出到 Microsoft Word

文章 [3][4] 告诉我们如何扩展 ReportViewer 的渲染能力。简而言之 - 有一个渲染到 HTML 的功能,但它是关闭的并且是硬编码的。要改变这种情况,必须修改 RenderingExtensions

RenderingExtensions = 
	reportViewer.LocalReport.m_previewService.ListRenderingExtensions( )

每个扩展都包含不言自明的字段

  • Name - 渲染扩展的内部名称
  • m_localizedName - 在导出下拉菜单中显示的本地化名称
  • m_isVisible, m_isExposedExternally - 对最终用户的可见性和可用性

但所有提到的代码都不是 public。实际上,我们必须使用反射来修改它。

private IList RenderingExtensions
{
    get
    {
        var service = reportViewer.LocalReport
            .GetType( )
            .GetField( "m_previewService",
		BindingFlags.NonPublic | BindingFlags.Instance )
            .GetValue( reportViewer.LocalReport );

        var extensions = service
            .GetType( )
            .GetMethod( "ListRenderingExtensions" )
            .Invoke( service, null );

        return (IList) extensions;
    }
}
private void EnableRenderExtension( string extensionName, string localizedExtensionName )
{
    foreach( var extension in RenderingExtensions )
    {
        // name = extension.Name;
        var name = extension
            .GetType( )
            .GetProperty( "Name" )
            .GetValue( extension, null )
            .ToString( );

        if( name == extensionName )
        {
            // extension.m_isVisible = true;
            extension
                .GetType( )
                .GetField( "m_isVisible",
			BindingFlags.NonPublic | BindingFlags.Instance )
                .SetValue( extension, true );

            // extension.m_isExposedExternally = true;
            extension
                .GetType( )
                .GetField( "m_isExposedExternally",
			BindingFlags.NonPublic | BindingFlags.Instance )
                .SetValue( extension, true );

            // extension.m_localizedName = localizedExtensionName;
            extension
                .GetType( )
                .GetField( "m_localizedName",
			BindingFlags.NonPublic | BindingFlags.Instance )
                .SetValue( extension, localizedExtensionName );
        }
    }
}

HTML 渲染扩展的内部名称是 HTML4.0。因此,启用导出到 *.html 的方法是

EnableRenderExtension( "HTML4.0", "MS Word" );

现在我们需要更改“导出到 Microsoft Word”的处理程序。否则我们将得到“.html”文件。首先,让我们关闭当点击“导出到 Microsoft Word”时出现的 ReportViewer 的导出对话框

reportViewer.ReportExport += ( sender, args ) =>
	args.Cancel = args.Extension.LocalizedName == "MS Word";

现在我们需要修改当点击“导出到 Microsoft Word”按钮时控件的行为。找到那个按钮是一个有趣的故事。你知道 ReportViewerToolStrip 可以通过 public 接口访问吗?

private T FindControl<t>( System.Windows.Forms.Control control )
    where T: System.Windows.Forms.Control
{
    if( control == null ) return null;

    if( control is T )
    {
        return (T) control;
    }

    foreach( System.Windows.Forms.Control subControl in control.Controls )
    {
        var result = FindControl<t>( subControl );
        if( result != null ) return result;
    }

    return null;
}

ToolStrip

ToolStrip = FindControl<system.windows.forms.toolstrip>( reportViewer );

ToolStrip 上的所有控件都有友好的名称(你可以用 reflector 自己看看 =)。导出按钮的名称是“export”。在 DropDown 上创建了它的子按钮。其中一个按钮就是我们正在寻找的“导出到 Microsoft Word”。

// Change export button handler
var exportButton = ToolStrip.Items[ "export" ] as
			System.Windows.Forms.ToolStripDropDownButton;

// Buttons are created on DropDownOpened so we can't assign handler before it
exportButton.DropDownOpened += delegate( object sender, EventArgs e )
{
    var button = sender as System.Windows.Forms.ToolStripDropDownButton;
    if( button == null ) return;

    foreach( System.Windows.Forms.ToolStripItem item in button.DropDownItems )
    {
        var extension = (RenderingExtension) item.Tag;
        if( extension.LocalizedName == "MS Word" )
        {
            item.Click += MSWordExport_Handler;
        }
    }
};

当用户点击它时,将调用 MSWordExport_Handler

private void MSWordExport_Handler( object sender, EventArgs args )
{
    // Ask user where to save
    var saveDialog = new SaveFileDialog
    {
        FileName = ReflectionHelper.GetPropertyValue
		( reportViewer.LocalReport, "DisplayNameForUse" ) + ".doc",
        DefaultExt = "doc",
        Filter = "MS Word (*.doc)|*.doc|All files (*.*)|*.*",
        FilterIndex = 0,
    };
    if( saveDialog.ShowDialog( ) != true ) return;

    // Create a report
    Warning[] warnings;
    using( var stream = File.Create( saveDialog.FileName ) )
    {
        reportViewer.LocalReport.Render(
            "HTML4.0",
            @"<DeviceInfo><ExpandContent>True</ExpandContent></DeviceInfo>",
            (CreateStreamCallback) delegate { return stream; },
            out warnings );
    }

    // Show user all warnings
    // NOTE: Default export handler doesn't do that
    if( warnings.Length > 0 )
    {
        var builder = new StringBuilder( );
        builder.AppendLine( "Please take notice that:" );

        warnings.Action( warning => builder.AppendLine( "- " + warning.Message ) );

        MessageBox.Show(
            builder.ToString( ),
            "Warnings",
            MessageBoxButton.OK,
            MessageBoxImage.Warning );
    }

    // Open created report
    // Process.Start( saveDialog.FileName );
}

ReflectionHelper 是一个获取默认报表名称的 private 属性值的类。ExpandContent 指示渲染器使用页面宽度的表格。否则内容将被压缩。Render 方法接受一个返回 streamdelegateRender 在该 stream 上执行。Render 退出时 stream 不会被关闭,因此我们必须自己确保关闭它。此外,默认的导出处理程序不会这样做,但我认为显示报表的警告很有用。

注意:还有一个问题需要强调。使用反射,可以将自定义渲染器添加到 ReportViewer。为什么不使用 SSRS 2008 中的 Word 渲染器呢?答案是 - 接口不兼容。是的,我经历了艰难的尝试才明白。

本地化

要本地化 ReportViewer GUI 界面,必须实现 IReportViewerMessages 并将实例分配给 Messages 属性。但是让我们做一些更有用的事情。让用户决定他想要什么语言。为此,我们需要一个列出所有可用语言的控件。我们需要将该控件托管在某个地方。在 ReportViewer 的某个地方将是完美的。

private void InitializeLocalization( )
{
    var separator = new System.Windows.Forms.ToolStripSeparator( );
    ToolStrip.Items.Add( separator );

    var language = new System.Windows.Forms.ToolStripComboBox( );
    language.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
    language.Items.Add( "English" );
    language.Items.Add( "Русский" );
    language.SelectedIndex = 0;
    language.SelectedIndexChanged += delegate
    {
        switch( (string) language.SelectedItem )
        {
        case "English":
            reportViewer.Messages = null;
            break;

        case "Русский":
            reportViewer.Messages = new RussianReportViewerMessages( );
            break;

        default:
            Debug.Assert( false, "Unknown language: " +
				(string) language.SelectedItem );
            break;
        }
    };
    ToolStrip.Items.Add( language );
}

自定义

作为甜点,让我们更改 ReportViewer 的一些图标,使其更具吸引力。

private void InitializeIcons( )
{
    // Fix find icons
    ToolStrip.Items[ "find" ].Image = Properties.Resources.Report_Find;
    ToolStrip.Items[ "findNext" ].Image = Properties.Resources.Report_FindNext;

    // Fix export icons
    var exportButton = ToolStrip.Items[ "export" ] as
			System.Windows.Forms.ToolStripDropDownButton;

    // Buttons are created on DropDownOpened so we can't assign Icon before it
    exportButton.DropDownOpened += delegate( object sender, EventArgs e )
    {
        var button = sender as System.Windows.Forms.ToolStripDropDownButton;
        if( button == null ) return;

        foreach( System.Windows.Forms.ToolStripItem item in button.DropDownItems )
        {
            var extension = (RenderingExtension) item.Tag;

            switch( extension.LocalizedName )
            {
            case "MS Word":
                item.Image = Properties.Resources.Report_Word;
                break;

            case "MS Excel":
                item.Image = Properties.Resources.Report_Excel;
                break;

            case "Adobe PDF":
                item.Image = Properties.Resources.Report_PDF;
                break;
            }
        }
    };
}

Using the Code

您可以使用代码中提供的整个 ReportViewerUserControl

<controls:ReportViewerUserControl
    EmbeddedReport="AdvancedReportViewer.Reports.SampleReport.rdlc"
    DataSource="{StaticResource samplePerson}" />

或者您只使用您需要的方面。我建议探索 ReportViewerUserControl.InitializeReportViewer 中的方面。

链接

没有以下文章,本文就不会诞生

  1. https://codeproject.org.cn/KB/cs/custreport.aspx - 自定义方式
  2. https://codeproject.org.cn/KB/printing/LocalizingReportViewer.aspx - 本地化方式
  3. https://codeproject.org.cn/KB/reporting-services/report-viewer-reflection.aspx - 探索方式
  4. http://beaucrawford.net/post/Enable-HTML-in-ReportViewer-LocalReport.aspx - 渲染方式

历史

  • 2009 年 4 月 9 日 - 首次发布
  • 2009 年 4 月 10 日 - 修复了 CreateReportDataSource 中的错误

版本 2 的思考

如果本文成功,我将在版本 2 中重点介绍以下方面

  • 带有对象数据源的子报表
  • 报表代码生成
  • 在报表中使用自定义代码
  • 使用 Microsoft Word COM interop 转换为真正的 *.doc
© . All rights reserved.