-1

I will need some kind of debounce event functionality, but I have a feeling I need to run the first object event it hit on (in cases where I just click arrow once, and it jump down one object), and everyone between the first and the last will be bounced off, and the last will be invoked (in cases where I finally stop scrolling down with the arrow.) How would you solve this?

    private DateTime lastEvent = DateTime.Now;
    private readonly TimeSpan EventDelay = TimeSpan.FromMilliseconds(500);

    private async Task SelectionChanged(object sender, EventArgs e)
    {
        if (DateTime.Now - lastEvent > EventDelay)
        {
           // DO STUFF
        }

        lastEvent = DateTime.Now;
    }
  • What have you tried already? Stack Overflow is not really a place to ask and answer hypothetical questions, but rather to look for answers of a concrete problem – MindSwipe Jul 03 '20 at 07:08
  • It sounds like [data virtualization](https://stackoverflow.com/q/981040/1997232) to me. – Sinatr Jul 03 '20 at 07:08
  • With that being said, I would use some sort of countdown. Say the user presses arrow down, that would trigger the event, but before actually executing anything I'd wait for something like ~500ms, and if the user presses arrow down again within that timeframe the countdown would restart . You could then implement something that would instantly execute the event on the first press, so when the timer isn't counting down – MindSwipe Jul 03 '20 at 07:11
  • @MindSwipe I tried something like this, I've updated the thread. Possible if you could provide a code example? – Bladeluster Jul 03 '20 at 07:14
  • Are you using the MVVM design pattern or code behind for this event? – MindSwipe Jul 03 '20 at 07:23
  • I'm using MVVM, so I don't have any code behind file for the view.xaml files – Bladeluster Jul 03 '20 at 07:46

2 Answers2

1

I use the debounce Operator from Rx.NET to solve the this kind of issues. From the docs:

Debounce - only emit an item from an Observable if a particular timespan has passed without it emitting another item

Note: the debounce operator is called Throttle in the .NET Version. The nuget package you need to add is called System.Reactive

You can create an Observable object from the SelectionChanged event of your ListView/GridView. Here is a small example:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        ListView.ItemsSource = new[] { "One", "Two", "Three", "Four" };

        var selectionChangedObservable = Observable.FromEventPattern
            <SelectionChangedEventHandler, SelectionChangedEventArgs>(
                h => ListView.SelectionChanged += h,
                h => ListView.SelectionChanged -= h);

        var debouncedObservable = selectionChangedObservable
            .Throttle(TimeSpan.FromSeconds(1))
            .ObserveOnDispatcher();

        debouncedObservable.Subscribe(SelectionChangedDebounced);
    }

    private void SelectionChangedDebounced(EventPattern<SelectionChangedEventArgs> ep)
    {
        Console.WriteLine($"Item {ListView.SelectedItem} selected");
    }
}

Thats the XAML part:

    <ListView x:Name="ListView" Margin="10">
        <ListView.View>
            <GridView>
                <GridViewColumn Header="Name" Width="120" DisplayMemberBinding="{Binding}" />
            </GridView>
        </ListView.View>
    </ListView>

With this code, every following SelectionChanged event is ignored until there was no event for 1 second. After one second, the SelectionChangedDebounced(...) method is called for the last item that has been selected.

Update: Using Throttle in your ViewModel

You can use this approach with any event. But your event handler delegate cannot have a return type. Be careful: When you call OnSelection?.DynamicInvoke(this, EventArgs.Empty); you dont await any of the returned Tasks! This can lead to all sorts of problems.

I think the best way would be to change the delegate type of your event handler from AsyncHandler to EventHandler. Then you can use Observable.FromEventPattern<EventHandler, EventArgs>(h => OnSelection += h, h => OnSelection -= h)... in your viewmodel, just like the example above shows. If you cant change the delegate type (maybe its from an external dependency), you could try using a Subject as a proxy:

class MainWindowViewModel
{
    private readonly Subject<Unit> _selectionChangedSubject;
    private string _selectedItem;

    // Note: Event delegates should not return values! -> use void instead of Task.
    public delegate Task AsyncHandler(object sender, EventArgs args);
    
    public event AsyncHandler OnSelection;

    public List<string> Items => new List<string>() { "One", "Two", "Three", "Four" };

    public string SelectedItem
    {
        get { return _selectedItem; }
        set { _selectedItem = value; RaiseEvent();  }
    }

    public MainWindowViewModel()
    {
        _selectionChangedSubject = new Subject<Unit>();
        _selectionChangedSubject
            .Throttle(TimeSpan.FromSeconds(1))
            .Subscribe(OnSelectionChangedDebounced);
        OnSelection += OnSelectionChanged;
    }

    private Task OnSelectionChanged(object sender, EventArgs args)
    {
        _selectionChangedSubject.OnNext(Unit.Default);
        return Task.CompletedTask;
    }

    private void OnSelectionChangedDebounced(Unit _)
    {
        Console.WriteLine($"Item selected: {SelectedItem}");
    }

    private void RaiseEvent()
    {
        // !! This does not await any of the returned Tasks!
        OnSelection?.DynamicInvoke(this, EventArgs.Empty);
    }
}
Tobias Hoefer
  • 1,058
  • 12
  • 29
  • Thanks for the answer... I'm doing the MVVM approach, is it possible to put this in the viewmodel instead? @TobiasHoefer – Bladeluster Jul 03 '20 at 07:47
  • My event is also of this type: How would it be if I adapt it to work with this? public delegate Task AsyncHandler(object sender, EventArgs args); public event AsyncHandler OnSelectionChanged; – Bladeluster Jul 03 '20 at 08:08
  • I updated the thread, I cannot use ListView.SelectionChanged :/ – Bladeluster Jul 03 '20 at 08:32
  • Yes you can use the code in your viewmodel. The problem is the delegate type of your event. Event delegates should not have a return type. You would need to update your event delagate from 'Task AsyncHandler(object, EventArgs)' to 'void AsyncHandler(object EventArgs)'. But then of course, its just the simple 'EventHandler' delegate. I will add a ViewModel example to my answer. – Tobias Hoefer Jul 03 '20 at 12:06
  • I think I will change to EventHandler instead then. Would you mind change your version to a EventHandler one? That fixes the non awaiting dynamic invoke? – Bladeluster Jul 03 '20 at 15:08
  • Could you explain my why I have to do Task.CompletedTask? Because I think I never did it in any of my async task methods... – Bladeluster Jul 03 '20 at 15:12
  • maybe you could show me how you would have done it to make it more correct, as I understood, my approach is not the best, as I'm using this asyncHandler – Bladeluster Jul 03 '20 at 15:25
  • what is the idea of using += -= part of the assignment of h do in this case? – Bladeluster Jul 05 '20 at 09:33
0

The simple pattern to use is to add a task.delay in your event and ignore any old ones until you run out of new ones.

You've comnplicated this in your basic design because you don't pass what is selected and hence what to work with into your handler. There is no difference between your events. They're all just "something happened" and then another "something happened".

If you consider a mouse move debounce, this may be clearer:

    private Point lastPosition = new Point();
    private async void ShaperCanvas_MouseMove(object sender, MouseEventArgs e)
    {
        await Task.Delay(200);
        Point pt = e.GetPosition(ShaperCanvas);
        if (lastPosition == pt)
        {

Here we can see if the user stopped moving the mouse.

In your code it seems you will want a LastSelectedItem instead. So your logic will be stash what's selected locally. Wait a bit. See if what is selected is different from what you started with. If it is different then there's another event happened so exit.

You may also want an isbusy flag so nothing can happen whilst one of these is being processed.

Something like:

private async Task SelectionChanged(object sender, EventArgs e)
{
    var LastSelectedItem = CurrentSelectedItem;
    await Task.Delay(200);
    if(CurrentSelectedItem != LastSelectedItem)
    { 
         resume;
    }

    // DO STUFF
Andy
  • 11,864
  • 2
  • 17
  • 20