【问题标题】:Debounce event/command in MVVM patternMVVM 模式中的去抖动事件/命令
【发布时间】:2020-05-30 11:27:41
【问题描述】:

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

在这个特定的场景中,我有一个视图模型优先的方法,它生成一个地图(和一些其他资源),然后将它们绑定到 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 的效果)。

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

我发现一篇涉及实现 DispatcherTimer 的文章,但是此代码的元素,例如 DispatcherPriorityDispatcher 在 UWP 中似乎不可用,因此除非存在替代方案,否则我认为这不会工作。

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

任何指针将不胜感激!

【问题讨论】:

  • 看看ReactiveUI 和该页面上的示例(右上角)
  • 不幸的是,ReactiveUI 依赖于 UWP 10.0.16299,因为应用程序需要在 10.0.14393 (LTSB) 的设备上运行,因此我们无法满足这一要求。

标签: c# mvvm uwp caliburn.micro


【解决方案1】:

您可以通过多种方式做到这一点。

  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 类跟踪已处理的最后一个 senderargs。处理方式如“传递给Throttle 进行处理”。只有当计时器结束并且没有发生其他事件时,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");
    }
}

更新

我正在努力弄清楚一切如何在 MVVM 环境中组合在一起。需要由事件触发的逻辑包含在 ViewModel 中,但 View 和 ViewModel 应该完全分开。

我想提几点:

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

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

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

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

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

【讨论】:

  • 谢谢!反应式方法几乎是完美的,但我正在努力弄清楚一切如何在 MVVM 环境中组合在一起。需要由事件触发的逻辑包含在 ViewModel 中,但 View 和 ViewModel 应该完全分开。
  • 谢谢,这真的很有帮助。 ReactiveUI 出现了很多,不幸的是 1607 构建要求意味着我不能在这个项目中使用它。我确实考虑过另一种方法,它涉及使用 CM 的 message.attach 来触发 ViewModel 中的属性设置器,然后用节流阀观察属性以触发方法,不确定在这种情况下这是否是更好的方法。
猜你喜欢
  • 2017-04-21
  • 2015-04-12
  • 1970-01-01
  • 2023-03-28
  • 2016-07-24
  • 1970-01-01
  • 1970-01-01
  • 2019-12-21
相关资源
最近更新 更多