Threading.Monitor.Enter that doesn't /quite/ block the thread

J

Jon Brunson

Is there a way that I could use Monitor.Enter on the GUI thread, but not
block things like a Forms.Timer from triggering. Something like this:

If Thread.CurrentThread is GuiThread Then
While Monitor.IsLocked(dbConnection)
Application.DoEvents()
End While
Else
Monitor.Enter(dbConnection)
End If

Only there's no such thing as IsLocked on Monitor
 
G

Guest

Let's back up and look at the goal you're trying to achieve. What behavior
are you trying to get? Describe the problem a little more.

-Chris
 
J

Jon Brunson

it's basically a data capture system, that has a worker thread which
keeps the handheld's database in sync with the one on the server.
Depending on the status of this synchronization process, I want to show
various icons on the main window of the app. The thing is, the sync and
gui threads both use the same db connection to talk to the local
database. When either thread is using the connection, they lock it using
Threading.Monitor.Enter, and release it when they're finished with
Monitor.Exit. My problem is, atm I'm using Invoke to show/hide these
icons, but say the user goes in to capture some data, and thus the gui
thread tries to read from the db connection, while the sync is in
progress and has the db connection object locked, it will break, and
wait for the sync to complete. However, if the sync thread invokes while
the gui thread is broken in this way, both threads get locked, and I'm
in a dead-lock situation.

I've tried various methods of getting around the use of Invoke, like
having a "status" object that the sync thread updates, and the gui
thread reads from on a Forms.Timer, but this fails when the gui thread
is broken by a Monitor. Using Daniel Moth's BeginInvoke work-around
(creates loads of worker threads, which get executed out of sequence).
Trying the BackgroundWorker from OpenNETCF (same as Dan's work around
really).

I plum out of ideas of what do do, or what to Google for, so I'm hoping
that someone here can save me.
 
D

Daniel Moth

What you could use is Monitor.TryEnter which is supported in CF v2.0 (are
you sure you cannot move to that?)

Regardless, you should have different locks for your database (and other
lower level tiers) and separate locks for your UI tier. In particular, if
the UI thread is blocked on a Monitor.Enter, your design should cater for
that to be available very fast; for example, a queue of objects gets
updated/added by a worker thread while the UI thread accesses/reads the
queue to obtain the object. The UI thread will only block for as long as an
Enqueue operation takes place (typically not long).

If you can post a small sample demonstrating the issue/need then we can
discuss it over something concrete.

Cheers
Daniel
 
D

Daniel Moth

Can you not use a thread to open the connection from the GUI (rather than
the GUI doing it directly)? This would eliminate the need to lock from the
UI thread which is the fundamental cause of your problems...

Cheers
Daniel
 
J

Jon Brunson

Daniel said:
What you could use is Monitor.TryEnter which is supported in CF v2.0 (are
you sure you cannot move to that?)

I'd love to move to CF2, but until there's support for CE 4.2 devices,
we can't, as most of our devices are CE 4.2. (Roll-on SP1)
Regardless, you should have different locks for your database (and other
lower level tiers) and separate locks for your UI tier.

Are you saying here that the UI tier should have it's own lock on the
SqlCe connection object?
In particular, if
the UI thread is blocked on a Monitor.Enter, your design should cater for
that to be available very fast;

So, any lock on any object should only really lock for a few seconds at
most?
for example, a queue of objects gets
updated/added by a worker thread while the UI thread accesses/reads the
queue to obtain the object. The UI thread will only block for as long as an
Enqueue operation takes place (typically not long).

That's not the kind of thing I'm using the lock for. The locking was put
in to fix another bug, where by the Sync thread would be compacting the
database (Data.sdf -> Compacted.sdf), and thus have closed the db
connection (to Data.sdf). If another thread came along and tried to use
the connection (to Data.sdf), it would find it was closed, open it, and
when the sync thread had finished compacting the db (Compacted.sdf), it
would attempt to delete Data.sdf and rename Compacted.sdf to Data.sdf,
obviously getting an IOException as Data.sdf was in use.
If the connection to Data.sdf was locked every time it was used, it
would mean that while compacting, the sync thread would have exclusive
access to it, and other threads would have to wait until the compact was
compete, and the compacted db was put in to place.
This is the only point where we need exclusive access to the db, if
there's a better way to do this, then please, let me know!
If you can post a small sample demonstrating the issue/need then we can
discuss it over something concrete.

I'll have a look see if I can simplify thing enough to post some code
here, but I wanted to respond to your notes as soon as
 
J

Jon Brunson

It's not so much the connection that's the problem, I'm also locking the
db connection when I read from it, and to move all of that code into
another thread and invoke them back, that would get rather messy.

One interesting thing you've just made me think of though is that when
I'm using the connection, I don't really have to lock it, I only need to
lock it when I open it. Is that right? Or am I missing a beat?
 
J

Jon Brunson

Daniel said:
If you can post a small sample demonstrating the issue/need then we can
discuss it over something concrete.

' Assume MyData.sdf exists, and contains a table called "SomeTable",
' which has two columns: ID int AUTO_INCREMENT,
' and Description nvarchar(255)

Private Shared __Connection As SqlCeConnection
Friend Shared Property _Connection() As SqlCeConnection
Get
If __Connection Is Nothing Then _
__Connection = New SqlCeConnection
Return __Connection
End Get
Set(ByVal Value As SqlCeConnection)
If Value Is Nothing Then _
Value = New SqlCeConnection
__Connection = Value
End Set
End Property
Public ReadOnly Property Connection() As SqlCeConnection
Get
Try
Threading.Monitor.Enter(_Connection)

If _Connection.ConnectionString Is Nothing OrElse
_Connection.ConnectionString.Trim().Length = 0 Then
_Connection.ConnectionString = "Data Source = 'MyData.sdf'"
End If

If _Connection.State <> ConnectionState.Open Then
_Connection.Open()
End If

Return _Connection

Catch
Throw
Finally
Threading.Monitor.Exit(_Connection)
End Try
End Get
End Property

Private Sub Form1_Activated(ByVal sender As Object, ByVal e As
System.EventArgs) Handles MyBase.Activated
Try
Me.ListBox1.DataSource = GetSomeData()
Me.ListBox1.ValueMember = "ID"
Me.ListBox1.DisplayMember = "Description"
Catch ex As Exception
MessageBox.Show(ex.Message)
End Try
End Sub

Public Function GetSomeData() As DataTable
Dim da As SqlCeDataAdapter
Try
Threading.Monitor.Enter(_Connection)

da = New SqlCeDataAdapter("SELECT ID, Description FROM SomeTable",
Connection)
Dim rtn As New DataTable
da.Fill(rtn)

Return rtn

Catch
Throw
Finally
If Not (da Is Nothing) Then da.Dispose()
Threading.Monitor.Exit(_Connection)
End Try
End Function

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles Button1.Click
Dim t As New Threading.Thread(AddressOf SyncNow)
t.Start()
End Sub

Private Const MaxValue As Integer = 100000
Private CurrentValue As Integer

Public Sub SyncNow()
Dim cmd As SqlCeCommand
Try
Threading.Monitor.Enter(_Connection)

cmd = New SqlCeCommand("INSERT INTO SomeTable (Description)
VALUES(?)", Connection)
cmd.Parameters.Add("@Description", SqlDbType.NVarChar)
For i As Integer = 1 To MaxValue
cmd.Parameters(0).Value = i
cmd.ExecuteNonQuery()
CurrentValue = i
Me.Invoke(New EventHandler(AddressOf UpdateProgressBar))
Next

Catch ex As Exception
MessageBox.Show(ex.Message)
Finally
Threading.Monitor.Exit(_Connection)
If Not (cmd Is Nothing) Then cmd.Dispose()
End Try
End Sub

Public Sub UpdateProgressBar(ByVal sender As Object, ByVal e As EventArgs)
Me.barTotal.Value = CInt((CurrentValue / MaxValue) * 100)
End Sub


' If you activated the form, while the "sync" was running, you get a
dead lock.
 
J

Jon Brunson

Jon said:
' Assume MyData.sdf exists, and contains a table called "SomeTable",
' which has two columns: ID int AUTO_INCREMENT,
' and Description nvarchar(255)

Private Shared __Connection As SqlCeConnection
Friend Shared Property _Connection() As SqlCeConnection
Get
If __Connection Is Nothing Then _
__Connection = New SqlCeConnection
Return __Connection
End Get
Set(ByVal Value As SqlCeConnection)
If Value Is Nothing Then _
Value = New SqlCeConnection
__Connection = Value
End Set
End Property
Public ReadOnly Property Connection() As SqlCeConnection
Get
Try
Threading.Monitor.Enter(_Connection)

If _Connection.ConnectionString Is Nothing OrElse
_Connection.ConnectionString.Trim().Length = 0 Then
_Connection.ConnectionString = "Data Source = 'MyData.sdf'"
End If

If _Connection.State <> ConnectionState.Open Then
_Connection.Open()
End If

Return _Connection

Catch
Throw
Finally
Threading.Monitor.Exit(_Connection)
End Try
End Get
End Property

Private Sub Form1_Activated(ByVal sender As Object, ByVal e As
System.EventArgs) Handles MyBase.Activated
Try
Me.ListBox1.DataSource = GetSomeData()
Me.ListBox1.ValueMember = "ID"
Me.ListBox1.DisplayMember = "Description"
Catch ex As Exception
MessageBox.Show(ex.Message)
End Try
End Sub

Public Function GetSomeData() As DataTable
Dim da As SqlCeDataAdapter
Try
Threading.Monitor.Enter(_Connection)

da = New SqlCeDataAdapter("SELECT ID, Description FROM SomeTable",
Connection)
Dim rtn As New DataTable
da.Fill(rtn)

Return rtn

Catch
Throw
Finally
If Not (da Is Nothing) Then da.Dispose()
Threading.Monitor.Exit(_Connection)
End Try
End Function

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles Button1.Click
Dim t As New Threading.Thread(AddressOf SyncNow)
t.Start()
End Sub

Private Const MaxValue As Integer = 100000
Private CurrentValue As Integer

Public Sub SyncNow()
Dim cmd As SqlCeCommand
Try
Threading.Monitor.Enter(_Connection)

cmd = New SqlCeCommand("INSERT INTO SomeTable (Description)
VALUES(?)", Connection)
cmd.Parameters.Add("@Description", SqlDbType.NVarChar)
For i As Integer = 1 To MaxValue
cmd.Parameters(0).Value = i
cmd.ExecuteNonQuery()
CurrentValue = i
Me.Invoke(New EventHandler(AddressOf UpdateProgressBar))
Next

Catch ex As Exception
MessageBox.Show(ex.Message)
Finally
Threading.Monitor.Exit(_Connection)
If Not (cmd Is Nothing) Then cmd.Dispose()
End Try
End Sub

Public Sub UpdateProgressBar(ByVal sender As Object, ByVal e As EventArgs)
Me.barTotal.Value = CInt((CurrentValue / MaxValue) * 100)
End Sub

Full code available at: http://www.tfxsoft.com/vitani/Form1.vb
 
D

Daniel Moth

So, any lock on any object should only really lock for a few seconds at
When it is the UI thread absolutely. In fact, I'd say anything more than a
second and you've lost your user (but everyone has their own standards).

Cheers
Daniel
 
D

Daniel Moth

another thread and invoke them back, that would get rather messy.
Messy or not, I don't see how else in your current approach you can
guarantee that the UI thread will not block for more than a second? You
simply shouldn't use Monitor.Enter from the UI thread if you think that
would take more than a very short period of time.
it when I open it. Is that right? Or am I missing a beat?
No idea. I don't work with SQL CE. Again, if you can move to CF v2.0 (SP1
shouldn't be that far away), then you can move to SQL CE 3.0 and have
multiple connections...

Cheers
Daniel
 
D

Daniel Moth

This is the only point where we need exclusive access to the db, if
there's a better way to do this, then please, let me know!
So the only reason you are locking on the object is becaue you are using the
lock to make the decision of whether you are doing a compaction or not? Why
not use an EventWaitHandle for that? Think of it as a global thread safe
flag. When you start compaction, set the flag so any other threads trying to
access your db first check it and then decide if they want to go ahead with
accessing the db themselves _or_ if they have to back off and check later.
The key here is that the check can return immediatelly.

EventWaitHandle is best available from SDF from OpenNETCF
http://www.danielmoth.com/Blog/2005/01/eventwaithandle.html

Cheers
Daniel
 
D

Daniel Moth

I hope my replies to your other posts (in this and other threads) give you
something to work at.

Just some quick comments on the code you posted:
1. The area with button1_click, syncnow, updateprogressbar is a classic fit
for using the BackgroundWorker instead. Do check that out.
2. If all you are doing in a Catch is Throw, then you might as well omit the
catch block.
3. Rather than explicit use of Monitor.Enter/Exit, do use SyncLock as it
will make your code clearer to read
4. If your UI needs to fetch some data (e.g. your activated method) and the
data cannot be read right now (e.g. your db is busy being used by something
else) do not block the UI. E.g. put a friendly message to the user that
allows them to cancel or retry (if you don't want to do that automatically).
At the moment you are kind of assuming that the data will be returned no
matter what.
5. Generally don't lock on properties and specifically don't lock on the
connection object. Use a dedicated object for this purpose (object obj = new
object())

Other than that, using the EventWaitHandle (WaitOne with timeout) as I
suggested is your best bet here.

Cheers
Daniel
 
J

Jon Brunson

Having taken your advice on-board (thank-you so VERY much), now seem to
have a working program, with no dead-locks!

How does this look?

[VB.NET]
Imports System.Data.SqlServerCe

Public Class Form1
Inherits System.Windows.Forms.Form

#Region " Windows Form Designer generated code "

'Form overrides dispose to clean up the component list.
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
MyBase.Dispose(disposing)
End Sub

'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
Friend WithEvents ListBox1 As System.Windows.Forms.ListBox
Friend WithEvents Button1 As System.Windows.Forms.Button
Friend WithEvents ProgressBar1 As System.Windows.Forms.ProgressBar
Friend WithEvents Label1 As System.Windows.Forms.Label
Private Sub InitializeComponent()
Me.ListBox1 = New System.Windows.Forms.ListBox
Me.Button1 = New System.Windows.Forms.Button
Me.ProgressBar1 = New System.Windows.Forms.ProgressBar
Me.Label1 = New System.Windows.Forms.Label
'
'ListBox1
'
Me.ListBox1.Location = New System.Drawing.Point(8, 8)
Me.ListBox1.Size = New System.Drawing.Size(168, 93)
'
'Button1
'
Me.Button1.Location = New System.Drawing.Point(104, 128)
Me.Button1.Text = "Sync Now"
'
'ProgressBar1
'
Me.ProgressBar1.Location = New System.Drawing.Point(8, 108)
Me.ProgressBar1.Size = New System.Drawing.Size(168, 12)
'
'Label1
'
Me.Label1.Font = New System.Drawing.Font("Tahoma", 9.0!,
System.Drawing.FontStyle.Bold)
Me.Label1.Location = New System.Drawing.Point(40, 32)
Me.Label1.Size = New System.Drawing.Size(104, 32)
Me.Label1.Text = "Populating, Please Wait"
Me.Label1.TextAlign = System.Drawing.ContentAlignment.TopCenter
Me.Label1.Visible = False
'
'Form1
'
Me.ClientSize = New System.Drawing.Size(186, 159)
Me.Controls.Add(Me.Label1)
Me.Controls.Add(Me.ProgressBar1)
Me.Controls.Add(Me.Button1)
Me.Controls.Add(Me.ListBox1)
Me.Font = New System.Drawing.Font("Tahoma", 8.25!,
System.Drawing.FontStyle.Regular)
Me.Text = "Form1"

End Sub

Public Shared Sub Main()
Application.Run(New Form1)
End Sub

#End Region

Public Sub New()
MyBase.New()
InitializeComponent()
bgWorker.WorkerReportsProgress = True
' I have no idea what AliceBlue looks like, I only have a
monochrome device to hand!
Label1.BackColor = Color.AliceBlue
End Sub

Private Const FILENAME As String = "\Program Files\MyApp\MyData.sdf"
Private Const MAXVALUE As Integer = 1000
Private WithEvents bgWorker As New
OpenNETCF.ComponentModel.BackgroundWorker
Private ConnectionLocker As New Object
Private bReporting As Boolean

Public ReadOnly Property Connection() As SqlCeConnection
Get
SyncLock ConnectionLocker

Static cnn As SqlCeConnection

If cnn Is Nothing Then _
cnn = New SqlCeConnection

If cnn.ConnectionString Is Nothing OrElse
cnn.ConnectionString.Trim().Length = 0 Then _
cnn.ConnectionString = "Data Source = '" + FILENAME + "'"

If Not IO.File.Exists(FILENAME) Then
Dim e As SqlCeEngine
Dim c As SqlCeCommand
Try
e = New SqlCeEngine(cnn.ConnectionString)
e.CreateDatabase()

cnn.Open()

c = New SqlCeCommand("CREATE TABLE SomeTable (ID int
IDENTITY, Description nvarchar(255))", cnn)
c.ExecuteNonQuery()

Finally
If Not (c Is Nothing) Then c.Dispose()
If Not (e Is Nothing) Then e.Dispose()
End Try
End If

If cnn.State <> ConnectionState.Open Then _
cnn.Open()

Return cnn

End SyncLock
End Get
End Property

Private Sub Form1_Activated(ByVal sender As Object, ByVal e As
System.EventArgs) Handles MyBase.Activated
Try
Me.Label1.Visible = True
Application.DoEvents()
Me.ListBox1.DataSource = Nothing
Me.ListBox1.ValueMember = "ID"
Me.ListBox1.DisplayMember = "Description"
Me.ListBox1.DataSource = GetSomeData()
Catch ex As Exception
MessageBox.Show(ex.Message)
Finally
Me.Label1.Visible = False
End Try
End Sub

Public Function GetSomeData() As DataTable
Dim da As SqlCeDataAdapter
Try
SyncLock ConnectionLocker

da = New SqlCeDataAdapter("SELECT ID, Description FROM
SomeTable", Connection)
Dim rtn As New DataTable
da.Fill(rtn)

Return rtn

End SyncLock
Finally
If Not (da Is Nothing) Then da.Dispose()
End Try
End Function

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles Button1.Click
bgWorker.RunWorkerAsync()
End Sub

Private Sub bgWorker_DoWork(ByVal sender As Object, ByVal e As
OpenNETCF.ComponentModel.DoWorkEventArgs) Handles bgWorker.DoWork
Dim cmd As SqlCeCommand
bReporting = False
Try
SyncLock ConnectionLocker

cmd = New SqlCeCommand("INSERT INTO SomeTable (Description)
VALUES(?)", Connection)
cmd.Parameters.Add("@Description", SqlDbType.NVarChar)
For i As Integer = 1 To MAXVALUE
cmd.Parameters(0).Value = i
cmd.ExecuteNonQuery()
If Not bReporting Then
bReporting = True
bgWorker.ReportProgress(CInt((i / MAXVALUE) * 100), i)
End If
Next

End SyncLock
Catch ex As Exception
MessageBox.Show(ex.Message)
Finally
If Not (cmd Is Nothing) Then cmd.Dispose()
End Try
End Sub

Private Sub bgWorker_ProgressChanged(ByVal sender As Object, ByVal e
As OpenNETCF.ComponentModel.ProgressChangedEventArgs) Handles
bgWorker.ProgressChanged
Me.ProgressBar1.Value = e.ProgressPercentage
Application.DoEvents()
bReporting = False
End Sub

Private Sub bgWorker_RunWorkerCompleted(ByVal sender As Object, ByVal
e As OpenNETCF.ComponentModel.RunWorkerCompletedEventArgs) Handles
bgWorker.RunWorkerCompleted
Me.ProgressBar1.Value = Me.ProgressBar1.Maximum
Application.DoEvents()
End Sub

End Class
[/VB.NET]
 
J

Jon Brunson

Daniel said:
2. If all you are doing in a Catch is Throw, then you might as well omit the
catch block.

Just a quick follow-up question on this one; If I'm catching a specific
error, but all others just get thrown up the stack, is the same true?
ie. should this:

Try
' some code
Catch ex as SqlException
' do something special
Catch
Throw
End Try

be just this?

Try
' some code
Catch ex as SqlException
' do something special
End Try
 
D

Daniel Moth

How does this look?
Well only you can test it so if it works as you want it to... that's fine!

Cheers
Daniel
--
http://www.danielmoth.com/Blog/

Jon Brunson said:
Having taken your advice on-board (thank-you so VERY much), now seem to
have a working program, with no dead-locks!

How does this look?

[VB.NET]
Imports System.Data.SqlServerCe

Public Class Form1
Inherits System.Windows.Forms.Form

#Region " Windows Form Designer generated code "

'Form overrides dispose to clean up the component list.
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
MyBase.Dispose(disposing)
End Sub

'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
Friend WithEvents ListBox1 As System.Windows.Forms.ListBox
Friend WithEvents Button1 As System.Windows.Forms.Button
Friend WithEvents ProgressBar1 As System.Windows.Forms.ProgressBar
Friend WithEvents Label1 As System.Windows.Forms.Label
Private Sub InitializeComponent()
Me.ListBox1 = New System.Windows.Forms.ListBox
Me.Button1 = New System.Windows.Forms.Button
Me.ProgressBar1 = New System.Windows.Forms.ProgressBar
Me.Label1 = New System.Windows.Forms.Label
'
'ListBox1
'
Me.ListBox1.Location = New System.Drawing.Point(8, 8)
Me.ListBox1.Size = New System.Drawing.Size(168, 93)
'
'Button1
'
Me.Button1.Location = New System.Drawing.Point(104, 128)
Me.Button1.Text = "Sync Now"
'
'ProgressBar1
'
Me.ProgressBar1.Location = New System.Drawing.Point(8, 108)
Me.ProgressBar1.Size = New System.Drawing.Size(168, 12)
'
'Label1
'
Me.Label1.Font = New System.Drawing.Font("Tahoma", 9.0!,
System.Drawing.FontStyle.Bold)
Me.Label1.Location = New System.Drawing.Point(40, 32)
Me.Label1.Size = New System.Drawing.Size(104, 32)
Me.Label1.Text = "Populating, Please Wait"
Me.Label1.TextAlign = System.Drawing.ContentAlignment.TopCenter
Me.Label1.Visible = False
'
'Form1
'
Me.ClientSize = New System.Drawing.Size(186, 159)
Me.Controls.Add(Me.Label1)
Me.Controls.Add(Me.ProgressBar1)
Me.Controls.Add(Me.Button1)
Me.Controls.Add(Me.ListBox1)
Me.Font = New System.Drawing.Font("Tahoma", 8.25!,
System.Drawing.FontStyle.Regular)
Me.Text = "Form1"

End Sub

Public Shared Sub Main()
Application.Run(New Form1)
End Sub

#End Region

Public Sub New()
MyBase.New()
InitializeComponent()
bgWorker.WorkerReportsProgress = True
' I have no idea what AliceBlue looks like, I only have a monochrome
device to hand!
Label1.BackColor = Color.AliceBlue
End Sub

Private Const FILENAME As String = "\Program Files\MyApp\MyData.sdf"
Private Const MAXVALUE As Integer = 1000
Private WithEvents bgWorker As New
OpenNETCF.ComponentModel.BackgroundWorker
Private ConnectionLocker As New Object
Private bReporting As Boolean

Public ReadOnly Property Connection() As SqlCeConnection
Get
SyncLock ConnectionLocker

Static cnn As SqlCeConnection

If cnn Is Nothing Then _
cnn = New SqlCeConnection

If cnn.ConnectionString Is Nothing OrElse
cnn.ConnectionString.Trim().Length = 0 Then _
cnn.ConnectionString = "Data Source = '" + FILENAME + "'"

If Not IO.File.Exists(FILENAME) Then
Dim e As SqlCeEngine
Dim c As SqlCeCommand
Try
e = New SqlCeEngine(cnn.ConnectionString)
e.CreateDatabase()

cnn.Open()

c = New SqlCeCommand("CREATE TABLE SomeTable (ID int IDENTITY,
Description nvarchar(255))", cnn)
c.ExecuteNonQuery()

Finally
If Not (c Is Nothing) Then c.Dispose()
If Not (e Is Nothing) Then e.Dispose()
End Try
End If

If cnn.State <> ConnectionState.Open Then _
cnn.Open()

Return cnn

End SyncLock
End Get
End Property

Private Sub Form1_Activated(ByVal sender As Object, ByVal e As
System.EventArgs) Handles MyBase.Activated
Try
Me.Label1.Visible = True
Application.DoEvents()
Me.ListBox1.DataSource = Nothing
Me.ListBox1.ValueMember = "ID"
Me.ListBox1.DisplayMember = "Description"
Me.ListBox1.DataSource = GetSomeData()
Catch ex As Exception
MessageBox.Show(ex.Message)
Finally
Me.Label1.Visible = False
End Try
End Sub

Public Function GetSomeData() As DataTable
Dim da As SqlCeDataAdapter
Try
SyncLock ConnectionLocker

da = New SqlCeDataAdapter("SELECT ID, Description FROM SomeTable",
Connection)
Dim rtn As New DataTable
da.Fill(rtn)

Return rtn

End SyncLock
Finally
If Not (da Is Nothing) Then da.Dispose()
End Try
End Function

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles Button1.Click
bgWorker.RunWorkerAsync()
End Sub

Private Sub bgWorker_DoWork(ByVal sender As Object, ByVal e As
OpenNETCF.ComponentModel.DoWorkEventArgs) Handles bgWorker.DoWork
Dim cmd As SqlCeCommand
bReporting = False
Try
SyncLock ConnectionLocker

cmd = New SqlCeCommand("INSERT INTO SomeTable (Description)
VALUES(?)", Connection)
cmd.Parameters.Add("@Description", SqlDbType.NVarChar)
For i As Integer = 1 To MAXVALUE
cmd.Parameters(0).Value = i
cmd.ExecuteNonQuery()
If Not bReporting Then
bReporting = True
bgWorker.ReportProgress(CInt((i / MAXVALUE) * 100), i)
End If
Next

End SyncLock
Catch ex As Exception
MessageBox.Show(ex.Message)
Finally
If Not (cmd Is Nothing) Then cmd.Dispose()
End Try
End Sub

Private Sub bgWorker_ProgressChanged(ByVal sender As Object, ByVal e As
OpenNETCF.ComponentModel.ProgressChangedEventArgs) Handles
bgWorker.ProgressChanged
Me.ProgressBar1.Value = e.ProgressPercentage
Application.DoEvents()
bReporting = False
End Sub

Private Sub bgWorker_RunWorkerCompleted(ByVal sender As Object, ByVal e
As OpenNETCF.ComponentModel.RunWorkerCompletedEventArgs) Handles
bgWorker.RunWorkerCompleted
Me.ProgressBar1.Value = Me.ProgressBar1.Maximum
Application.DoEvents()
End Sub

End Class
[/VB.NET]

Daniel said:
I hope my replies to your other posts (in this and other threads) give
you something to work at.

Just some quick comments on the code you posted:
1. The area with button1_click, syncnow, updateprogressbar is a classic
fit for using the BackgroundWorker instead. Do check that out.
2. If all you are doing in a Catch is Throw, then you might as well omit
the catch block.
3. Rather than explicit use of Monitor.Enter/Exit, do use SyncLock as it
will make your code clearer to read
4. If your UI needs to fetch some data (e.g. your activated method) and
the data cannot be read right now (e.g. your db is busy being used by
something else) do not block the UI. E.g. put a friendly message to the
user that allows them to cancel or retry (if you don't want to do that
automatically). At the moment you are kind of assuming that the data will
be returned no matter what.
5. Generally don't lock on properties and specifically don't lock on the
connection object. Use a dedicated object for this purpose (object obj =
new object())

Other than that, using the EventWaitHandle (WaitOne with timeout) as I
suggested is your best bet here.

Cheers
Daniel
 
J

Jon Brunson

Nothing jumps out at you saying "OMG!! WHAT'S THIS MAN DOING??!?!?!
That's TOTALLY the wrong way to do that!!!" then? :blush:)

Daniel said:
How does this look?
Well only you can test it so if it works as you want it to... that's fine!

Cheers
Daniel
--
http://www.danielmoth.com/Blog/

Jon Brunson said:
Having taken your advice on-board (thank-you so VERY much), now seem to
have a working program, with no dead-locks!

How does this look?

[VB.NET]
Imports System.Data.SqlServerCe

Public Class Form1
Inherits System.Windows.Forms.Form

#Region " Windows Form Designer generated code "

'Form overrides dispose to clean up the component list.
Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
MyBase.Dispose(disposing)
End Sub

'NOTE: The following procedure is required by the Windows Form Designer
'It can be modified using the Windows Form Designer.
'Do not modify it using the code editor.
Friend WithEvents ListBox1 As System.Windows.Forms.ListBox
Friend WithEvents Button1 As System.Windows.Forms.Button
Friend WithEvents ProgressBar1 As System.Windows.Forms.ProgressBar
Friend WithEvents Label1 As System.Windows.Forms.Label
Private Sub InitializeComponent()
Me.ListBox1 = New System.Windows.Forms.ListBox
Me.Button1 = New System.Windows.Forms.Button
Me.ProgressBar1 = New System.Windows.Forms.ProgressBar
Me.Label1 = New System.Windows.Forms.Label
'
'ListBox1
'
Me.ListBox1.Location = New System.Drawing.Point(8, 8)
Me.ListBox1.Size = New System.Drawing.Size(168, 93)
'
'Button1
'
Me.Button1.Location = New System.Drawing.Point(104, 128)
Me.Button1.Text = "Sync Now"
'
'ProgressBar1
'
Me.ProgressBar1.Location = New System.Drawing.Point(8, 108)
Me.ProgressBar1.Size = New System.Drawing.Size(168, 12)
'
'Label1
'
Me.Label1.Font = New System.Drawing.Font("Tahoma", 9.0!,
System.Drawing.FontStyle.Bold)
Me.Label1.Location = New System.Drawing.Point(40, 32)
Me.Label1.Size = New System.Drawing.Size(104, 32)
Me.Label1.Text = "Populating, Please Wait"
Me.Label1.TextAlign = System.Drawing.ContentAlignment.TopCenter
Me.Label1.Visible = False
'
'Form1
'
Me.ClientSize = New System.Drawing.Size(186, 159)
Me.Controls.Add(Me.Label1)
Me.Controls.Add(Me.ProgressBar1)
Me.Controls.Add(Me.Button1)
Me.Controls.Add(Me.ListBox1)
Me.Font = New System.Drawing.Font("Tahoma", 8.25!,
System.Drawing.FontStyle.Regular)
Me.Text = "Form1"

End Sub

Public Shared Sub Main()
Application.Run(New Form1)
End Sub

#End Region

Public Sub New()
MyBase.New()
InitializeComponent()
bgWorker.WorkerReportsProgress = True
' I have no idea what AliceBlue looks like, I only have a monochrome
device to hand!
Label1.BackColor = Color.AliceBlue
End Sub

Private Const FILENAME As String = "\Program Files\MyApp\MyData.sdf"
Private Const MAXVALUE As Integer = 1000
Private WithEvents bgWorker As New
OpenNETCF.ComponentModel.BackgroundWorker
Private ConnectionLocker As New Object
Private bReporting As Boolean

Public ReadOnly Property Connection() As SqlCeConnection
Get
SyncLock ConnectionLocker

Static cnn As SqlCeConnection

If cnn Is Nothing Then _
cnn = New SqlCeConnection

If cnn.ConnectionString Is Nothing OrElse
cnn.ConnectionString.Trim().Length = 0 Then _
cnn.ConnectionString = "Data Source = '" + FILENAME + "'"

If Not IO.File.Exists(FILENAME) Then
Dim e As SqlCeEngine
Dim c As SqlCeCommand
Try
e = New SqlCeEngine(cnn.ConnectionString)
e.CreateDatabase()

cnn.Open()

c = New SqlCeCommand("CREATE TABLE SomeTable (ID int IDENTITY,
Description nvarchar(255))", cnn)
c.ExecuteNonQuery()

Finally
If Not (c Is Nothing) Then c.Dispose()
If Not (e Is Nothing) Then e.Dispose()
End Try
End If

If cnn.State <> ConnectionState.Open Then _
cnn.Open()

Return cnn

End SyncLock
End Get
End Property

Private Sub Form1_Activated(ByVal sender As Object, ByVal e As
System.EventArgs) Handles MyBase.Activated
Try
Me.Label1.Visible = True
Application.DoEvents()
Me.ListBox1.DataSource = Nothing
Me.ListBox1.ValueMember = "ID"
Me.ListBox1.DisplayMember = "Description"
Me.ListBox1.DataSource = GetSomeData()
Catch ex As Exception
MessageBox.Show(ex.Message)
Finally
Me.Label1.Visible = False
End Try
End Sub

Public Function GetSomeData() As DataTable
Dim da As SqlCeDataAdapter
Try
SyncLock ConnectionLocker

da = New SqlCeDataAdapter("SELECT ID, Description FROM SomeTable",
Connection)
Dim rtn As New DataTable
da.Fill(rtn)

Return rtn

End SyncLock
Finally
If Not (da Is Nothing) Then da.Dispose()
End Try
End Function

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles Button1.Click
bgWorker.RunWorkerAsync()
End Sub

Private Sub bgWorker_DoWork(ByVal sender As Object, ByVal e As
OpenNETCF.ComponentModel.DoWorkEventArgs) Handles bgWorker.DoWork
Dim cmd As SqlCeCommand
bReporting = False
Try
SyncLock ConnectionLocker

cmd = New SqlCeCommand("INSERT INTO SomeTable (Description)
VALUES(?)", Connection)
cmd.Parameters.Add("@Description", SqlDbType.NVarChar)
For i As Integer = 1 To MAXVALUE
cmd.Parameters(0).Value = i
cmd.ExecuteNonQuery()
If Not bReporting Then
bReporting = True
bgWorker.ReportProgress(CInt((i / MAXVALUE) * 100), i)
End If
Next

End SyncLock
Catch ex As Exception
MessageBox.Show(ex.Message)
Finally
If Not (cmd Is Nothing) Then cmd.Dispose()
End Try
End Sub

Private Sub bgWorker_ProgressChanged(ByVal sender As Object, ByVal e As
OpenNETCF.ComponentModel.ProgressChangedEventArgs) Handles
bgWorker.ProgressChanged
Me.ProgressBar1.Value = e.ProgressPercentage
Application.DoEvents()
bReporting = False
End Sub

Private Sub bgWorker_RunWorkerCompleted(ByVal sender As Object, ByVal e
As OpenNETCF.ComponentModel.RunWorkerCompletedEventArgs) Handles
bgWorker.RunWorkerCompleted
Me.ProgressBar1.Value = Me.ProgressBar1.Maximum
Application.DoEvents()
End Sub

End Class
[/VB.NET]

Daniel said:
I hope my replies to your other posts (in this and other threads) give
you something to work at.

Just some quick comments on the code you posted:
1. The area with button1_click, syncnow, updateprogressbar is a classic
fit for using the BackgroundWorker instead. Do check that out.
2. If all you are doing in a Catch is Throw, then you might as well omit
the catch block.
3. Rather than explicit use of Monitor.Enter/Exit, do use SyncLock as it
will make your code clearer to read
4. If your UI needs to fetch some data (e.g. your activated method) and
the data cannot be read right now (e.g. your db is busy being used by
something else) do not block the UI. E.g. put a friendly message to the
user that allows them to cancel or retry (if you don't want to do that
automatically). At the moment you are kind of assuming that the data will
be returned no matter what.
5. Generally don't lock on properties and specifically don't lock on the
connection object. Use a dedicated object for this purpose (object obj =
new object())

Other than that, using the EventWaitHandle (WaitOne with timeout) as I
suggested is your best bet here.

Cheers
Daniel
--
http://www.danielmoth.com/Blog/

"Jon Brunson" <jon.brunson@innovation-NOSPAM-software-DOT-co-DOT-uk>
wrote in message Jon Brunson wrote:
Daniel Moth wrote:
If you can post a small sample demonstrating the issue/need then we
can discuss it over something concrete.

' Assume MyData.sdf exists, and contains a table called "SomeTable",
' which has two columns: ID int AUTO_INCREMENT,
' and Description nvarchar(255)

Private Shared __Connection As SqlCeConnection
Friend Shared Property _Connection() As SqlCeConnection
Get
If __Connection Is Nothing Then _
__Connection = New SqlCeConnection
Return __Connection
End Get
Set(ByVal Value As SqlCeConnection)
If Value Is Nothing Then _
Value = New SqlCeConnection
__Connection = Value
End Set
End Property
Public ReadOnly Property Connection() As SqlCeConnection
Get
Try
Threading.Monitor.Enter(_Connection)

If _Connection.ConnectionString Is Nothing OrElse
_Connection.ConnectionString.Trim().Length = 0 Then
_Connection.ConnectionString = "Data Source = 'MyData.sdf'"
End If

If _Connection.State <> ConnectionState.Open Then
_Connection.Open()
End If

Return _Connection

Catch
Throw
Finally
Threading.Monitor.Exit(_Connection)
End Try
End Get
End Property

Private Sub Form1_Activated(ByVal sender As Object, ByVal e As
System.EventArgs) Handles MyBase.Activated
Try
Me.ListBox1.DataSource = GetSomeData()
Me.ListBox1.ValueMember = "ID"
Me.ListBox1.DisplayMember = "Description"
Catch ex As Exception
MessageBox.Show(ex.Message)
End Try
End Sub

Public Function GetSomeData() As DataTable
Dim da As SqlCeDataAdapter
Try
Threading.Monitor.Enter(_Connection)

da = New SqlCeDataAdapter("SELECT ID, Description FROM SomeTable",
Connection)
Dim rtn As New DataTable
da.Fill(rtn)

Return rtn

Catch
Throw
Finally
If Not (da Is Nothing) Then da.Dispose()
Threading.Monitor.Exit(_Connection)
End Try
End Function

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As
System.EventArgs) Handles Button1.Click
Dim t As New Threading.Thread(AddressOf SyncNow)
t.Start()
End Sub

Private Const MaxValue As Integer = 100000
Private CurrentValue As Integer

Public Sub SyncNow()
Dim cmd As SqlCeCommand
Try
Threading.Monitor.Enter(_Connection)

cmd = New SqlCeCommand("INSERT INTO SomeTable (Description)
VALUES(?)", Connection)
cmd.Parameters.Add("@Description", SqlDbType.NVarChar)
For i As Integer = 1 To MaxValue
cmd.Parameters(0).Value = i
cmd.ExecuteNonQuery()
CurrentValue = i
Me.Invoke(New EventHandler(AddressOf UpdateProgressBar))
Next

Catch ex As Exception
MessageBox.Show(ex.Message)
Finally
Threading.Monitor.Exit(_Connection)
If Not (cmd Is Nothing) Then cmd.Dispose()
End Try
End Sub

Public Sub UpdateProgressBar(ByVal sender As Object, ByVal e As
EventArgs)
Me.barTotal.Value = CInt((CurrentValue / MaxValue) * 100)
End Sub


Full code available at: http://www.tfxsoft.com/vitani/Form1.vb
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top