c# - MVVM 模式中的去抖动事件/命令

标签 c# mvvm uwp caliburn.micro

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

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

查看型号

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)]" />
...

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

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

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

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

任何指针将不胜感激!

最佳答案

你可以通过几种方式做到这一点。

  • 响应式(Reactive)扩展

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

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

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

    我通过订阅 UIElement 进行了测试的PointerMoved事件。触发 HandleEvent如果我们继续前进,则多次。与 Throttle ,但是,事件处理程序只执行一次。这是我们停止移动后间隔过去的时间。

    主页.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");
        }
    }
    
  • 定制的东西

  • 我们的定制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");
        }
    }
    

    更新

    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.



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

  • 长话短说,人们选择 ReactiveUI 而不是 Rx.NET 进行 WPF 开发是有原因的。
    从 View 背后的代码 (_.xaml.cs) 中,您可以访问:
  • 靠山ViewModel
  • 一个保持松散耦合的绑定(bind)系统

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

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

    关于c# - MVVM 模式中的去抖动事件/命令,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60230128/

    相关文章:

    javascript - 当输入字段更新时,如何更新 knockout.js 中的 observableArray 项

    c# - 实现 CollectionChanged

    c# - ResourceDictionary Source(通用 Windows 平台)中的 Uri 语法

    c# - 在 Quartz.Net 中重构作业定义(通用函数)

    c# - 如何通过在页面上显示半透明控件来禁用Windows Phone上的所有控件?

    c# - 如何在触发器属性中基于 dotnet core 的 Azure 函数中读取 settings.json 中的配置值

    c# - 桌面桥toast打开应用程序

    sqlite - 如何在 UWP 项目中使用预先设计好的 SQLite?

    c# - 如何手动查找仅由 DataType 而不是 key 定义的 DataTemplate?

    c# - 使用 Asp.net mvc 或 web 服务会更好吗?