Here we have a Grid
with a Button
. When the user clicks the button, a method in a Utility class is executed which forces the application to receive a click on Grid. The code flow must stop here and not continue until the user has clicked on the Grid
.
I have had a similar question before here:
Wait till user click C# WPF
In that question, I got an answer using async/await which works, but since I am going to use it as part of an API, I do not want to use async/await, since the consumers will have then to mark their methods with async which I do not want.
How do I write Utility.PickPoint(Grid grid)
method to achieve this goal?
I saw this which may help but did not fully understood it to apply here to be honest:
Blocking until an event completes
Consider it as something like Console.ReadKey() method in a Console application. When we call this method, the code flow stops until we enter some value. The debugger does not continue until we enter something. I want the exact behavior for PickPoint() method. The code flow will stop until the user clicks on the Grid.
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="3*"/>
<RowDefinition Height="1*"/>
</Grid.RowDefinitions>
<Grid x:Name="View" Background="Green"/>
<Button Grid.Row="1" Content="Pick" Click="ButtonBase_OnClick"/>
</Grid>
</Window>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
// do not continue the code flow until the user has clicked on the grid.
// so when we debug, the code flow will literally stop here.
var point = Utility.PickPoint(View);
MessageBox.Show(point.ToString());
}
}
public static class Utility
{
public static Point PickPoint(Grid grid)
{
}
}
"How to block code flow until an event is fired?"
Your approach is wrong. Event-driven does not mean blocking and waiting for an event. You never wait, at least you always try hard to avoid it. Waiting is wasting resources, blocking threads and maybe introducing the risk of a deadlock or zombie thread (in case the release signal is never raised).
It should be clear that blocking a thread to wait for an event is an anti-pattern as it contradicts the idea of an event.
You generally have two (modern) options: implement an asynchronous API or an event-driven API. Since you don't want to implement your API asynchronous, you are left with the event-driven API.
The key of an event-driven API is, that instead of forcing the caller to synchronously wait for a result or poll for a result, you let the caller continue and send him a notification, once the result is ready or the operation has completed. Meanwhile, the caller can continue to execute other operations.
When looking at the problem from a threading perspective, then the event-driven API allows the calling thread e.g., the UI thread, which executes the button's event handler, to be free to continue to handle e.g. other UI related operations, like rendering UI elements or handling user input like mouse movement and key presses. The event-driven API has the same effect or goal like an asynchronous API, although it is far less convenient.
Since you didn't provide enough details on what you are really trying to do, what Utility.PickPoint()
is actually doing and what the result of the task is or why the user has to click on the `Grid, I can't offer you a better solution. I just can offer a general pattern of how to implement your requirement.
Your flow or the goal is obviously divided into at least two steps to make it a sequence of operations:
Grid
with at least two constraints:
This requires at two notifications (events) for the client of the API to allow non-blocking interaction:
You should let your API implement this behavior and constraints by exposing two public methods and two public events.
Since this implementation only allows a single (non-concurrent) call to the API it's also recommended to expose a IsBusy
property to indicate a running sequence. This allows polling the current state before starting a new sequence, although it is recommended to wait for the completed event to execute subsequent calls.
Utility.cs
class Utility
{
public event EventHandler InitializePickPointCompleted;
public event EventHandler<PickPointCompletedEventArgs> PickPointCompleted;
public bool IsBusy { get; set; }
private bool IsPickPointInitialized { get; set; }
// The prefix 'Begin' signals the caller or client of the API,
// that he also has to end the sequence explicitly
public void BeginPickPoint(param)
{
// Implement constraint 1
if (this.IsBusy)
{
// Alternatively just return or use Try-do pattern
throw new InvalidOperationException("BeginPickPoint is already executing. Call EndPickPoint before starting another sequence.");
}
// Set the flag that a current sequence is in progress
this.IsBusy = true;
// Execute operation until caller interaction is required.
// Execute in background thread to allow API caller to proceed with execution.
Task.Run(() => StartOperationNonBlocking(param));
}
public void EndPickPoint(param)
{
// Implement constraint 2 and 3
if (!this.IsPickPointInitialized)
{
// Alternatively just return or use Try-do pattern
throw new InvalidOperationException("BeginPickPoint must have completed execution before calling EndPickPoint.");
}
// Execute operation until caller interaction is required.
// Execute in background thread to allow API caller to proceed with execution.
Task.Run(() => CompleteOperationNonBlocking(param));
}
private void StartOperationNonBlocking(param)
{
... // Do something
// Flag the completion of the first step of the sequence (to guarantee constraint 2)
this.IsPickPointInitialized = true;
// Request caller interaction to kick off EndPickPoint() execution
OnInitializePickPointCompleted();
}
private void CompleteOperationNonBlocking(param)
{
// Execute goal and get the result of the completed task
Point result = ExecuteGoal();
// Reset API sequence (allow next client invocation)
this.IsBusy = false;
this.IsPickPointInitialized = false;
// Notify caller that execution has completed and the result is available
OnPickPointCompleted(result);
}
private void OnInitializePickPointCompleted()
{
// Set the result of the task
this.InitializePickPointCompleted?.Invoke(this, EventArgs.Empty);
}
private void OnPickPointCompleted(Point result)
{
// Set the result of the task
this.PickPointCompleted?.Invoke(this, new PickPointCompletedEventArgs(result));
}
}
PickPointCompletedEventArgs.cs
class PickPointCompletedEventArgs : AsyncCompletedEventArgs
{
public Point Result { get; }
public PickPointCompletedEventArgs(Point result)
{
this.Result = result;
}
}
MainWindow.xaml.cs
partial class MainWindow : Window
{
private Utility Api { get; set; }
public MainWindow()
{
InitializeComponent();
this.Api = new Utility();
}
private void StartPickPoint_OnButtonClick(object sender, RoutedEventArgs e)
{
this.Api.InitializePickPointCompleted += RequestUserInput_OnInitializePickPointCompleted;
// Invoke API and continue to do something until the first step has completed.
// This is possible because the API will execute the operation on a background thread.
this.Api.BeginPickPoint();
}
private void RequestUserInput_OnInitializePickPointCompleted(object sender, EventArgs e)
{
// Cleanup
this.Api.InitializePickPointCompleted -= RequestUserInput_OnInitializePickPointCompleted;
// Communicate to the UI user that you are waiting for him to click on the screen
// e.g. by showing a Popup, dimming the screen or showing a dialog.
// Once the input is received the input event handler will invoke the API to complete the goal
MessageBox.Show("Please click the screen");
}
private void FinishPickPoint_OnGridMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
this.Api.PickPointCompleted += ShowPoint_OnPickPointCompleted;
// Invoke API to complete the goal
// and continue to do something until the last step has completed
this.Api.EndPickPoint();
}
private void ShowPoint_OnPickPointCompleted(object sender, PickPointCompletedEventArgs e)
{
// Cleanup
this.Api.PickPointCompleted -= ShowPoint_OnPickPointCompleted;
// Get the result from the PickPointCompletedEventArgs instance
Point point = e.Result;
// Handle the result
MessageBox.Show(point.ToString());
}
}
MainWindow.xaml
<Window>
<Grid MouseLeftButtonUp="FinishPickPoint_OnGridMouseLeftButtonUp">
<Button Click="StartPickPoint_OnButtonClick" />
</Grid>
</Window>
Events raised on a background thread will execute their handlers on the same thread. Accessing a DispatcherObject
like a UI element from a handler, which is executed on a background thread, requires the critical operation to be enqueued to the Dispatcher
using either Dispatcher.Invoke
or Dispatcher.InvokeAsync
to avoid cross-thread exceptions.
Read the remarks about DispatcherObject
to learn more about this phenomenon called dispatcher affinity or thread affinity.
For a convenient usage of the API I suggest to marshall all events to the original context of the caller either by capturing and using the caller's SynchronizationContext
or by using AsyncOperation
(or the AsyncOperationManager
).
The above example can be easily enhanced by providing cancellation (recommended) e.g. by exposing a Cancel()
method e.g., PickPointCancel()
and progress reporting (preferably using Progress<T>
).
Because you were approaching me to find a "better" blocking solution, given me the example of console applications, I felt to convince you, that your perception or point of view is totally wrong.
"Consider a Console application with these two lines of code in it.
var str = Console.ReadLine(); Console.WriteLine(str);
What happens when you execute the application in debug mode. It will stop at the first line of code and force you to enter a value in Console UI and then after you enter something and press Enter, it will execute the next line and actually print what you entered. I was thinking about exactly the same behavior but in WPF application."
A console application is something totally different. The threading concept is a little different. Console applications don't have a GUI. Just input/output/error streams. You can't compare the architecture of a console application to a rich GUI application. This won't work. You really must understand and accept this.
Also don't get deceived by the looks. Do you know what is happening inside Console.ReadLine
? How it is implemented? Is it blocking the main thread and in parallel it reads input? Or is it just polling?
Here is the original implementation of Console.ReadLine
:
public virtual String ReadLine()
{
StringBuilder sb = new StringBuilder();
while (true)
{
int ch = Read();
if (ch == -1)
break;
if (ch == '\r' || ch == '\n')
{
if (ch == '\r' && Peek() == '\n')
Read();
return sb.ToString();
}
sb.Append((char)ch);
}
if (sb.Length > 0)
return sb.ToString();
return null;
}
As you can see it's a simple synchronous operation. It polls for user input in an "infinite" loop. No magic block and continue.
WPF is build around a rendering thread and a UI thread. Those threads keep always spinning in order to communicate with the OS like handling user input - keeping the application responsive. You never want to pause/block this thread as it will stop the framework from doing essential background work, like responding to mouse events - you don't want the mouse to freeze:
waiting = thread blocking = unresponsiveness = bad UX = annoyed users/customers = trouble in the office.
Sometimes, the application flow requires to wait for input or a routine to complete. But we don't want to block the main thread.
That's why people invented complex asynchronous programming models, to allow waiting without blocking the main thread and without forcing the developer to write complicated and erroneous multithreading code.
Every modern application framework offers asynchronous operations or an asynchronous programming model, to allow the development of simple and efficient code.
The fact that you are trying hard to resist asynchronous programming model, shows some lack of understanding to me. Every modern developer prefers an asynchronous API over a synchronous one. No serious developer cares to use the await
keyword or to declare his method async
. Nobody. You are the first I encounter who complains about asynchronous APIs and who finds them inconvenient to use.
If I would check your framework, which targets to solve UI related problems or make UI related tasks easier, I would expect it to be asynchronous - all the way.
UI related API which isn't asynchronous is waste, as it will complicate my programming style, therefore my code which therefore becomes more error-prone and difficult to maintain.
A different perspective: when you acknowledge that waiting blocks the UI thread, is creating a very bad and undesirable user experience as the UI will freeze until the waiting is over, now that you realize this, why would you offer an API or plugin model which encourages a developer to do exactly this - implement waiting?
You don't know what the 3rd party plugin will do and how long a routine will take until it completes. This is simply a bad API design. When your API operates on the UI thread then the caller of your API must be able to make non-blocking calls to it.
If you deny the only cheap or graceful solution, then use an event-driven approach as shown in my example.
It does what you want: start a routine - wait for user input - continue execution - accomplish goal.
I really tried several times to explain why waiting/blocking is a bad application design. Again, you can't compare a console UI to a rich graphical UI, where e.g. input handling alone is a multitude more complex than just listening to the input stream. I really don't know your experience level and where you started, but you should start to embrace the asynchronous programming model. I don't know the reason why you try to avoid it. But it's not wise at all.
Today asynchronous programming models are implemented everywhere, on every platform, compiler, every environment, browser, server, desktop, database - everywhere. The event-driven model allows to achieve the same goal, but it's less convenient to use (subscribe/unsubscribe to/from events, read docs (when there are docs) to learn about the events), relying on background threads. Event-driven is old-fashioned and should only be used when asynchronous libraries are not available or not applicable.
As a side-note: the .NET Framwork (.NET Standard) offers the TaskCompletionSource
(among other purposes) to provide a simple way to convert an existing even-driven API into an asynchronous API.
"I have seen the exact behavior in Autodesk Revit."
Behavior (what you experience or observe) is much different from how this experience is implemented. Two different things. Your Autodesk is very likely using asynchronous libraries or language features or some other threading mechanism. And it is also context related. When the method that is on your mind is executing on a background thread then the developer may choose to block this thread. He has either a very good reason to do this or just made a bad design choice. You are totally on the wrong track ;) Blocking is not good.
(Is the Autodesk source code open source? Or how do you know how it is implemented?)
I don't want to offend you, please believe me. But please reconsider to implement your API asynchronous. It's only in your head that developers don't like to use async/await. You obviously got the wrong mindset. And forget about that console application argument - it's nonsense ;)
UI related API MUST use async/await whenever possible. Otherwise, you leave all the work to write non-blocking code to the client of your API. You would force me to wrap every call to your API into a background thread. Or to use less comfortable event handling. Believe me - every developer rather decorates his members with async
, than doing event handling. Every time you use events you might risk a potential memory leak - depends on some circumstances, but the risk is real and not rare when programming careless.
I really hope you understand why blocking is bad. I really hope you decide to use async/await to write a modern asynchronous API. Nevertheless, I showed you a very common way to wait non-blocking, using events, although I urge you to use async/await.
"The API will allow the programmer to have access to the UI and etc. Now suppose the programmer wants to develop an add-in that when a button is clicked, the final user is asked to pick a point in the UI"
If you don't want to allow the plugin to have direct access to UI elements, you should provide an interface to delegate events or expose internal components via abstracted objects.
The API internally will subscribe to UI events on behalf of the Add-in and then delegates the event by exposing a corresponding "wrapper" event to the API client. Your API must offer some hooks where the Add-in can connect to access specific application components. A plugin API acts like an adapter or facade to give externals access to internals.
To allow a degree of isolation.
Take a look at how Visual Studio manages plugins or allows us to implement them. Pretend you want to write a plugin for Visual Studio and do some research on how to do this. You will realize that Visual Studio exposes its internals via an interface or API. E.G. you can manipulate the code editor or get information about the editor's content without real access to it.
I personally think this is being over-complicated by everyone, but maybe I am not fully understanding the reason why this needs to be done a certain way, but it seems like a simple bool check can be used here.
First and foremost, make your grid hit-testable by setting the Background
and IsHitTestVisible
properties, or else it won't even capture mouse clicks.
<grid MouseLeftButtonUp="Grid_MouseLeftButtonUp" IsHitTestVisible="True" Background="Transparent">
Next create a bool value that can store whether the "GridClick" event should occur. When the grid is clicked, check that value and the perform execution from the grid click event if it's awaiting the click.
Example:
bool awaitingClick = false;
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
awaitingClick=true;
}
private void Grid_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
//Stop here if the program shouldn't do anything when grid is clicked
if (!awaitingClick) { return; }
//Run event
var point = Utility.PickPoint(View);
MessageBox.Show(point.ToString());
awaitingClick=false;//Reset
}
I tried a few things but i'm unable to make it without async/await
. Because if we don't use it it causes DeadLock
or UI is Blocked and then we are enable to take Grid_Click
input.
private async void ToolBtn_OnClick(object sender, RoutedEventArgs e)
{
var senderBtn = sender as Button;
senderBtn.IsEnabled = false;
var response = await Utility.PickPoint(myGrid);
MessageBox.Show(response.ToString());
senderBtn.IsEnabled = true;
}
public static class Utility
{
private static TaskCompletionSource<bool> tcs;
private static Point _point = new Point();
public static async Task<Point> PickPoint(Grid grid)
{
tcs = new TaskCompletionSource<bool>();
_point = new Point();
grid.MouseLeftButtonUp += GridOnMouseLeftButtonUp;
await tcs.Task;
grid.MouseLeftButtonUp -= GridOnMouseLeftButtonUp;
return _point;
}
private static void GridOnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
// do something here ....
_point = new Point { X = 23, Y = 34 };
// do something here ....
tcs.SetResult(true); // as soon its set it will go back
}
}
You could block asynchronously using a SemaphoreSlim
:
public partial class MainWindow : Window, IDisposable
{
private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(0, 1);
public MainWindow()
{
InitializeComponent();
}
private async void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
var point = Utility.PickPoint(View);
// do not continue the code flow until the user has clicked on the grid.
// so when we debug, the code flow will literally stop here.
await _semaphoreSlim.WaitAsync();
MessageBox.Show(point.ToString());
}
private void View_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
//click on grid detected....
_semaphoreSlim.Release();
}
protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
Dispose();
}
public void Dispose() => _semaphoreSlim.Dispose();
}
You cannot, and you don't want to either, block the dispatcher thread synchronously because then it will never be able to handle the click on the Grid
, i.e. it cannot both be blocked and handle events at the same time.
Technically it is possible with AutoResetEvent
and without async/await
, but there is a significant drawback:
public static Point PickPoint(Grid grid)
{
var pointPicked = new AutoResetEvent(false);
grid.MouseLeftButtonUp += (s, e) =>
{
// do whatever after the grid is clicked
// signal the end of waiting
pointPicked.Set();
};
// code flow will stop here and wait until the grid is clicked
pointPicked.WaitOne();
// return something...
}
The drawback: If you call this method directly in a button event handler as your sample code does, deadlocking will occur and you will see that the application stops responding. Because you are using the only UI thread for waiting the user's click, it cannot respond to any user's action, including the user's click on the grid.
Consumers of the method should invoke it in another thread to prevent deadlocks. If it can be guaranteed, that's fine. Otherwise, you need to invoke the method like this:
private void ButtonBase_OnClick(object sender, RoutedEventArgs e)
{
// here I used ThreadPool, but you may use other means to run on another thread
ThreadPool.QueueUserWorkItem(new WaitCallback(Capture));
}
private void Capture(object state)
{
// do not continue the code flow until the user has clicked on the grid.
// so when we debug, the code flow will literally stop here.
var point = Utility.PickPoint(View);
MessageBox.Show(point.ToString());
}
It may cause more trouble to consumers of your API except they used to manage their own threads. That's why async/await
was invented.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With