Отменить событие / команду в шаблоне MVVM - PullRequest
1 голос
/ 14 февраля 2020

Я нахожусь в процессе создания универсального windows приложения с использованием Caliburn.Micro, к сожалению, из-за определенных аппаратных ограничений нам необходимо настроить таргетинг на 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).

В идеале я хотел бы иметь возможность регулировать / отменять это событие, чтобы элементы карты обрабатывались только тогда, когда представление не было перемещено, скажем, на 300 мс.

Я нашел статью, в которой упоминалось реализации DispatcherTimer, однако элементы этого кода, такие как DispatcherPriority и Dispatcher, по-видимому, не доступны в UWP, поэтому, если не существует альтернатив, я не думаю, что это будет работать.

I Я взглянул на System.Reactive, но это казалось исключительно сложным для того, чего я хочу достичь.

Любые указатели будут очень признательны!

1 Ответ

1 голос
/ 15 февраля 2020

Это можно сделать несколькими способами.

  1. Реактивные расширения

Требуемое поведение может быть достигнуто с помощью оператора Throttle.

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 обработчик событий выполняется только один раз. Это когда интервал прошел после , мы прекращаем движение.

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");
    }
}
Что-то нестандартное

Наш пользовательский класс Throttle отслеживает последние sender и args, которые были обработаны. Обрабатывается как в «передано на 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. Логика c, которая должна быть вызвана событием, содержится внутри ViewModel, но View и ViewModel должны быть полностью отделены.

Есть пара вещей, которые я хотел бы упомянуть:

  • Вы правы насчет необходимости разделения интересов, но многим разработчикам неясно, что именно это влечет за собой. Модель представления должна полностью не знать, кто слушает, в этом нет никаких сомнений. Но представление зависит от модели представления, чтобы получить свои данные, так что представление может знать о модели представления. Проблема, скорее, заключается в том, чтобы сделать это в слабосвязанной форме, ie. использование привязок и контрактов вместо прямого доступа к членам модели представления.
  • Вот почему мне не особенно нравятся действия Калибурна. С cal:Message.Attach нет контракта (например, ICommand) для отделения синтаксиса представления от модели представления. Конечно, в игре есть привязки, поэтому вы по-прежнему получаете разделенные слои MVVM.

Короче говоря, есть причина, по которой люди выбирают ReactiveUI вместо Rx. NET для разработки WPF. Из кода представления (_.xaml.cs) он дает вам доступ к:

  • Поддержка ViewModel
  • Система привязки, чтобы все это было свободно связано

И, конечно, ReactiveCommands, что также пригодится в вашем случае использования.

Заключительные мысли, если ваш вид имеет тот же срок службы, что и модель вашего вида (ie они располагаются вместе), вы можете прагматически c об этом и получить модель представления через DataContext вашего представления.

...