MVVM 模式中的去抖 event/command

Debounce event/command in MVVM pattern

我正在使用 Caliburn.Micro 构建通用 windows 应用程序,不幸的是,由于某些硬件限制,我们需要以 Windows 10 1607 为目标,因此无法实施任何包依赖于 .NET Standard / UWP 16299,这包括 ReactiveUI。

在这个特定的场景中,我有一个视图模型优先的方法,它生成一个地图(和一些其他资源),然后将它们绑定到 XAML 视图中的一个地图视图。理想情况下,我想在通过 ViewpointChanged 事件移动地图时触发一个过程。

查看模型

public class ExampleViewModel : Screen
{
    public ExampleViewModel()
    {
        Map = new Map();
    }

    public Map Map { get; set; }
    public BindableCollection<MapItems> MapItems { get; set; }

    private UpdateMapItems(Envelope visibleArea)
    {
        // The visibleArea param will include the current viewpoint of the map view
        // This method will effectively generate the appropriate map items based on the current coordinates
    }
}

查看

...
<MapView x:Name="MapView" Map="{Binding Map}" cal:Message.Attach="[Event ViewpointChanged] = [Action UpdateMapItems(MapView.VisibleArea.Extent)]" />
...

现在这个技术上可行,但有一个主要缺陷,即地图的每次移动都会多次触发 ViewpointChanged 事件(例如与 OnMouseMove 的效果类似)。

理想情况下,我希望能够 throttle/debounce 此事件,以便仅当视图未移动 300 毫秒时才处理地图项。

我找到了一篇涉及实现 DispatcherTimer 的文章,但是 UWP 中似乎没有此代码的元素,例如 DispatcherPriorityDispatcher,因此除非存在替代方案,我认为这行不通。

我看过 System.Reactive,但这对于我要实现的目标来说似乎异常复杂。

任何指点将不胜感激!

您可以通过多种方式完成此操作。

  1. 响应式扩展

可以使用 Throttle 运算符实现所需的行为。

Observable
.FromEventPattern<EventArgs>(MapView, nameof(ViewpointChanged));
.Throttle(TimeSpan.FromMilliSeconds(300));
.Subscribe(eventPattern => vm.UpdateMapItems(eventPattern.Sender.VisibleArea.Extent));

当使用 FromEventPattern 时,我们将事件映射到 EventPattern 的实例,其中包括事件的 Sender(源)。

我通过订阅 UIElementPointerMoved 事件进行了测试。如果我们继续移动,它会多次触发 HandleEvent。但是,对于 Throttle,事件处理程序只执行一次。这是当间隔过去 之后 我们停止移动。

MainPage.xaml

<Page
    x:Class="..."
    ...
    >
    <Grid>
        <Button x:Name="MyUIElement" Content="Throttle Surface"
                Height="250" Width="250" HorizontalAlignment="Center"/>
    </Grid>
</Page>

MainPage.xaml.cs

public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();

        Observable
            .FromEventPattern<PointerRoutedEventArgs>(MyUIElement, nameof(UIElement.PointerMoved))
            .Throttle(TimeSpan.FromMilliseconds(300))
            .Subscribe(eventPattern => HandleEvent(eventPattern.Sender, eventPattern.EventArgs));
    }

    private void HandleEvent(object source, PointerRoutedEventArgs args)
    {
        Debug.WriteLine("Pointer Moved");
    }
}
  1. 一些习惯

我们的自定义 Throttle class 跟踪最后处理的 senderargs。处理如 "passed to Throttle for processing"。只有当计时器结束并且没有其他事件发生时,eventHandler(作为构造函数参数传递)才真正执行。

public class Throttle<TEventArgs>
{
    private readonly DispatcherTimer _timer;
    private object _lastSender;
    private TEventArgs _lastEventArgs;

    public Throttle(EventHandler<TEventArgs> eventHandler, TimeSpan interval)
    {
        _timer = new DispatcherTimer
        {
            Interval = interval
        };
        _timer.Tick += (s, e) =>
        {
            _timer.Stop();
            eventHandler(_lastSender, _lastEventArgs);
        };
    }

    public void ProcessEvent(object sender, TEventArgs args)
    {
        _timer.Stop();
        _timer.Start();

        _lastSender = sender;
        _lastEventArgs = args;
    }
}

MainPage.xaml.cs

public sealed partial class MainPage : Page
{
    private readonly Throttle<PointerRoutedEventArgs> _throttle;

    public MainPage()
    {
        this.InitializeComponent();

        var interval = TimeSpan.FromMilliseconds(300);
        _throttle = new Throttle<PointerRoutedEventArgs>(HandleEvent, interval);
        MyUIElement.PointerMoved += (sender, e) => _throttle.ProcessEvent(sender, e);
    }

    private void HandleEvent(object sender, PointerRoutedEventArgs e)
    {
        Debug.WriteLine("Pointer Moved");
    }
}

更新

I'm struggling to work out how everything fits together in an MVVM environment. The logic that needs to be triggered by the event is contained within the ViewModel but the View and ViewModel should be entirely separate.

有几件事我想提一下:

  • 关于关注点分离的必要性,您是对的,但许多开发人员不清楚这到底意味着什么。视图模型应该完全不知道谁在听,这一点毫无疑问。但是视图依赖于视图模型来获取它的数据,所以视图知道视图模型是可以的。问题更多是关于以松散耦合的方式这样做,即。使用绑定和契约而不是直接访问视图模型成员。
  • 这就是我不是特别喜欢 Caliburn 的 Actions 的原因。使用 cal:Message.Attach 没有合同(例如 ICommand)将视图语法与视图模型分离。当然,有绑定在起作用,因此您仍然可以获得解耦的 MVVM 层。

长话短说,人们选择 ReactiveUI 而不是 Rx.NET 进行 WPF 开发是有原因的。 从后面的视图代码 (_.xaml.cs) 中,您可以访问:

  • 靠山ViewModel
  • 保持松散耦合的绑定系统

当然还有 ReactiveCommands,这在您的用例中也会派上用场。

最后的想法,如果您的视图与您的视图模型具有相同的生命周期(即它们被处置在一起),您可以务实一点并通过视图的 DataContext 获取视图模型。