Related
I am working on a side project in VB, it is a network monitoring tool to pings a number of devices which should come back as successful or failed. I have extreme limits in programming so forgive me.
I am using buttons, a total of 34 for each device that I want to ping that returns a success or fail which will color code green(success) and red(failed) but I am sure there is a better way? Right now, my code is stuck on one button, I cant figure out how to step to the next one on the list. In my code, I have it commented out of the results I want produced which is where I am stuck on.
The text file contains all my IP addresses I want to ping separated by a comma.
Basically, when the form is running, it will display each button as green or red, depending on if the device is online or not. I want the code to loop every 2 minutes as well to keep the devices up to date. Literally a device monitoring tool. I was able to get it to work using 34 different End If statements but that is messy and a lot of work to maintain. Any assistance would be helpful.
Using MyReader As New Microsoft.VisualBasic.FileIO.TextFieldParser("\\txt file location\device.txt")
MyReader.TextFieldType = FileIO.FieldType.Delimited
MyReader.SetDelimiters(",")
Dim currentRow As String()
Dim MyLen() As String = {"Button1", "Button2", "Button3", "Button4", "Button5", "Button6", "Button7", "Button8", "Button9", "Button10", "Button11", "Button12", "Button13", "Button14", "Button15", "Button16", "Button17", "Button18", "Button19", "Button20", "Button21", "Button22", "Button23", "Button24", "Button25", "Button26", "Button27", "Button28", "Button29", "Button30", "Button31", "Button32", "Button33", "Button34"}
While Not MyReader.EndOfData
Try
currentRow = MyReader.ReadFields()
Dim currentField As String
For Each currentField In currentRow
If My.Computer.Network.Ping(currentField) Then
MsgBox(MyLen)
'MyLen = Color.LimeGreen
Else
MsgBox(MyLen)
'MyLen.Text = "Failed"
'MyLen.BackColor = Color.Red
End If
Next
Catch ex As Microsoft.VisualBasic.FileIO.MalformedLineException
MsgBox("Line " & ex.Message & "is not valid and will be skipped.")
End Try
End While
End Using
enter image description here
Here is some code that takes a different approach. To try it create a new Form app with only a FlowLayoutPanel and Timer on it. Use the default names. It might be above your skill level but using the debugger you might learn something. Or not.
Public Class Form1
Private MyButtons As New List(Of Button)
Private Sub Form1_Shown(sender As Object, e As EventArgs) Handles Me.Shown
Timer1.Enabled = False 'started later
Timer1.Interval = CInt(TimeSpan.FromMinutes(2).TotalMilliseconds) '<<<< Change >>>>
Dim path As String = "\\txt file location\device.txt"
Dim text As String = IO.File.ReadAllText(path) 'use this
''for testing >>>>>
'Dim text As String = "10.88.0.70, 10.88.0.122,192.168.0.15, 10.88.0.254, 1.2.3.4" ''for testing
''for testing <<<<<
Dim spltCHs() As Char = {","c, " "c, ControlChars.Tab, ControlChars.Cr, ControlChars.Lf}
Dim IPs() As String = text.Split(spltCHs, StringSplitOptions.RemoveEmptyEntries)
For Each addr As String In IPs
Dim b As New Button
Dim p As New MyPinger(addr)
p.MyButton = b
b.Tag = p 'set tag to the MyPinger for this address
b.AutoSize = True
b.Font = New Font("Lucida Console", 10, FontStyle.Bold)
b.BackColor = Drawing.Color.LightSkyBlue
'center text in button
Dim lAddr As String = p.Address
Dim t As String = New String(" "c, (16 - lAddr.Length) \ 2)
Dim txt As String = t & lAddr & t
b.Text = txt.PadRight(16, " "c)
b.Name = "btn" & lAddr.Replace("."c, "_"c)
AddHandler b.Click, AddressOf SomeButton_Click 'handler for button
MyButtons.Add(b) 'add button to list
Next
'sort by IP
MyButtons = (From b In MyButtons
Select b Order By DirectCast(b.Tag, MyPinger).Address(True)).ToList
For Each b As Button In MyButtons
FlowLayoutPanel1.Controls.Add(b) 'add button to panel
Next
FlowLayoutPanel1.Anchor = AnchorStyles.Bottom Or AnchorStyles.Left Or AnchorStyles.Right Or AnchorStyles.Top
Timer1.Enabled = True 'start the timer
End Sub
Private Sub SomeButton_Click(sender As Object, e As EventArgs)
'if button clicked ping it
Dim b As Button = DirectCast(sender, Button) 'which button
b.BackColor = MyPinger.UnknownColor
Dim myP As MyPinger = DirectCast(b.Tag, MyPinger) ''get the MyPinger for this
myP.DoPing() 'do the ping
End Sub
Private Async Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
Timer1.Enabled = False
Dim myPs As New List(Of MyPinger)
For Each b As Button In MyButtons
b.BackColor = MyPinger.UnknownColor
Dim myP As MyPinger = DirectCast(b.Tag, MyPinger)
myPs.Add(myP)
Next
Dim t As Task
t = Task.Run(Sub()
Threading.Thread.Sleep(25)
For Each myP As MyPinger In myPs
myP.DoPing()
Next
End Sub)
Await t
Timer1.Enabled = True
End Sub
End Class
Public Class MyPinger
Public Shared ReadOnly UpColor As Drawing.Color = Drawing.Color.LightGreen
Public Shared ReadOnly DownColor As Drawing.Color = Drawing.Color.Red
Public Shared ReadOnly UnknownColor As Drawing.Color = Drawing.Color.Yellow
Private _ip As Net.IPAddress
Private _ping As Net.NetworkInformation.Ping
Public LastReply As Net.NetworkInformation.PingReply
Private Shared ReadOnly PingTMO As Integer = 2500
Private _waiter As New Threading.AutoResetEvent(True)
Public MyButton As Button
Public Sub New(IPAddr As String)
Me._ip = Net.IPAddress.Parse(IPAddr) 'will throw exception if IP invalid <<<<<
Me._ping = New Net.NetworkInformation.Ping 'create the ping
'do initial ping
Dim t As Task = Task.Run(Sub()
Threading.Thread.Sleep(25) 'so init has time
Me.DoPingAsync()
End Sub)
End Sub
Private Async Sub DoPingAsync()
If Me._waiter.WaitOne(0) Then 'only one at a time for this IP
Me.LastReply = Await Me._ping.SendPingAsync(Me._ip, PingTMO)
Dim c As Drawing.Color
Select Case Me.LastReply.Status
Case Net.NetworkInformation.IPStatus.Success
c = UpColor
Case Else
c = DownColor
End Select
Me.SetButColor(c)
Me._waiter.Set()
End If
End Sub
Public Sub DoPing()
Me.DoPingAsync()
End Sub
Private Sub SetButColor(c As Drawing.Color)
If Me.MyButton IsNot Nothing Then
If Me.MyButton.InvokeRequired Then
Me.MyButton.BeginInvoke(Sub()
Me.SetButColor(c)
End Sub)
Else
Me.MyButton.BackColor = c
End If
End If
End Sub
Public Function TheIP() As Net.IPAddress
Return Me._ip
End Function
Public Function Address(Optional LeadingZeros As Boolean = False) As String
Dim rv As String = ""
If LeadingZeros Then
Dim byts() As Byte = Me._ip.GetAddressBytes
For Each b As Byte In byts
rv &= b.ToString.PadLeft(3, "0"c)
rv &= "."
Next
Else
rv = Me._ip.ToString
End If
Return rv.Trim("."c)
End Function
End Class
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
I'm pretty new to multithreading, but I am trying to go through each listviewitem where subitem 1 is ".." (quequed).. The code I have right now goes through the first 2, but how can I make it continue with the rest? I am currently running two threads.
I have made this little example application to test out multithreading, which I can then apply to my other application once I get a hang of it.. I pretty much want it to go through each item where status is "..", and then it makes the time go up to 10000 until it continues with the other items, until there are none left.. Any help would be appreciated.
Public Class Form1
Dim i As Integer
Dim i2 As Integer
Dim thread As System.Threading.Thread
Dim thread2 As System.Threading.Thread
Public Class CParameters
Public Property LID As Integer
End Class
Private Sub startMe(ByVal param As Object)
Dim p As CParameters = CType(param, CParameters)
Do Until i = 10000
i = i + 1
ListView1.Items(p.LID).SubItems(1).Text = "Running"
ListView1.Items(p.LID).SubItems(2).Text = i
If i >= 10000 Then
ListView1.Items(p.LID).SubItems(1).Text = "OK"
End If
Loop
End Sub
Private Sub startMe2(ByVal param As Object)
Dim p As CParameters = CType(param, CParameters)
Do Until i = 10000
i = i + 1
ListView1.Items(p.LID).SubItems(1).Text = "Running"
ListView1.Items(p.LID).SubItems(2).Text = i
If i >= 10000 Then
ListView1.Items(p.LID).SubItems(1).Text = "OK"
End If
Loop
End Sub
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
thread = New System.Threading.Thread(AddressOf startMe)
Dim parameters As New CParameters
parameters.LID = "0"
thread.Start(parameters)
'Thread 2
thread2 = New System.Threading.Thread(AddressOf startMe2)
parameters = New CParameters
parameters.LID = "1"
thread2.Start(parameters)
End Sub
End Class
If you're using .NET 4.5 I strongly recommend the Task based approach. This approach makes asynchronous programming so much easier. I recreated the form you described with a ListView. I added a 4th column for the counter.
Public Class CParameters
Public Property LID As Integer
End Class
Private Async Function startThreadsAsync() As Task
'WARNING: Await does not create a new thread. Task.run does.
Await crunchThreadAsync(New CParameters() With {.LID = 0}) 'Create first task
Await crunchThreadAsync(New CParameters() With {.LID = 1}) 'Create second task
Await crunchThreadAsync(New CParameters() With {.LID = 2}) 'Create third task
End Function
Private Sub UpdateListItem(ByVal pid As Integer, ByVal Status As String, ByVal Time As String, ByVal Count As String)
If Not ListView1.InvokeRequired Then 'If on the main ui thread update
Dim lp As ListViewItem = ListView1.Items(pid)
lp.SubItems(0).Text = pid.ToString
lp.SubItems(1).Text = Status
lp.SubItems(2).Text = Time & "ms"
lp.SubItems(3).Text = Count
Else 'If not on the main ui thread invoke the listviews original thread(ui thread)
ListView1.BeginInvoke(Sub()
Dim lp As ListViewItem = ListView1.Items(pid)
lp.SubItems(0).Text = pid.ToString
lp.SubItems(1).Text = Status
lp.SubItems(2).Text = Time & "ms"
lp.SubItems(3).Text = Count
End Sub)
End If
End Sub
Private Async Function crunchThreadAsync(ByVal param As CParameters) As Task(Of Boolean)
'Setting start text
UpdateListItem(param.LID, "Running", "", "0")
Dim cnt As Integer = 0
Dim p As CParameters = param
Dim l As New Stopwatch() 'For displaying total time spent crunching
l.Start()
'We're not leaving the UI thread.'
'Create new thread for crunching
Dim result As Boolean = Await Task.Run(Function()
Do Until cnt = 10000
cnt = cnt + 1
If cnt Mod 1000 = 0 Then
UpdateListItem(param.LID, "", "", cnt.ToString)
End If
Loop
Return True
End Function)
'We're back in the UI thread again.
l.Stop()
UpdateListItem(param.LID, "Finished", l.ElapsedMilliseconds.ToString, cnt.ToString)
End Function
Private Async Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
TextBox1.Text &= "Starting crunch." & Environment.NewLine
Await startThreadsAsync()
TextBox1.Text &= "Finished crunch." & Environment.NewLine
End Sub
I do not update the ui thread every tick of each counter for a few reasons: The amount of updates on the ui thread would lock up the ui. There is no way around this except to decide when it's necessary to actually update the Ui. In this case I do it every 1000 ticks.
I am new to vb.net multithreading, and I am facing a issue.
For threading I am using system.threading.thread function.
I know I can stop a thread using thread.abort call. But that's not a good choice (I have searched a bit and came to know that). This also will not work because my running threads needs some cleanup before cancellation.
So, I'm trying to do some kind of action that set a variable to UI thread like
dim bolStop as Boolean
So, if my UI thread set that Boolean value as true then all running thread can access/notice that change and can start cleanup process before stop/cancel the thread itself.
hope that makes sense...
best regards
Edit:1 (Source Is Pasted Here as Suggested By #DrunkenCodeMonkey
This is the code for Main Form (Form1.vb) (I think i need something here and also on the thread module (class) too) so, below i will paste module code too
Public Class Form1
Private objThreadList As ArrayList
Dim bolStop As Boolean
Private Sub cmd1_Click(sender As System.Object, e As System.EventArgs) Handles cmd1.Click
Dim intTLoop As Integer
sPrepareThreading()
For intTLoop = 1 To 5
sSendThread("", "Zakir", "Biplob")
Next
'sFinishThreading()
MessageBox.Show("Done!")
End Sub
Sub sPrepareThreading()
Dim intTLoop As Integer
objThreadList = New ArrayList
For intTLoop = 1 To 3
objThreadList.Add(Nothing)
Next intTLoop
End Sub
Sub sSendThread(strDraft As String, ByVal strTitle As String, ByVal strSubHeader As String)
Dim bolFalied As Boolean
Dim intTLoop As Integer
Dim objItem As System.Windows.Forms.ListViewItem
Dim objTData As objThreadCrossData
Dim objThreadClass As clsThread
Dim objNewThread As System.Threading.Thread
Zakir_RecheckThread:
My.Application.DoEvents()
If bolStop = True Then
Exit Sub
End If
bolFalied = True
For intTLoop = 1 To 3
bolFalied = True
If objThreadList(intTLoop - 1) Is Nothing Then
bolFalied = False
Else
If CType(objThreadList(intTLoop - 1), System.Threading.Thread).IsAlive = False Then
bolFalied = False
Else
bolFalied = True
End If
End If
If bolFalied = False Then
objTData = New objThreadCrossData
objTData.ID = ListView1.Items.Count
objTData.bolStop = False
objTData.strTime = DateTime.Now.ToString
objItem = ListView1.Items.Add(strTitle)
objItem.SubItems.Add(strSubHeader)
objItem = Nothing
objThreadClass = New clsThread(objTData, "", Me)
objNewThread = New System.Threading.Thread(AddressOf objThreadClass.StartThread)
objNewThread.IsBackground = True
objNewThread.Start()
objThreadList(intTLoop - 1) = objNewThread
Exit For
End If
Next
If bolFalied = True Then
GoTo Zakir_RecheckThread
Exit Sub
End If
End Sub
Public Sub ReceiveThreadMessage(ByVal objCrosData As Object)
Dim objTDataV1 As objThreadCrossData
objTDataV1 = CType(objCrosData, objThreadCrossData)
ListView1.Items(objTDataV1.ID).SubItems(1).Text = objTDataV1.strProgress
End Sub
Private Sub cmd2_Click(sender As System.Object, e As System.EventArgs) Handles cmd2.Click
REM This is where i like to stop the running thred either by setting bolStop = True and all running thead check that variable.
REM or by chate the thread (Thread Data Object Property naemd "bolStop" value"
REM please help me out...
End Sub
End Class
Code For Thread Module (clsThread.vb):
Public Class clsThread
Private m_Counter As Integer = 0
Private m_MainWindow As Form
Dim m_ThreadCrorssData As objThreadCrossData
Private Delegate Sub NotifyMainWindow(ByVal objCrosData As objThreadCrossData)
'We need an object of this deletegate
Private m_NotifyMainWindow As NotifyMainWindow
Public Sub New(ByVal objCrossData As Object, ByVal strXML As String, ByRef MainWindow As Form1)
m_MainWindow = MainWindow
m_ThreadCrorssData = CType(objCrossData, objThreadCrossData)
'We need to point our delegate to the Method, which we want to call from this thread
m_NotifyMainWindow = AddressOf MainWindow.ReceiveThreadMessage
End Sub
Public Sub StartThread()
While m_Counter < 100
m_Counter = m_Counter + 1
rem for example here i like to check for the value that whether user asked to stop
m_ThreadCrorssData.strProgress = CStr(m_Counter)
m_MainWindow.Invoke(m_NotifyMainWindow, m_ThreadCrorssData)
rem for example also here i like to check for the value that whether user asked to stop
'wait for some time before continuing loop
System.Threading.Thread.Sleep(1000)
End While
End Sub
End Class
Here is the Code For Module (Thread Object Data)
Module mdlCMM
Public Class objThreadCrossData
Property strProgress As String
Property bolStop As Boolean = False
Property strTime As String
Property ID As Integer
Property intStatusNum As Integer
Property strStatusValue As String
End Class
End Module
I am trying to multi thread my application so as it is visible while it is executing the process, this is what I have so far:
Private Sub SendPOST(ByVal URL As String)
Try
Dim DataBytes As Byte() = Encoding.ASCII.GetBytes("")
Dim Request As HttpWebRequest = TryCast(WebRequest.Create(URL.Trim & "/webdav/"), HttpWebRequest)
Request.Method = "POST"
Request.ContentType = "application/x-www-form-urlencoded"
Request.ContentLength = DataBytes.Length
Request.Timeout = 1000
Request.ReadWriteTimeout = 1000
Dim PostData As Stream = Request.GetRequestStream()
PostData.Write(DataBytes, 0, DataBytes.Length)
Dim Response As WebResponse = Request.GetResponse()
Dim ResponseStream As Stream = Response.GetResponseStream()
Dim StreamReader As New IO.StreamReader(ResponseStream)
Dim Text As String = StreamReader.ReadToEnd()
PostData.Close()
Catch ex As Exception
If ex.ToString.Contains("401") Then
TextBox2.Text = TextBox2.Text & URL & "/webdav/" & vbNewLine
End If
End Try
End Sub
Public Sub G0()
Dim siteSplit() As String = TextBox1.Text.Split(vbNewLine)
For i = 0 To siteSplit.Count - 1
Try
If siteSplit(i).Contains("http://") Then
SendPOST(siteSplit(i).Trim)
Else
SendPOST("http://" & siteSplit(i).Trim)
End If
Catch ex As Exception
End Try
Next
End Sub
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Dim t As Thread
t = New Thread(AddressOf Me.G0)
t.Start()
End Sub
However, the 'G0' sub code is not being executed at all, and I need to multi thread the 'SendPOST' as that is what slows the application.
Catch ex As Exception
End Try
A very effective way to stop .NET from telling you what you did wrong. Not knowing why it doesn't work is however the inevitable outcome.
Delete that.
Public Class Form1
'This just shows some concepts of threading.
'it isn't intended to do anything
'requires a Button, and two Labels
'
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles Button1.Click
'starts / stops a test thread
'isRun = 0 no thread running, start one
'isRun = 1 thread running, stop it
If Threading.Interlocked.Read(isRun) = 0L Then
'start thread
Threading.Interlocked.Increment(isRun)
t = New Threading.Thread(AddressOf showTime)
'simple threading app - display time about twice per second
t.IsBackground = True 'from a background thread
t.Start()
Else
'stop thread
Threading.Interlocked.Exchange(isRun, 0L)
t.Join() 'wait for thread to end
Threading.Monitor.Enter(listLock)
intervalList.Clear() 'clear the list
Threading.Monitor.Exit(listLock)
Label1.Text = "Stop"
Label2.Text = ""
End If
End Sub
Dim t As Threading.Thread
Dim intervalList As New List(Of Double)
Dim listLock As New Object
Dim isRun As Long = 0L
Private Sub showTime()
Dim dlgt As New UpdLblDel(AddressOf UpdateLabel) 'delegate for UI access
Dim lastDateTime As DateTime = Nothing
Do
Dim d As DateTime = DateTime.Now
If lastDateTime <> Nothing Then
'record difference of times - check sleep interval
Threading.Monitor.Enter(listLock)
intervalList.Add((d - lastDateTime).TotalMilliseconds)
Threading.Monitor.Exit(listLock)
End If
lastDateTime = DateTime.Now
dlgt.BeginInvoke(d, Nothing, Nothing) 'update the UI - note immediate return
Threading.Thread.Sleep(500) 'sleep for approx. 500 ms.
Loop While Threading.Interlocked.Read(isRun) = 1L
End Sub
Delegate Sub UpdLblDel(ByVal theTime As Object)
Private Sub UpdateLabel(ByVal theTime As Object)
If Threading.Interlocked.Read(isRun) = 1L Then
If Label1.InvokeRequired Then 'prevent cross-thread errors
Label1.BeginInvoke(New UpdLblDel(AddressOf UpdateLabel), theTime)
Exit Sub
Else
Label1.Text = CType(theTime, DateTime).ToString("HH:mm:ss.f") 'show the time from the background thread
End If
If Threading.Interlocked.Read(intervalList.Count) >= 10L Then
'take average
Threading.Monitor.Enter(listLock)
Dim avg As Double = intervalList.Sum / intervalList.Count 'sum all of the intervals / count
intervalList.Clear() 'clear the list
intervalList.Add(avg) 'forward the average
Label2.Text = avg.ToString("n2") 'show average
Threading.Monitor.Exit(listLock)
End If
End If
End Sub
End Class
You have to wrap the method that accesses the UI component in a delegate (it doesn't have to be a named delegate; it can be anonymous or an Action or Func), and then pass that to Me.Invoke, as others have alluded to.
In this example, I'm wrapping the split functionality in a lambda, and assigning that lambda to a variable of type Func(Of String()). I then pass that variable to Me.Invoke.
Public Sub G0()
Dim siteSplitFunc As Func(Of String()) = Function() _
TextBox1.Text.Split(vbNewLine.ToCharArray())
Dim siteSplit As String() = CType(Me.Invoke(siteSplitFunc), String())
For i = 0 To siteSplit.Count - 1
Try
If siteSplit(i).Contains("http://") Then
SendPOST(siteSplit(i).Trim)
Else
SendPOST("http://" & siteSplit(i).Trim)
End If
Catch ex As Exception
'Do something useful
End Try
Next
End Sub
You cannot access UI object directly from a thread.
When you want to read/write a textbox, you have to do it in the UI thread. This can be done by using Invoke. Or better yet, send/receive the information with parameters.
Here's a Delegate and a matching method. You call the method to update the textbox and it figures out if it should proxy the method for you by basically asking form if its on the same thread:
Private Delegate Sub UpdateTextBoxDelegate(ByVal text As String)
Private Sub UpdateTextBox(ByVal text As String)
If Me.InvokeRequired Then
Me.Invoke(New UpdateTextBoxDelegate(AddressOf UpdateTextBox), text)
Else
TextBox2.Text &= text
End If
End Sub
To use it, just change your catch statement to:
If ex.ToString.Contains("401") Then
UpdateTextBox(URL & "/webdav/" & vbNewLine)
End If