среда, 25 августа 2010 г.

Экспорт данных в Microsoft Excel 2007

Сейчас мы рассмотрим еще одну стандартную задачу: создание отчетов в Microsoft Excel 2007. Можно, конечно, использовать для отчетов какие-нибудь специальные инструменты вроде Crystal Reports, но не все умеют пользоваться подобными инструментами (я имею в виду тех, кто будет заполнять шаблон отчета на стороне заказчика).

Можно выделить два различных вида экспорта:

1. Создание нового файла с отчетом и наполнение его данными.

2. Получение готового шаблона отчета (с форматированием, подписями и т.д.) и наполнение его данными.

Мне пришлось сталкнуться со вторым типом генерации отчетов, так что дальше речь пойдет именно об этом типе.

Первое, что мы сделаем – это создадим Silverlight страницу, с котором будет осуществляться переход на aspx страницу, которая будет возвращать отчет в формате Microsoft Excel 2007.

Предположим, что запросы для нашего отчета будт принимать в качестве параметов значения идентификатора проекта (не важно, что это значит), с которым ассоциированы данные, и две даты: дата, с которой нужно начинать выбирать данные и конечная дата (в UI будем использовать номер квартала).

Подготовим следующую страницу:

Report.xaml:

<navigation:Page 
   x:Class="MyApplication.Reports"
   xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
   xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
   xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
   xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
   xmlns:riaControls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.DomainServices"
     xmlns:my="clr-namespace:MyApplication.Web.Services"
   mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480" 
   Style="{StaticResource PageStyle}">
 
     <Grid x:Name="LayoutRoot">
         <ScrollViewer x:Name="PageScrollViewer" Style="{StaticResource PageScrollViewerStyle}">
 
             <StackPanel x:Name="ContentStackPanel" Style="{StaticResource ContentStackPanelStyle}">
                 <TextBlock x:Name="HeaderText" Style="{StaticResource HeaderTextStyle}"
                            Text="{Binding Path=ApplicationStrings.ReportsPageTitle, Source={StaticResource ResourceWrapper}}"
                                />
 
                 <Grid>
                     <Grid.ColumnDefinitions>
                         <ColumnDefinition Width="100"/>
                         <ColumnDefinition Width="*"/>
                     </Grid.ColumnDefinitions>
 
                     <Grid.RowDefinitions>
                         <RowDefinition Height="30" />
                         <RowDefinition Height="30" />
                         <RowDefinition Height="30" />
                     </Grid.RowDefinitions>
 
 
                     <TextBlock Text="Project:" Grid.Row="0" Grid.Column="0" VerticalAlignment="Center"/>
                     <ComboBox Height="23" Width="200" Margin="10,0,0,0" ItemsSource="{Binding ElementName=projectDomainDataSource, Path=Data}" DisplayMemberPath="Name" Name="cbProject"
                                    Grid.Row="0" Grid.Column="1" HorizontalAlignment="Left">
                         <ComboBox.ItemsPanel>
                             <ItemsPanelTemplate>
                                 <VirtualizingStackPanel />
                             </ItemsPanelTemplate>
                         </ComboBox.ItemsPanel>
                     </ComboBox>
 
 
                     <riaControls:DomainDataSource AutoLoad="True" Height="0" LoadedData="projectDomainDataSource_LoadedData" Name="projectDomainDataSource" QueryName="GetProjectsQuery" Width="0">
                         <riaControls:DomainDataSource.DomainContext>
                             <my:MyApplicationDomainContext />
                         </riaControls:DomainDataSource.DomainContext>
                     </riaControls:DomainDataSource>
 
                     <TextBlock Text="Period From: " Grid.Row="1" Grid.Column="0" VerticalAlignment="Center"/>
                     <ComboBox x:Name="cbPeriodFrom" Margin="10,0,0,0" Width="200"  Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left" Height="23"/>
                     <TextBlock Text="Period To: "  Grid.Row="2" Grid.Column="0"  VerticalAlignment="Center"/>
                     <ComboBox x:Name="cbPeriodTo" Margin="10,0,0,0" Width="200"   Grid.Row="2" Grid.Column="1" HorizontalAlignment="Left" Height="23"/>
 
                 </Grid>
                 <Button Name="btnGenerate" Margin="0,10,0,0" Width="120" Height="23" Content="Generate" HorizontalAlignment="Left" Click="btnGenerate_Click" />
             </StackPanel>
 
 
         </ScrollViewer>
     </Grid>
 

</navigation:Page>


Report.xaml.cs:



namespace MyApplication

{
     using System.Windows.Controls;
     using System.Windows.Navigation;
     using System;
     using System.Windows.Browser;
     using System.Collections.Generic;
     using MyApplication.Helpers;
     using MyApplication.BLL;
 
     /// <summary>
     /// <see cref="Page"/> class to present information about the current application.
     /// </summary>
     public partial class Reports : Page
     {
         private List<string> _quartals;
         /// <summary>
         /// Creates a new instance of the <see cref="Reports"/> class.
         /// </summary>
         public Reports()
         {
             if (WebContext.Current.User.IsAuthenticated)
             {
                 InitializeComponent();
             }
 
             this.Title = ApplicationStrings.ReportsPageTitle;
         }
 
         private void Initialize()
         {
             // fill quartal list
             _quartals = new List<string>();
 
             int quarter = DateTime.Today.Month / 3 + 1;
             int year = DateTime.Today.Year;
 
 
             for (int i = 0; i < 10; i++)
             {
                 _quartals.Add(string.Format("Q{0} {1}", quarter, year));
 
                 quarter--;
 
                 if (quarter < 1)
                 {
                     quarter = 4;
                     year--;
                 }
             }
             cbPeriodFrom.ItemsSource = _quartals;
             cbPeriodFrom.SelectedIndex = 0;
 
             cbPeriodTo.ItemsSource = _quartals;
             cbPeriodTo.SelectedIndex = 0;
         }
 
         /// <summary>
         /// Executes when the user navigates to this page.
         /// </summary>
         protected override void OnNavigatedTo(NavigationEventArgs e)
         {
             if (!WebContext.Current.User.IsAuthenticated)
             {
                 this.NavigationService.Navigate(new Uri("/Home", UriKind.Relative));
                 return;
             }
             Initialize();       


}
 
 
         private void btnGenerate_Click(object sender, System.Windows.RoutedEventArgs e)
         {
             // validate data
             var dateFrom = DateHelper.GetDateFromQuarterString(cbPeriodFrom.SelectedItem as string);
             var dateTo = DateHelper.GetDateFromQuarterString(cbPeriodTo.SelectedItem as string);
 
             if (cbProject.SelectedIndex == -1)
             {
                 HtmlPage.Window.Alert("Select project");
                 return;
             }
 
             if (dateFrom < dateTo)
             {
                 HtmlPage.Window.Navigate(
                     new Uri(string.Format("GetReport.aspx?id={0}&from={1}&to={2}", (cbProject.SelectedItem as Project).ID, dateFrom.ToShortDateString(), dateTo.ToShortDateString()),
                     UriKind.Relative));
             }
             else
             {
                 HtmlPage.Window.Alert("Period from should be less than period to");
             }
         }
 
         private void projectDomainDataSource_LoadedData(object sender, LoadedDataEventArgs e)
         {
 
             if (e.HasError)
             {
                 System.Windows.MessageBox.Show(e.Error.ToString(), "Load Error", System.Windows.MessageBoxButton.OK);
                 e.MarkErrorAsHandled();
             }
         }
     }

}


Ничего интересного тут нет, мы используем HtmlPage.Window.Navigate() для редиректа пользователя на aspx страницу. Все параметры передаются в строке запроса.



Код aspx также не слишком сложный, но в нем будут определенные хитрости. Положим шаблон для нашего отчета (пока не важно, что он содержит, главное, что файл для Microsoft Excel 2007) в папку App_Data.



Сначала я копирую шаблон отчета во спомогательный файл со случайным именем. Далее, нужно пройтись по нему некоторым кодом, которого пока, но будем считать, что он есть и что-то делает.



Далее нужно отправить файл пользователю через Response.



Тут почти всегда возникают сложности. Но, вроде, у меня получилось найти неплохое решение.



Microsoft Excel 2007 использует формат Open XML, и его нельзя передавать как стандартный Excel файл, нужно использовать специальный заголовок:



Response.Clear();

            Response.AppendHeader("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");


            Response.AppendHeader("Content-Disposition", "attachment; filename=Certificate.xlsx;");


            Response.Charset = Encoding.Unicode.WebName;


            Response.TransmitFile(newFileName);


            Response.End();



Весь код aspx страницы выглядит так:



GetReport.aspx:



<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="GetReport.aspx.cs" Inherits="HuronLoans.Web.GetReport" %>
 

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 

<html xmlns="http://www.w3.org/1999/xhtml">

<head runat="server">
     <title></title>

</head>

<body>
     <form id="form1" runat="server">
     <div>
    
     </div>
     </form>

</body>

</html>


GetReport.aspx.cs



using System;

using System.Collections.Generic;

using System.Linq;

using System.Web;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Data.SqlClient;

using System.IO;

using MyApplication.BLL;

using System.Text;
 
 

namespace MyApplication.Web

{
     public partial class GetReport : System.Web.UI.Page
     {
         protected void Page_Load(object sender, EventArgs e)
         {
             int id = 0;
             DateTime from = DateTime.Now;
             DateTime to = DateTime.Now;
             try
             {
                 id = Convert.ToInt32(Request.QueryString["id"]);
                 from = Convert.ToDateTime(Request.QueryString["from"]);
                 to = Convert.ToDateTime(Request.QueryString["to"]);
 


             }
             catch (Exception ex)
             {               


return;
             }
 
             string newFileName = Server.MapPath(@"~\App_Data\w1"+Guid.NewGuid().ToString().Replace("-", "") +".xlsx");
             var pathToReport = Server.MapPath(@"~\App_Data\MyReportTemplate.xlsx");
             File.Copy(pathToReport, newFileName);



             ReportCreator.Create(newFileName, id, from, to);



             Response.Clear();
             Response.AppendHeader("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
            
             Response.AppendHeader("Content-Disposition", "attachment; filename=Report.xlsx;");
             Response.Charset = Encoding.Unicode.WebName;
 
             Response.TransmitFile(newFileName);
             Response.End();
 
             try
             {
                 File.Delete(newFileName);
             }
             catch (Exception ex)
             {
                
             }
         }
     }

}


Теперь нужно написать собственно обработчик шаблонов Excel файлов. Для того, чтобы наиболее удобно (пожалуй, более хорошего ничего и нет) работать с форматов Open XML в C# нужно использовать Open XML SDK 2.



Данная библиотека предлагет достаточно типизированные методы для работы с файлами для Office 2007. Библиотека, конечно, ужасно неудобная, но, все равно, это лучше чем то, что было раньше.



Так или иначе, для того, чтобы работать с Microsoft Excel 2007 нужно установить Open XML SDK 2 и подключить к проекту сборку DocumentFormat.OpenXml.dll.



Вообще существует достаточно много способов работы с файлами в формате Microsoft Office 2007, но всегда обязательно нужно проверять лицензионную политику своего решения. Некоторые варианты требуют обязательной установки Microsoft Office на сервер, при этом существуют определенные лицензионные ограничения на «многопользовательское» использование Microsoft Office. На сколько я понимаю, то, что мы будем делать дальше как раз и является таким «многопользовательским» использованием.



Есть ли какие-то ограничения при использовании Open XML SDK я не знаю, но думаю, что их нет. Более того, данная библитека теоретически может работать в несколько поток (или как-то так), чего не могут позволить практически все остальные решения.

Комментариев нет:

Отправить комментарий