I've created a library in an attempt to handle all of the "quirks" of using Crystal Reports in a Visual Studio (VB.NET) project. I've pulled together all the elements that have presented challenges to me in the past - setting/updating parameter and formula values, printing (including page ranges), and setting logon credentials - and put them into reusable methods that all seem to work well when I generate the reports individually.
However, I've run into a scenario where I want to reuse the same report object in a loop to print multiple variations with different data sets/parameter(s) so that I can "easily" reuse the same printer settings and other options without re-prompting the user for each iteration. In this case, I'm working with an internally built DataSet object (built by someone other than me) and my Crystal Report file's Data Source is pointing to the .xsd file for structure.
EDIT: Forgot to mention that the report was created in CR Developer v11.5.12.1838 and the VB.NET library project is targetting the 4.7.2 .NET framework and using the v13.0.3500.0 (runtime v2.0.50727) of the Crystal libraries.
My intent is/was to instantiate a new report object outside the loop, then just re-set and refresh the report's data source and parameter values on each iteration of the loop. Unfortunately, it seems that if I do it this way, the report won't correctly pick up either the parameter values, the updated data source, or both. I've been trying several variations of code placement (because I know that the order in which things are done is very important to the Crystal Reports engine), but none of it seems to work the way I believe it should.
If I instantiate a new report object inside the loop, it will correctly generate the reports for each iteration with the correct data source and parameter values. Of course, it resets all of the internal properties of my class to "default", which kinda defeats the purpose. (Yes, I know I could pass additional/other parameters to a constructor to achieve the effect, but that seems an extremely "brute-force" solution, and I'd much rather get it to work the way I have in mind).
AND NOW FOR SOME CODE
Here is a pared-down/obfuscated version of the most recent iteration of the calling method (currently a part of a button click event handler). Every attempt I've made to instantiate a reusable object seems to result in some sort of failure. In this version, it loads the report and correctly passes along the parameter value, but the data source is completely empty resulting in a blank report. In other variations (I've discarded that code now), when I actually try to print/export/show the report, it fails with a COM exception: Missing parameter values.
I've tried using the .Refresh and .ReportClientDocument.VerifyDatabase methods separately, but those don't make a difference. When I check the parameters at runtime, it appears that the CR parameter/value and query results have been populated, but any method that makes any changes after the initialization just seems to "break" the report.
Dim ReportName As String = "\\SERVERNAME\Applications\Reports\ClientActiveCustomerSummary.rpt"
Dim Report As Common.CRReport = Nothing
Try
ReportData = ClientDataSet.Tables("ActiveSummary").Copy
ReportData = GetClientActiveSummaryData
Catch ex As Exception
MessageBox.Show(ex.Message & vbCrLf & vbCrLf &
"Error while retrieving client customer summary report data.",
"ERROR", MessageBoxButtons.OK, MessageBoxIcon.Error)
End Try
If ReportData.Rows.Count > 0 Then
Report = New Common.CRReport(Common.CRReport.ReportSourceType.ADODataSet, New IO.FileInfo(ReportName), ClientDataSet)
For Each LVItem As ListViewItem In checkItemsMP
Dim ClientQuery = From ap In ReportData.AsEnumerable
Where ap.Field(Of Object)("mp") = LVItem.SubItems("mp").Text
Order By ap.Field(Of Object)("customername")
Select ap
ClientDataSet.Tables("ActiveSummary").Merge(ClientQuery.CopyToDataTable)
Report.ReportParameters.Clear()
Report.AddReportParameter("ClientName", LVItem.SubItems("clientname").Text)
Report.GenerateReport()
ClientDataSet.Tables("ActiveSummary").Clear()
ClientQuery = Nothing
Next LVItem
For Each LVItem As ListViewItem In checkItemsBN
Dim BranchName As String = LVItem.SubItems("clientname").Text & " " & LVItem.SubItems("branchname").Text
Dim BranchQuery = From ap In ReportData.AsEnumerable
Where (ap.Field(Of Object)("clientname") & " " & ap.Field(Of Object)("branchname")) = BranchName
Order By ap.Field(Of Object)("customername")
Select ap
ClientDataSet.Tables("ActiveSummary").Merge(BranchQuery.CopyToDataTable)
Report.ReportParameters.Clear()
Report.AddReportParameter("ClientName", BranchName)
Report.GenerateReport()
ClientDataSet.Tables("ActiveSummary").Clear()
BranchQuery = Nothing
Next LVItem
Else
MessageBox.Show("NO RECORDS FOUND", "Information", MessageBoxButtons.OK, MessageBoxIcon.Information)
ReportData.Dispose()
ReportData = Nothing
End If
Obviously, in this case, I'm passing in an ADO.NET DataSet object and using a value retrieved from a ListView on the form itself for the value of the report's single parameter. Again, if I instantiate a new CRReport object on each iteration of the loop, the report will create normally with the correct data and parameter value, but it prompts the user each time for the report creation options (print/show/export, then - if "print" is selected - again for which printer to use).
And here is the reporting class. (Please understand that this is a work in progress and is far from "production quality" code):
REPORT OBJECT (CRReport)
Imports System.IO
Imports CrystalDecisions.Shared
Imports CrystalDecisions.CrystalReports.Engine
Imports CrystalDecisions.ReportAppServer.DataDefModel
Imports System.Drawing
Imports System.Windows.Forms
Public Class CRReport
Inherits ReportDocument
Public Enum ReportSourceType
PostgreSQL = 1
MySQL = 2
ADODataSet = 3
XML = 4
CSV = 5
Access = 6
End Enum
Public Enum GenerateReportOption
None = 0
DisplayOnScreen = 1
SendToPrinter = 2
ExportToFile = 3
MailToRecipient = 4
End Enum
Public Property ReportFile As FileInfo
Public Property ExportPath As String
Public Property ReportParameters As List(Of CRParameter)
Public Property ReportFormulas As List(Of CRFormula)
Public Property SourceType As ReportSourceType
Private Property XMLDataSource As FileInfo
Private Property ADODataSet As System.Data.DataSet
Private Property ReportOption As GenerateReportOption
Private WithEvents DocumentToPrint As Printing.PrintDocument
#Region "PUBLIC METHODS"
#Region "CONSTRUCTORS"
Public Sub New(ByVal SourceType As ReportSourceType, ByVal CurrentReportFile As FileInfo)
Me.Initialize()
Me.SourceType = SourceType
Me.ReportFile = CurrentReportFile
PrepareReport()
End Sub
Public Sub New(ByVal SourceType As ReportSourceType, ByVal CurrentReportFile As FileInfo, ByVal XMLFile As FileInfo)
Me.Initialize()
Me.SourceType = SourceType
Me.ReportFile = CurrentReportFile
Me.XMLDataSource = XMLFile
PrepareReport()
End Sub
Public Sub New(ByVal SourceType As ReportSourceType, ByVal CurrentReportFile As FileInfo, ByVal ADODataSource As System.Data.DataSet)
Me.Initialize()
Me.SourceType = SourceType
Me.ReportFile = CurrentReportFile
Me.ADODataSet = ADODataSource
PrepareReport()
End Sub
Public Sub New(ByVal SourceType As ReportSourceType, ByVal CurrentReportFile As FileInfo, ByVal CurrentExportPath As String)
Me.Initialize()
Me.SourceType = SourceType
Me.ReportFile = CurrentReportFile
Me.ExportPath = CurrentExportPath
If Not Me.ExportPath Is Nothing AndAlso Not String.IsNullOrEmpty(Me.ExportPath) Then
Dim ExportFile As New IO.FileInfo(Me.ExportPath)
If Not IO.Directory.Exists(ExportFile.DirectoryName) Then
IO.Directory.CreateDirectory(ExportFile.DirectoryName)
End If
End If
PrepareReport()
End Sub
#End Region
Public Sub AddReportParameter(ByVal CurrentParameterName As String, ByVal CurrentParameterValue As Object)
If Not String.IsNullOrEmpty(CurrentParameterName) Then
Dim NewParameter As New CRParameter(Me, CurrentParameterName, CurrentParameterValue)
Me.ReportParameters.Add(NewParameter)
End If
End Sub
Public Sub AddReportFormula(ByVal CurrentFormulaName As String, ByVal CurrentFormulaValue As Object)
If Not String.IsNullOrEmpty(CurrentFormulaName) Then
Dim NewFormula As New CRFormula(Me, CurrentFormulaName, CurrentFormulaValue)
Me.ReportFormulas.Add(NewFormula)
End If
End Sub
Public Sub GenerateReport(ByVal ReportOption As GenerateReportOption)
If Me.ReportOption = GenerateReportOption.None Then
' THIS DIALOG IS SOLELY FOR PROMPTING THE USER FOR HOW TO GENERATE THE REPORT
Dim ReportDialog As New dlgGenerateReport
Me.ReportOption = ReportDialog.GetReportGenerationOption()
End If
If Not Me.ReportOption = GenerateReportOption.None Then
Select Case ReportOption
Case GenerateReportOption.DisplayOnScreen
Me.ShowReport()
Case GenerateReportOption.SendToPrinter
Me.PrintReport()
Case GenerateReportOption.ExportToFile
Me.ExportReport()
End Select
End If
End Sub
#End Region
#Region "PRIVATE METHODS"
Private Sub Initialize()
Me.ReportFile = Nothing
Me.ExportPath = String.Empty
Me.ADODataSet = Nothing
Me.XMLDataSource = Nothing
Me.ReportParameters = New List(Of CRParameter)
Me.ReportFormulas = New List(Of CRFormula)
Me.SourceType = ReportSourceType.XML
Me.ReportOption = GenerateReportOption.None
End Sub
Private Sub PrepareReport()
If Not Me.ReportFile Is Nothing Then
Me.Load(Me.ReportFile.FullName)
Me.DataSourceConnections.Clear()
SetReportConnectionInfo()
If Me.ReportFormulas.Count > 0 Then
For Each Formula As CRFormula In Me.ReportFormulas
Formula.UpdateFormulaField()
Next Formula
End If
If Me.ReportParameters.Count > 0 Then
For Each Parameter As CRParameter In Me.ReportParameters
Parameter.UpdateReportParameter()
Next Parameter
End If
Me.Refresh()
Me.ReportClientDocument.VerifyDatabase()
End If
End Sub
Private Sub SetReportConnectionInfo()
If Me.SourceType = ReportSourceType.PostgreSQL Then
Dim CRDatabase As CrystalDecisions.CrystalReports.Engine.Database = Me.Database
Dim CRTables As CrystalDecisions.CrystalReports.Engine.Tables = CRDatabase.Tables
Dim CRConnectionInfo As New CrystalDecisions.Shared.ConnectionInfo
Dim DBUsername As String = Utility.GetUsername
Dim DBPassword As String = Utility.GetPassword
With CRConnectionInfo
.DatabaseName = <DATABASENAME>
.ServerName = <HOSTNAME>
.UserID = DBUsername
.Password = DBPassword
End With
For Each CRTable As CrystalDecisions.CrystalReports.Engine.Table In CRTables
Dim CRTableLogonInfo As CrystalDecisions.Shared.TableLogOnInfo = CRTable.LogOnInfo
CRTableLogonInfo.ConnectionInfo = CRConnectionInfo
CRTable.ApplyLogOnInfo(CRTableLogonInfo)
Next CRTable
ElseIf Me.SourceType = ReportSourceType.ADODataSet Then
Dim CRDatabase As CrystalDecisions.CrystalReports.Engine.Database = Me.Database
Dim CRTables As CrystalDecisions.CrystalReports.Engine.Tables = CRDatabase.Tables
For Each CRTable As CrystalDecisions.CrystalReports.Engine.Table In CRTables
For Each ADOTable As DataTable In ADODataSet.Tables
If CRTable.Name.ToUpper.Trim = ADOTable.TableName.ToUpper.Trim Then
CRTable.SetDataSource(ADOTable)
Exit For
End If
Next ADOTable
Next CRTable
Me.ReportClientDocument.VerifyDatabase()
ElseIf Me.SourceType = ReportSourceType.XML Then
If Not Me.XMLDataSource Is Nothing AndAlso Me.XMLDataSource.Exists Then
Dim CRDatabaseAttributes As New CrystalDecisions.ReportAppServer.DataDefModel.PropertyBag
Dim CRLogonProperties As New CrystalDecisions.ReportAppServer.DataDefModel.PropertyBag
Dim CRConnectionDetails As New CrystalDecisions.ReportAppServer.DataDefModel.ConnectionInfo
Dim CRTable As CrystalDecisions.ReportAppServer.DataDefModel.Table
Dim CRTables As CrystalDecisions.ReportAppServer.DataDefModel.Tables = Me.ReportClientDocument.DatabaseController.Database.Tables
Dim XMLData As New System.Data.DataSet
XMLData.ReadXml(Me.XMLDataSource.FullName)
With CRLogonProperties
.Add("File Path ", Me.XMLDataSource.FullName)
.Add("Internal Connection ID", "{be7cdac3-6a64-4923-8177-898ab55d0fa0}")
End With
With CRDatabaseAttributes
.Add("Database DLL", "crdb_adoplus.dll")
.Add("QE_DatabaseName", "")
.Add("QE_DatabaseType", "")
.Add("QE_LogonProperties", CRLogonProperties)
.Add("QE_ServerDescription", Me.XMLDataSource.Name.Substring(0, Me.XMLDataSource.Name.Length - Me.XMLDataSource.Extension.Length))
.Add("QE_SQLDB", "False")
.Add("SSO Enabled", "False")
End With
With CRConnectionDetails
.Attributes = CRDatabaseAttributes
.Kind = CrystalDecisions.ReportAppServer.DataDefModel.CrConnectionInfoKindEnum.crConnectionInfoKindCRQE
.UserName = ""
.Password = ""
End With
For I As Integer = 0 To XMLData.Tables.Count - 1
CRTable = New CrystalDecisions.ReportAppServer.DataDefModel.Table
With CRTable
.ConnectionInfo = CRConnectionDetails
.Name = XMLData.Tables(I).TableName
.QualifiedName = XMLData.Tables(I).TableName
.[Alias] = XMLData.Tables(I).TableName
End With
Me.ReportClientDocument.DatabaseController.SetTableLocation(CRTables(I), CRTable)
Next I
Me.ReportClientDocument.VerifyDatabase()
End If
End If
End Sub
Private Sub PrintReport()
If Me.DocumentToPrint Is Nothing Then
' THIS IS WHY I WANT TO REUSE THE REPORTING OBJECT
' IF I CAN SET/SAVE THE PRINT DOCUMENT/SETTINGS WITHIN THE OBJECT,
' THE USER SHOULD ONLY HAVE TO RESPOND ONCE FOR ANY ITERATIONS
' USING THE SAME REPORT OBJECT
Dim SelectPrinter As New PrintDialog
Dim PrinterSelected As DialogResult = DialogResult.Cancel
Me.DocumentToPrint = New Printing.PrintDocument
With SelectPrinter
.Document = DocumentToPrint
.AllowPrintToFile = False
.AllowSelection = False
.AllowCurrentPage = False
.AllowSomePages = False
.PrintToFile = False
.UseEXDialog = True
End With
PrinterSelected = SelectPrinter.ShowDialog()
If PrinterSelected = DialogResult.OK Then
SendToPrinter()
End If
Else
SendToPrinter()
End If
End Sub
Private Sub SendToPrinter()
Dim Copies As Integer = DocumentToPrint.PrinterSettings.Copies
Dim PrinterName As String = DocumentToPrint.PrinterSettings.PrinterName
Dim LastPageNumber As Integer = 1
' IF THE PARAMETER VALUE DOESN'T GET PASSED/UPDATED PROPERLY
' THIS LINE WILL THROW A COM EXCEPTION 'MISSING PARAMETER VALUE'
LastPageNumber = Me.FormatEngine.GetLastPageNumber(New CrystalDecisions.Shared.ReportPageRequestContext())
Me.PrintOptions.CopyTo(DocumentToPrint.PrinterSettings, DocumentToPrint.DefaultPageSettings)
If DocumentToPrint.PrinterSettings.SupportsColor Then
DocumentToPrint.DefaultPageSettings.Color = True
End If
Me.PrintOptions.CopyFrom(DocumentToPrint.PrinterSettings, DocumentToPrint.DefaultPageSettings)
Me.PrintOptions.PrinterName = PrinterName
Me.PrintOptions.PrinterDuplex = CType(DocumentToPrint.PrinterSettings.Duplex, PrinterDuplex)
Me.PrintToPrinter(Copies, True, 1, LastPageNumber)
End Sub
Private Function ExportReport() As IO.FileInfo
Dim ExportFile As IO.FileInfo = Nothing
If Not Me.ExportPath Is Nothing AndAlso Not String.IsNullOrEmpty(Me.ExportPath) Then
ExportFile = New IO.FileInfo(Me.ExportPath)
If Not ExportFile.Exists Then
Me.ExportToDisk(ExportFormatType.PortableDocFormat, ExportFile.FullName)
Else
Dim Response As DialogResult = DialogResult.Cancel
Response = MessageBox.Show(ExportFile.Name & " already exists in this location." & vbCrLf & vbCrLf &
"Do you want to overwrite the existing file?" & vbCrLf & vbCrLf &
"Click [Y]ES to overwrite the existing file" & vbCrLf &
"Click [N]O to create a new file" & vbCrLf &
"Click [C]ANCEL to cancel the export process",
"PDF ALREADY EXISTS",
MessageBoxButtons.YesNoCancel, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button2)
If Response = DialogResult.Yes Then
ExportFile.Delete()
ElseIf Response = DialogResult.No Then
ExportFile = New IO.FileInfo(Common.Utility.IncrementExistingFileName(Me.ExportPath))
Else
ExportFile = Nothing
End If
If Not ExportFile Is Nothing Then
Me.ExportToDisk(ExportFormatType.PortableDocFormat, ExportFile.FullName)
End If
End If
End If
Return ExportFile
End Function
Private Sub ShowReport()
Dim ReportViewer As New frmReportPreview
With ReportViewer
.rptViewer.ReportSource = Nothing
.rptViewer.ReportSource = Me
.WindowState = FormWindowState.Maximized
.rptViewer.RefreshReport()
' Set zoom level: 1 = Page Width, 2 = Whole Page, 25-100 = zoom %
.rptViewer.Zoom(1)
.rptViewer.Show()
.Show()
End With
End Sub
Private Sub EmailReport(ByRef ReportMail As System.Net.Mail.MailMessage)
Dim ReportAttachment As IO.FileInfo = ExportReport()
If Not ReportAttachment Is Nothing AndAlso ReportAttachment.Exists Then
ReportMail.Attachments.Add(New System.Net.Mail.Attachment(ReportAttachment.FullName))
If Utility.SendEmailMessage(ReportMail) Then
End If
End If
End Sub
#End Region
I've tried adding calls to the PrepareReport method (again) in the GenerateReport method of CRReport class so that it would reset the data source and parameter values, but it seems that it still doesn't get everything properly set up in the actual Crystal Report object for report generation. My experience so far has been that I have to set all of this on instantiation for some reason or it just fails completely.
For reference purposes, the parameters and formulae for Crystal are encapsulated in their own classes:
PARAMETER OBJECT (CRParameter)
#Region "CRYSTAL REPORTS PARAMETER CLASS"
Public Class CRParameter
Public Property CurrentReport As CRReport
Public Property ParameterName As String
Public Property ParameterValue As Object
Public Sub New(ByVal Report As CRReport)
Me.CurrentReport = Report
Me.ParameterName = String.Empty
Me.ParameterValue = Nothing
End Sub
Public Sub New(ByVal Report As CRReport, ByVal CurrentParameterName As String, ByVal CurrentParameterValue As Object)
Me.CurrentReport = Report
Me.ParameterName = CurrentParameterName
Me.ParameterValue = CurrentParameterValue
UpdateReportParameter()
End Sub
Friend Sub UpdateReportParameter()
If Not Me.CurrentReport Is Nothing Then
If Not String.IsNullOrEmpty(Me.ParameterName) Then
Dim CRFieldDefinitions As ParameterFieldDefinitions = Nothing
Dim CRFieldDefinition As ParameterFieldDefinition = Nothing
Dim CRValues As ParameterValues = Nothing
Dim CRDiscreteValue As ParameterDiscreteValue = Nothing
Try
CRFieldDefinitions = Me.CurrentReport.DataDefinition.ParameterFields
CRFieldDefinition = CRFieldDefinitions.Item(Me.ParameterName)
CRValues = CRFieldDefinition.CurrentValues
CRValues.Clear()
CRDiscreteValue = New ParameterDiscreteValue
CRDiscreteValue.Description = Me.ParameterName
CRDiscreteValue.Value = Me.ParameterValue
CRValues.Add(CRDiscreteValue)
CRFieldDefinition.ApplyCurrentValues(CRValues)
CRFieldDefinition.ApplyDefaultValues(CRValues)
Catch ex As Exception
Throw
Finally
If Not CRFieldDefinitions Is Nothing Then
CRFieldDefinitions.Dispose()
End If
If Not CRFieldDefinition Is Nothing Then
CRFieldDefinition.Dispose()
End If
If Not CRValues Is Nothing Then
CRValues = Nothing
End If
If Not CRDiscreteValue Is Nothing Then
CRDiscreteValue = Nothing
End If
End Try
End If
End If
End Sub
End Class
#End Region
FORMULA OBJECT (CRFormula)
I realize this falls outside the scope of the original question, but for the sake of completeness, I wanted to include it in case someone else might be looking for code to use.
#Region "CRYSTAL REPORTS FORMULA VALUE CLASS"
Public Class CRFormula
Public Property CurrentReport As CRReport
Public Property FormulaName As String
Public Property FormulaValue As Object
Public Sub New(ByVal Report As CRReport)
Me.CurrentReport = Report
Me.FormulaName = String.Empty
Me.FormulaValue = Nothing
End Sub
Public Sub New(ByVal Report As CRReport, ByVal NewFormulaName As String, ByVal NewFormulaValue As Object)
Me.CurrentReport = Report
Me.FormulaName = NewFormulaName
Me.FormulaValue = NewFormulaValue
'UpdateFormulaField()
End Sub
Friend Sub UpdateFormulaField()
If Not Me.CurrentReport Is Nothing Then
If Not String.IsNullOrEmpty(Me.FormulaName) Then
Try
If Me.FormulaValue Is Nothing Then
Me.FormulaValue = ""
Me.CurrentReport.DataDefinition.FormulaFields(Me.FormulaName).Text = Me.FormulaValue.ToString
ElseIf TypeOf Me.FormulaValue Is String AndAlso String.IsNullOrEmpty(Convert.ToString(Me.FormulaValue)) Then
Me.FormulaValue = ""
Me.CurrentReport.DataDefinition.FormulaFields(Me.FormulaName).Text = Me.FormulaValue.ToString
ElseIf TypeOf Me.FormulaValue Is String AndAlso Not String.IsNullOrEmpty(Convert.ToString(Me.FormulaValue)) Then
Me.FormulaValue = "'" & Me.FormulaValue.ToString & "'"
Me.CurrentReport.DataDefinition.FormulaFields(Me.FormulaName).Text = Me.FormulaValue.ToString
ElseIf TypeOf Me.FormulaValue Is Date Then
Me.FormulaValue = "'" & Convert.ToDateTime(Me.FormulaValue).ToString("yyyy-MM-dd") & "'"
Me.CurrentReport.DataDefinition.FormulaFields(Me.FormulaName).Text = Me.FormulaValue.ToString
Else
Me.CurrentReport.DataDefinition.FormulaFields(Me.FormulaName).Text = Me.FormulaValue.ToString
End If
Catch ex As Exception
End Try
End If
End If
End Sub
End Class
#End Region
I've tried to include as much detail and information about the challenge I'm facing as possible, but please feel free to ask any questions about any of it if you require clarification.
Okay, I believe I've found the cause of the problem, and it's one of those "quirks" I mentioned at the top of my question. The Crystal Reports engine is very particular about the order of certain events, and this is one of those cases. In my original PrepareReport() method, I had the calls to the .Refresh() and .VerifyDatabase() methods executing last. This (apparently) effectively "resets" the parameters/data source, so everything I had above it was basically nullified.
So, I went back through some older code to look at how I've worked with individual Crystal Reports in the past and found that calling the .Refresh() and .VerifyDatabase() methods prior to attempting to set parameter and/or formula values seems to work as expected, so I moved those two lines up in the PrepareReport() code and tried again. It all seemed to work correctly. Several tests later, and the order of execution appears to be the culprit here. Now my PrepareReport() method looks like this:
Private Sub PrepareReport()
If Not Me.CRReportFile Is Nothing Then
Me.CrystalReport = New CrystalDecisions.CrystalReports.Engine.ReportDocument
Me.CrystalReport.Load(Me.CRReportFile.FullName)
Me.CrystalReport.DataSourceConnections.Clear()
SetReportConnectionInfo()
'MOVED THIS UP IN THE EXECUTION ORDER
Me.CrystalReport.Refresh()
Me.CrystalReport.ReportClientDocument.VerifyDatabase()
If Me.ReportFormulas.Count > 0 Then
For Each Formula As CRFormula In Me.ReportFormulas
Formula.UpdateFormulaField(Me.CrystalReport)
Next Formula
End If
If Me.ReportParameters.Count > 0 Then
For Each Parameter As CRParameter In Me.ReportParameters
Parameter.UpdateReportParameter(Me.CrystalReport)
Next Parameter
End If
' THE REFRESH() & VERIFYDATABASE() METHOD CALLS USED TO BE DOWN HERE
End If
End Sub
TL;DR VERSION FOR THOSE INTERESTED IN ADDITIONAL INFO
A couple of other things I tried while troubleshooting, none of which resulted in complete success, although some produced varying degrees:
I tried disposing of the base object with MyBase.Dispose() (Obviously this was a bad idea). Of course, I couldn't instantiate a new base object without entirely recreating the CRReport object, which was what I was trying to avoid in the first place.
I removed the inheritance from the class and created a private variable for a CrystalDecisions.CrystalReports.Engine.ReportDocument object that could be instantiated independently of my class. Even though it may seem unnecessary, this actually wasn't a horrible idea as I'll explain later.
I tried several other variations of code placement that failed in one way or another.
Since everything seems to be working now with the revised PrepareReport() code, I've done a bit of refactoring. Instead of calling this method multiple times (once at instantiation and once at report generation), I removed the calls from the constructors and put a single call to it in the GenerateReport() method.
A SLIGHT "HICCUP"
I did some additional testing using the ShowReport() method (display it on the screen instead of printing it on paper), and there was some "weirdness", so I had to make adjustments. In my calling method (the button click event), I tried to dispose of the CRReport object after generating all of the reports, but that caused me not to be able to switch pages after the reports were generated/displayed (I got a NullReferenceException - Object reference not set to an instance of an object). A minor tweak later, and I could get the reports to stay instantiated but, due to the data set being overridden by later iterations, it wasn't always showing the correct data in each window.
This is where my removing of the inheritance comes into play. I created a private CrystalDecisions.CrystalReports.Engine.ReportDocument object for the class that could be reinstantiated and passed around a bit that would retain only the data associated with that particular instance of the report. I refactored the code for the CRReport, CRParameter, and CRFormula objects to use that new private variable instead, and everything looks like it's behaving exactly as expected.
HERE'S THE FULL REVISED CODE
Please remember, not all of this has been fully tested. I've yet to test the ExportReport() method b/c I need to clean up a couple of things there, and the EmailReport() method has a long way to go. I've only tested it with the ADO.NET DataSet during this run, although the code used for XML and PostgreSQL has worked in the past.
REPORT OBJECT (CRReport)
Public Class CRReport
Public Property CRReportFile As FileInfo
Public Property ReportParameters As List(Of CRParameter)
Public Property ReportFormulas As List(Of CRFormula)
Public Property SourceType As ReportSourceType
Public Property ExportPath As String
Get
Return ExportReportToPath
End Get
Set(value As String)
If Not value Is Nothing AndAlso Not String.IsNullOrEmpty(value) Then
Dim ExportFile As New IO.FileInfo(value)
If Not IO.Directory.Exists(ExportFile.DirectoryName) Then
IO.Directory.CreateDirectory(ExportFile.DirectoryName)
End If
ExportReportToPath = ExportFile.FullName
End If
End Set
End Property
Private Property XMLDataSource As FileInfo
Private Property ADODataSet As System.Data.DataSet
Private Property ReportOption As GenerateReportOption
Private CrystalReport As CrystalDecisions.CrystalReports.Engine.ReportDocument
Private ExportReportToPath As String
Public Enum ReportSourceType
PostgreSQL = 1
MySQL = 2
ADODataSet = 3
XML = 4
CSV = 5
Access = 6
End Enum
Public Enum GenerateReportOption
None = 0
DisplayOnScreen = 1
SendToPrinter = 2
ExportToFile = 3
MailToRecipient = 4
End Enum
Private WithEvents DocumentToPrint As Printing.PrintDocument
#Region "PUBLIC METHODS"
Public Sub New(ByVal CurrentReportFile As FileInfo, ByVal XMLFile As FileInfo)
Me.Initialize()
Me.SourceType = ReportSourceType.XML
Me.CRReportFile = CurrentReportFile
Me.XMLDataSource = XMLFile
End Sub
Public Sub New(ByVal CurrentReportFile As FileInfo, ByVal ADODataSource As System.Data.DataSet)
Me.Initialize()
Me.SourceType = ReportSourceType.ADODataSet
Me.CRReportFile = CurrentReportFile
Me.ADODataSet = ADODataSource
End Sub
Public Sub AddReportParameter(ByVal CurrentParameterName As String, ByVal CurrentParameterValue As Object)
If Not String.IsNullOrEmpty(CurrentParameterName) Then
Dim NewParameter As New CRParameter(CurrentParameterName, CurrentParameterValue)
Me.ReportParameters.Add(NewParameter)
End If
End Sub
Public Sub AddReportFormula(ByVal CurrentFormulaName As String, ByVal CurrentFormulaValue As Object)
If Not String.IsNullOrEmpty(CurrentFormulaName) Then
Dim NewFormula As New CRFormula(CurrentFormulaName, CurrentFormulaValue)
Me.ReportFormulas.Add(NewFormula)
End If
End Sub
Public Sub GenerateReport()
If Me.ReportOption = GenerateReportOption.None Then
Dim ReportDialog As New dlgGenerateReport
Me.ReportOption = ReportDialog.GetReportGenerationOption()
End If
If Not Me.ReportOption = GenerateReportOption.None Then
GenerateReport(Me.ReportOption)
End If
End Sub
Public Sub GenerateReport(ByVal ReportOption As GenerateReportOption)
If Me.ReportOption = GenerateReportOption.None Then
Dim ReportDialog As New dlgGenerateReport
Me.ReportOption = ReportDialog.GetReportGenerationOption()
End If
If Not Me.ReportOption = GenerateReportOption.None Then
PrepareReport()
Select Case ReportOption
Case GenerateReportOption.DisplayOnScreen
Me.ShowReport()
Case GenerateReportOption.SendToPrinter
Me.PrintReport()
Case GenerateReportOption.ExportToFile
Me.ExportReport()
End Select
End If
End Sub
Private Sub PrintReport()
If Me.DocumentToPrint Is Nothing Then
Dim SelectPrinter As New PrintDialog
Dim PrinterSelected As DialogResult = DialogResult.Cancel
Me.DocumentToPrint = New Printing.PrintDocument
Me.CrystalReport.PrintOptions.CopyTo(Me.DocumentToPrint.PrinterSettings, Me.DocumentToPrint.DefaultPageSettings)
With SelectPrinter
.Document = DocumentToPrint
.AllowPrintToFile = False
.AllowSelection = False
.AllowCurrentPage = False
.AllowSomePages = False
.PrintToFile = False
.UseEXDialog = True
End With
PrinterSelected = SelectPrinter.ShowDialog()
If PrinterSelected = DialogResult.OK Then
SendToPrinter()
End If
Else
SendToPrinter()
End If
End Sub
Private Sub SendToPrinter()
If Not Me.DocumentToPrint Is Nothing Then
Dim Copies As Integer = Me.DocumentToPrint.PrinterSettings.Copies
Dim PrinterName As String = Me.DocumentToPrint.PrinterSettings.PrinterName
Dim LastPageNumber As Integer = 1
LastPageNumber = Me.CrystalReport.FormatEngine.GetLastPageNumber(New CrystalDecisions.Shared.ReportPageRequestContext())
Me.CrystalReport.PrintOptions.CopyFrom(Me.DocumentToPrint.PrinterSettings, Me.DocumentToPrint.DefaultPageSettings)
Me.CrystalReport.PrintOptions.PrinterName = PrinterName
Me.CrystalReport.PrintOptions.PrinterDuplex = CType(Me.DocumentToPrint.PrinterSettings.Duplex, PrinterDuplex)
Me.CrystalReport.PrintToPrinter(Copies, True, 1, LastPageNumber)
End If
End Sub
Private Function ExportReport() As IO.FileInfo
Dim ExportFile As IO.FileInfo = Nothing
If Not Me.ExportPath Is Nothing AndAlso Not String.IsNullOrEmpty(Me.ExportPath) Then
ExportFile = New IO.FileInfo(Me.ExportPath)
If Not ExportFile.Exists Then
Me.CrystalReport.ExportToDisk(ExportFormatType.PortableDocFormat, ExportFile.FullName)
Else
Dim Response As DialogResult = DialogResult.Cancel
Response = MessageBox.Show(ExportFile.Name & " already exists in this location." & vbCrLf & vbCrLf &
"Do you want to overwrite the existing file?" & vbCrLf & vbCrLf &
"Click [Y]ES to overwrite the existing file" & vbCrLf &
"Click [N]O to create a new file" & vbCrLf &
"Click [C]ANCEL to cancel the export process",
"PDF ALREADY EXISTS",
MessageBoxButtons.YesNoCancel, MessageBoxIcon.Exclamation, MessageBoxDefaultButton.Button2)
If Response = DialogResult.Yes Then
ExportFile.Delete()
ElseIf Response = DialogResult.No Then
ExportFile = New IO.FileInfo(Common.Utility.IncrementExistingFileName(Me.ExportPath))
Else
ExportFile = Nothing
End If
If Not ExportFile Is Nothing Then
Me.CrystalReport.ExportToDisk(ExportFormatType.PortableDocFormat, ExportFile.FullName)
End If
End If
End If
Return ExportFile
End Function
Private Sub ShowReport()
Dim ReportViewer As New frmReportPreview
With ReportViewer
.rptViewer.ReportSource = Nothing
.rptViewer.ReportSource = Me.CrystalReport
.WindowState = FormWindowState.Maximized
.rptViewer.RefreshReport()
' Set zoom level: 1 = Page Width, 2 = Whole Page, 25-100 = zoom %
.rptViewer.Zoom(1)
.rptViewer.Show()
.Show()
End With
End Sub
Private Sub EmailReport(ByRef ReportMail As System.Net.Mail.MailMessage)
Dim ReportAttachment As IO.FileInfo = ExportReport()
If Not ReportAttachment Is Nothing AndAlso ReportAttachment.Exists Then
ReportMail.Attachments.Add(New System.Net.Mail.Attachment(ReportAttachment.FullName))
If Utility.SendEmailMessage(ReportMail) Then
End If
End If
End Sub
Public Overloads Sub Dispose()
Me.CrystalReport.Dispose()
If Not Me.DocumentToPrint Is Nothing Then
Me.DocumentToPrint.Dispose()
End If
End Sub
#End Region
#Region "PRIVATE METHODS"
Private Sub Initialize()
Me.CrystalReport = Nothing
Me.CRReportFile = Nothing
Me.ExportPath = String.Empty
Me.ADODataSet = Nothing
Me.XMLDataSource = Nothing
Me.ReportParameters = New List(Of CRParameter)
Me.ReportFormulas = New List(Of CRFormula)
Me.SourceType = ReportSourceType.XML
Me.ReportOption = GenerateReportOption.None
End Sub
Private Sub PrepareReport()
If Not Me.CRReportFile Is Nothing Then
Me.CrystalReport = New CrystalDecisions.CrystalReports.Engine.ReportDocument
Me.CrystalReport.Load(Me.CRReportFile.FullName)
Me.CrystalReport.DataSourceConnections.Clear()
SetReportConnectionInfo()
Me.CrystalReport.Refresh()
Me.CrystalReport.ReportClientDocument.VerifyDatabase()
If Me.ReportFormulas.Count > 0 Then
For Each Formula As CRFormula In Me.ReportFormulas
Formula.UpdateFormulaField(Me.CrystalReport)
Next Formula
End If
If Me.ReportParameters.Count > 0 Then
For Each Parameter As CRParameter In Me.ReportParameters
Parameter.UpdateReportParameter(Me.CrystalReport)
Next Parameter
End If
End If
End Sub
Private Sub SetReportConnectionInfo()
If Me.SourceType = ReportSourceType.PostgreSQL Then
Dim CRDatabase As CrystalDecisions.CrystalReports.Engine.Database = Me.CrystalReport.Database
Dim CRTables As CrystalDecisions.CrystalReports.Engine.Tables = CRDatabase.Tables
Dim CRConnectionInfo As New CrystalDecisions.Shared.ConnectionInfo
With CRConnectionInfo
.DatabaseName = <DBNAME>
.ServerName = <HOSTNAME>
.UserID = Utility.GetDBUsername
.Password = Utility.GetDBPassword
End With
For Each CRTable As CrystalDecisions.CrystalReports.Engine.Table In CRTables
Dim CRTableLogonInfo As CrystalDecisions.Shared.TableLogOnInfo = CRTable.LogOnInfo
CRTableLogonInfo.ConnectionInfo = CRConnectionInfo
CRTable.ApplyLogOnInfo(CRTableLogonInfo)
Next CRTable
ElseIf Me.SourceType = ReportSourceType.ADODataSet Then
Dim CRDatabase As CrystalDecisions.CrystalReports.Engine.Database = Me.CrystalReport.Database
Dim CRTables As CrystalDecisions.CrystalReports.Engine.Tables = CRDatabase.Tables
For Each CRTable As CrystalDecisions.CrystalReports.Engine.Table In CRTables
For Each ADOTable As DataTable In ADODataSet.Tables
If CRTable.Name.ToUpper.Trim = ADOTable.TableName.ToUpper.Trim Then
CRTable.SetDataSource(ADOTable)
Exit For
End If
Next ADOTable
Next CRTable
ElseIf Me.SourceType = ReportSourceType.XML Then
If Not Me.XMLDataSource Is Nothing AndAlso Me.XMLDataSource.Exists Then
Dim CRDatabaseAttributes As New CrystalDecisions.ReportAppServer.DataDefModel.PropertyBag
Dim CRLogonProperties As New CrystalDecisions.ReportAppServer.DataDefModel.PropertyBag
Dim CRConnectionDetails As New CrystalDecisions.ReportAppServer.DataDefModel.ConnectionInfo
Dim CRTable As CrystalDecisions.ReportAppServer.DataDefModel.Table
Dim CRTables As CrystalDecisions.ReportAppServer.DataDefModel.Tables = Me.CrystalReport.ReportClientDocument.DatabaseController.Database.Tables
Dim XMLData As New System.Data.DataSet
XMLData.ReadXml(Me.XMLDataSource.FullName)
With CRLogonProperties
.Add("File Path ", Me.XMLDataSource.FullName)
.Add("Internal Connection ID", "{be7cdac3-6a64-4923-8177-898ab55d0fa0}")
End With
With CRDatabaseAttributes
.Add("Database DLL", "crdb_adoplus.dll")
.Add("QE_DatabaseName", "")
.Add("QE_DatabaseType", "")
.Add("QE_LogonProperties", CRLogonProperties)
.Add("QE_ServerDescription", Me.XMLDataSource.Name.Substring(0, Me.XMLDataSource.Name.Length - Me.XMLDataSource.Extension.Length))
.Add("QE_SQLDB", "False")
.Add("SSO Enabled", "False")
End With
With CRConnectionDetails
.Attributes = CRDatabaseAttributes
.Kind = CrystalDecisions.ReportAppServer.DataDefModel.CrConnectionInfoKindEnum.crConnectionInfoKindCRQE
.UserName = ""
.Password = ""
End With
For I As Integer = 0 To XMLData.Tables.Count - 1
CRTable = New CrystalDecisions.ReportAppServer.DataDefModel.Table
With CRTable
.ConnectionInfo = CRConnectionDetails
.Name = XMLData.Tables(I).TableName
.QualifiedName = XMLData.Tables(I).TableName
.[Alias] = XMLData.Tables(I).TableName
End With
Me.CrystalReport.ReportClientDocument.DatabaseController.SetTableLocation(CRTables(I), CRTable)
Next I
End If
End If
End Sub
#End Region
End Class
PARAMETER OBJECT (CRParameter)
#Region "CRYSTAL REPORTS PARAMETER CLASS"
Public Class CRParameter
Public Property ParameterName As String
Public Property ParameterValue As Object
Public Sub New(ByVal CurrentParameterName As String, ByVal CurrentParameterValue As Object)
Me.ParameterName = CurrentParameterName
Me.ParameterValue = CurrentParameterValue
End Sub
Friend Sub UpdateReportParameter(ByRef CurrentReport As CrystalDecisions.CrystalReports.Engine.ReportDocument)
If Not CurrentReport Is Nothing Then
If Not String.IsNullOrEmpty(Me.ParameterName) Then
Using ReportFieldDefinitions As ParameterFieldDefinitions = CurrentReport.DataDefinition.ParameterFields
Using ReportParameter As ParameterFieldDefinition = ReportFieldDefinitions.Item(Me.ParameterName)
Dim ReportValues As ParameterValues = ReportParameter.CurrentValues
Dim NewValue As New ParameterDiscreteValue
ReportValues.Clear()
NewValue.Description = Me.ParameterName
NewValue.Value = Me.ParameterValue
ReportValues.Add(NewValue)
ReportParameter.ApplyCurrentValues(ReportValues)
ReportParameter.ApplyDefaultValues(ReportValues)
End Using
End Using
End If
End If
End Sub
End Class
#End Region
FORMULA OBJECT (CRFormula)
#Region "CRYSTAL REPORTS FORMULA VALUE CLASS"
Public Class CRFormula
Public Property FormulaName As String
Public Property FormulaValue As Object
Public Sub New(ByVal NewFormulaName As String, ByVal NewFormulaValue As Object)
Me.FormulaName = NewFormulaName
Me.FormulaValue = NewFormulaValue
End Sub
Friend Sub UpdateFormulaField(ByRef CurrentReport As CrystalDecisions.CrystalReports.Engine.ReportDocument)
If Not CurrentReport Is Nothing Then
If Not String.IsNullOrEmpty(Me.FormulaName) Then
Try
If Me.FormulaValue Is Nothing Then
Me.FormulaValue = ""
CurrentReport.DataDefinition.FormulaFields(Me.FormulaName).Text = Me.FormulaValue.ToString
ElseIf TypeOf Me.FormulaValue Is String AndAlso String.IsNullOrEmpty(Convert.ToString(Me.FormulaValue)) Then
Me.FormulaValue = ""
CurrentReport.DataDefinition.FormulaFields(Me.FormulaName).Text = Me.FormulaValue.ToString
ElseIf TypeOf Me.FormulaValue Is String AndAlso Not String.IsNullOrEmpty(Convert.ToString(Me.FormulaValue)) Then
Me.FormulaValue = "'" & Me.FormulaValue.ToString & "'"
CurrentReport.DataDefinition.FormulaFields(Me.FormulaName).Text = Me.FormulaValue.ToString
ElseIf TypeOf Me.FormulaValue Is Date Then
Me.FormulaValue = "'" & Convert.ToDateTime(Me.FormulaValue).ToString("yyyy-MM-dd") & "'"
CurrentReport.DataDefinition.FormulaFields(Me.FormulaName).Text = Me.FormulaValue.ToString
Else
CurrentReport.DataDefinition.FormulaFields(Me.FormulaName).Text = Me.FormulaValue.ToString
End If
Catch ex As Exception
End Try
End If
End If
End Sub
End Class
#End Region
Microsoft SQL Database
VB.NET in Visual Studio 2013
I am currently using a form for launching the crystal reports and it works perfectly fine for reports that has no sub-reports. The code I have used to build it is an adaptation from https://apps.support.sap.com/sap/support/knowledge/public/en/1676673.
Imports CrystalDecisions.Shared
Imports CrystalDecisions.CrystalReports.Engine
Public Class Crystal
Public RPTSRC As String
Public RPTNAME As String
Private Sub configureCRYSTALREPORT()
Dim myConnectionInfo As New ConnectionInfo()
myConnectionInfo.DatabaseName = "DBNAME"
myConnectionInfo.UserID = "CR"
myConnectionInfo.Password = "1234"
setDBLOGONforREPORT(myConnectionInfo)
End Sub
Private Sub setDBLOGONforREPORT(ByVal myconnectioninfo As ConnectionInfo)
Dim mytableloginfos As New TableLogOnInfos()
mytableloginfos = CrystalReportViewer1.LogOnInfo
For Each myTableLogOnInfo As TableLogOnInfo In mytableloginfos
myTableLogOnInfo.ConnectionInfo = myconnectioninfo
Next
End Sub
Private Sub MGM_PT_MNT_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Me.Size = New Size((Screen.PrimaryScreen.Bounds.Width) - 10, (Screen.PrimaryScreen.Bounds.Height) - 35)
Me.Location = New Point((Screen.PrimaryScreen.WorkingArea.Width - Me.Width) / 2, (Screen.PrimaryScreen.WorkingArea.Height - Me.Height) / 2)
Me.Text = RPTNAME
CrystalReportViewer1.ReportSource = RPTSRC
configureCRYSTALREPORT()
Application.DoEvents()
End Sub
End Class
As per my understanding, this only manages to pass the values only to the main report and hence the sub report requests for the credentials.Unfortunately, the KPI report I have developed requires 2 subreports to be a part of the main report and the Management cannot be expected to type the username and password to view this. Furthermore, we are using SQL Authentication only.
Can someone please help me to pass the login details to the SubReports for VB.NET? Any help appreciated .
EDIT:
Thanks to Jonathan, I got the answer to my question and would like to make it easier for others too who are also in search of a similar requirement.
Imports CrystalDecisions.Shared
Imports CrystalDecisions.CrystalReports.Engine
Public Class Crystal
'This is only needed if you would like another form to send the Report Name (RPTNAME) and the Location (RPTSRC). Location can be a Shared UNC Path.
Public RPTSRC As String
Public RPTNAME As String
Private Sub configureCRYSTALREPORT(report As ReportDocument)
Dim myConnectionInfo As New ConnectionInfo()
myConnectionInfo.DatabaseName = "DBNAME"
myConnectionInfo.UserID = "Username"
myConnectionInfo.Password = "Password"
setDBLOGONforREPORT(myConnectionInfo)
configureSubREPORT(report, myConnectionInfo)
End Sub
Private Sub setDBLOGONforREPORT(ByVal myconnectioninfo As ConnectionInfo)
Dim mytableloginfos As New TableLogOnInfos()
mytableloginfos = CrystalReportViewer1.LogOnInfo
For Each myTableLogOnInfo As TableLogOnInfo In mytableloginfos
myTableLogOnInfo.ConnectionInfo = myconnectioninfo
Next
End Sub
Private Sub configureSubREPORT(report As ReportDocument, info As ConnectionInfo)
Dim rD As ReportDocument
Dim table As Table
For Each rD In report.Subreports
For Each table In rD.Database.Tables
table.LogOnInfo.ConnectionInfo = info
table.ApplyLogOnInfo(table.LogOnInfo)
Next
Next
End Sub
Private Sub MGM_PT_MNT_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Me.Size = New Size((Screen.PrimaryScreen.Bounds.Width) - 8, (Screen.PrimaryScreen.Bounds.Height) - 44)
Me.Location = New Point((Screen.PrimaryScreen.WorkingArea.Width - Me.Width) / 2, (Screen.PrimaryScreen.WorkingArea.Height - Me.Height) / 2)
Me.Text = RPTNAME
Dim report = New ReportDocument
report.Load(RPTSRC)
CrystalReportViewer1.ReportSource = report
configureCRYSTALREPORT(report)
'EnableDatabaseLogonPrompt = "false"
Application.DoEvents()
End Sub
End Class
It is a very long time since I used VB, so please forgive if the syntax is not right, but I do have CR Sub-Reports in c#.
What you need to do, is pass the logoninfo down to all the sub-reports. Something like this:
Change your load sub:
Instead of:
CrystalReportViewer1.ReportSource = RPTSRC
configureCRYSTALREPORT()
Do
Dim report = New ReportDocument
report.Load(RPTSRC)
CrystalReportViewer1.ReportSource = report
configureCRYSTALREPORT(report)
Then make sub routine
Private Sub configureSubREPORT(report as ReportDocument, info as ConnectionInfo)
Dim rD As ReportDocument
Dim table As Table
For Each rD In report.Subreports
For Each table In rD.Database.Tables
table.LogOnInfo.ConnectionInfo = info
table.ApplyLogOnInfo(table.LogOnInfo)
Next
Next
End Sub
Finally change existing sub as follows:
Private Sub configureCRYSTALREPORT(report AS ReportDocument)
Dim myConnectionInfo As New ConnectionInfo()
myConnectionInfo.DatabaseName = "DBNAME"
myConnectionInfo.UserID = "CR"
myConnectionInfo.Password = "1234"
setDBLOGONforREPORT(myConnectionInfo)
configureSubREPORT(report, myConnectionInfo)
End Sub
That should do it. But as I said before I am very rusty on VB!
Looking to get a list of available databases from a user defined sql server. The code below properly queries available servers but now I'm trying to find the available databases on the selected server.
Thoughts?
Dim dt As Data.DataTable = Nothing, dr As Data.DataRow = Nothing
dt = System.Data.Sql.SqlDataSourceEnumerator.Instance.GetDataSources()
For Each dr In dt.Rows
cbAvailableSQLServers.Items.Add(dr.Item(0).ToString)
Next
Firstly, your code is going to get default instances OK but will not show the names of named instances. Here's how I've populated a ComboBox with SQL Server instances in the past:
Private Sub serverCombo_DropDown(ByVal sender As Object, ByVal e As System.EventArgs) Handles serverCombo.DropDown
If Me.populateServerList Then
'Enumerate available SQL Server instances.'
Dim serverTable As DataTable = SqlDataSourceEnumerator.Instance.GetDataSources()
Dim upperBound As Integer = serverTable.Rows.Count - 1
Dim serverNames(upperBound) As String
For index As Integer = 0 To upperBound
If serverTable.Rows(index).IsNull("InstanceName") Then
serverNames(index) = CStr(serverTable.Rows(index)("ServerName"))
Else
serverNames(index) = String.Format("{0}\{1}", _
serverTable.Rows(index)("ServerName"), _
serverTable.Rows(index)("InstanceName"))
End If
Next
Dim currentServerName As String = Me.serverCombo.Text
With Me.serverCombo
.BeginUpdate()
.Items.Clear()
.Items.AddRange(serverNames)
.SelectedItem = currentServerName
.Text = currentServerName
.EndUpdate()
End With
Me.populateServerList = False
End If
End Sub
Here's how I populated the database list for a server in the same application:
Private Sub databaseCombo_DropDown(ByVal sender As Object, ByVal e As System.EventArgs) Handles databaseCombo.DropDown
Using connection As New SqlConnection(Me.GetConnectionString(False))
Try
connection.Open()
'Enumerate available databases.'
Me.databaseCombo.DataSource = connection.GetSchema("Databases")
Catch
MessageBox.Show("Unable to connect.", _
"Connection Error", _
MessageBoxButtons.OK, _
MessageBoxIcon.Error)
End Try
End Using
End Sub
Private Function GetConnectionString(ByVal includeDatabase As Boolean) As String
Dim builder As New SqlConnectionStringBuilder()
'Build a connection string from the user input.'
builder.DataSource = Me.serverCombo.Text
builder.IntegratedSecurity = Me.integratedSecurityOption.Checked
builder.UserID = Me.userText.Text
builder.Password = Me.passwordText.Text
If includeDatabase Then
builder.InitialCatalog = Me.databaseCombo.Text
End If
Return builder.ConnectionString
End Function
You can either query sys.databases to get list of databases for a particular server instance or you need to use Microsoft.SqlServer.Management.Smo namespace to programmatically query them
Firstly, i have a grdData at my main page. After choosing the data i want and went to another page using
Request.QueryString("id")
In that page i would like to make another grdData using the
Request.QueryString("id")
but came upon an error by
Value of type 'cfeedback' cannot be converted to 'system.collections.arraylist'
Below are my codes
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
Dim objArrayList As New ArrayList
Dim objCDBFeedback As New CDBFeedback
Dim intGuestID2 As Integer
intGuestID2 = Request.QueryString("id")
objArrayList = objCDBFeedback.getFeedBack(intGuestID2)
grdResult.DataSource = objArrayList
grdResult.DataBind()
grdResult.HeaderRow.BackColor = Drawing.Color.AliceBlue
grdResult.RowStyle.BackColor = Drawing.Color.BlanchedAlmond
grdResult.AlternatingRowStyle.BackColor = Drawing.Color.LightSalmon
grdResult.Columns(0).Visible = True
End Sub
My Function
Public Function getFeedBack(ByVal pintGuestID1 As Integer) As CFeedback
Dim objCmd As New MySqlCommand
Dim objCn As New MySqlConnection(connectionString)
Dim objAdapter As New MySqlDataAdapter
Dim strSQL As String = ""
Dim objDs As New DataSet
Dim objDataRow As DataRow
strSQL = "SELECT * FROM tblFeedback WHERE strGuestCodeFB=" & pintGuestID1
objCmd.CommandText = strSQL
objCmd.Connection = objCn
objAdapter.SelectCommand = objCmd
objCn.Open()
objAdapter.Fill(objDs, "tblFeedback")
objDataRow = objDs.Tables("tblFeedback").Rows(0)
Dim objCFeedback As New CFeedback
objCFeedback.Feedback = objDataRow.Item("strGuestCompanyTI")
objCn.Close()
Return objCFeedback
End Function
My Class
Public Class CFeedback
Private strGuestCodeFB As Integer
Private strFeedBackFB As String
Public Property GuestId() As String
Get
Return strGuestCodeFB
End Get
Set(ByVal value As String)
strGuestCodeFB = value
End Set
End Property
Public Property Feedback() As String
Get
Return strFeedBackFB
End Get
Set(ByVal value As String)
strFeedBackFB = value
End Set
End Property
End Class
So is it possible to have a grdData base on querystring?
The very first thing that you need to do is edit your code behind and add the following two lines at the top:
Option Explicit On
Option Strict On
This will show you at least one error: assigning a type of CFeedback to a type of ArrayList.
You will need to determine what the appropriate resolution to this is, but I suspect that you want to return an ArrayList or generic List from GetFeedback instead of just the one item.
So, among other changes, you will want to change pageload to look something like:
Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
Dim objCDBFeedback As New CDBFeedback
Dim intGuestID2 As Integer
intGuestID2 = CInt(Request.QueryString("id"))
Dim cValues As System.Collections.Generic.List(Of CFeedback)
cValues = objCDBFeedback.getFeedBack(intGuestID2)
grdResult.DataSource = cValues
grdResult.DataBind()
grdResult.HeaderRow.BackColor = Drawing.Color.AliceBlue
grdResult.RowStyle.BackColor = Drawing.Color.BlanchedAlmond
grdResult.AlternatingRowStyle.BackColor = Drawing.Color.LightSalmon
grdResult.Columns(0).Visible = True
grdResult.Visible = cValues.Count <> 0
End Sub
And the getFeeback method to look something like:
Public Function getFeedBack(ByVal pintGuestID1 As Integer) As System.Collections.Generic.List(Of CFeedback)
Dim cValues As New System.Collections.Generic.List(Of CFeedback)
Using objCn As New MySqlConnection(connectionString)
Using objCmd As New MySqlCommand
Dim strSQL As String = ""
strSQL = "SELECT * FROM tblFeedback WHERE strGuestCodeFB=" & pintGuestID1
objCmd.CommandText = strSQL
objCmd.Connection = objCn
objCn.Open()
Using oReader As MySqlDataReader = objCmd.ExecuteReader
Do While oReader.Read
Dim objCFeedback As New CFeedback
objCFeedback.Feedback = oReader.Item("strGuestCompanyTI")
cValues.Add(objCFeedback)
Loop
End Using
objCn.Close()
End Using
End Using
Return cValues
End Function
I am trying to search for the occurence of a string within MS Access database with 1000000 records using VB.Net. The program does not respond once it is run or it is taking a very long time. However, if I reduce the size of the database the program runs.
Below is the code Could anyone let me know how do I go about it.. Thanks in advance.
Imports System.Data.SqlClient
Public Class Form1
Dim rcount As Integer
Dim conn As New ADODB.Connection
Dim rcset As New ADODB.Recordset
Dim sqlStr As String
Public Sub openConn()
Dim strConnect As String
strConnect = "DRIVER={Microsoft Access Driver (*.mdb)}; DefaultDir=" & Application.StartupPath & ";DBQ=atg;UID=;PWD=;"
If conn.State = 0 Then
conn.ConnectionString = strConnect
conn.Open()
End If
End Sub
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
End Sub
Public Sub rcdSet(ByVal sqlStr As String)
If conn.State = 0 Then
Call openConn()
End If
If rcset.State = 1 Then
rcset.Close()
rcset = Nothing
rcset = New ADODB.Recordset
rcset.CursorLocation = ADODB.CursorLocationEnum.adUseServer
rcset.CursorType = ADODB.CursorTypeEnum.adOpenDynamic
rcset.LockType = ADODB.LockTypeEnum.adLockOptimistic
End If
rcset.Open(sqlStr, conn)
End Sub
Private Sub btn_search_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles btn_search.Click
Dim sql As String
Dim rs As New ADODB.Recordset
Dim rs2 As New ADODB.Recordset
Dim res As String
res = ""
Call openConn()
Dim SearchString_exist As New List(Of String)(txt_exist.Text.Split(Environment.NewLine))
' Dim SearchString_not_exist As New List(Of String)(txt_not_exist.Text.Split(Environment.NewLine))
' Do Until SearchString_exist.Count = 0
sql = "SELECT * FROM ATG WHERE Term like '%cam%'"
rs.Open(sql, conn, ADODB.CursorTypeEnum.adOpenDynamic, ADODB.LockTypeEnum.adLockOptimistic)
While (rs.EOF <> True)
res += rs.Fields("Term").Value + Environment.NewLine
rs.MoveNext()
End While
rs.Close()
SearchString_exist.RemoveAt(0)
txt_exist.Lines = SearchString_exist.ToArray
'Loop
txt_result.Text = res
End Sub
End Class
If you're saying that the application is unresponsive once you click the button, that's because you're searching your Access database on the same thread that updates you application - that means that the application will appear busy until your database search is done (which could take a while). If you want to do this in a background thread and leave your application responsive during the search, you can do that using the threadpool:
http://www.dotnetperls.com/threadpool-vbnet
Also, the query runs better with fewer records because it has less data to search through, suggesting that it may not be indexed well. You could add an index on the "Term" field, which will help speed things up - you'd have to see if it helped. Access will have to do an index scan, not a faster index seek, so the response won't be immediate, but it will may help cut down your wait time significantly. Here's a Microsoft article that shows how to do this:
http://office.microsoft.com/en-us/access-help/create-and-use-an-index-to-improve-performance-HA010210347.aspx
If you have 1,000,000 records, why aren't you using SQL Server? How much traffic is this application going to get? I wouldn't expect any site to scale with MS-Access as its data store.