Бизнес-обьекты с плагинной архитектурой
На этой страничке я продолжу делится опытом написания бизнес-обьектов. Здесь мы посмотрим, как писать динамически подзагружаемые, точнее для начала чуть попроще - динамически вызываемые ПЛАГИНЫ для собственного бизнес-обьекта.
Конечно, любой бизнес-обьект имеет множество режимов работы. Если бы это было бы не так, в нем бы вообще отсутствовал какой-либо смысл. Ведь простой вызов процедуры для формирования рекордсета можно сделать на форме - часто всего одной строкой. Но бизнес-обьекты инкапсулируют (прячут в себе) довольно сложную логику. На этой страничке я рассказал о бизнес-обьекте, который имееет десять режимов работы. Здесь мы рассмотрим еще более интересный вариант - количество режимов ВООБЩЕ не ограничено!
В данному случае наш бизнес-объект будет поставлять данные для вот такого контрольчика:
Исходный текст которого приведен ниже:
00001: <%@ Control Language="VB" AutoEventWireup="false" CodeFile="DynamicReport.ascx.vb" Inherits="Manager_PDF_DynamicReport" %> 00002: 00003: <%@ Register Assembly="CrystalDecisions.Web, Version=10.2.3600.0, Culture=neutral, PublicKeyToken=692fbea5521e1304" 00004: Namespace="CrystalDecisions.Reporting.WebControls" TagPrefix="cc1" %> 00005: 00006: <%@ Register Assembly="CrystalDecisions.Web, Version=10.2.3600.0, Culture=neutral, PublicKeyToken=692fbea5521e1304" 00007: Namespace="CrystalDecisions.Web" TagPrefix="CR" %> 00008: 00009: <table><tr><td> 00010: <asp:Label ID="Label2" runat="server" Text="ReportType" Width="95px"></asp:Label></td><td style="width: 303px"> 00011: <asp:DropDownList ID="DynamicReportSelector" runat="server" DataSourceID="XmlDataSource1" 00012: DataTextField="ReportSourceFile" DataValueField="ReportSourceFile" Width="300px" AutoPostBack="True" AppendDataBoundItems="true"> 00013: <asp:ListItem>Select...</asp:ListItem> 00014: </asp:DropDownList><asp:XmlDataSource ID="XmlDataSource1" runat="server" DataFile="~/App_Code/CrystalBLL/CrystalSourceList.xml"> 00015: </asp:XmlDataSource></td> 00016: <td>StartPage </td><td> 00017: <asp:RequiredFieldValidator ID="RequiredFieldValidator5" runat="server" ControlToValidate="txStartPage" ErrorMessage="*"></asp:RequiredFieldValidator> 00018: <asp:RangeValidator ID="RangeValidator3" runat="server" ControlToValidate="txStartPage" ErrorMessage="*" MaximumValue="1000" MinimumValue="1"></asp:RangeValidator> 00019: <asp:TextBox ID="txStartPage" runat="server" MaxLength="5" Width="30px">1</asp:TextBox></td></tr> 00020: </table> 00021: <br /> 00022: <CR:CrystalReportViewer ID="CrystalReportViewer1" runat="server" />
В данном случае XmlDataSource, ссылается на такой вот конфигурационный файлик, описывающий актуальное подмножество отчетов и параметры этих отчетов.
При работе на страничке этот контрол выглядит примерно так (за глючок по дизайну сорри, тут дизайн не резиновый, а фиксированный и этим занимается отдельный человек):
Итак, надеюсь смысл этого бизнес-объекта понятен. Но в чем же его фишка? А фишка вся вот в таком небольшом движке:
00001: Imports CrystalDecisions.CrystalReports.Engine, CrystalDecisions.Shared 00002: 00003: #Region "Спецификация динамически подзагружаемых модулей" 00004: 00005: 00006: Public NotInheritable Class CrystalReportNameAttribute 00007: Inherits System.Attribute 00008: 00009: Private _ReportFileName As String 00010: 00011: Public Sub New(ByVal ReportFileName As String) 00012: _ReportFileName = ReportFileName 00013: End Sub 00014: 00015: Public ReadOnly Property ReportFileName() As String 00016: Get 00017: ReportFileName = _ReportFileName 00018: End Get 00019: End Property 00020: End Class 00021: 00022: Public Interface IPrepareDataTableForReport 00023: Function IGetDataTable() As System.Data.DataView 00024: End Interface 00025: 00026: #End Region 00027: 00028: ''' <summary> 00029: ''' собственно движок, создающий экземпляры классов, готовящих рекордсеты 00030: ''' </summary> 00031: Public Class CrReportBLL 00032: Public Function GetReport(ByVal CrystalReportFileName As String, ReportDirectory as string) As CrystalDecisions.CrystalReports.Engine.ReportDocument 00033: 'ReportDirectory - в этой диретории лежат и файлы отчетов и конфигурационнный файл 00034: Dim SourceDirectory As String = System.IO.Path.GetDirectoryName(HttpContext.Current.Server.MapPath(ReportDirectory)) 00035: Dim reportPath As String = System.IO.Path.Combine(SourceDirectory, CrystalReportFileName) 00036: Dim Report As ReportDocument = New ReportDocument() 00037: Report.Load(reportPath) 00038: 'посмотрели - есть ли такой подзагружаемый модуль 00039: Dim MyAss As System.Reflection.Assembly = System.Reflection.Assembly.GetExecutingAssembly 00040: Dim AttrType As System.Type = GetType(CrystalReportNameAttribute) 00041: For Each OneType As System.Type In MyAss.GetTypes 00042: If OneType.GetCustomAttributes(AttrType, True).Length > 0 Then 00043: 'это один из подзагружаемых модулей - помеченных собственным специальным атрибутом 00044: If CType(OneType.GetCustomAttributes(AttrType, True)(0), CrystalReportNameAttribute).ReportFileName = CrystalReportFileName Then 00045: 'именно этот ран-тайм класс помечен маркером формирования рекордсета для заданного в комбешнике отчета 00046: 'создаем обьект найденного типа с пустым списком параметров для конструктора 00047: Dim MyDynamicInstance As Object = OneType.GetConstructor(Type.EmptyTypes).Invoke(New Object() {}) 00048: 'и вызвали в этом динамически созданном обьекте метод (без параметров) 00049: Dim ResultDataView As System.Data.DataView = OneType.GetMethod("GetDataTable").Invoke(MyDynamicInstance, New Object() {}) 00050: 'загружаем данные в отчет 00051: Report.SetDataSource(ResultDataView) 00052: Return Report 00053: End If 00054: End If 00055: Next 00056: End Function 00057: End Class
Как видите весь смысл тут в том, что для каждого конкретного отчета в текущей рабочей сборке находится класс, готовящий рекордсеты для отчета. Маркером принадлежности класса отчету служит определенный мною спецатрибут. Класс, готовящий рекордсеты должен содержать обязательную функцию, что определено интерфейсом.
В принципе тут несложно изменить ссылку на сборку, чтоб загружать плагины не из текущей сборки, а из ОТДЕЛЬНОЙ, сделанной в отдельном библиотечном проекте, но я особого смысла в этом не вижу, ибо собственно RPT-Файлы редактируются встроенным дизайнером студии - памяти они занимаю в домене приложения немного, а громоздкость приложения увеличится.
Вот и вся хитрость таких плагинов - нашли нужный класс, создали экземпляр, и вызвали в экземпляре нужный метод. Если класс статический (Module) или метод статический (Shared) - то вызывать можно и без создания экземпляра.
Теперь остается только программировать собственно плагины, готовящие рекордсеты, не вникая в работу движка и всего остального:
Отчетов могут быть сотни, для каждого из них надо просто сделать плагин, готовящий рекордсет и описать параметры отчета в настроечном XML-файлике. И просто пользоваться этим моим движком.
Для этого еще вам потребуется собственно текст контрола, потребителя сервиса этого бизнес-объекта:
00001: Partial Class Manager_PDF_DynamicReport 00002: Inherits System.Web.UI.UserControl 00003: 00004: <ComponentModel.Description("Указывать, когда надо чтобы инструментальная панель не пропала")> _ 00005: Public Property LoadReport() As Boolean 00006: Get 00007: LoadReport = ViewState("SaveReportPanel") 00008: End Get 00009: Set(ByVal value As Boolean) 00010: ViewState("SaveReportPanel") = value 00011: End Set 00012: End Property 00013: 00014: Protected Sub Manager_PDF_DynamicReport_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load 00015: If IsPostBack Then 00016: If Request.Form("__EVENTTARGET").EndsWith("DynamicReportSelector") Then 00017: 'это постбек от комбешника выбора отчета - не делать ничего 00018: Else 00019: If Me.LoadReport Then 00020: 'если с главной странички запрошена загрузка отчета - то делаем это 00021: DynamicReportSelector_SelectedIndexChanged(Nothing, Nothing) 00022: End If 00023: End If 00024: End If 00025: End Sub 00026: 00027: Dim Reporter As New CrReportBLL 00028: Protected Sub DynamicReportSelector_SelectedIndexChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles DynamicReportSelector.SelectedIndexChanged 00029: If DynamicReportSelector.SelectedValue <> "Select..." Then 00030: CrystalReportViewer1.ReportSourceID = "" 00031: 'Загружаем обьект отчета в инструментальную панель (CrystalReportViewer) 00032: CrystalReportViewer1.ReportSource = Reporter.GetReport(DynamicReportSelector.SelectedValue, XmlDataSource1.DataFile) 00033: 'Дали отчету параметр - начальный номер страницы отчета 00034: If CrystalReportViewer1.ParameterFieldInfo("StartPageNum") IsNot Nothing Then 00035: SetReportParm("StartPageNum", CInt(txStartPage.Text)) 00036: End If 00037: 'Установили отчету остальные параметры, заданные в конфигурационном файле 00038: Dim CrystalConfigureXML As New System.Xml.XmlDocument 00039: CrystalConfigureXML.Load(HttpContext.Current.Server.MapPath(XmlDataSource1.DataFile)) 00040: Dim CurrentReportDescriptions As System.Xml.XmlElement = CrystalConfigureXML.SelectSingleNode("/Crystal/Report[@" & "ReportSourceFile='" & DynamicReportSelector.SelectedValue & "']") 00041: If CurrentReportDescriptions Is Nothing Then Throw New Exception("Xml formal for Cryslal configure changed") 00042: If CurrentReportDescriptions.Attributes.Count > 1 Then 00043: For Each OneAttr As System.Xml.XmlAttribute In CurrentReportDescriptions.Attributes 00044: If OneAttr.Name <> "ReportSourceFile" Then 00045: SetReportParm(OneAttr.Name, OneAttr.Value) 00046: End If 00047: Next 00048: End If 00049: End If 00050: End Sub 00051: 00052: Private Sub SetReportParm(ByVal ReportParmName As String, ByVal Value As Object) 00053: Dim ExecParms As CrystalDecisions.Shared.ParameterValues = New CrystalDecisions.Shared.ParameterValues() 00054: Dim OnePrm As CrystalDecisions.Shared.ParameterDiscreteValue = New CrystalDecisions.Shared.ParameterDiscreteValue() 00055: OnePrm.Value = Value 00056: ExecParms.Add(OnePrm) 00057: CrystalReportViewer1.ParameterFieldInfo(ReportParmName).CurrentValues = ExecParms 00058: End Sub 00059: 00060: End Class
Небольшое дополнение. В процессе эксплуатации вышележащий текст модуля дозагрузки плагинов дополнился подробнейшими сообщениями об ошибках:
00001: Public Class CrReportBLL 00002: Public Function GetReport(ByVal CrystalReportFileName As String, ByVal ReportDirectory As String) As CrystalDecisions.CrystalReports.Engine.ReportDocument 00003: Dim AttrIsPresent As Boolean = False 00004: 'ReportDirectory - в этой диретории лежат и файлы отчетов и конфигурационнный файл 00005: Dim SourceDirectory As String = System.IO.Path.GetDirectoryName(HttpContext.Current.Server.MapPath(ReportDirectory)) 00006: Dim reportPath As String = System.IO.Path.Combine(SourceDirectory, CrystalReportFileName) 00007: Dim Report As ReportDocument = New ReportDocument() 00008: Report.Load(reportPath) 00009: 'посмотрели - есть ли такой подзагружаемый модуль 00010: Dim MyAss As System.Reflection.Assembly = System.Reflection.Assembly.GetExecutingAssembly 00011: Dim AttrType As System.Type = GetType(CrystalReportNameAttribute) 00012: For Each OneType As System.Type In MyAss.GetTypes 00013: If OneType.GetCustomAttributes(AttrType, True).Length > 0 Then 00014: 'это один из подзагружаемых модулей - помеченных собственным специальным атрибутом 00015: AttrIsPresent = True 00016: If CType(OneType.GetCustomAttributes(AttrType, True)(0), CrystalReportNameAttribute).ReportFileName = CrystalReportFileName Then 00017: 'именно этот ран-тайм класс помечен маркером формирования рекордсета для заданного в комбешнике отчета 00018: Dim MyDynamicInstance As Object,ResultDataView As System.Data.DataView, GetDataTableMethod as System.Reflection.MethodInfo 00019: Try 00020: 'создаем обьект найденного типа с пустым списком параметров для конструктора 00021: MyDynamicInstance = OneType.GetConstructor(Type.EmptyTypes).Invoke(New Object() {}) 00022: Catch ex As Exception 00023: Throw New Exception("Для отчета " & reportPath & vbcrlf & "не удалось динамически создать экземпляр типа " & OneType.AssemblyQualifiedName & vbCrLf & "находящийся в сборке " & MyAss.Location & "." & vbCrLf & ex.Message) 00024: End Try 00025: Try 00026: 'нашли обязательный метод, специфицированный в интерфейсе 00027: GetDataTableMethod=OneType.GetMethod("GetDataTable") 00028: Catch ex As Exception 00029: Throw New Exception("Для отчета " & reportPath & vbcrlf & "метод GetDataTable типа " & OneType.AssemblyQualifiedName & vbCrLf & "сборки " & MyAss.Location & " не найден." & vbCrLf & ex.Message) 00030: End Try 00031: Try 00032: 'и вызвали в этом динамически созданном обьекте метод (без параметров) 00033: ResultDataView = GetDataTableMethod.Invoke(MyDynamicInstance, New Object() {}) 00034: Catch ex As Exception 00035: Throw New Exception("Для отчета " & reportPath & vbcrlf & "метод GetDataTable типа " & OneType.AssemblyQualifiedName & vbCrLf & "сборки " & MyAss.Location & " не вернул тип System.Data.DataView." & vbCrLf & ex.Message) 00036: End Try 00037: If ResultDataView IsNot Nothing Then 00038: Try 00039: 'загружаем данные в отчет 00040: Report.SetDataSource(ResultDataView) 00041: Return Report 00042: Catch ex As Exception 00043: Throw New Exception("Не удалось загрузить данные в отчет " & reportPath & vbCrLf & ex.Message) 00044: End Try 00045: Else 00046: Return Nothing 00047: End If 00048: End If 00049: End If 00050: Next 00051: If AttrIsPresent Then 00052: Throw New Exception("Для отчета " & reportPath & vbCrLf & "в сборке " & MyAss.Location & vbCrLf & "не найден класс, помеченный атрибутом " & AttrType.Name & " и предназначенный для обработки этого отчета.") 00053: Else 00054: Throw New Exception("Для отчета " & reportPath & vbCrLf & "в сборке " & MyAss.Location & vbCrLf & "не найден класс, помеченный атрибутом " & AttrType.Name & ".") 00055: End If 00056: End Function 00057: End Class
|