۱۳۸۹/۰۸/۰۱

معرفي يك ابزار گزارشگيري رايگان مخصوص WPF


تا صحبت از گزارشگيري به ميان بيايد احتمالا معرفي ابزارهاي تجاري مانند Reporting services ، كريستال ريپورت، stimulsoft.com ، fast-report.com و امثال آن درصدر ليست توصيه كنندگان و مشاوران قرار خواهند داشت. اما خوب براي ايجاد يك گزارشگيري ساده حتما نيازي نيست تا به اين نوع ابزارهاي تجاري مراجعه كرد. ابزار رايگان و سورس باز جالبي هم در اين باره جهت پروژه‌هاي WPF در دسترس است:



در ادامه در طي يك مثال قصد داريم از اين كتابخانه استفاده كنيم:

1) تنظيم وابستگي‌ها
پس از دريافت كتابخانه فوق، ارجاعات زير بايد به پروژه شما اضافه شوند:
CodeReason.Reports.dll (از پروژه فوق) و ReachFramework.dll (جزو اسمبلي‌هاي استاندارد دات نت است)

2) تهيه منبع داده‌ گزارش
كتابخانه‌ي فوق به صورت پيش فرض با DataTable‌ كار مي‌كند. بنابراين كوئري‌هاي شما يا بايد خروجي DataTable داشته باشد يا بايد از يك سري extension methods براي تبديل IEnumerable به DataTable استفاده كرد (در پروژه پيوست شده در پايان مطلب، اين موارد موجود است).
براي مثال فرض كنيد مي‌خواهيم ركوردهايي را از نوع كلاس Product زير در گزارش نمايش دهيم:

namespace WpfRptTests.Model
{
public class Product
{
public string Name { set; get; }
public int Price { set; get; }
}
}
3) تعريف گزارش
الف) اضافه كردن فايل تشكيل دهنده ساختار و ظاهر گزارش
گزارش‌‌هاي اين كتابخانه مبتني است بر اشياء FlowDocument استاندارد WPF . بنابراين از منوي پروژه گزينه‌ي Add new item در قسمت WPF آن يك FlowDocument جديد را به پروژه اضافه كنيد ( بايد دقت داشت كه Build action اين فايل بايد به Content تنظيم گردد). ساختار ابتدايي اين FlowDocument به صورت زير خواهد بود كه به آن FlowDirection و FontFamily مناسب جهت گزارشات فارسي اضافه شده است. همچنين فضاي نام مربوط به كتابخانه‌ي گزارشگيري CodeReason.Reports نيز بايد اضافه گردد.
<FlowDocument xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
FlowDirection="RightToLeft" FontFamily="Tahoma"
xmlns:xrd="clr-namespace:CodeReason.Reports.Document;assembly=CodeReason.Reports"
PageHeight="29.7cm" PageWidth="21cm" ColumnWidth="21cm">

</FlowDocument>

مواردي كه در ادامه ذكر خواهند شد محتواي اين گزارش را تشكيل مي‌دهند:
ب) مشخص سازي خواص گزارش

<xrd:ReportProperties>
<xrd:ReportProperties.ReportName>SimpleReport</xrd:ReportProperties.ReportName>
<xrd:ReportProperties.ReportTitle>گزارش از محصولات</xrd:ReportProperties.ReportTitle>
</xrd:ReportProperties>
در اينجا ReportName و ReportTitle بايد مقدار دهي شوند (دو dependency property كه در كتابخانه‌ي CodeReason.Reports تعريف شده‌اند)

ج) مشخص سازي Page Header و Page Footer
اگر مي‌خواهيد عباراتي در بالا و پايين تمام صفحات گزارش تكرار شوند مي‌توان از SectionReportHeader و SectionReportFooter اين كتابخانه به صورت زير استفاده كرد:
    <xrd:SectionReportHeader PageHeaderHeight="2" Padding="10,10,10,0" FontSize="12">
<Table CellSpacing="0">
<Table.Columns>
<TableColumn Width="*" />
<TableColumn Width="*" />
</Table.Columns>
<TableRowGroup>
<TableRow>
<TableCell>
<Paragraph>
<xrd:InlineContextValue PropertyName="ReportTitle" />
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Right">
<xrd:InlineDocumentValue PropertyName="PrintDate" Format="dd.MM.yyyy HH:mm:ss" />
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>
</Table>
</xrd:SectionReportHeader>

<xrd:SectionReportFooter PageFooterHeight="2" Padding="10,0,10,10" FontSize="12">
<Table CellSpacing="0">
<Table.Columns>
<TableColumn Width="*" />
<TableColumn Width="*" />
</Table.Columns>
<TableRowGroup>
<TableRow>
<TableCell>
<Paragraph>
نام كاربر:
<xrd:InlineDocumentValue PropertyName="RptBy" Format="dd.MM.yyyy HH:mm:ss" />
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Right">
صفحه
<xrd:InlineContextValue PropertyName="PageNumber" FontWeight="Bold" /> از
<xrd:InlineContextValue PropertyName="PageCount" FontWeight="Bold" />
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>
</Table>
</xrd:SectionReportFooter>

دو نكته در اينجا حائز اهميت هستند: xrd:InlineDocumentValue و xrd:InlineContextValue
InlineDocumentValue را مي‌توان در كد‌هاي برنامه به صورت سفارشي اضافه كرد. بنابراين هر جايي كه نياز بود مقدار ثابتي از طريق كد نويسي به گزارش تزريق و اضافه شود مي‌توان از InlineDocumentValue استفاده كرد. براي مثال در كدهاي ViewModel برنامه كه در ادامه ذكر خواهد شد دو مقدار PrintDate و RptBy به صورت زير تعريف و مقدار دهي شده‌اند:
data.ReportDocumentValues.Add("PrintDate", DateTime.Now);
data.ReportDocumentValues.Add("RptBy", "وحيد");
براي مشاهده مقادير مجاز مربوط به InlineContextValue به فايل ReportContextValueType.cs سورس كتابخانه مراجعه كنيد كه شامل PageNumber, PageCount, ReportName, ReportTitle است و توسط CodeReason.Reports به صورت پويا تنظيم خواهد شد.

د) مشخص سازي ساختار توليدي گزارش

<Section Padding="80,10,40,10" FontSize="12">
<Paragraph FontSize="24" TextAlignment="Center" FontWeight="Bold">
<xrd:InlineContextValue PropertyName="ReportTitle" />
</Paragraph>
<Paragraph TextAlignment="Center">
گزارش از ليست محصولات در تاريخ:
<xrd:InlineDocumentValue PropertyName="PrintDate" Format="dd.MM.yyyy HH:mm:ss" />
توسط:
<xrd:InlineDocumentValue PropertyName="RptBy" Format="dd.MM.yyyy HH:mm:ss" />
</Paragraph>
<xrd:SectionDataGroup DataGroupName="ItemList">
<Table CellSpacing="0" BorderBrush="Black" BorderThickness="0.02cm">
<Table.Resources>
<!-- Style for header/footer rows. -->
<Style x:Key="headerFooterRowStyle" TargetType="{x:Type TableRowGroup}">
<Setter Property="FontWeight" Value="DemiBold"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="Background" Value="LightGray"/>
</Style>

<!-- Style for data rows. -->
<Style x:Key="dataRowStyle" TargetType="{x:Type TableRowGroup}">
<Setter Property="FontSize" Value="12"/>
</Style>

<!-- Style for data cells. -->
<Style TargetType="{x:Type TableCell}">
<Setter Property="Padding" Value="0.1cm"/>
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="0.01cm"/>
</Style>
</Table.Resources>

<Table.Columns>
<TableColumn Width="0.8*" />
<TableColumn Width="0.2*" />
</Table.Columns>
<TableRowGroup Style="{StaticResource headerFooterRowStyle}">
<TableRow>
<TableCell>
<Paragraph TextAlignment="Center">
<Bold>نام محصول</Bold>
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Center">
<Bold>قيمت</Bold>
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>

<TableRowGroup Style="{StaticResource dataRowStyle}">
<xrd:TableRowForDataTable TableName="Product">
<TableCell>
<Paragraph>
<xrd:InlineTableCellValue PropertyName="Name" />
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Center">
<xrd:InlineTableCellValue PropertyName="Price" AggregateGroup="Group1" />
</Paragraph>
</TableCell>
</xrd:TableRowForDataTable>
</TableRowGroup>

<TableRowGroup Style="{StaticResource headerFooterRowStyle}">
<TableRow>
<TableCell>
<Paragraph TextAlignment="Right">
<Bold>جمع كل</Bold>
</Paragraph>
</TableCell>
<TableCell>
<Paragraph TextAlignment="Center">
<Bold>
<xrd:InlineAggregateValue AggregateGroup="Group1"
AggregateValueType="Sum"
EmptyValue="0"
FontWeight="Bold" />
</Bold>
</Paragraph>
</TableCell>
</TableRow>
</TableRowGroup>

</Table>

<Paragraph TextAlignment="Center" Margin="5">
در اين گزارش
<xrd:InlineAggregateValue AggregateGroup="Group1"
AggregateValueType="Count"
EmptyValue="هيچ"
FontWeight="Bold" /> محصول با جمع كل قيمت
<xrd:InlineAggregateValue AggregateGroup="Group1"
AggregateValueType="Sum"
EmptyValue="0"
FontWeight="Bold" /> وجود دارند.
</Paragraph>
</xrd:SectionDataGroup>
</Section>
براي اينكه بتوان اين قسمت‌ها را بهتر توضيح داد، نياز است تا تصاوير مربوط به خروجي اين گزارش نيز ارائه شوند:




در ابتدا توسط دو پاراگراف، عنوان گزارش و يك سطر زير آن نمايش داده شده‌اند. بديهي است هر نوع شيء و فرمت مجاز در FlowDocument را مي‌توان در اين قسمت نيز قرار داد.
سپس يك SectionDataGroup جهت نمايش ليست آيتم‌ها اضافه شده و داخل آن يك جدول كه بيانگر ساختار جدول نمايش ركوردهاي گزارش مي‌باشد، ايجاد گرديده است.
سه TableRowGroup در اين جدول تعريف شده‌اند.
TableRowGroup هاي اولي و آخري دو سطر اول و آخر جدول گزارش را مشخص مي‌كنند (سطر عناوين ستون‌ها در ابتدا و سطر جمع كل در پايان گزارش)
از TableRowGroup مياني براي نمايش ركوردهاي مرتبط با نام جدول مورد گزارشگيري استفاده شده است. توسط TableRowForDataTable آن نام اين جدول بايد مشخص شود كه در اينجا همان نام كلاس مدل برنامه است. به كمك InlineTableCellValue، خاصيت‌هايي از اين كلاس را كه نياز است در گزارش حضور داشته باشند، ذكر خواهيم كرد. نكته‌ي مهم آن AggregateGroup ذكر شده است. توسط آن مي‌توان اعمال جمع، محاسبه تعداد، حداقل و حداكثر و امثال آن‌را كه در فايل InlineAggregateValue.cs سورس كتابخانه ذكر شده‌اند، به فيلدهاي مورد نظر اعمال كرد. براي مثال مي‌خواهيم جمع كل قيمت را در پايان گزارش نمايش دهيم به همين جهت نياز بود تا يك AggregateGroup را براي اين منظور تعريف كنيم.
از اين AggregateGroup در سومين TableRowGroup تعريف شده به كمك xrd:InlineAggregateValue جهت نمايش جمع نهايي استفاده شده است.
همچنين اگر نياز بود در پايان گزارش اطلاعات بيشتري نيز نمايش داده شود به سادگي مي‌توان با تعريف يك پاراگراف جديد، اطلاعات مورد نظر را نمايش داد.

4) نمايش گزارش تهيه شده
نمايش اين گزارش بسيار ساده است. View برنامه به صورت زير خواهد بود:
<Window x:Class="WpfRptTests.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:CodeReason.Reports.Controls;assembly=CodeReason.Reports"
xmlns:vm="clr-namespace:WpfRptTests.ViewModel"
Title="MainWindow" WindowState="Maximized" Height="350" Width="525">
<Window.Resources>
<vm:ProductViewModel x:Key="vmProductViewModel" />
</Window.Resources>
<Grid DataContext="{Binding Source={StaticResource vmProductViewModel}}">
<c:BusyDecorator IsBusyIndicatorHidden="{Binding RptGuiModel.IsBusyIndicatorHidden}">
<DocumentViewer Document="{Binding RptGuiModel.Document}" />
</c:BusyDecorator>
</Grid>
</Window>

تعريف ابتدايي RptGuiModel به صورت زير است (جهت مشخص سازي مقادير IsBusyIndicatorHidden و Document در حين بايندينگ اطلاعات):

using System.ComponentModel;
using System.Windows.Documents;

namespace WpfRptTests.Model
{
public class RptGuiModel
{
public IDocumentPaginatorSource Document { get; set; }
public bool IsBusyIndicatorHidden { get; set; }
}
}
و اين View اطلاعات خود را از ViewModel زير دريافت خواهد نمود:
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using CodeReason.Reports;
using WpfRptTests.Helper;
using WpfRptTests.Model;

namespace WpfRptTests.ViewModel
{
public class ProductViewModel
{
#region Constructors (1)

public ProductViewModel()
{
RptGuiModel = new RptGuiModel();
if (Stat.IsInDesignMode) return;
//انجام عمليات نمايش گزارش در يك ترد ديگر جهت قفل نشدن ترد اصلي برنامه
showReportAsync();
}

#endregion Constructors

#region Properties (1)

public RptGuiModel RptGuiModel { set; get; }

#endregion Properties

#region Methods (3)

// Private Methods (3)

private static List<Product> getProducts()
{
var products = new List<Product>();
for (var i = 0; i < 100; i++)
products.Add(new Product { Name = string.Format("Product{0}", i), Price = i });

return products;
}

private void showReport()
{
try
{
//Show BusyIndicator
RptGuiModel.IsBusyIndicatorHidden = false;

var reportDocument =
new ReportDocument
{
XamlData = File.ReadAllText(@"Report\SimpleReport.xaml"),
XamlImagePath = Path.Combine(Environment.CurrentDirectory, @"Report\")
};

var data = new ReportData();

// تعريف متغيرهاي دلخواه و مقدار دهي آن‌ها
data.ReportDocumentValues.Add("PrintDate", DateTime.Now);
data.ReportDocumentValues.Add("RptBy", "وحيد");

// استفاده از يك سري اطلاعات آزمايشي به عنوان منبع داده
data.DataTables.Add(getProducts().ToDataTable());

var xps = reportDocument.CreateXpsDocument(data);
//انقياد آن به صورت غير همزمان در ترد اصلي برنامه
DispatcherHelper.DispatchAction(
() => RptGuiModel.Document = xps.GetFixedDocumentSequence()
);
}
catch (Exception ex)
{
//وجود اين مورد ضروري است زيرا بروز استثناء در يك ترد به معناي خاتمه آني برنامه است
//todo: log errors
}
finally
{
//Hide BusyIndicator
RptGuiModel.IsBusyIndicatorHidden = true;
}
}

private void showReportAsync()
{
var thread = new Thread(showReport);
thread.SetApartmentState(ApartmentState.STA); //for DocumentViewer
thread.Start();
}

#endregion Methods
}
}

توضيحات:
براي اينكه حين نمايش گزارش، ترد اصلي برنامه قفل نشود، از ترد استفاده شد و استفاده ترد به همراه DocumentViewer كمي نكته دار است:
- ترد تعريف شده بايد از نوع STA باشد كه در متد showReportAsync مشخص شده است.
- حين باينديگ Document توليد شده توسط كتابخانه‌ي گزارشگيري به خاصيت Document كنترل، حتما بايد كل عمليات در ترد اصلي برنامه صورت گيرد كه سورس كلاس DispatcherHelper را در فايل پيوست خواهيد يافت.

كل عمليات اين ViewModel در متد showReport رخ مي‌دهد، ابتدا فايل گزارش بارگذاري مي‌شود، سپس متغيرهاي سفارشي مورد نظر تعريف و مقدار دهي خواهند شد. در ادامه يك سري داده آزمايشي توليد و به DataTables گزارش ساز اضافه مي‌شوند. در پايان XPS Document متناظر آن توليد شده و به كنترل نمايشي برنامه بايند خواهد شد.

دريافت سورس اين مثال