Crystal Reports – słowem wstępu dla początkującego

Tak jak napisałem w jednym z wcześniejszych postów nie zajmuję się już tworzeniem raportów w Crystal Reports. Nie oznacza to jednak, że nie piszę kodu, który wykorzystuje te raporty. Dlatego też postanowiłem zebrać w jednym miejscu informację, które zaczynającemu pracę z Crystal Reports developerowi służyłyby pomocą (przy czym jest to pomoc ze strony samouka, ale lepsza tak niż żadna).

1. Środowisko pracy

Teoretycznie wystarczy pobrać odpowiednią wersję ze strony producenta zainstalować i możemy działać. No właśnie teoretycznie wystarczy tyle. W praktyce powstaje pytanie czy chcemy raporty tworzyć samemu czy podpinać dostarczone przez kogoś innego. Różnica polega na posiadaniu właściwej wersji Visual Studio. Jeśli mamy wersję Express to o tworzeniu własnych raportów w VS możemy zapomnieć potrzebna nam jest wersja Professional.

2. Referencje do projektu

W celu uruchomienia raportów potrzebujemy trzech referencji:

  • CrystalDecisions.CrystalReports.Engine
  • CrystalDecisions.ReportSource
  • CrystalDecisions.Shared

I w zależności od rodzaju aplikacji:
Dla WindowsForms:

  • CrystalDecisions.Windows.Forms

Dla WPF:

  • SAPBusinessObjects.WPF.Viewer
  • SAPBusinessObjects.WPF.ViewerShared

Dla Web Forms:

  • CrystalDecisions.Web

Dla Asp.Net MVC nie potrzeba niczego więcej (podobno w WebFormsach wykorzystuje się CrystalDecisions.Web).
Te dodatkowe referencje potrzebne są aby wyświetlać raporty. W Visual Studio 2012 zauważyłem, że tworząc projekt możemy wybrać specjalny projekt typu Reports w wersji dla WindowsForms i dla WPFa.

3. Właściwa wersja .net

Jeśli piszemy aplikacje w .net 4.5 to nie ma problemu, ale jeśli nasz kod musi działać na maszynie dla, której szczyt możliwości to .net 4.0 lub 3.5 to możemy mieć przykrą niespodziankę (szczególnie jeśli raport jest nową funkcjonalnością już istniejącego programu). Czy zastanawialiście się może nad tym ile jest wersji .net 4.0 (albo 3.5)? Jeśli ktoś uważa to pytanie za głupie to musi wiedzieć, że istnieje coś takiego jak Client Profile. Jeśli spróbujemy skompilować program korzystający z Crystal Reports mając ustawiony Client Profile to spodziewajmy się komunikatu o braku referencji do CrystalDecision (pomimo tego, że referencje do nich przed chwilą dodaliśmy). Crystal Report wymaga pełnego .neta i tyle.

4. Pierwszy raport

Zakończyliśmy etap konfiguracji środowiska więc czas wreszcie coś napisać. Ponieważ nigdy nie wiadomo w jakiej technologi przyjdzie nam tworzyć postanowiłem przygotować źródła zawierające kilka rozwiązań (WinForms, WPF, Nancy – jako zamiennik MVC). Cały kod został umieszczony na GitHubie.
Wersja z prostym raportem znajduje się pod tagiem SimpleReport
Tworzymy nowy raport (Add=>New Item=>Reporting=>Crystal Report jeśli nie mamy takiej opcji to albo działamy na ekspresie albo nie zainstalowaliśmy Crystala) o nazwie HelloWorldReport (wybieramy opcję pusty raport). W nagłówku mamy wyświetlać napis (czyli oklepane “Hello World!”). Pozwolę sobie nie tłumaczyć jak to zrobić gdyż każdy choć trochę rozgarnięty poradzi sobie z tym zadaniem. Po zapisaniu raportu poza raportami zostanie utworzony plik cs z klasą o nazwie takiej jak raport.
No dobra mamy raport to teraz trzeba go wyświetlić. Aby poradzić sobie z tym zadaniem potrzebujemy kontrolkę, która to wyświetli.

WinForms
Tworzymy nową formę do której przeciągamy (brrr) kontrolkę o nazwie CrystalReportViewer. Ustawiamy jej właściwość DataSource na obiekt typu DataDocument

viewer.ReportSource=new HelloWorldReport();

i w momencie wyświetlenia takiej formy wyświetla się nam raport

WPF
w xamlu w definicji okna dodajemy

xmlns:cr="clr-namespace:SAPBusinessObjects.WPF.Viewer;assembly=SAPBusinessObjects.WPF.Viewer"

a podspodem np. coś takiego

<Grid>
    <cr:CrystalReportsViewer x:Name="Viewer" />
<Grid>

następnie code behind (ble! no ale to tylko przykład)

viewer.ViewerCore.ReportSource = new HelloWorldReport();

i znów tak samo w momencie wyświetlania się okna wyświetla się raport.

WebForms
To akurat technologia w której mam najmniejsze doświadczenie więc mój opis będzie teoretyczny bez przykładu na GitHubie.
Najpierw rejestracja przestrzeni zawierającej CrystalReportViewer

<%@ Register TagPrefix = "cr" Namespace="CrystalDecisions.Web" Assembly="CrystalDecisions.Web" %>

A teraz jej wywołanie w obrębie strony

<cr:CrystalReportViewer id=Viewer runat="SERVER"></cr:CrystalReportViewer>

na koniec pozostaje code behind

protected void Page_Load(object sender, EventArgs e)
{
    Viewer.ReportSource = new HelloWorldReport();
}

Nancy
Niektórzy pewnie się dziwią czemu przykład jest w Nancy a nie Asp.Net MVC odpowiedź jest prosta. Mechanika tworzenia raportu jest taka sama, ale przykłady są lżejsze. Wracając do sedna zastanawiacie się może czemu nie ma dodatkowych referencji dla Viewera tak jak w WPF, WinFormsach lub WebFormsach. Otóż według mojej najlepszej wiedzy po prostu nie ma Viewera a jedynym sposobem jest konwersja na inny format i takie wyświetlenie raportu.
Np możemy skonwertować nasz raport do pdf i użyć go jako odpowiedzi na żądanie:

var report = new HelloWorldReport();
report.ExportToHttpResponse(ExportFormatType.PortableDocFormat, HttpContext.Current.Response, false, report.Name);

Oczywiście nie musimy tworzyć obiektów (przynajmniej jawnie) klasy którą chcemy wyświetlić. Istnieje również alternatywna metoda*.

ReportDocument report = new ReportDocument();
report.Load(reportPath);

i tak przygotowany obiekt typu ReportDocument ustawiamy jako ReportSource. Czyli możemy w aplikacji odpalać raporty przygotowane poza aplikacją (o ile chcemy).

Tylko, że to co wyświetliliśmy na razie to zwykły statyczny raport czas na małą komplikację.

5. Przekazywanie parametrów

Wersja z raportem z przekazywanym parametrem znajduje się pod tagiem ReportsWithParameter
Przekazywanie parametrów to prosta sprawa. A zarazem jest to kwestia, na której łatwo się potknąć. Zaczynamy zabawę od stworzenia nowego raportu o nazwie HelloYou. W FieldExploerze odnajdujemy składnik o nazwie Parameter Fields i dodajemy nowy parametr o nazwie Name (wielkość liter nie ma znaczenia) o typie znakowym.

Field Exploer
W nagłówku raportu dodajemy pole tekstowe (Text object), w którym wpisujemy treść powitania a w miejsce, w którym chcemy wyświetlić parametr przeciągamy odpowiedni parametr z Field Exploera. Nasze pole tesktowe powinno wyglądać mniej więcej tak:

Hello {?Name} !

Uwaga! Próba wpisania tego z ręki spowoduje wyświetlenie dokładnie tego napisu więc konieczne jest przeciąganie! Istne Mouse Driven Development.
Odpalamy więc raport według jednego z powyższych sposobów i oczom naszym zamiast raportu ukazuje się takie oto okno (w WinForms i WPF).

Enter Parameter Window
Za to w aplikacji internetowej zostaniemy uraczeni błędem typu ParameterFieldCurrentValueException.
W celu dodania parametrów używamy takiej składni:

var report = new HelloYou();
report.SetParameterValue("Name",value);

Musimy jednak uważać na właściwe nazwy parametrów bo w momencie przekazania czegoś nieprzewidzianego przez raport zostaniemy uraczeni bardzo nieprzyjemnym COMException. A w momencie gdy przekażemy wartość złego typu dostaniemy ParameterFieldException.
W celu ułatwienia sobie pracy napisałem taką oto klasę do generowania raportów:

public class ReportGenerator : IReportGenerator
{
    public ReportDocument GenerateReport(ReportDocument report, IDictionary<string, object> parameters = null)
    {
        if (report == null)
            throw new ArgumentException("Report cannot be null");
        AddParametersToReport(report, parameters);
        return report;
    }

    private static void AddParametersToReport(ReportDocument report, IDictionary<string, object> parameters)
    {
        if (parameters != null)
        {
            foreach (var parameter in parameters)
            {
                report.SetParameterValue(parameter.Key, parameter.Value);
            }
        }
    }
}

Dzięki niej mam możliwość łatwego generowania raportów (poniżej zostanie ona rozbudowana o następne opcje). Dla bardziej czytelnego kodu nie ma w nim bloku try-catch, ale w kodzie produkcyjnym jest to niezbędne.

6. Dostęp do danych

Głównym celem raportów jest wypisywanie w formie czytelnej dla użytkownika danych. Powstaje kwestia jak te dane pobrać. Zanim o tym trochę teorii.
Dane do raportów mogą być załadowane na dwa sposoby pull i push.
Z pullem mamy do czynienia gdy raport wie skąd ma mieć załadowane dane a z pushem gdy zna strukturę, ale dane muszą być do niego przesłane. Raporty pull są łatwiejsze do tworzenia (istnieje możliwość podglądu bez uruchomienia programu wyniku uruchomienia raportu), ale w momencie gdy zmieni się struktura bądź położenie danych rodzi to dodatkowe problemy. Niech za przykład posłuży sytuacja w której miałem raport podłączony do ściśle określonego ODBC a na wybranym stanowisku miały być uruchamiane raporty dla różnych baz. Musiałem stworzyć metodę, która w momencie uruchamiania raportu zmieni wpis w ODBC na właściwy. Kolejnym minusem używania był nietypowy sterownik ODBC, który nie istniał w wersji x64 bądź działał wadliwie (podczas gdy aplikacja była skompilowana na Any Cpu i uruchomiona w środowisku 64 bitowym), albo dostęp do danych o znanej strukturze, ale różnym kodowaniu (i czasami odczyt 25 znakowego pola tekstowego jako tablicy typu byte). Korzystając z ODBC musimy być również świadomi co w danym ODBC siedzi. Kiedyś za żadne skarby nie mogłem uruchomić prawie żadnego raportu ponieważ na nowo zainstalowanej maszynie ODBC dla Postgresa traktowało wartości logiczne jako .. ciąg znaków, albo dlaczego nie wyświetla opisu mimo, że dane są w bazie (maksymalna długość ciągu była ustawiona na 255 a baza “pozwalała” na przechowywanie w tej kolumnie do 400 znaków znów zaawansowane opcje ODBC Postgresa).
Co wcale nie oznacza, że push nie ma wad. Jak dziś pamiętam mój pierwszy raport z podpiętym DataSetem gdy chciałem zobaczyć bez uruchomienia programu raport wynikowy (nie byłem świadomy, że to nie działa) i gdy zamiast nazw kontrahentów zobaczyłem nazwy kolorów, zamiast kwot dokumentów losowe liczby a zamiast numerów faktur dni tygodnia. Inną ciekawą sytuacją było gdy podczas przeprowadzania drobnej zmiany w DataSecie uszkodzeniu uległy plik cs DataSeta i zamiast jednego mieliśmy ich kilka co uniemożliwiało działanie aplikacji (trzeba było je skasować i usunąć informację z pliku csproj). Dodatkowo uzupełnianie danych musimy przeprowadzić programowo więc raport nie może być użyty w zewnętrznym Crystal Report Viewer.
Dlatego też przedstawiając sposób załadowania danych na przykładach pokaże wyłącznie push a pull tylko informacyjnie omówię.
PULL
Z Field Exloera wybieramy Datasase Fielsds a następni wybieramy interesujące nas źródło danych a z niego interesujące nas tabele (bądź widoki). Zawsze możemy zmienić decyzję i dodać (bądź odjąć) kolejne tabele. Nie wiem jak w przypadku innych baz ale w przypadku Postgresa nie było możliwości dodania funkcji. Przynajmniej nie wprost. Jeśli jednak ktoś by tego potrzebował to należy wchodząc w opcje źródła danych wyłączyć pokazywanie tabeli i widoków po czym odświeżyć i cieszyć się z funkcji.

Enter Parameter Window
PUSH
Wersja z Datasetem znajduje się pod tagiem ReportFromDataSet
Dane do raportów mogą być również przekazane poprzez DataSet albo klasę. Na początek DataSet. Dodajemy do projektu DataSet nazywamy go People tworzymy w nim dwie kolumny FirstName i LastName. Następnie dodajemy raport do którego jako źródło danych wskazujemy nasz DataSet. Wybieramy co i gdzie wyświetlić. Uzupełniamy naszego DataSeta danymi. Podłączamy DataSeta jako źródło danych do raportu (wszak to push czyli to my wypychamy dane).

report.SetDataSource(ds);

I jesteśmy uraczeni komunikatem o błędzie. FileNotFoundException nie można załadować pliku crdb_adoplus.dll. Szukamy pliku i znajdujemy go ale w innej lokalizacji niż wskazana. Problem leży gdzie indziej. Otóż w celu zapewnienia zgodności z .netem 2.0 komponenty CR runtime są kompilowane tak aby mogły z nim współpracować. Trzeba wtedy przerobić appconfig by wyglądało podobnie do tego:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <startup useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
</configuration>

Na pytanie czy można podpiąć kilka DataSet do jednego raportu odpowiadam nie bezpośrednio, jeden raport-jeden (niezależny) DataSet.

Wersja z klasą jako źródłem danych znajduje się pod tagiem ReportFromClass
Klasę podłączamy podobnie tzn. w Field Exploerze dodajemy nowe źródło danych wybieramy z .net object konkretną klasę (musi być wcześniej zbuildowana). Jej publiczne właściwości (nie pola) możemy teraz ustawiać w raporcie (tak jak z DataSeta). Musimy tylko pamiętać o tym, że o ile tworząc raport jako źródło danych używamy klasy to podłączając podłączamy IEnumerable.

var list=new List<Team>();
report.SetDataSource(list);

7. Subreporty

Ostateczna wersja czyli subreporty + wszystko omówione wcześniej znajdziemy pod tagiem ReportsWithSubreports
Ostatnią kwestią, którą chciałem dzisiaj omówić są subreporty. Przydają gdy chcemy jakąś część (np nagłówek z adresem) powtórzyć w wielu raportach, gdy w ramach raportu musimy zrobić jakiś podraport, itp. zastosowań nie zabraknie. Do subreportów możemy przekazywać to wszystko co normalnie. Ze względów praktycznych ja wszystkie parametry przekazuje do raportu głównego a on rozprowadza do raportów podrzędnych. Ponieważ piszę ten kod w tej chwili naciąłem się właśnie na zapomniany feature Crystala: ustawiając parametry i ReportSource dla tego samego raportu ustaw najpierw ReportSource bo parametry i tak zostaną zresetowane. Subreporty mogą być wbudowane w główny raport jaki i mogą być używane jako osobne pliki. Należy zwrócić szczególną uwagę na edycję subreportów. Tak aby nie poprawiać subreportu wchodząc na niego z poziomu głównego raportu(pracujemy wtedy na kopi!). Po poprawie subreportu należy go przeładować ręcznie (inaczej będziemy mieć stary stan). To może jeszcze sposób ładowania danych do subreportów

report.Subreports["SubReportTwo.rpt"].SetDataSource(ds);

Podsumowanie

Być może po przeczytaniu tego przydługiego artykułu ktoś może zniechęcić się do Crystal Reports, ale moim zdaniem nie jest tak źle. Co nie znaczy, że nie mogło być lepiej. Mój opis dotyczył części programistycznej pozostaje jeszcze duża część, dotycząca opisu działania samego programu (formuły, warunki), ale nie czuję się na siłach by to opisać. Ogólnie to bardzo potężne narzędzie, które pomaga w pracy ale czasami może napsuć sporo krwi. Aby udowodnić, że nie jest tak źle na koniec anegdota. Pewnego pięknego dnia dostałem zadanie poprawienia wydruku powstałego w czymś takim jak MicrosoftFoxPro. Niezrażony nieznajomością narzędzia zabralem się za robotę i już po 5 minutach miałem gotowe to co chciałem. Pozostała jeszcze kwestia precyzyjnego umieszczenia tego na wydruku. Nie mogłem nigdzie znaleźć lupy, standardowe kombinacje powiększenia nie działały więc udałem się do wujka google i znalazłem tam odpowiedź Kup większy monitor. I tym optymistycznym akcentem kończymy na dziś.

* Wstyd się przyznać, ale przez długi czas uważałem tą metodę za jedyną. Na usprawiedliwienie mogę mieć tylko fakt, że tak zostałem nauczony.

2 thoughts on “Crystal Reports – słowem wstępu dla początkującego

  1. Pingback: dotnetomaniak.pl
  2. Cześć.

    Świetny artykuł na początek drogi z CR.
    Dobrze by było wyjaśnić jeszcze kwestie licencjonowania CR dla developerów. Z tego co wiem, to są tam jakieś magiczne różnice gdy się embeduje do aplikacji klenckiej (np WPF), inaczej gdy do aplikacji serwerowej itd. Nie zawsze poprostu licencja jest darmowa.

Comments are closed.