0

I have a table in a SQL Server database that represents a log file of some actions inserted from a running windows service. Everything is working well.

But, I have a Windows application that gets the latest rows that have been inserted in the log table and views it in a DataGridView. While developing this application I depended on Using SqlDependency in a Windows Application from MSDN. It is working well, but when the log table receives a large number of log details, the Windows app hangs up and the main thread pool becomes too busy.

I want to run the same code referenced in the previous link in a separated thread pool by using Thread class or BackgroundWorker control. This means a thread for using UI controls and another one for listening to the database changes and get it into the DataGridView.

You can see the UI screenshot from this link "UI"

No. (1): This GroupBox represents the UI tools which users can use it while monitoring.

No. (2): The Start button is responsible for beginning to listen and receive updates from the database and refill the DataGridView.

No. (3): This grid represents the new logs that have been inserted in the database.

No. (4): This number (38 changes) represents the count of listening of sql dependency to the database changes.

My code: public partial class frmMain : Form { SqlConnection conn;

    const string tableName = "OutgoingLog";
    const string statusMessage = "{0} changes have occurred.";
    int changeCount = 0;

    private static DataSet dataToWatch = null;
    private static SqlConnection connection = null;
    private static SqlCommand command = null;

    public frmMain()
    {
        InitializeComponent();
    }

    private bool CanRequestNotifications()
    {
        // In order to use the callback feature of the
        // SqlDependency, the application must have
        // the SqlClientPermission permission.
        try
        {
            SqlClientPermission perm = new SqlClientPermission(PermissionState.Unrestricted);

            perm.Demand();

            return true;
        }
        catch
        {
            return false;
        }
    }

    private void dependency_OnChange(object sender, SqlNotificationEventArgs e)
    {
        // This event will occur on a thread pool thread.
        // Updating the UI from a worker thread is not permitted.
        // The following code checks to see if it is safe to
        // update the UI.
        ISynchronizeInvoke i = (ISynchronizeInvoke)this;

        // If InvokeRequired returns True, the code
        // is executing on a worker thread.
        if (i.InvokeRequired)
        {
            // Create a delegate to perform the thread switch.
            OnChangeEventHandler tempDelegate = new OnChangeEventHandler(dependency_OnChange);

            object[] args = { sender, e };

            // Marshal the data from the worker thread
            // to the UI thread.
            i.BeginInvoke(tempDelegate, args);

            return;
        }

        // Remove the handler, since it is only good
        // for a single notification.
        SqlDependency dependency = (SqlDependency)sender;

        dependency.OnChange -= dependency_OnChange;

        // At this point, the code is executing on the
        // UI thread, so it is safe to update the UI.
        ++changeCount;
        lblChanges.Text = String.Format(statusMessage, changeCount);
        this.Refresh();

        // Reload the dataset that is bound to the grid.
        GetData();
    }

    private void GetData()
    {
        // Empty the dataset so that there is only
        // one batch of data displayed.
        dataToWatch.Clear();

        // Make sure the command object does not already have
        // a notification object associated with it.
        command.Notification = null;

        // Create and bind the SqlDependency object
        // to the command object.
        SqlDependency dependency = new SqlDependency(command);

        dependency.OnChange += new OnChangeEventHandler(dependency_OnChange);

        using (SqlDataAdapter adapter = new SqlDataAdapter(command))
        {
            adapter.Fill(dataToWatch, tableName);

            dgv.DataSource = dataToWatch;
            dgv.DataMember = tableName;
            dgv.FirstDisplayedScrollingRowIndex = dgv.Rows.Count - 1;
        }
    }

    private void btnStart_Click(object sender, EventArgs e)
    {
        changeCount = 0;
        lblChanges.Text = String.Format(statusMessage, changeCount);

        // Remove any existing dependency connection, then create a new one.
        SqlDependency.Stop("<my connection string>");
        SqlDependency.Start("<my connection string>");

        if (connection == null)
        {
            connection = new SqlConnection("<my connection string>");
        }

        if (command == null)
        {
            command = new SqlCommand("select * from OutgoingLog", connection);
        }

        if (dataToWatch == null)
        {
            dataToWatch = new DataSet();
        }

        GetData();
    }

    private void frmMain_Load(object sender, EventArgs e)
    {
        btnStart.Enabled = CanRequestNotifications();
    }

    private void frmMain_FormClosing(object sender, FormClosingEventArgs e)
    {
        SqlDependency.Stop("<my connection string>");
    }
}

What I want exactly: when user click the Start button, the application run the code in a separated thread pool.

Ahmed Negm
  • 865
  • 1
  • 11
  • 30
  • Mixing the dependency with eg a Backgroundworker is going to complicate things. Maybe consider using just one. – H H Jun 10 '13 at 22:30
  • The interesting exercise for this application would be an application listener subscribing to the win service and the service sending signal that log tables updated. At that time, win app would go and acquire new data. – T.S. Jun 11 '13 at 03:00
  • Thanks for your suppor, but I can't divide the application into two sections as you said. I have to develop it depending on the previous explanation which in post. I just need to run the same code into a separated **thread pool** or run it using the **BackgroundWorker**. – Ahmed Negm Jun 11 '13 at 07:39

1 Answers1

1

First if I understand well the change notification is already executed on another thread so using one more threading layer should be useless.

Indeed what causes the application to hang is the update of the UI, on the UI thread.

Could you show the code responsible for this update please?

If there is a lot of notifications visual update will be longer and you can't do much:

  • update the grid by chunks to smooth the update: instead of inserting 1000 new records you run 10 updates of 100 records, but you take the risk of being overwhelmed by data if you don't process them fast enough

  • using a collection that handles notification natively like a BindingList could help

Moreover what you can do to enhance the user experience, avoiding the unpleasant "it hangs" effect, is displaying a progress bar or a simple spinner.

UPDATE:

So if the first part of the GetData function is the bottleneck then indeed you could use another thread, e.g. from the thread-pool:

private void GetData()
{
    // Start the retrieval of data on another thread to let the UI thread free
    ThreadPool.QueueUserWorkItem(o =>
    {
        // Empty the dataset so that there is only
        // one batch of data displayed.
        dataToWatch.Clear();

        // Make sure the command object does not already have
        // a notification object associated with it.
        command.Notification = null;

        // Create and bind the SqlDependency object
        // to the command object.
        SqlDependency dependency = new SqlDependency(command);

        dependency.OnChange += new OnChangeEventHandler(dependency_OnChange);

        using (SqlDataAdapter adapter = new SqlDataAdapter(command))
        {
            adapter.Fill(dataToWatch, tableName);

            // Update the UI
            dgv.Invoke(() =>
                {
                    dgv.DataSource = dataToWatch;
                    dgv.DataMember = tableName;
                    dgv.FirstDisplayedScrollingRowIndex = dgv.Rows.Count - 1;
                });
        }
    });
}

So the only part that will run on the UI thread is the update of the datagrid.

Not tested but hope this helps...

LAST? UPDATE:

With some synchronization to avoid concurrent execution:

AutoResetEvent running = new AutoResetEvent(true);

private void GetData()
{
    // Start the retrieval of data on another thread to let the UI thread free
    ThreadPool.QueueUserWorkItem(o =>
    {
        running.WaitOne();

        // Empty the dataset so that there is only
        // one batch of data displayed.
        dataToWatch.Clear();

        // Make sure the command object does not already have
        // a notification object associated with it.
        command.Notification = null;

        // Create and bind the SqlDependency object
        // to the command object.
        SqlDependency dependency = new SqlDependency(command);

        dependency.OnChange += new OnChangeEventHandler(dependency_OnChange);

        using (SqlDataAdapter adapter = new SqlDataAdapter(command))
        {
            adapter.Fill(dataToWatch, tableName);

            running.Set();

            // Update the UI
            dgv.Invoke(() =>
                {
                dgv.DataSource = dataToWatch;
                dgv.DataMember = tableName;
                dgv.FirstDisplayedScrollingRowIndex = dgv.Rows.Count - 1;
                });
        }
    });
}
Pragmateek
  • 13,174
  • 9
  • 74
  • 108
  • Thanks a lot for your support sir. Let me explain the idea in details. Forget that there is windows service do something, but remember the following: I have a datagrid, and the sql dependency always gets the rows from the database depending on sql query. So, I can not use the UI controls while filling datagrid, this looks like using an infinity loop in your application. So, I want to run the code the link "http://msdn.microsoft.com/en-us/library/a52dhwx7%28v=vs.80%29.aspx" in a separated thread pool or a BackgroundWorker to allow users to use the UI with ease. You can go to link to see code. – Ahmed Negm Jun 11 '13 at 09:11
  • I updated my question, you can see the code script and the UI screenshot. Thanks a lot. – Ahmed Negm Jun 11 '13 at 09:40
  • Thanks again for your kindly help :), but when I used your code I got this error **"There is already an open DataReader associated with this Command which must be closed first."**, although I have no data reader and I am using **"MultipleActiveResultSets=True"** in connection string to avoid these errors. – Ahmed Negm Jun 11 '13 at 12:26
  • Hmm, this might be due to 2 GetData running concurrently. I've updated my answer. Hope this time all will be OK... – Pragmateek Jun 11 '13 at 13:22
  • Thanks a lot for your kindly support, this is run away of the main thread pool in a separated thread. But, the problem now is that the Grid not filled with data although the data table has rows. What do you think about it ? – Ahmed Negm Jun 11 '13 at 13:43
  • If you add a breakpoint on "dgv.DataSource = dataToWatch;" could you check it is correctly executed with the right data. The first time the *WaitOne* should not block. Moreover you could secure the "running.Set()" statement using a **try/finally** block to ensure it is always executed. – Pragmateek Jun 11 '13 at 14:07
  • I am sorry to have disturbed you :(. But the situation is different now, I got errors and something else of odd bugs and the data table filled correctly time and time again not. The application with database script uploaded on "http://www.mediafire.com/?kskfm8lo6l6kyli" .. Kindly review it sir and feed me back. Thanks. – Ahmed Negm Jun 11 '13 at 16:07
  • No problem, but I won't be able to test it fully. Could you elaborate more on the errors, possibly on another SO question if they are not related. – Pragmateek Jun 11 '13 at 16:12
  • another link here: http://www.mediafire.com/download/kskfm8lo6l6kyli/OutgoingSMS_Monitor.rar – Ahmed Negm Jun 11 '13 at 16:22
  • No, the error related occurred because of using threading in SqlDependancy. Before using your perfect ideas and suggestions the error did not occur. – Ahmed Negm Jun 11 '13 at 16:28
  • I asked a new question here: http://stackoverflow.com/questions/17051595/unstable-application-uses-sqldependency-several-states-and-errors. – Ahmed Negm Jun 11 '13 at 18:54