4

What I have is a string property for a username, connected to a text entry through MVVM, that whenever it is set, I call a method that checks the server to see if the username is available. Now I don't want this being called on every single time a key is typed, what I want instead is for it to detect when the user has stopped typing.

Currently this is my code...

private string _username;
public string Username
{
    get => _username;
    set
    {
        SetProperty(ref _username, value);
        Task.Run(async () => await CheckUsernameExists(1000));
    }
}

Then the CheckUsernameExists() method...

/// <summary>
/// Checks if the username already exists
/// </summary>
/// <returns></returns>
public async Task CheckUsernameExists(int timeoutInMilliseconds)
{
    await Task.Delay(timeoutInMilliseconds);

    try
    {...

However this does not retrigger it, it just delays the call by 1 second.

I got the idea for a retriggerable delay from Unreal Engine 4 https://docs.unrealengine.com/en-US/BlueprintAPI/Utilities/FlowControl/RetriggerableDelay/index.html

What happens with the UE4 one is, the function is called once. Then it starts the timer on the retriggerable delay. If the function is called again while that timer is running, it restarts the timer. Then, only when it finishes, does it call the code after it.

Does anyone know how to do this in C#? Thanks!

div
  • 905
  • 1
  • 7
  • 20
Lewis
  • 566
  • 4
  • 21

3 Answers3

2

The standard way to do it is to use CancellationTokenSource objects. Create a new one every time the property changes, after cancelling the previous one.

private string _username;
public string Username
{
    get => _username;
    set
    {
        SetProperty(ref _username, value);
        CheckUsernameExistsAsync(value, 1000);
    }
}

private CancellationTokenSource _cts;

private async void CheckUsernameExistsAsync(string username, int timeout)
{
    try
    {
        _cts?.Cancel();
        _cts = new CancellationTokenSource();
        var cancellationToken = _cts.Token;
        await Task.Delay(timeout, cancellationToken);
        // Check if the username exists
        // Use the same cancellationToken if the API supports it
    }
    catch (TaskCanceledException)
    {
        // Ignore the exception
    }
    catch (Exception)
    {
        // Log the exception
    }
}

Caveat: the class CancellationTokenSource is disposable, but I didn't dispose it in the example above. I am not certain if this is a big deal or not. Probably it isn't. Here is a relevant question: When to dispose CancellationTokenSource?

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
1

Here is the code which starts delayed check on each type, but only last call actually checks the user name:

private int _checkUsernameCalls = 0;
public string Username
{
    get => _username;
    set
    {
        SetProperty(ref _username, value);
        _checkUsernameCalls++;
        Task.Delay(TimeSpan.FromSeconds(1)).ContinueWith(t => CheckUsernameExists());
    }
}

void CheckUsernameExists()
{
    if (--_checkUsernameCalls > 0)
    {
        return;
    }
    // actual checks...
}
tgralex
  • 794
  • 4
  • 14
  • `Note:` You probably need to use Lock() while accessing _checkUsernameCalls variable (or make it thread safe property), since it will be multi-treading environment. – tgralex Aug 26 '19 at 21:14
1

...what I want instead is for it to detect when the user has stopped typing.

To call your function that checks the server only after the user stop typing it's actually a pretty simple logic. You can do this by starting a System.Timers.Timer whenever the user type a character in your control (let's say a text box). Set the timer to trigger Elapsed event when the timer reaches, let's say 150 milliseconds. Finally you can subscribe to the Elapsed event and assign the function that makes the request on the server. Every time the user enter a new input to the text box, the timer will be reset and you will get the behavior you want (as the Elapsed event will only be fired when the timer reaches the delay you set).

andresantacruz
  • 1,676
  • 10
  • 17