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);
}
}