Unity - Make a Sync Method that waits for user input - c#

I have basic knowledge of C# with WinForms and WPF. But I am new to Unity.
I want to make a series of Message Boxes those appear one after another(once user close current than next box will show). It is very easy to use MessageBox.Show("Hello World"); in WinForms.
void myFun(){
MessageBox.Show("Hello World1");//First Box
//Some Code
MessageBox.Show("Hello World2");//Another
MessageBox.Show("Hello World3");//Finally Results
}
In WinForms(or WPF) Code after MessageBox.Show(); will not Executed before we Give input to close.
I want to make same MessageBox in unity with GameObject where code will not execute before GameObject is SetActive(False); by user command(mouse Click on Background or Yes/No Buttons).
Thanks in Advance
sorry for bad English

One of the best approaches becoming popular in Unity the last years is an Async approach. So I highly recommend you to learn this to have even more powerful tool than Coroutines and to avoid Callback Hell in case of Action usage.
Let's implement it this Async approach together.
Firstly, we need that MessageBox implementation. Let's say it has 2 buttons: Confirmation and Cancellation. Here's it:
public MessageBox : MonoBehaviour
{
[SerializeField]
private Button _confirmationButton;
[SerializeField]
private Button _cancelButton;
private TaskCompletionSource<bool> _tcs = new TaskCompletionSource<bool>();
public void OnConfirmationButtonClick()
{
// we're passing `true` if user clicks `Confirm`
_tcs.SetResult(true);
}
public void OnCancellationButtonClick()
{
// we're passing `false` if user clicks `Cancel`
_tcs.SetResult(false);
}
public async Task<bool> ShowAsync()
{
gameObject.SetActive(true);
// recreate an instance to not use the `SetResult` value of previous showing
_tcs = new TaskCompletionSource<bool>();
// the execution stops here ASYNCHRONOUSLY, so the UI thread is not blocked.
// It just awaits until we set any result to `_tcs`
await _tcs.Task;
gameObject.SetActive(false);
}
}
And now we can show a few message boxes and await for their input. Moreover, we may know what exactly button was clicked: Confirmation or Cancellation:
public async Task YourOwnMethodAsync()
{
// Let's assume you've instantiated 3 message boxes already and have their references
var resultOfFirstMessageBox = await _messageBox1.ShowAsync();
Debug.Log($"Showing the first message box shown. Result: {resultOfFirstMessageBox}");
var resultOfSecondMessageBox = await _messageBox2.ShowAsync();
Debug.Log($"Showing the second message box shown. Result: {resultOfSecondMessageBox}");
var resultOfThirdMessageBox = await _messageBox3.ShowAsync();
Debug.Log($"Showing the third message box shown. Result: {resultOfThirdMessageBox}");
}
If you need even more detailed description, just let me know.

Related

Wait for eventhandlers, if it is declared async

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.
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.

Windows Phone 8.1 runtime show message box on OnNavigatedFrom (back button)

I have been fighting with this for a while...
I'm using Windows Phone 8.1 Runtime (not silverlight) and I have the following code:
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
if (!ExitWithoutSave().Result) return;
this.navigationHelper.OnNavigatedFrom(e);
}
private async Task<bool> ExitWithoutSave()
{
MessageDialog dialog = new MessageDialog("There are unsaved changes, are you sure you wish to leave?", "Unsaved changes");
dialog.Commands.Clear();
dialog.Commands.Add(new Windows.UI.Popups.UICommand("Yes") { Id = 0 });
dialog.Commands.Add(new Windows.UI.Popups.UICommand("No") { Id = 1 });
dialog.DefaultCommandIndex = 0;
dialog.CancelCommandIndex = 1;
var result = await dialog.ShowAsync();
if (result.Label == "No")
{
canceled = true;
}
return canceled;
}
Basically, I want to ask the user if he wishes to leave without saving, if he says no, then I want to block this functionality.
The problem is, if there's an await during the execution of the OnNavigatedFrom, Windows phone thinks the app has broken and the UI gets blocked.
Is there any way to correctly show a message box on pressing the back button?
If not, is it possible to disable the back button entirely for this page?
Thanks,
Keran
Edit 15-11-2015:
Just wanted to bump this post. I had no luck using HardwareButton events together with Navigation Helper, MessageBoxes still don't work. I can't even cancel the back button press.
So I wanted to renew my question: What is the best way to create a confirm message box on back button press on Windows Phone 8.1 Runtime? F. e. message: "You have unsaved changes, do you wish to exit?": Yes / No.
You can use following event .
HardwareButtons.BackPressed += HardwareButtons_BackPressed;
void HardwareButtons_BackPressed(object sender, BackPressedEventArgs e)
{
}

"Only a single ContentDialog can be open at any time." error while opening another contentdialog

I am using Windows.UI.Xaml.Controls.ContentDialog to show a confirmation. And based on the response from the first dialog I would (or would not) show another dialog. But, when I am trying to open the second content dialog it throws : "Only a single ContentDialog can be open at any time." error. Even though in the UI, first dialog would be closed but somehow I am still not able to open the second dialog. Any idea?
I have created some code to handle this type of conundrum in my Apps:
public static class ContentDialogMaker
{
public static async void CreateContentDialog(ContentDialog Dialog, bool awaitPreviousDialog) { await CreateDialog(Dialog, awaitPreviousDialog); }
public static async Task CreateContentDialogAsync(ContentDialog Dialog, bool awaitPreviousDialog) { await CreateDialog(Dialog, awaitPreviousDialog); }
static async Task CreateDialog(ContentDialog Dialog, bool awaitPreviousDialog)
{
if (ActiveDialog != null)
{
if (awaitPreviousDialog)
{
await DialogAwaiter.Task;
DialogAwaiter = new TaskCompletionSource<bool>();
}
else ActiveDialog.Hide();
}
ActiveDialog = Dialog;
ActiveDialog.Closed += ActiveDialog_Closed;
await ActiveDialog.ShowAsync();
ActiveDialog.Closed -= ActiveDialog_Closed;
}
public static ContentDialog ActiveDialog;
static TaskCompletionSource<bool> DialogAwaiter = new TaskCompletionSource<bool>();
private static void ActiveDialog_Closed(ContentDialog sender, ContentDialogClosedEventArgs args) { DialogAwaiter.SetResult(true); }
}
To use these Methods, you need to create the ContentDialog and its content in a variable, then pass the variable, and bool into the Method.
Use CreateContentDialogAsync(), if you require a callback in your app code, say if you have a button in your Dialog, and you want wait for a button press, and then get the value from the form in code after the dialog.
Use CreateContentDialog(), if you don't need to wait for the Dialog to complete in your UI Code.
Use awaitPreviousDialog to wait for the previous dialog to complete before showing the next Dialog, or set false, to remove the previous Dialog, then show the next Dialog, say, if you want to show an Error Box, or the next Dialog is more important.
Example:
await ContentDialogMaker.CreateContentDialogAsync(new ContentDialog
{
Title = "Warning",
Content = new TextBlock
{
Text = "Roaming Appdata Quota has been reached, if you are seeing this please let me know via feedback and bug reporting, this means that any further changes to data will not be synced across devices.",
TextWrapping = TextWrapping.Wrap
},
PrimaryButtonText = "OK"
}, awaitPreviousDialog: true);
William Bradley's approach above is good. Just to polish it up a bit, here is an extension method to submit and await the showing of a content dialog; the dialog will be shown after all the other content dialogs that have already been submitted. Note: by the time the user clicks through earlier backlogged dialogs you may no longer want to show the dialog that you have submitted; to indicate this you may pass a predicate that will be tested after the other dialogs have been dismissed.
static public class ContentDialogExtensions
{
static public async Task<ContentDialogResult> EnqueueAndShowIfAsync( this ContentDialog contentDialog, Func<bool> predicate = null)
{
TaskCompletionSource<Null> currentDialogCompletion = new TaskCompletionSource<Null>();
TaskCompletionSource<Null> previousDialogCompletion = null;
// No locking needed since we are always on the UI thread.
if (!CoreApplication.MainView.CoreWindow.Dispatcher.HasThreadAccess) { throw new NotSupportedException("Can only show dialog from UI thread."); }
previousDialogCompletion = ContentDialogExtensions.PreviousDialogCompletion;
ContentDialogExtensions.PreviousDialogCompletion = currentDialogCompletion;
if (previousDialogCompletion != null) {
await previousDialogCompletion.Task;
}
var whichButtonWasPressed = ContentDialogResult.None;
if (predicate == null || predicate()) {
whichButtonWasPressed = await contentDialog.ShowAsync();
}
currentDialogCompletion.SetResult(null);
return whichButtonWasPressed;
}
static private TaskCompletionSource<Null> PreviousDialogCompletion = null;
}
Another way might be to use a SemaphoreSlim(1,1).
"Only a single ContentDialog can be open at a time"
This statement is not entirely true. You can only ShowAsync one ContentDialog at a time. All you need to do is hide the current ContentDialog before opening another one. Then, after the "await ShowAsync" of the second ContentDailog, your simply call "var T = this.ShowAync()" to unhide it. Example:
public sealed partial class MyDialog2 : ContentDialog
{
...
}
public sealed partial class MyDialog1 : ContentDialog
{
...
private async void Button1_Click(object sender, RoutedEventArgs e)
{
// Hide MyDialog1
this.Hide();
// Show MyDialog2 from MyDialog1
var C = new MyDialog2();
await C.ShowAsync();
// Unhide MyDialog1
var T = ShowAsync();
}
}
I know this is slightly old, but one simpler solution instead of going through all this pain is to just register a callback for the ContentDialog_Closed event. By this point you can be sure the previous dialog has been closed, and can open your next dialog. :)
Only a single ContentDialog can be open at any time.
That is a fact. (I was really surprised, but just for a moment)
You can't have more than one at any time and it is more like guideline from Microsoft, because it's really messy to have multiple dialogs on top of each other filled with content.
Try to change your UX to display only one sophisticated ContentDialog and for all other messages use MessageDialog - it supports multiple buttons(only two for phones, but more on desktop) for user response but without Checkboxes or similar "smart"-content stuff.
In my case MessageDialogs were really helpful, but in some areas I used chained ContentDialogs but for that you must await the first one, and open second right after without any exceptions. In your case it seems like ContentDialog was not fully closed when you tried to open next one.
Hope it helps!
I like this answer https://stackoverflow.com/a/47986634/942855, this will allow us ot handle binding all events.
So extended it a little to check the multiple calls to show dialog.
private int _dialogDisplayCount;
private async void Logout_OnClick(object sender, RoutedEventArgs e)
{
try
{
_dialogDisplayCount++;
ContentDialog noWifiDialog = new ContentDialog
{
Title = "Logout",
Content = "Are you sure, you want to Logout?",
PrimaryButtonText = "Yes",
CloseButtonText = "No"
};
noWifiDialog.PrimaryButtonClick += ContentDialog_PrimaryButtonClick;
//await noWifiDialog.ShowAsync();
await noWifiDialog.EnqueueAndShowIfAsync(() => _dialogDisplayCount);
}
catch (Exception exception)
{
_rootPage.NotifyUser(exception.ToString(), NotifyType.DebugErrorMessage);
}
finally
{
_dialogDisplayCount = 0;
}
}
modified predicate
public class Null { private Null() { } }
public static class ContentDialogExtensions
{
public static async Task<ContentDialogResult> EnqueueAndShowIfAsync(this ContentDialog contentDialog, Func<int> predicate = null)
{
TaskCompletionSource<Null> currentDialogCompletion = new TaskCompletionSource<Null>();
// No locking needed since we are always on the UI thread.
if (!CoreApplication.MainView.CoreWindow.Dispatcher.HasThreadAccess) { throw new NotSupportedException("Can only show dialog from UI thread."); }
var previousDialogCompletion = _previousDialogCompletion;
_previousDialogCompletion = currentDialogCompletion;
if (previousDialogCompletion != null)
{
await previousDialogCompletion.Task;
}
var whichButtonWasPressed = ContentDialogResult.None;
if (predicate == null || predicate() <=1)
{
whichButtonWasPressed = await contentDialog.ShowAsync();
}
currentDialogCompletion.SetResult(null);
return whichButtonWasPressed;
}
private static TaskCompletionSource<Null> _previousDialogCompletion;
}

Using async / wait, UI Freezes after setting content

Got some problem and cant't get why it appears.
I'm using .net 4.5 / C# and I try to set Content to a ContentControl after an async function succeeded.
The main focus of what i want to to in that part of the programm is to switch between an own WPF Loading animation (Usercontrol IsLoading) and a PDF Content (Usercontrol PDFDokument). The PDF is internal loaded & rendered inside the "PDFDokument" and works already very well.
If more information is needed, every asking is welcome.
For you to know i would say i'm still at a beginning level of developing. (the first of three years :) )
public ucPDFDocument PDFDokument = new ucPDFDocument();
public ucLoading IsLoading = new ucLoading();
protected async void lstSuchergebnis_DoubleClickItem(object sender, MouseButtonEventArgs e)
{
var tempAkte = ((ListViewItem)sender).Content as Akten;
tbctrlResultPanel.SelectedIndex = 1;
PDFDokument.IsDataChangeAllowed(false);
contSwapControls.Content = IsLoading;
await PDF(tempAkte);
contSwapControls.Content = PDFDokument; **<-- after executing this line, the ui freezes**
}
private Task PDF(Akten paramAkte)
{
Akten _tempAkte = paramAkte;
return Task.Run(() => { PDFDokument.LoadPDFDokument(_tempAkte.akt_ID, ref _DaKsManger); });
}
I tried different ways of using that async loading, but nothing solved that problem.
Hope someone got an idea how to solve that :)
Thanks a lot!!!
The only thing that is async is PDF(...), which you await. Setting the content is not being executed in an async manner. The content you are setting happens on the UI thread. (which is the only way to do so, since you are modifying the UI, which can only happen on the thread it is created on)

C# WinForms: Waiting for a button press inside an infinite loop

I'm making a simple Guess-The-Number game with a GUI. I need to wait on a loop waiting for the user to input a number in a text box and press "OK". How do I wait for an event inside a loop?
Note: I don't want message boxes. This is done in the main window, hence the need to wait for input.
EDIT: I should have explained myself better. I know that there's a loop inside the GUI. What I want is another loop inside a method. Maybe there's a better way to do this. I could code stuff inside the button's event handler, now that I think about it. Although I'd need global variables. Whataver, I'll think about it, but I hope my question is clearer now.
EDIT 2: Sorry that my question wasn't clear and the edit didn't do much help. First of all, the code is too big to be posted here. I'd probably have to post a screenshot of the GUI, so it wouldn't be of much use. Basically, I have two fields, "Max number" and "Number of allowed guesses". The user enters these two and clicks "Play". A new panel becomes available, with a text box and a "Guess" button. The user enters a guess, and the program checks to see if it's correct.
The purpose of the second infinite loop is to avoid global variables. See, each time the user clicks "Play", the game has to generate a new random number as the correct guess. If everything is done inside a method, no problem. But if the "Guess" button's event handler is called multiple times, the number has to be stored as an instance variable of the Form. Sure, it's not big deal, but I think the number should be a property of the method directing the current game, not of the Form.
I'd also have to keep track of the remaining number of guesses outside of the method. Again, it's no big deal. I just want to avoid globals if I can.
Again, I'm sorry that my question wasn't too clear. I'm kind of tired, and I didn't feel like writing too much. If this still isn't clear, then don't bother. I'll think of something.
C# automatically loops infinitely waiting for events until your form is closed. You just need to respond to the button click event.
Jason Down's suggestion is wise, create a new GuessingGame class and add it to your project. I know you're worried about "global variables" (which everyone is taught in school never to use unless you absolutely have to), but think about your design specifications for a minute.
But if the "Guess" button's event handler is called multiple times, the number has to be stored as an instance variable of the Form. Sure, it's not big deal, but I think the number should be a property of the method directing the current game, not of the Form.
As an alternative, store an instance of your GuessingGame class in the form. This is not a global variable! You said so yourself, the point of the game is keep track of the guesses and generate new numbers to guess every time "Play" is clicked. If you store an instance of the game in the form then open another form (e.g. a Help or About box), then the game's instance would not be available (thus, not global).
The GuessingGame object is going to look something like:
public class GuessingGame
{
private static Random _RNG = new Random();
private bool _GameRunning;
private bool _GameWon;
private int _Number;
private int _GuessesRemaining;
public int GuessesRemaining
{
get { return _GuessesRemaining; }
}
public bool GameEnded
{
get { return !_GameRunning; }
}
public bool GameWon
{
get { return _GameWon; }
}
public GuessingGame()
{
_GameRunning = false;
_GameWon = false;
}
public void StartNewGame(int numberOfGuesses, int max)
{
if (max <= 0)
throw new ArgumentOutOfRangeException("max", "Must be > 0");
if (max == int.MaxValue)
_Number = _RNG.Next();
else
_Number = _RNG.Next(0, max + 1);
_GuessesRemaining = numberOfGuesses;
_GameRunning = true;
}
public bool MakeGuess(int guess)
{
if (_GameRunning)
{
_GuessesRemaining--;
if (_GuessesRemaining <= 0)
{
_GameRunning = false;
_GameWon = false;
return false;
}
if (guess == _Number)
{
_GameWon = true;
return true;
}
else
{
return false;
}
}
else
{
throw new Exception("The game is not running. Call StartNewGame() before making a guess.");
}
}
}
This way, all the data related to the game is encapsulated within the class. Hooking up the events is easy in the codebehind of the form:
GuessingGame game = new GuessingGame();
private void btnPlay_Click(object sender, EventArgs e)
{
int numberOfGuesses = Convert.ToInt32(txtNumberOfGuesses.Text);
int max = Convert.ToInt32(txtMax.Text);
game.StartNewGame(numberOfGuesses, max);
}
private void btnGuess_Click(object sender, EventArgs e)
{
int guess = Convert.ToInt32(txtGuess.Text);
bool correct = game.MakeGuess(guess);
if (correct)
lblWin.Visible = true;
if (game.GameEnded)
{
// disable guess button, show loss label
}
}
You should probably look for a book to actually learn windows programming.
The very basics:
1) There is already an infinite loop deep down in the windows code somewhere. Any windows program is constantly looping and scanning for input.
2) Once input is found, this loop fires off an Event.
3) Your mission, should you choose to accept it, is to write event handlers to handle those events.
you are most likely doing it wrong as it has already been pointed out, but you can use this
Application.DoEvents();
to process events when you are on an actual loop
to do it the right way
- don't use a loop
- use an edit box for the input, then a button
- implement the button onclick event
Yes, and What if I am waiting for Speech events, it could happen anytime event when a function is running, I need to handle that without recursively call a function

Categories

Resources