如何使用 Reactive UI 在单击按钮与按下按钮(按住)后触发不同的操作 [英] How to use Reactive UI to trigger a different action following a button click vs button press (hold)

查看:38
本文介绍了如何使用 Reactive UI 在单击按钮与按下按钮(按住)后触发不同的操作的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在尝试实现一个 UI 控件,用户可以在其中单击按钮以稍微移动某个东西,或者按住按钮并在按住按钮的同时让该东西移动.

I'm trying to implement a UI control where the user can click a button to have a thing move by a little, or hold the button down and have the thing move while the button is held down.

假设我有 TaskStartMove(), TaskStopMove()TaskMoveStep().按钮点击应该执行 MoveStep()按住按钮应开始移动,然后在释放按钮时立即停止移动.移动发生时应忽略快速点击(双击),并且每秒发送的 MoveStep 命令不应超过 2x.还需要一些故障安全措施,可以在出现错误或长时间超时(例如 5 分钟)后停止移动.

Let's say I have Task<Unit> StartMove(), Task<Unit> StopMove() and Task<Unit> MoveStep(). The button click should perform the MoveStep() and the button hold should start the move and then stop the move immediately when the button is released. Rapid clicks (double clicks) should be ignored while the move is happening and there should not be more than 2x MoveStep commands sent per second. There also needs to be some fail safe which stops the move on an error or after a long timeout of let's say 5 mins.

按钮按下由 Button 对象上的一个属性表示,当用户按下按钮时触发 true 值,释放按钮时触发 false 值,这值在常规 WPF 按钮上称为 IsPressed.真值后跟假值小于一秒表示点击,真值后跟假值多于一秒后表示保持(该秒值也可以调整为半秒).

The button press is represented by a property on the Button object, which fires a true value when the user presses the button and a false when it is released, this value is called IsPressed on the regular WPF button. A true value followed by a false value less than a second later represents a click and a true value followed by a false value more than a second later represents a hold (this sec value could also be tuned to half a second).

问题归结为获取以随机间隔到达的此类真/假值流(想想:猴子正在随机按下按钮)并从该流中确定按钮是被点击还是被按住.基于此,应该触发操作:MoveStep 用于单击和 StartMove 然后 StopMove 用于按钮保持.

The question boils down to taking a stream of such true / false values that arrive at a random interval (think: The monkey is pressing the button randomly) and determining from this stream if the button is clicked or held down. Based on this, the actions should be triggered: MoveStep for a click and StartMove then StopMove for a button hold.

我终于找到了一些有用的东西.

I finally got something together that kinda works.

到目前为止我有 MainWindow

So far I have MainWindow

public partial class MainWindow : Window, IViewFor<AppViewModel>
{
    public AppViewModel ViewModel { get; set; }
    object IViewFor.ViewModel { get => ViewModel; set => ViewModel = value as AppViewModel; }

    public MainWindow()
    {
        ViewModel = new AppViewModel();
        DataContext = ViewModel;
        InitializeComponent();

        this.WhenAnyValue(x => x.MoveLeftButton.IsPressed).InvokeCommand(this, x => x.ViewModel.MoveLeftCommand);

    }

    protected override void OnClosing(CancelEventArgs e)
    {
        ViewModel.Dispose();
        base.OnClosing(e);
    }
}

一个 AppViewModel

An AppViewModel

public class AppViewModel : ReactiveObject, IDisposable
{
    public ReactiveCommand<bool, bool> MoveLeftCommand { get; protected set; }

    public AppViewModel()
    {
        MoveLeftCommand = ReactiveCommand.CreateFromTask<bool, bool>(isPressed => _MoveLeft(isPressed));

        MoveLeftCommand.Buffer(TimeSpan.FromMilliseconds(500))
            .Do(x => _InterpretCommand(x))
            .Subscribe(x => Console.WriteLine($"{TimeStamp} {string.Join(",", x)}"))

    }

    private Task<bool> _MoveLeft(bool isPressed)
    {
        return Task.Run(() => isPressed); // Just to set a breakpoint here really
    }

    private static void _InterpretCommand(IList<bool> listOfBools)
    {
        if (listOfBools == null || listOfBools.Count == 0)
        {
            return;
        }

        if (listOfBools.First() == false)
        {
            Console.WriteLine("Stop move");
            return;
        }

        if (listOfBools.Count == 1 && listOfBools.First() == true)
        {
            Console.WriteLine("Start move");
            return;
        }

        if (listOfBools.Count >= 2)
        {
            Console.WriteLine("Click move");
            return;
        }
    }
}

而我的 MainWindow.xaml 真的只是

And my MainWindow.xaml is really just

        <Button x:Name="MoveLeftButton" Content="Left"/>

随机序列示例

        var rands = new Random();
        rands.Next();


        var better = Observable.Generate(
            true,
            _ => true,
            x => !x,
            x => x,
            _ => TimeSpan.FromMilliseconds(rands.Next(1000)))
            .Take(20);

        better.Buffer(TimeSpan.FromMilliseconds(500))
            .Do(x => _InterpretCommand(x))
            .Subscribe(x => Console.WriteLine($"{TimeStamp} {string.Join(",", x)}"));

    static string TimeStamp => DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff", CultureInfo.InvariantCulture);

这会产生输出

2017-10-06 19:11:54.231 
Start move
2017-10-06 19:11:54.720 True
2017-10-06 19:11:55.220 
Stop move
2017-10-06 19:11:55.719 False,True
Stop move
2017-10-06 19:11:56.221 False
Start move
2017-10-06 19:11:56.719 True
Stop move
2017-10-06 19:11:57.222 False
2017-10-06 19:11:57.719 
Start move
2017-10-06 19:11:58.220 True
Stop move
2017-10-06 19:11:58.720 False
2017-10-06 19:11:59.219 
Click move
2017-10-06 19:11:59.719 True,False
2017-10-06 19:12:00.217 
Start move
2017-10-06 19:12:00.719 True
Stop move
2017-10-06 19:12:01.221 False
Click move
2017-10-06 19:12:01.722 True,False
Start move
2017-10-06 19:12:02.217 True
2017-10-06 19:12:02.722 
Stop move
2017-10-06 19:12:03.220 False
2017-10-06 19:12:03.720 
Start move
2017-10-06 19:12:04.217 True
Stop move
2017-10-06 19:12:04.722 False
Start move
2017-10-06 19:12:05.220 True
Stop move
2017-10-06 19:12:05.516 False

推荐答案

从这个答案中得到洞察:https://stackoverflow.com/a/46629909/377562我把一些非常有效的东西串在一起!

With insight from this answer: https://stackoverflow.com/a/46629909/377562 I stringed together something that works great!

BufferWithClosingValue 来自链接的答案:

public static IObservable<IList<TSource>> BufferWithClosingValue<TSource>(
    this IObservable<TSource> source, 
    TimeSpan maxTime, 
    TSource closingValue)
{
    return source.GroupByUntil(_ => true,
                               g => g.Where(i => i.Equals(closingValue)).Select(_ => Unit.Default)
                                     .Merge(Observable.Timer(maxTime).Select(_ => Unit.Default)))
                 .SelectMany(i => i.ToList());
}

随机序列示例:

var alternatingTrueFalse = Observable.Generate(
    true,
    _ => true,
    x => !x,
    x => x,
    _ => TimeSpan.FromMilliseconds(new Random().Next(1000)))
    .Take(40).Publish().RefCount();

var bufferedWithTime = alternatingTrueFalse.BufferWithClosingValue(TimeSpan.FromMilliseconds(500), false);

var clicks = bufferedWithTime.Where(x => x.Count() == 2).ThrottleFirst(TimeSpan.FromMilliseconds(500));
var holdStarts = bufferedWithTime.Where(x => x.Count() == 1 && x.First() == true);
var holdStops = bufferedWithTime.Where(x => x.Count() == 1 && x.First() == false);

clicks.Select(_ => "Click").DumpTimes("Clicks");
holdStarts.Select(_ => "Hold Start").DumpTimes("Hold Start");
holdStops.Select(_ => "Hold Stop").DumpTimes("Hold stop");

使用此答案中的 ThrottleFirst/SampleFirst 实现:https:///stackoverflow.com/a/27160392/377562

Using the ThrottleFirst / SampleFirst implementation from this answer: https://stackoverflow.com/a/27160392/377562

示例输出

2017-10-08 16:58:14.549 - Hold Start-->Hold Start :: 6
2017-10-08 16:58:15.032 - Hold stop-->Hold Stop :: 7
2017-10-08 16:58:15.796 - Clicks-->Click :: 7
2017-10-08 16:58:16.548 - Clicks-->Click :: 6
2017-10-08 16:58:17.785 - Hold Start-->Hold Start :: 5
2017-10-08 16:58:18.254 - Hold stop-->Hold Stop :: 7
2017-10-08 16:58:19.294 - Hold Start-->Hold Start :: 8
2017-10-08 16:58:19.728 - Hold stop-->Hold Stop :: 7
2017-10-08 16:58:20.186 - Clicks-->Click :: 6

这似乎没有我在其他一些尝试中遇到的竞争条件问题,所以我喜欢它!

This doesn't seem to have any race condition problems that I've had with some other attempts at solving this, so I like it!

这篇关于如何使用 Reactive UI 在单击按钮与按下按钮(按住)后触发不同的操作的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆