(ASP.NET) ASP.NET (2007 год)

Бизнес-обьекты с плагинной архитектурой

На этой страничке я продолжу делится опытом написания бизнес-обьектов. Здесь мы посмотрим, как писать динамически подзагружаемые, точнее для начала чуть попроще - динамически вызываемые ПЛАГИНЫ для собственного бизнес-обьекта.

Конечно, любой бизнес-обьект имеет множество режимов работы. Если бы это было бы не так, в нем бы вообще отсутствовал какой-либо смысл. Ведь простой вызов процедуры для формирования рекордсета можно сделать на форме - часто всего одной строкой. Но бизнес-обьекты инкапсулируют (прячут в себе) довольно сложную логику. На этой страничке я рассказал о бизнес-обьекте, который имееет десять режимов работы. Здесь мы рассмотрим еще более интересный вариант - количество режимов ВООБЩЕ не ограничено!


В данному случае наш бизнес-объект будет поставлять данные для вот такого контрольчика:




Исходный текст которого приведен ниже:

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


Comments ( )
<00>  <01>  <02>  <03>  <04>  <05>  <06>  <07>  <08>  <09>  <10>  <11>  <12>  <13>  <14>  <15>  <16>  <17>  <18>  <19>  <20>  <21>  <22>  <23
Link to this page: //www.vb-net.com/asp2/24/index.htm
<SITEMAP>  <MVC>  <ASP>  <NET>  <DATA>  <KIOSK>  <FLEX>  <SQL>  <NOTES>  <LINUX>  <MONO>  <FREEWARE>  <DOCS>  <ENG>  <CHAT ME>  <ABOUT ME>  < THANKS ME>