Пользовательский обработчик событий вызывается дважды?

#c# #.net #events

#c# #.net #Мероприятия

Вопрос:

Я создал обработчик событий, который просто возвращает список объектов, которые я получаю от веб-службы по завершении вызова.

Теперь я пошел дальше и запустил приложение в режиме отладки и обнаружил, что при первом вызове события оно работает отлично, но сразу после его завершения событие запускается во второй раз. Я проверил и абсолютно уверен, что не вызываю событие более одного раза в классе получателя.

Это мой первый опыт создания пользовательских обработчиков событий внутри моих приложений, поэтому я не совсем уверен, что реализация на 100% точна.

Есть идеи о том, что может быть причиной этого? Является ли способ, которым я создал обработчик событий, точным?

Это класс DataHelper

 public class DataHelper
{
    public delegate void DataCalledEventHandler(object sender, List<DataItem> dateItemList);
    public event DataCalledEventHandler DataCalled;

    public DataHelper()
    {

    }

    public void CallData()
    {
        List<DataItem> dataItems = new List<DataItem>();
        //SOME CODE THAT RETURNS DATA
        DataCalled(this, dataItems);
    }
}
  

Здесь я подписался на свое событие:

 protected override void OnNavigatedTo(System.Windows.Navigation.NavigationEventArgs e)
{
   GetNewDataItems();
}
private void GetNewDataItems()
        {

                try
                {
                    DataHelper dataHelper = new DataHelper();
                    dataHelper.CallData();
                    dataHelper.DataCalled  = new DataHelper.DataCalledEventHandler(dataHelper_DataCalled);

                }
                catch
                {
                   //Handle any errors
                }
            }
    }

    void dataHelper_DataCalled(object sender, List<DataItem> dataItemsList)
    {
        //Do something with results
        //NOTE: THIS IS WHERE THE EXCEPTION OCCURS WHEN EVENT IS FIRED FOR SECOND TIME
    }
  

Комментарии:

1. Не могли бы вы показать код, в котором вы подписываетесь на событие?

2. @WouterdeKort Конечно, дай мне секунду.

3. Почему вы подписываетесь на событие DataCalled после вызова CallData ? Этот способ CallData вызовет исключение, потому что на DataCalled событие нет подписчиков, если вы не подписались в другом месте.

4. Фрагмент кода очень странный, трудно представить, почему вы подписываетесь на событие после вызова метода. Он никогда не должен срабатывать. Публикуйте фактический код, не редактируйте.

Ответ №1:

Вероятно, вы добавили делегата дважды, возможно ли это?

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

Вероятно, вы сделали что-то вроде…

 private Class1 instance1;

void callback(...)
{
}

void myfunction()
{
    this.instance1.DataCalled  = this.callback;
    this.instance1.DataCalled  = this.callback;
}
  

Если нет, попробуйте добавить точку останова, где вы подписываетесь на событие, и посмотрите, вызывается ли оно дважды.

В качестве примечания, вы всегда должны проверять значение null при вызове события, если нет подписчика, вы можете получить исключение NullReferenceException. Я бы также посоветовал вам использовать переменную для хранения делегата события, чтобы избежать риска сбоя многопоточности.

 public void CallData()
{
    List<DataItem> dataItems = new List<DataItem>();
    var handler = this.DataCalled;
    if (handler != null)
        handler(this, dataItems);
}
  

Редактировать: поскольку теперь я вижу код, очевидно, что каждый раз, когда вы вызываете метод GetNewDataItems, вы каждый раз приписываете событие.
Таким образом, вы подписываетесь только один раз, например, в конструкторе, или сохраняете свою переменную где-нибудь, или отменяете регистрацию события по завершении.

Этот код также содержит вероятную утечку памяти: каждый раз, когда вы добавляете делегат, вы поддерживаете как экземпляр, содержащий событие, так и экземпляр, содержащий подписанный метод, по крайней мере, до тех пор, пока на оба не будут даны ссылки.

Вы можете попробовать сделать что-то вроде этого…

 void dataHelper_DataCalled(object sender, List<DataItem> dataItemsList)
{
    // Deregister the event...
    (sender as Class1).DataCalled -= dataHelper_DataCalled; 

    //Do something with results
}
  

Однако таким образом вы должны убедиться, что, если во время регистрации события не возникнет исключения, событие будет запущено или у вас снова возникнут утечки памяти.

Возможно, вместо события вам нужен просто делегат. Конечно, вы должны установить для своего поля делегата значение null, когда хотите освободить делегата.

 // in data helper class

private DataHelper.DataCalledEventHandler myFunctor;

public void CallData(DataHelper.DataCalledEventHandler functor)
{
    this.myFunctor = functor;
    //SOME CODE THAT RETURNS DATA
}

// when the call completes, asynchronously...
private void WhenTheCallCompletes()
{
    var functor = this.myFunctor;
    if (functor != null)
    {
        this.myFunctor = null;
        List<DataItem> dataItems = new List<DataItem>();
        functor(this, dataItems);
    }
}
    
// in your function
...    dataHelper.CallData(this.dataHelper_DataCalled);    ...
  

Комментарии:

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

2. Спасибо за отличный отзыв. Я смог реализовать все, что вы упомянули. Что касается проблемы, я понял это. Сначала мне пришлось преодолеть ошибку «Исключение с нулевой ссылкой», используя ваш пример. Затем я понял, что выполняю вызовы с использованием HttpWebRequest в моем классе DataHelper. В этом случае вы должны использовать BeginInvoke для маршалирования данных в потоке пользовательского интерфейса. Как ни странно, когда возникло исключение «Недопустимый доступ между потоками», это происходило внутри метода, который вызвал событие после завершения операции.

Ответ №2:

Приведенные ниже строки в вашем коде должны быть перевернуты. Это

Эти строки

 dataHelper.CallData();
dataHelper.DataCalled  = new DataHelper.DataCalledEventHandler(dataHelper_DataCalled);
  

Должно быть:

 dataHelper.DataCalled  = new DataHelper.DataCalledEventHandler(dataHelper_DataCalled);
dataHelper.CallData();
  

Потому что сначала вам нужно подключить обработчик событий, а затем вызвать другие методы для объекта, которые могут вызвать событие

Комментарии:

1. Да, верно! Не заметил этого 🙂 По этой причине передача его в качестве параметра в функции кажется более естественным выбором.

2. @Ankur Да, это плохая привычка. Я никогда не прекращал это делать, потому что он никогда не выдавал исключение для меня. Но определенно не кажется правильным делать это в обратном направлении. Я буду вводить его таким образом в будущем. Спасибо за совет.

3. Ну, это не только плохая привычка, поскольку это код, который выполняется в другом потоке, он может завершиться ДО того, как вы подпишетесь на событие, поэтому ваш обратный вызов может случайно пропустить выполнение. Это опасно и непредсказуемо.