使用 MVVM 在 WPF 中阻止并行任务 UI 输入

Parallel task blocking UI input in WPF using MVVM

提问人:Amparo Walter 提问时间:11/17/2023 更新时间:11/17/2023 访问量:51

问:

我遇到了一个(可能不是这样)奇怪的问题,我正在更新物理(更新位置),但似乎无法再移动我的相机(或任何其他用户输入)。

当按下“播放”按钮时,主循环被调用,该按钮只是启动物理引擎。但是,当位置数据更新(通过引擎)时,我想更新场景中的所有模型。我读过一些关于 Dispacher 的文章,它可以更新模型,并且没有我尝试访问其他线程正在使用的错误。SetViewport()

模型会以应有的方式更新,并且 Dispatcher 运行良好(或不工作,idk)

问题是我似乎无法再注册用户输入了。

奇怪的是,在调试模式下(我使用的是Visual Studio),它有时会注册。我不知道为什么。

  • 用于处理键盘输入的代码在 MazeView 中处理。
  • 用于处理“play”命令的代码是 MazeModelView 中的处理程序(使用 ICommand) 该函数启动一个新任务,该任务启动无限游戏循环。(我打算有一个暂停按钮)
  • 引擎逻辑在“逻辑”项目中处理。

这是我的代码:

MazeView.cs

        public MazeView()
        {

            InitializeComponent();
            //logic = MazeViewModel.Logic;
            Viewport.PreviewKeyDown += new KeyEventHandler(OnKeyDownHandler);
            Viewport.Focusable = true;
            Viewport.Focus();

            //subscribe to event when maze is updated.
            //Update gets called when maze is generated or changed.
            MazeViewModel.MazeUpdated += SetViewport;
            if(MazeViewModel.CurrentInstance == null)
            {
                throw new NullReferenceException("Could not find viewmodel!");
            }
            model = MazeViewModel.CurrentInstance;
        }
/*
* .... rest of code
*/

private void OnKeyDownHandler(object sender, KeyEventArgs e)
        {
            Key k = e.Key;
            Vector3D cameraMoveDirection = new(0, 0, 0);
            double yaw = 0, pich = 0, roll = 0;
            double zoom = 0;
            switch (k)
            {
                case Key.Z:
                    cameraMoveDirection.Z = 1;
                    break;
                case Key.S:
                    cameraMoveDirection.Z = -1;
                    break;
                case Key.Q:
                    cameraMoveDirection.X = -1;
                    break;
                case Key.D:
                    cameraMoveDirection.X = 1;
                    break;
                case Key.Space: 
                    cameraMoveDirection.Y = 1;
                    break;
                case Key.F:
                    cameraMoveDirection.Y = -1;
                    break;
                case Key.A:
                    yaw = 1;
                    break;
                case Key.E:
                    yaw = -1;
                    break;
                case Key.W:
                    zoom = 1;
                    break;
                case Key.X:
                    zoom = -1;
                    break;

            }
            model.MoveCamera(cameraMoveDirection);
            model.RotateCamera(yaw, pich, roll);
            model.ZoomCamera(zoom);
            e.Handled = true;
        }

        private void SetViewport()
        {
            Viewport.Children.Clear();

            //lighting
            DirectionalLight light = new()
            {
                Color = Colors.White,
                Direction = new Vector3D(-1, -1, -1)
            };

            Model3DGroup lightGroup = new();
            lightGroup.Children.Add(light);

            AmbientLight ambLight = new()
            {
                Color = new Color()
                {
                    R = 88,
                    G = 88,
                    B = 88
                }
            };
            lightGroup.Children.Add(ambLight);

            Viewport.Children.Add(new ModelVisual3D() { Content = lightGroup });

            Model3DGroup modelGroup = new();
            //models
            foreach (Model3D model in MazeViewModel.Models)
            {
                modelGroup.Children.Add(model);
            }
            Viewport.Children.Add(new ModelVisual3D { Content = modelGroup });
        }
}

MazeViewModel.cs

/*
* .... rest of code
*/
        public void OnMazeUpdate()
        {
            SetModels();
            MazeUpdated?.Invoke(this, new EventArgs());
        }

        private IPhysicsEngine engine;
        private CancellationTokenSource gameLoopCancellationTokenSource;
        public void PlayGame()
        {
            if(Logic == null)
            {
                throw new NullReferenceException("Can't start game because logic is null!");
            }
            engine = Logic.GetEngine(ref _maze, ref _ball);
            gameLoopCancellationTokenSource = new();
            engine.Start();
            while(!gameLoopCancellationTokenSource.Token.IsCancellationRequested)
            {
                engine.Update();
                Application.Current.Dispatcher.Invoke(() =>
                {
                    OnMazeUpdate();
                });
            }

        }

PlayCommand.cs

    internal class PlayGameCommand : CommandBase
    {

        private readonly MazeViewModel model;
        public PlayGameCommand(MazeViewModel model) 
        {
            this.model = model;
            MazeViewModel.MazeUpdated += OnCanExecuteChanged;
        }

        private void OnCanExecuteChanged(object? sender, EventArgs e)
        {
            OnCanExecuteChanged();
        }

        public override void Execute(object? parameter)
        {
            Task.Run(() => model.PlayGame());
        }

        public override bool CanExecute(object? parameter)
        {
            return model.Maze != null;
        }
    }
C# WPF MVVM

评论

0赞 Andy 11/18/2023
两个猜测。你在某处有一个紧密的循环来刻录 ui 线程。在带有 vs attached 的调试模式下,它可能偶尔会干扰该循环以允许您输入。异步和撒一些 await task.delay(100) 可能会有所帮助。或者,但类似。也许你有太多的dispatcher.invoke在进行,以至于model.move等没有机会启动。
0赞 Amparo Walter 11/20/2023
我确实有一个相当紧密的循环,因为无限循环连续调用。这个想法是让它在“后台”做它的事情,并单独处理用户输入,以尽量减少输入延迟。你谈论异步和等待,尽管我不知道如何在这种情况下使用它。engine.update()Dispatcher.Invoke()
0赞 Andy 11/21/2023
在最简单的时候。从字面上看,无论游戏循环是什么方法,都要把“异步”这个词贴进去。用谷歌搜索一下。然后你可以添加 await task.delay(100)。learn.microsoft.com/en-us/dotnet/api/......
0赞 Amparo Walter 11/21/2023
是的,我得到了延迟,但是通过施加延迟,这有点违背了快速异步物理引擎的目的

答: 暂无答案