SqlDependency с использованием BackgroundWorker

У меня есть таблица в базе данных SQL Server, которая представляет файл журнала некоторых действий, вставленных из работающей службы Windows. Все работает хорошо.

Но у меня есть приложение Windows, которое получает последние строки, вставленные в таблицу журнала, и просматривает их в файле DataGridView. При разработке этого приложения я полагался на Использование SqlDependency в приложении Windows из MSDN. Он работает хорошо, но когда таблица журнала получает большое количество сведений журнала, приложение Windows зависает, а основной пул потоков становится слишком занятым.

Я хочу запустить тот же код, указанный в предыдущей ссылке, в отдельном пуле потоков, используя класс Thread или элемент управления BackgroundWorker. Это означает поток для использования элементов управления пользовательского интерфейса и еще один для прослушивания изменений в базе данных и передачи их в файл DataGridView.

Вы можете увидеть скриншот пользовательского интерфейса по этой ссылке "UI"

№ (1): этот GroupBox представляет инструменты пользовательского интерфейса, которые пользователи могут использовать во время мониторинга.

№ (2): Кнопка Старт отвечает за начало прослушивания и получения обновлений из базы данных, а также за пополнение DataGridView.

№ (3): Эта сетка представляет новые журналы, которые были вставлены в базу данных.

№ (4): это число (38 изменений) представляет количество прослушиваний зависимости sql от изменений в базе данных.

Мой код: общедоступный частичный класс 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>");
    }
}

Что именно я хочу: когда пользователь нажимает кнопку Пуск, приложение запускает код в отдельном пуле потоков.


person Ahmed Negm    schedule 10.06.2013    source источник
comment
Смешивание зависимости, например, с Backgroundworker, усложнит ситуацию. Возможно, рассмотрите возможность использования только одного.   -  person Henk Holterman    schedule 11.06.2013
comment
Интересным упражнением для этого приложения может быть прослушиватель приложения, подписывающийся на службу win, и служба, отправляющая сигнал об обновлении таблиц журнала. В это время приложение win будет собирать новые данные.   -  person T.S.    schedule 11.06.2013
comment
Спасибо за поддержку, но я не могу разделить приложение на две части, как вы сказали. Я должен развить его в зависимости от предыдущего объяснения, которое находится в посте. Мне просто нужно запустить тот же код в отдельный пул потоков или запустить его с помощью BackgroundWorker.   -  person Ahmed Negm    schedule 11.06.2013


Ответы (1)


Во-первых, если я правильно понимаю, уведомление об изменении уже выполняется в другом потоке, поэтому использование еще одного уровня потоков должно быть бесполезным.

Действительно, причиной зависания приложения является обновление пользовательского интерфейса в потоке пользовательского интерфейса.

Не могли бы вы показать код, отвечающий за это обновление, пожалуйста?

Если уведомлений много, визуальное обновление будет длиннее и многого не сделаешь:

  • обновляйте сетку по частям, чтобы сгладить обновление: вместо вставки 1000 новых записей вы выполняете 10 обновлений по 100 записей, но рискуете перегрузиться данными, если не обработаете их достаточно быстро

  • использование коллекции, которая обрабатывает уведомления нативно, как BindingList, может помочь

Кроме того, чтобы улучшить взаимодействие с пользователем, избегая неприятного эффекта «зависания», можно отобразить индикатор выполнения или простой счетчик.

ОБНОВИТЬ:

Итак, если первая часть функции GetData является узким местом, вы действительно можете использовать другой поток, например. из пула потоков:

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;
                });
        }
    });
}

Таким образом, единственная часть, которая будет выполняться в потоке пользовательского интерфейса, — это обновление сетки данных.

Не проверено, но надеюсь, что это поможет...

ПРОШЛОЙ? ОБНОВИТЬ:

С некоторой синхронизацией, чтобы избежать одновременного выполнения:

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;
                });
        }
    });
}
person Pragmateek    schedule 11.06.2013
comment
Большое спасибо за вашу поддержку, сэр. Позвольте мне объяснить идею в деталях. Забудьте, что служба Windows что-то делает, но помните следующее: у меня есть сетка данных, и зависимость sql всегда получает строки из базы данных в зависимости от запроса sql. Итак, я не могу использовать элементы управления пользовательского интерфейса при заполнении сетки данных, это похоже на использование бесконечного цикла в вашем приложении. Итак, я хочу запустить код по ссылке msdn.microsoft.com/en-us/library/a52dhwx7%28v=vs.80%29.aspx в отдельном пуле потоков или BackgroundWorker, чтобы пользователи могли легко использовать пользовательский интерфейс. Вы можете перейти по ссылке, чтобы увидеть код. - person Ahmed Negm; 11.06.2013
comment
Я обновил свой вопрос, вы можете увидеть сценарий кода и скриншот пользовательского интерфейса. Большое спасибо. - person Ahmed Negm; 11.06.2013
comment
Еще раз спасибо за вашу любезную помощь :), но когда я использовал ваш код, я получил эту ошибку Уже есть открытое DataReader, связанное с этой командой, которую нужно сначала закрыть., хотя у меня нет чтения данных Я использую MultipleActiveResultSets=True в строке подключения, чтобы избежать этих ошибок. - person Ahmed Negm; 11.06.2013
comment
Хм, это может быть связано с тем, что 2 GetData работают одновременно. Я обновил свой ответ. Надеюсь, в этот раз все будет хорошо... - person Pragmateek; 11.06.2013
comment
Большое спасибо за вашу любезную поддержку, это убегает из основного пула потоков в отдельный поток. Но теперь проблема в том, что сетка не заполнена данными, хотя в таблице данных есть строки. Что вы думаете об этом ? - person Ahmed Negm; 11.06.2013
comment
Если вы добавите точку останова в dgv.DataSource = dataToWatch; не могли бы вы проверить, правильно ли он выполняется с правильными данными. В первый раз WaitOne не должен блокироваться. Кроме того, вы можете защитить оператор running.Set() с помощью блока try/finally, чтобы он всегда выполнялся. - person Pragmateek; 11.06.2013
comment
Извините, что побеспокоил :(. Но сейчас другая ситуация, у меня ошибки и еще что-то из непонятных багов, а таблица данных заполняется правильно и снова нет. Приложение со скриптом базы данных загружено на mediafire.com/?kskfm8lo6l6kyli .. Пожалуйста, просмотрите его, сэр, и дайте ответ. Спасибо. - person Ahmed Negm; 11.06.2013
comment
Ничего страшного, но полностью протестировать не смогу. Не могли бы вы подробнее рассказать об ошибках, возможно, о другом вопросе SO, если они не связаны. - person Pragmateek; 11.06.2013
comment
еще одна ссылка здесь: mediafire.com/download/kskfm8lo6l6kyli/OutgoingSMS_Monitor.rar - person Ahmed Negm; 11.06.2013
comment
Нет, ошибка возникла из-за использования потоков в SqlDependancy. До использования ваших идеальных идей и предложений ошибка не возникала. - person Ahmed Negm; 11.06.2013
comment
Я задал здесь новый вопрос: stackoverflow.com/questions/17051595/. - person Ahmed Negm; 11.06.2013