高级报表查看器






4.79/5 (30投票s)
本文展示了如何扩展 Visual Studio 2008 中附带的 ReportViewer 控件。最重要的扩展是增加了导出到 Microsoft Word 的功能
引言
本文展示了如何扩展 Visual Studio 2008 中附带的 ReportViewer
控件。最重要的扩展是增加了导出到 Microsoft Word 的功能。

文章亮点
- 集成到 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( ".", "_" );
}
}
使用显示单个对象数据的报表可能看起来很奇怪。但这实际上是我项目的主要用例。无论如何,代码足够灵活,可以接受集合、DataSet
和 DataTable
作为数据。
导出到 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”按钮时控件的行为。找到那个按钮是一个有趣的故事。你知道 ReportViewer
的 ToolStrip
可以通过 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
方法接受一个返回 stream
的 delegate
。Render
在该 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
中的方面。
链接
没有以下文章,本文就不会诞生
- https://codeproject.org.cn/KB/cs/custreport.aspx - 自定义方式
- https://codeproject.org.cn/KB/printing/LocalizingReportViewer.aspx - 本地化方式
- https://codeproject.org.cn/KB/reporting-services/report-viewer-reflection.aspx - 探索方式
- http://beaucrawford.net/post/Enable-HTML-in-ReportViewer-LocalReport.aspx - 渲染方式
历史
- 2009 年 4 月 9 日 - 首次发布
- 2009 年 4 月 10 日 - 修复了
CreateReportDataSource
中的错误
版本 2 的思考
如果本文成功,我将在版本 2 中重点介绍以下方面
- 带有对象数据源的子报表
- 报表代码生成
- 在报表中使用自定义代码
- 使用 Microsoft Word COM interop 转换为真正的 *.doc