0

I have an option form where the user has to enter parameters for a mini-game, going from 8 to 32. My problem is that as soon as I start typing, if I insert a number under 8 (I want to put 20, for example), the event activates as soon as I type 2 and turn it into 8.

private void TXBheight_TextChanged(object sender, EventArgs e)
    {

        if(int.Parse(TXBheight.Text) < 8)
        {
            TXBheight.Text = "8";
        }
        else if (int.Parse(TXBheight.Text) > 32)
        {
            TXBheight.Text = "32";
        }
    }

Is there any easy way to make a delay, or wait until I finish typing?

For those who identify this question as a possible duplicate, i took a look, and the possible answers are from 6 years ago. During that time, languages and compilers evolve, so maybe there is something new we can all learn from

Amine Fellous
  • 83
  • 1
  • 8
  • text_changed event fires every time the text changes. – Rainbow May 12 '18 at 11:40
  • Yes, and thats why i need a delay, i cant tip a complete number. – Amine Fellous May 12 '18 at 11:41
  • Why not create a Task and just check it everytime text changes. If it is running then do nothing if not do your thing there – keysl May 12 '18 at 11:42
  • You can't delay an event, it's better to add a label and update it based on what's being typed in the textbox – Rainbow May 12 '18 at 11:42
  • You can use the Textbox Leave event... – DxTx May 12 '18 at 11:43
  • You got interesting propositions, but I would not chose anything from these. I created a custom control for this purpose *(but the code is at work)*. You have to check in for "active mode" and for end of editing. Simple implementation: On `TextChanged` check check only for maximal number. Only in `Leave` event check for minimal number. – Julo May 12 '18 at 12:03
  • @Julo im still a begginer, so i dont understand what you try to tell me XD – Amine Fellous May 12 '18 at 12:06
  • @DT dont work (or maybe i dont know how to properly use it) – Amine Fellous May 12 '18 at 12:12
  • 1
    you can limit the range of input in a NumericUpDown control, ComboBox, ListBox, etc. – Slai May 12 '18 at 12:18
  • Possible duplicate of [C# wait for user to finish typing in a Text Box](https://stackoverflow.com/questions/8001450/c-sharp-wait-for-user-to-finish-typing-in-a-text-box) – Ginxxx May 12 '18 at 12:28
  • @TheGinxx009 that post date from more than 6 years ago, with old VS. Im asking again in case there is something new. – Amine Fellous May 12 '18 at 13:46

5 Answers5

2

Instead of using a TextChanged event, use the TextBox _Validating event and the _Validated event. The _Validating event is fired only when the text box loses focus, i.e., when the user clicks on another control, e.g., a Button or another TextBox. When this happens, the _Validating event is fired and you test the value in the text box. If it's invalid, you cancel the _Validating event. If its valid, you DON'T cancel the _Validating event, and as a a result the _Validated event is fired. In the _Validated event, you do what you neeed to do when the input data is valid. Use an errorprovider to inform the user when the input data is invalid.

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();

        errorProvider1.SetError(TXBheight, "");

        //NEW NEW NEW
        buttonCancel.CausesValidation = false;
    }

    private void Button1_Click(object sender, EventArgs e)
    {
        // do what is needed when the button is clicked
    }

    private void TXBheight_Validating(object sender, CancelEventArgs e)
    {

        errorProvider1.SetError(TXBheight, "");

        if (String.IsNullOrEmpty(TXBheight.Text))
        {
            errorProvider1.SetError(TXBheight, "Height is a required field");
            e.Cancel = true;
            return;
        }

        if (int.Parse(TXBheight.Text) < 8)
        {
            errorProvider1.SetError(TXBheight, "Height must be GE 8");
            e.Cancel = true;
            return;
        }

        if (int.Parse(TXBheight.Text) > 32)
        {
            errorProvider1.SetError(TXBheight, "Height must be LE 32");
            e.Cancel = true;
            return;
        }

    }

    private void TXBheight_Validated(object sender, EventArgs e)
    {
        //this event is fired when the data is valid, i.e., 
        // if e.Cancel in the _Validating method is NOT set to cancel

    }

    //NEW NEW NEW
    private void ButtonCancel_Click(object sender, EventArgs e)
    {
        AutoValidate = AutoValidate.Disable;
        Close();
    }

    // NEW #2

    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        if (e.CloseReason == CloseReason.UserClosing)
        {
            DialogResult result = MessageBox.Show("Do you really want to exit?", "Dialog Title", MessageBoxButtons.YesNo);
            if (result == DialogResult.Yes)
            {
                Environment.Exit(0);
            }
            else
            {
                e.Cancel = true;
            }
        }
        else
        {
            e.Cancel = true;
        }
    }
}
VA systems engineer
  • 2,856
  • 2
  • 14
  • 38
  • This is also a possible solution, but I personally do not like the `Validating` event. When you do not have the correct value you can not leave the field and, you can not even close the application. – Julo May 12 '18 at 12:26
  • @Julo: You can overcome the application issue using `CancelEventArgs`. See accepted answer to [Cancel validation event on custom UserControl when form closing](https://stackoverflow.com/questions/12790706/cancel-validation-event-on-custom-usercontrol-when-form-closing). You can overcome the field issue by providing the user a `Cancel button` and then use the technique in [disable validation of errorprovider when click cancel button](https://stackoverflow.com/questions/1995213/disable-validation-of-errorprovider-when-click-cancel-button) (See both answers) – VA systems engineer May 12 '18 at 12:42
  • I know, but when you write a strict validating function, you can not exit the field *(the focus will remain on the control)*. When you press the window close button, the application tries to leave the current control, but this is cancelled and focus is returned to the control, where you need to fill the correct value. But the second problem is also, you can write anything in the control (in this case character) and it will report error only when you try to leave the control. Therefore I always use approach, where the value is checked on change *(with support of incomplete value)* and on leave. – Julo May 12 '18 at 12:53
  • @Julo: I just modified my code and tested the ability to close the app (or form) when an invalid value has been detected (because the text box lost focus) AND the user clicks the cancel button. See my code above. Other than that, the app cannot know the user is done entering data until they leave the field – VA systems engineer May 12 '18 at 13:02
  • Other than that, the app cannot know the user is done entering data until they leave the field, either by tabbing to another textbox, clicking on a "I'm Done, Please validate all my input" button, or clicking on a "I give up" button – VA systems engineer May 12 '18 at 13:08
  • I think the obvious failure of the "timer" approach is a zip code field. I enter the 1st digit of a zip code, realize that I don't know what the zip cod is, bring up a browser to find the zip code, and meanwhile, the app has "timed out" on me even though I'm not done entering data. Bad App ! :-) – VA systems engineer May 12 '18 at 13:12
  • Thank for your code. This is more interesting approach, than expected. I learned again something new. There is still the problem with "x" button *(on top of window; standard Windows Close)* in current code, but I see that this approach *(when used in a correct way)* is not as bad as my old experiences. But I still prefer another type of approach. This is too much code. – Julo May 12 '18 at 13:19
  • I will try to understand and try it XD. Then i will see how i can test if the strng is numerical or not. Im making a minesweep game and those are the options for the game size. – Amine Fellous May 12 '18 at 13:49
  • @Julo: Ok, so added one more event method: `Form1_FormClosing` fired on Form `FormClosing` event. This event is fired when the user clicks on the control box `X` button. I stole this method from JBelter on [Close Form Button Event](https://stackoverflow.com/questions/17796151/close-form-button-event). Same deal: Enter an invalid Height value, click on the `Go` button, the `errorprovider` fires. Click on the control box `X` button, and the form closes. Also, I don't see any more code than you have to write anyway. You'd just be trading `TextChanged` methods for `_Validating` methods. – VA systems engineer May 12 '18 at 14:04
  • You need to write more code, at leas in the way how I use it. I create a control *(derived from original `TextBox` with automatic validation)* and set the rule *(integer, real number range, or regular expression)*. Then I simply place the control on form and there is nothing more to do, than limit the "OK button" when any of controls do not meet the rules (`okButton.Enabled = text1.HasValidValue && text2.HasValidValue`). With this approach you need to add special code for each "Cancel button" *(in my case there is no code)* and code for Form Closing. And that for all the forms... – Julo May 12 '18 at 14:35
  • @Julo: we're gonna get yelled out for a long conversation :-). Since a user can only enter data into one field at a time, I see just one Cancel button for the entire form. I agree with you otherwise – VA systems engineer May 12 '18 at 14:39
  • I used only the `_Validating` to corect the answer, instead of `TextChanged` and its exactly what i needed. Thanx a lot for this easy and practical answer. – Amine Fellous May 12 '18 at 14:48
1

The simple answer is: (C# 7 style)

public partial class Form1 : Form
{
  public Form1()
  {
    InitializeComponent();
    this.textBox1.TextChanged += TextBox1_TextChanged;
    this.textBox1.Leave += TextBox1_Leave;
  }

  private void TextBox1_TextChanged(object sender, EventArgs e)
  {
    string text = this.textBox1.Text;
    if (!int.TryParse(text, NumberStyles.Integer, CultureInfo.CurrentCulture, out int number))
    {
      this.textBox1.Text = "";
      return;
    }

    if (number > 32)
    {
      this.textBox1.Text = "32";
    }
  }

  private void TextBox1_Leave(object sender, EventArgs e)
  {
    string text = this.textBox1.Text;
    if (!int.TryParse(text, NumberStyles.Integer, CultureInfo.CurrentCulture, out int number))
    {
      this.textBox1.Text = "8";
      return;
    }

    if (number > 32)
    {
      this.textBox1.Text = "32";
    }

    if (number < 8)
    {
      this.textBox1.Text = "8";
    }
  }

I standardly do this with controlling the pressed keys and text changes (inclusive paste) to check correct content of the window. Unfortunately I have the code only for Borland C++ Builder and VS6 at work. Recreating this code is not that simple (too much code), therefore only the simple answer.

Julo
  • 1,102
  • 1
  • 11
  • 19
1

Use Microsoft's Reactive Framework and this becomes easy. Just do this:

private void Form1_Load(object sender, EventArgs e)
{
    IObservable<long> query =
        Observable
            .FromEventPattern<EventHandler, EventArgs>(
                h => TXBheight.TextChanged += h,
                h => TXBheight.TextChanged -= h)
            .Select(x => Observable.Timer(TimeSpan.FromMilliseconds(250.0)))
            .Switch()
            .ObserveOn(this);

    IDisposable subscription = query.Subscribe(ep =>
    {
        if (int.Parse(TXBheight.Text) < 8)
        {
            TXBheight.Text = "8";
        }
        else if (int.Parse(TXBheight.Text) > 32)
        {
            TXBheight.Text = "32";
        }
    });
}

Now there is a 250.0 millisecond delay after the last character is typed before your code runs. If a new character is typed before the 250.0 milliseconds is up then a new timer starts and the old one doesn't fire.

The .ObserveOn(this) code marshalls the timer back to the UI thread.

Just NuGet "System.Reactive" and "System.Reactive.Windows.Forms". Also add using System.Reactive.Linq; at the top of your class.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • Elegant (even if its a little bit of a solution looking for a problem), but I think an example of the obvious failure of the "timer" approach is a zip code field. I enter the 1st digit of a zip code, realize that I don't know what the zip code is, bring up a browser to find the zip code, and meanwhile, the app has "timed out" on me even though I'm not done entering data. This is annoying :-) – VA systems engineer May 12 '18 at 13:58
  • @NovaSysEng - Thank you. I agree it can be annoying, but why does this apply to a zip code more than anything else? – Enigmativity May 13 '18 at 06:49
  • Zip code is just an example. I just think that using the "timer approach" to solve the problems caused by using the `TextChanged` event to validate user input just results in more problems. See my answer to this question - I think using the `Validating` and `Validated` events are a better solution. You just have to include the code that allows the user to cancel the operation even if there is invalid input. – VA systems engineer May 13 '18 at 10:38
  • @NovaSysEng - Calling `subscription.Dispose()` will cancel the code. The OP did ask about making a delay. That's what my answer was targeting. – Enigmativity May 13 '18 at 10:47
0

You can use the return as your breakpoint, when the user hit enter then you run your code.

You can use that with the KeypressEvent.

private void textBox1_KeyPress(object sender, KeyPressEventArgs e)
{
    char ch = e.KeyChar; // Getting the Key that was pressed

    if (ch == 13) // Checking if it equal to 13 ASCII code for return 
    {
        if (int.Parse(textBox1.Text) < 8)
        {
            textBox1.Text = ""; // emptying the textbox
            textBox1.AppendText("8"); // using AppendText() to keep the cursor at the end
        }
        else if (int.Parse(textBox1.Text) > 32)
        {
            textBox1.Text = "";
            textBox1.AppendText("32");
        }
        e.Handled = true; // To say that the event was handled.
    }
}
Rainbow
  • 6,772
  • 3
  • 11
  • 28
  • Good one, work fine. But will keep looking for a more fluid way, without the need to keep pressing enter. – Amine Fellous May 12 '18 at 12:08
  • What you're trying to do is impossible, you'll have to use some type of trigger, because a single digit number is the same as a two or three digits number, you can't know what the user is about to insert. – Rainbow May 12 '18 at 12:12
-2

Why not create task and check if it is completed before executing?

private Task task; //declare it at the top

private void TXBheight_TextChanged(object sender, EventArgs e)
{
   if(task?.Status == TaskStatus.Running) return;

   task =  Task.Run( () =>
   {
        if(int.Parse(TXBheight.Text) < 8)
        {
            TXBheight.Text = "8";
        }
        else if (int.Parse(TXBheight.Text) > 32)
        {
            TXBheight.Text = "32";
        } 
   });
}
keysl
  • 2,127
  • 1
  • 12
  • 16