Delphi programming blog
Источник: http://teran.karelia.pro/articles/item_4482.html
 

RTTI #3

Опубликовано 22.07.2011 г. 00:18

Третья статья на тему RTTI, на сей раз не относится к атрибутам, а будет нацелена на получение информации о методах класса и их вызове. Вернее, по задумке, вызова одного нужного метода.

Пару лет назад, когда я пришел на новое место работы и начал изучать Delphi, был один проект, в котором использовались отчеты FastRepport, для чего был сделан модуль отчетов в виде отдельного дата-модуля. Обычно эти отчеты строились прямо в коде, с использованием CrossView, т.е не построение из БД и т.п а динамические. Построение велось с помощью события OnBeforePrint (если мне не изменяет память), и в дата-модуле была кучка методов а-ля BuildReport1().... BuildReportN() (а может это я их потом привел к одному виду, не помню). Суть в том, что каждый такой метод имел одну и ту же сигнатуру и являлся обработчиком сего события построения, где заполнялся CrossView. Некоторые запросы при этом грузили шаблон из файла, некоторые просто создавали CrossView. Но суть всего этого в том, что процедура вызова этого обработчика была страшна. К сожалению я уже не вспомню как это выглядело, но суть была в том, что был один объект отчета TFrxReport и страшный метод (непосредственно назначенный обработчик события OnBeforePrint объекта отчета)в котором было что то вида:
if reportName = 'table1' then BuildReport1() 
else if reportName = 'table2'  then BuildReport2()
else if ...
В дальнейшем данный код был переработан, были внедрены классы описания отчета. Класс содержал информацию, например, как заголовок отчета (для экспорта в меню), название отчета - для указания в самом отчете, файл отчета - если требовалось загрузить шаблон из файла, и форма запроса параметров - метакласс формы параметров, если перед показом отчета необходимо было указать, например, год. Ну и собственно конечно же указывались данные обработчики построения. В этой статье я попробую кратко воссоздать тот функционал, и расширить его использованием механизма RTTI для вызова обработчиков построения отчета. К сожалению у меня сейчас нет FastReport и я не могу использовать эти компоненты, так что будем лишь предполагать работу с отчетом. Да и вообще не важно какой там у нас генератор отчетов. Сначала опишу форму параметров - это форма имеющая свойство типа TStringList. Параметры будут передаваться через него, для моих целей этого было достаточно. Окно формы параметров показываться будет модально, так что единственная кнопка для примера будет иметь ModalResul = mrOk. родительский класс:
    TCustomParamsForm = class(TForm)
      public
        ParamsList : TStringList;
    end;
    TParamsFormClass = class of TCustomParamsForm;
Как видим кроме родительского, есть и соответствующий метакласс. Для примера сама форма запроса параметров у меня такая: комбобокс yearCombo с перечислением нескольких лет, и кнопка ОК. При нажатии кнопки, выбранные параметры записываются в paramList:
procedure TFrmParams1.OkButtonClick(Sender: TObject);
begin
    ParamsList.Values['year'] := YearCombo.items[YearCombo.itemIndex];
end;
поскольку форма показывается модально, и установлен modalResult кнопки, то больше ничего делать не требуется. Далее, отчет который использует данную форму, естественно знает, какие параметры форма записала в список, и в процедуре построения получит их. Теперь класс описания отчета:
    TReportDefinition = class(TObject)
      strict private
         FId : integer;
         FCaption : string;
         FTitle : string;
         FReportFile : string;
         FParamsForm : TParamsFormClass;
      public
        constructor Create(aCaption: string; aId : integer; aReportFile : string = ''; aParamsForm : TParamsFormClass = nil);
        property ID : integer read FId;
        property Caption : string read FCaption;
        property Title: string read FTitle write FTitle;
        property ReportFile : string read FReportFile;
        property ParamsForm : TParamsFormClass read FParamsForm;
    end;
Как видим здесь ничего особо нет: идентификатор отчета, заголовок, название, метакласс формы параметров, файл шаблона. Т.е. никаких действий по большому счету класс не производит. Следующее - небольшой "менеджер отчетов":
    TReportManager = class(TObject)
      strict private
        FReportList : TObjectList<TReportDefinitin>;
        FReportParams : TStringList;
        FReportBuilder : TReportBuilder;
        procedure RegisterReports();
      public
        constructor Create();
        procedure BuildReportMenu(parentMenuItem : TMenuItem);
        procedure OnMenuItemClick(Sender : TObject);
        procedure LoadReport(reportFile : string);
    end;
Суть его в чем - собрать все описания отчетов в список, построить меню для интерфейса, и провести действия по нажатиям этого меню. Список описаний хранится в поле FreportList, параметры, которые передаются в форму параметров - FReportParams. Регистрация отчетов происходит например таким образом:
procedure TReportManager.RegisterReports();
var rItem : TReportDefinition;
begin
    rItem := TReportDefinition.Create('отчет1', 1);
    rItem.Title := 'заголовок отчета1';
    FReportList.Add(rItem);

    rItem := TReportDefinition.Create('второй отчет', 2, 'report2.fr3', TFrmParams1);
    rItem.Title := 'заголовок отчета номер 2';
    FReportList.Add(rItem);
end;
Теперь строится меню отчетов - главная форма передает менеджеру корневой элемент меню:
procedure TReportManager.BuildReportMenu(parentMenuItem: TMenuItem);
var mItem : TMenuITem;
    i : integer;
begin
    for i:= 0 to FReportList.Count - 1 do begin
        mItem := TMenuItem.Create(parentMenuItem);

        mItem.Tag := i;
        mItem.Caption := FReportList[i].Caption;
        mItem.OnClick := self.OnMenuItemClick;

        parentMenuItem.Add(mItem);
    end;
end;
где мы назначаем tag элемента, и указываем обработчик нажатия. Он конечно же для всех общий, и будет обратно извлекать значение поля tag, получая номер вызванного отчета. Выше в описании класса TReportManager вы могли заметить поле FReportBuilder. Это класс который аккумулирует методы построения отчетов. Методы будут названы BuildReport + ID отчета.
    TReportBuilder = class(TObject)
      strict private
        FParams : TStringList;
      public
        constructor Create(paramsList : TStringList);
        procedure BuildReport1();
        procedure BuildReport2();
        procedure BuildReport3();
    end;
И конечно же класс должен иметь доступ к параметрам, которые возвращены из формы. Так что все тот же объект TStringList от менеджера, передаваемый в формы, передается и сюда, в качестве параметра конструктора. В виде заглушек для тестирования методы можно описать как нибудь так (для примера извлечения параметров отчета обратно):
procedure TReportBuilder.BuildReport2();
begin
    ShowMessage('buildReport2: Year = ' + FParams.Values['year']);
end;
Конечно же при использовании FastReport этот метод реализует обработчик события OnBeforePrint и имеет параметр Sender: TfrxReportComponent. И собственно строит отчет, например, заполняет CrossView. Вернемся обратно к менеджеру отчетов - сам обработчик нажатия пункта меню. с FastReport его код следует разделить на два метода, первая часть так и остается обработчиком нажатия. Далее вызывается метод TFrxReport.ShowReport(), а последняя часть переносится в обработчик onBeforePrint сего отчета.
procedure TReportManager.OnMenuItemClick(Sender: TObject);
var index : integer;
    rItem : TReportDefinition;
    pForm : TCustomParamsForm;
    ctx : TRttiContext;
    t : TRttiType;
    buildMethodName : string;
    buildMethod : TRttiMethod;
begin
    index := TMenuItem(sender).Tag;
    rItem := FReportList[index];

    if assigned(rItem.ParamsForm) then begin
        FReportParams.Clear();
        pForm := rItem.ParamsForm.Create(nil);
        pForm.ParamsList := self.FReportParams;
        if mrOk <>  pForm.ShowModal() then exit;
    end;

    if rItem.ReportFile <>  '' then LoadReport(rItem.ReportFile);

    // report.showReport();
    // Report.OnBeforePrint:
    if rItem.ID < 0 then exit;
    ctx := TRttiContext.Create();
    try
        t := ctx.GetType(TReportBuilder);
        buildMethodName := Format('BuildReport%d', [rItem.ID]);
        buildMethod := t.GetMethod(buildMethodName);
        if assigned(buildMethod) then
            buildMethod.Invoke(FReportBuilder, []);   // [param  - frxComponent]
    finally
        ctx.Free();
    end;
end;
Во первых, мы получаем описание отчета, который был вызван. Затем если указана форма параметров, то с помощью ее метакласса мы ее создаем. Следующим шагом, при необходимости мы загружаем шаблон отчета. Теперь подходит очередь показать отчет,и все что ниже - это обработчик события onBeforePrint. Суть здесь очень маленькая: вызывать метод объекта по его имени. Все что нам нужно - получить RTTI информацию о типе, найти нужный метод по имени и вызвать его, передав в случае fastReport параметр. Информацию мы конечно же получаем для класса TReportBuilder, и среди его методов ищем нужный метод построения отчета. Конечно для таких целей не обязательно использовать RTTI. Есть и другие механизмы вызова метода по имени. Базовый класс TObject позволяет получать адрес точки входа в метода по имени метода с помощью TObject.MethodAddress. Но с RTTI это на мой взгляд проще (на самом деле стандартными средствами я никогда не пробовал этого делать, но сдается мне это было бы сложнее (: ). В общем все, что написано выше, свелось к маленькой сути - демонстрации вызова метода по имени средствами RTTI, что реализуется в несколько строк кода. Все же остальное - вариация для применения, так как не всегда сразу приходит идея о том, как можно использовать те или иные возможности, т.е пример с возможно полезной нагрузкой (:. На самом деле все это очень просто реализуется и без RTTI, а с использованием событий - объекту TReportDefinition добавляется свойство - событие, указывающее на нужный метод TReportBuilder. Т.е. вместо ID отчета сразу указывается обработчик события. Но и подобная реализация иногда может оказаться полезной.
Метки:  rtti  |  FastReport 

Комментарии

sw
22.07.2011 в 01:39
У старого FastReport (2) действительно была проблема - если надо было передавать параметры в отчёт, то форму для этих параметров надо было как-то строить в приложении. А для этого, надо было где-то хранить список возможных параметров (и их типов) для конретного шаблона. А если ещё отчёт надо было построить как-то не стандартно...

Однако в FastReport 3 уже был добавлен дизайнер форм и скрипт. Т.е. если отчёту нужны какие-то параметры, то разработчик отчёта в самом дизайнере FastReport'а создаёт форму (или даже несколько форм) и на форме размещает необходимые элементы управления. А если надо построить нестандартный отчёт, то всю логику его построения можно описать в скрипте. Вобщем, это всё позволило максимально отделить шаблоны отчётов от кода приложения.


> стандартными средствами я никогда не пробовал этого делать
ter, Вы же создаёте объект типа TReportDefinition, логично именно в этом объекте хранить ссылку на BuildReportXXX. Либо (что правильнее с точки зрения ООП) для каждого отчёта создавать свой класс-наследник от TReportDefinition и в нём реализовывать методы создания/отображения формы и построения отчёта. Использовать тут RTII можно разве лишь в академических целях...

Вообще, RTTI тема сама по себе интересная... но на практике как-то мало применима. Самая распространённая и избитая тема - это инспекторы объектов (в т.ч. и инспектор объектов IDE Delphi использует RTTI).
Я же RTTI использовал лишь один раз: когда писал собственный механизм для локализации приложений, который пробегал по всем компонентам форм и смотрел свойства Caption и Hint.

P.S.: Интересно было бы почитать, где и как люди используют RTTI в реальных приложениях :)
ter
22.07.2011 в 11:50
так то оно да, для фастрепорта однако не все редакции поддерживают скрипты, так что не всегда получится сделать формы с параметрами, да и не всегда строится отдельный отчет, чтобы он сохранялся в файле именно. а иногда и просто заполняется crossview динамически. т.е если отчет строится по данным из внутренних структур/массивов из программы то здесь запрос параметром средствами генератора отчетов наверное не очень прокатит.

а интерес к RTTI тут и правда чисто академический, о чем я в принципе тоже упомянул "демонстрация вызова метода по имени" (с)

О том что логичнее хранить ссылку на метода в TReportDefinition я тоже писал - использовать событие. а-ля TReportDefinition.onBuildReport = TReportBuilder.BuilderReportXXX.

Но с точки зрения ООП лучше создавать наследника для каждого отчета, в этом согласен тоже, но будет слишком много как то избыточного кода. так что вариант с событиями и объединением всех обработчиков событий в одном класса типа TReportBuilder кажется более компактным решением.
skydweller
28.07.2011 в 12:45
> Вообще, RTTI тема сама по себе интересная… но на практике как-то мало применима.

Не являюсь программистом Delphi, но из статьи могу судить, что RTTI является реализацией механизма рефлексии(отражения)в Delphi. Рефлексия очень удобна и позволяет создавать более гибкий код. Из практики могу привести примеры:
1. когда Таблица/дата грид/или т.п. должны выводить данных хранящиеся в public-свойствах списка объекта(например IEnumerable), о DataClass’е неизвестно ничего.
2. Аспектно-ориентированный подход при реализации например плагинов.
ter
28.07.2011 в 16:01
часто ли на практике бывает так, что о дата-классе ничего не известно?
skydweller
29.07.2011 в 10:35
Довольно часто, если не завязываться на конкретный источник данных и не обрекать себя на бесконечный рефакторинг :)

public class MyTable
{
...
  // В этом методе происходит разбор источника данных и вызов AddColumn и AddCell
  public void DataSource(IEnumerable dataSource){...}       
  public AddColumn(...){...}
  public AddCell(...){...}
...
}
...
var table = new MyTable();
table.DataSource(MyList); // Где MyList - это объект реализующий IEnumerable

чем со вторым
public class MyTable
{
...
  public AddColumn(...){...}
  public AddCell(...){...}
...
}
...
var table = new MyTable();
table.AddColumn(...);
...
table.AddColumn(...);

for(var i = 0; i < MyList.Lenght; i++){
  table.AddCell(MyList[i].MyPropetry1, ...);
  table.AddCell(MyList[i].MyPropetry2, ...);
...
}

Имхо, работать с первым вариантом удобнее, не так ли?
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно