0

I working on real-time search. At this moment on property setter which is bounded to edit text, I call a method which calls API and then fills the list with the result it looks like this:

    private string searchPhrase;
    public string SearchPhrase
    {
        get => searchPhrase;
        set
        {
            SetProperty(ref searchPhrase, value);

            RunOnMainThread(SearchResult.Clear);
            isAllFriends = false;
            currentPage = 0;

            RunInAsync(LoadData);
        }   
    }

    private async Task LoadData()
    {

        var response = await connectionRepository.GetConnections(currentPage, 
    pageSize, searchPhrase);

                foreach (UserConnection uc in response)
                {
                    if (uc.Type != UserConnection.TypeEnum.Awaiting)
                    {
                        RunOnMainThread(() =>
                            SearchResult.Add(new ConnectionUser(uc)));
                    }
                }
    }

But this way is totally useless because of it totally mashup list of a result if a text is entering quickly. So to prevent this I want to run this method async in a property but if a property is changed again I want to kill the previous Task and star it again. How can I achieve this?

Golden Panda
  • 83
  • 1
  • 5
  • 1
    Well that depends on `RunInAsync` which you haven't shown. Async tasks can be cancelled, so cancel the task before you start a new one. – Ashley Medway Apr 06 '18 at 11:35
  • firstly checkout Rx throttle, it is designed for this. Then as the examples show inject in a cancellation token and cancel as necessary. – InContext Apr 06 '18 at 12:11

3 Answers3

1

Some informations from this thread:

  • create a CancellationTokenSource

    var ctc = new CancellationTokenSource();
    
  • create a method doing the async work

    private static Task ExecuteLongCancellableMethod(CancellationToken token)
    {
        return Task.Run(() =>
        {
           token.ThrowIfCancellationRequested();
           // more code here
    
           // check again if this task is canceled
           token.ThrowIfCancellationRequested();
    
           // more code
         }
    }
    

It is important to have this checks for cancel in the code.

Execute the function:

var cancellable = ExecuteLongCancellableMethod(ctc.Token);

To stop the long running execution use

 ctc.Cancel();

For further details please consult the linked thread.

Butti
  • 359
  • 2
  • 6
0

This question can be answered in many different ways. However IMO I would look at creating a class that

  1. Delays itself automatically for X (ms) before performing the seach
  2. Has the ability to be cancelled at any time as the search request changes.

Realistically this will change your code design, and should encapsulate the logic for both 1 & 2 in a separate class.

My initial thoughts are (and none of this is tested and mostly pseudo code).

class ConnectionSearch
{
    public ConnectionSearch(string phrase, Action<object> addAction)
    {
        _searchPhrase = phrase;
        _addAction = addAction;
        _cancelSource = new CancellationTokenSource();
    }

    readonly string _searchPhrase = null;
    readonly Action<object> _addAction;
    readonly CancellationTokenSource _cancelSource;

    public void Cancel()
    {
        _cancelSource?.Cancel();
    }

    public async void PerformSearch()
    {
        await Task.Delay(300); //await 300ms between keystrokes
        if (_cancelSource.IsCancellationRequested)
            return;

        //continue your code keep checking for 

        //loop your dataset
        //call _addAction?.Invoke(uc);
    }
}

This is basic, really just encapsulates the logic for both points 1 & 2, you will need to adapt the code to do the search.

Next you could change your property to cancel a previous running instance, and then start another instance immediatly after something like below.

ConnectionSearch connectionSearch;
string searchPhrase;
public string SearchPhrase
{
    get => searchPhrase;
    set
    {
        //do your setter work
        if(connectionSearch != null)
        {
            connectionSearch.Cancel();
        }
        connectionSearch = new ConnectionSearch(value, addConnectionUser);
        connectionSearch.PerformSearch();
    }
}

void addConnectionUser(object uc)
{
    //pperform your add logic.. 
}

The code is pretty straight forward, however you will see in the setter is simply cancelling an existing request and then creating a new request. You could put some disposal cleanup logic in place but this should get you started.

Nico
  • 12,493
  • 5
  • 42
  • 62
0

You can implement some sort of debouncer which will encapsulate the logics of task result debouncing, i.e. it will assure if you run many tasks, then only the latest task result will be used:

public class TaskDebouncer<TResult>
{
    public delegate void TaskDebouncerHandler(TResult result, object sender);
    public event TaskDebouncerHandler OnCompleted;
    public event TaskDebouncerHandler OnDebounced;

    private Task _lastTask;
    private object _lock = new object();

    public void Run(Task<TResult> task)
    {
        lock (_lock)
        {
            _lastTask = task;
        }

        task.ContinueWith(t =>
        {
            if (t.IsFaulted)
                throw t.Exception;

            lock (_lock)
            {
                if (_lastTask == task)
                {
                    OnCompleted?.Invoke(t.Result, this);
                }
                else
                {
                    OnDebounced?.Invoke(t.Result, this);
                }
            }
        });
    }

    public async Task WaitLast()
    {
        await _lastTask;
    }
}

Then, you can just do:

private readonly TaskDebouncer<Connections[]> _connectionsDebouncer = new TaskDebouncer<Connections[]>();

public ClassName()
{
    _connectionsDebouncer.OnCompleted += OnConnectionUpdate;
}

public void OnConnectionUpdate(Connections[] connections, object sender)
{
    RunOnMainThread(SearchResult.Clear);

    isAllFriends = false;
    currentPage = 0;

    foreach (var conn in connections)
        RunOnMainThread(() => SearchResult.Add(new ConnectionUser(conn)));
}

private string searchPhrase;
public string SearchPhrase
{
    get => searchPhrase;
    set
    {
        SetProperty(ref searchPhrase, value);

        _connectionsDebouncer.Add(RunInAsync(LoadData));
    }   
}

private async Task<Connection[]> LoadData()
{
    return await connectionRepository
        .GetConnections(currentPage, pageSize, searchPhrase)
        .Where(conn => conn.Type != UserConnection.TypeEnum.Awaiting)
        .ToArray();
}

It is not pretty clear what RunInAsync and RunOnMainThread methods are.
I guess, you don't actually need them.

Yeldar Kurmangaliyev
  • 33,467
  • 12
  • 59
  • 101