-1

I have the following class

public class Foo
{
    private string? _bar;
    public event EventHandler<CancelPropertyChangingEventArgs>? CancelPropertyChanging;
    public string? Bar
    {
        get => _bar;
        set
        {
            if (CancelPropertyChanging is { } cancelPropertyChanging)
            {
                var eventArgs = new CancelPropertyChangingEventArgs()
                {
                    Cancel = false,
                    NewValue = value,
                    OldValue = _bar,
                    PropertyName = nameof(Bar),
                };
                cancelPropertyChanging(this, eventArgs);
                if (eventArgs.Cancel)
                    return;
            }

            _bar = value;
        }
    }

    public override string ToString() => Bar ?? "";
}

Where I can register to the event CancelPropertyChanging and potentially cancel a setter.

Everything works as expected when no async/await is involved.

With the following code.

var foo = new Foo();
foo.Bar = "Init Value";
foo.CancelPropertyChanging += Foo_CancelPropertyChanging;

foo.Bar = "Hello World";
foo.Bar = "Hello World 2";
Console.WriteLine(foo.Bar);
Console.WriteLine(foo.Bar == "Hello World 2" ? "Error" : "Correct");

void Foo_CancelPropertyChanging(object? sender, CancelPropertyChangingEventArgs e)
{
    Console.WriteLine($"Changing Sync - OldValue: {e.OldValue} | NewValue: {e.NewValue}");

    if (Convert.ToString(e.NewValue) == "Hello World 2")
        e.Cancel = e.Cancel || true;
}

I am getting this output:

Changing Sync - OldValue: Init Value | NewValue: Hello World
Changing Sync - OldValue: Hello World | NewValue: Hello World 2
Hello World
Correct

So I did successfully Cancel the setting of Hello World 2 into the Bar property of my Foo object.

The same code will fail when I declare the event handler async and introduce a await Task.Delay(1_000);

How could I await for all event handlers to really finish even if they are declared as async?

There isn't really a way to just say

await cancelPropertyChanging(this, eventArgs);

I wouldn't know if someone somewhere would register an event handler and mark it async and does what not, the second this happens my code will give undesired results.

Here you may find a demo of the code above:

https://dotnetfiddle.net/GWhk3w

Notice:

That this code demonstrates the issue at hand in the easiest setup I could think of, the acutal issue is more meaning full than a cancable setter, but it revolves around events and the ability to cancel, where I am facing the wrong cancel signal.

Edit:

A maybe realistic example would be.

Imagine you have a WPF window and register to the Closing event, which has a Cancel member.

https://learn.microsoft.com/en-us/dotnet/api/system.windows.window.closing?view=windowsdesktop-6.0

No one would stop me from writing this

async void WpfWindow_Closing(object sender, CancelEventArgs e)
{
    await Task.Delay(10_000);
    e.Cancel = true;
}

How does the Window - if it actually does - wait for this code to finish to know if I set the Cancel member, and actually cancel the close of the window.

Rand Random
  • 7,300
  • 10
  • 40
  • 88
  • Does the `CancelPropertyChanging` event have to be of type `EventHandler`, or is there flexibility on that? – madreflection Mar 08 '22 at 19:21
  • @madreflection - as long as I could register something and can say .Cancel = true, I am open for changes – Rand Random Mar 08 '22 at 19:22
  • @madreflection - updated the question, with a maybe more practical example – Rand Random Mar 08 '22 at 19:32
  • You also have to consder that a proper handler for this would have to be async, and properties can't be async. You would have to have a method with a signature like `Task SetBar(string? value)` so you can await the event handler(s) correctly. That still meets the "register something and say .Cancel = true;" requirement, but it starts to deviate from your intended use as a property. – madreflection Mar 08 '22 at 19:33
  • 1
    `EventHandler` does not return anything, so there will be nothing to await. Even if it were to return `Task`, potentially you would have multiple event handlers attached to the same event, and it would only return the last. There is a reason for async event handlers being called "fire and forget". Simply put, you can't use `EventHandler` for this. – Lasse V. Karlsen Mar 08 '22 at 19:34
  • @madreflection - I actually believed that event handlers are magic in this regard, so I was surprised it just didn't work out of the box – Rand Random Mar 08 '22 at 19:34
  • 1
    Additionally, there would be no safe way to call it inside the property setter anyway, as properties can't have async accessors. As such, you would have to fiddle with sync over async, doing "tricks" such as `.ConfigureAwaiter(false).GetAwaiter().GetResult()`, with the potential to deadlock. This is not recommended. In short, async event handlers will not be usable for you. – Lasse V. Karlsen Mar 08 '22 at 19:36
  • 1
    You're probably looking for something like this: https://dotnetfiddle.net/VBjELQ . If that's okay, I'll post it as an answer. – madreflection Mar 08 '22 at 19:47
  • @madreflection - wrote the question seconds before leaving office, please be patient with me going to evaluate it tomorrow – Rand Random Mar 08 '22 at 19:50
  • 2
    As a side note, `b || true` is `true`, so `e.Cancel = e.Cancel || true;` can be reduced to `e.Cancel = true;`. The only reason to logically combine in that manner is if you had another variable that could have either value. Something to think about in the meantime. – madreflection Mar 08 '22 at 19:56
  • These two questions might be a bit relevant: [Awaiting Asynchronous function inside FormClosing Event](https://stackoverflow.com/questions/16656523/awaiting-asynchronous-function-inside-formclosing-event) / [.NET Async in shutdown methods?](https://stackoverflow.com/questions/58406366/net-async-in-shutdown-methods) – Theodor Zoulias Mar 09 '22 at 02:01
  • @madreflection - about the bool copy and paste error, I simply copied the line from my real code and changed false to true without thinking - the idea of `e.Cancel = e.Cancel || false;` is to keep the true in e.Cancel instead of overwriting it with false – Rand Random Mar 09 '22 at 09:06
  • @madreflection - had a look at your code looks nice, and seems to do what I am looking for, feel free to move it to an answer, though I must say, it would only solve the issue around my own events and wouldn't help with the window closing problem – Rand Random Mar 09 '22 at 09:11
  • @madreflection - just noticed that your solution is the paragraph `The Task-Returning Delegate Solution` on Stephen's Blog – Rand Random Mar 09 '22 at 09:13
  • 1
    Interesting. I hadn't read the blog yet. I'm not going to write an answer, if Stephen Cleary has answered *and* covered it in the linked blog. – madreflection Mar 09 '22 at 09:16
  • 1
    `e.Cancel = e.Cancel || false;` is functionally a no-op. If that's the statement for an `if` condition, just use empty braces. – madreflection Mar 09 '22 at 09:20

1 Answers1

4

How does the Window - if it actually does - wait for this code to finish to know if I set the Cancel member, and actually cancel the close of the window.

It doesn't.

The normal pattern there is to always cancel the close of the window (possibly replacing it with a "hide" instead of close), do the (asynchronous) operation, and then do the actual close.

How could I await for all event handlers to really finish even if they are declared as async?

Well, there are ways to do this, as noted on my blog. But here's the thing: your event-invoking code must await for all the handlers to complete one way or another. And since you can't await in a property setter, awaiting the event handlers won't do you any good.

Like the WPF workaround, the best solution is probably a broader re-design. In this case, you can introduce the idea of (a queue of) pending changes that are only applied after all asynchronous checks have been done.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810