2015-02-26 4 views
3

У меня есть репозиторий данных, который обеспечивает уровень сохранения для моделей моего приложения. Весь доступ к диску является асинхронным, поэтому весь мой уровень сохранения написан с помощью async/await. Мой хранилище данных позволяет другим модулям подписаться на изменения в данных:В ожидании выполнения обработчиков событий

public event EventHandler<Journaling.DataChangeEventArgs> DataChanged; 
protected void OnDataChanged(Journaling.Action a) 
{ 
     if (DataChanged != null) 
     { 
      DataChanged(this, new Journaling.DataChangeEventArgs(a)); 
     } 
} 

Когда я говорю хранилище, чтобы удалить объект, на который ссылается на другие объекты, он удаляет эти другие объекты.

public async Task<bool> DeleteAsync(Models.BaseModel model) 
{    
     await DeleteDependentModelsAsync(model).ConfigureAwait(false); 
     if (await connection.DeleteAsync(model).ConfigureAwait(false) != 1) 
      return false; 
     else 
     { 
      var deleteAction = new Journaling.DeleteAction(model); 
      OnDataChanged(deleteAction); 
      return true; 
     } 
} 

Эта настройка работает в некоторой степени, но у меня есть проблема при удалении объектов, которые ссылаются на другие объекты. Рассмотрим следующий пример:

Object X 
    Object A1: references X 
    Object A2: references X 
    Object A3: references X 

У меня есть модуль регистратора, который подписывается на изменения в хранилище данных, и выводит их в файл. Журналу иногда требуется получить дополнительные данные из репозитория, чтобы сделать его доступным для чтения человеком. Бревно при удалении объекта X должно быть:

A1 deleted (parent: X, more information contained in X) 
A2 deleted (parent: X, more information contained in X) 
A3 deleted (parent: X, more information contained in X) 
X deleted 

Проблема заключается в том, что OnDataChange не ожидают выполнения обработчиков событий. Из-за этого репозиторий данных удаляет A1-A3, а затем X до того, как обработчик событий регистратора даже вызывается один раз. Но обработчик события должен получить некоторую информацию о X, что уже невозможно, поскольку хранилище данных уже удалено X.

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

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

ответ

4

У меня есть blog post on the subject of "asynchronous events". В общем, я рекомендую использовать «отсрочки», которые являются концепцией из API хранилища Windows.

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

public class DataChangeEventArgs : EventArgs 
{ 
    private readonly DeferralManager _deferrals; 

    public DataChangeEventArgs(DeferralManager deferrals, Journaling.Action a) 
    { 
    _deferrals = deferrals; 
    } 

    public IDisposable GetDeferral() 
    { 
    return deferrals.GetDeferral(); 
    } 
} 

Тогда вы поднимаете событие как таковое:

protected Task OnDataChangedAsync(Journaling.Action a) 
{ 
    var handler = DataChanged; 
    if (handler == null) 
    return Task.FromResult<object>(null); // or TaskConstants.Completed 

    var deferrals = new DeferralManager(); 
    var args = new Journaling.DataChangeEventArgs(deferrals, a); 
    handler(args); 
    return deferrals.SignalAndWaitAsync(); 
} 

Затем код потребления может использовать отсрочку, если ему необходимо использовать await:

async void DataChangedHandler(object sender, Journaling.DataChangeEventArgs args) 
{ 
    using (args.GetDeferral()) 
    { 
    // Code can safely await in here. 
    } 
} 
+0

Учитывая эту модель, для обработчика такого события кажется необычайно легким, чтобы не знать, что им нужно, чтобы они «использовали» там, или даже если знали, иногда забывали. Когда делегат обработчика возвращает «Задача», это делает намного сложнее для любого, добавляющего обработчик, чтобы каким-то образом не указать на завершение. Они * могли *, но им нужно было бы более или менее пытаться стрелять в ногу, что гораздо меньше касается. Это добавляет немного работы коду, запускающему событие, но я думаю, что компромисс стоит. – Servy

+0

Я думаю, что оба варианта жизнеспособны. Недостатками сигнатуры события, возвращающей задачу, является то, что она вынуждает всех обработчиков иметь асинхронную подпись и что она не поддерживает обратную совместимость. Это правда, что разработчики могут забыть отсрочки, но это общий шаблон в API WinStore, поэтому многие разработчики знакомы с ними. –

+0

Если вас это беспокоит, вы можете делать то, что вы делаете для любого другого члена, который должен поддерживать как синхронные, так и асинхронные версии: предоставить оба. Имейте синхронное событие и асинхронное событие. – Servy

2

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

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

Вам также необходимо изменить подпись OnDataChanged, чтобы вернуть Task, чтобы вызывающий мог узнать, когда он закончит.

public event Func<Journaling.Action, Task> DataChanged; 
protected Task OnDataChanged(Journaling.Action a) 
{ 
    var handlers = DataChanged; 
    if (handlers == null) 
     return Task.FromResult(0); 

    var tasks = DataChanged.GetInvocationList() 
     .Cast<Func<Journaling.Action, Task>>() 
     .Select(handler => handler(a)); 
    return Task.WhenAll(tasks); 

} 
+0

Спасибо за ваш ответ. Однако я выбрал подход Стивенса, потому что было проще не менять подпись события. – SebastianR

Смежные вопросы