【问题标题】:"Are you sure?" prompts. Part of the ViewModel or purely the view?“你确定吗?”提示。 ViewModel 的一部分还是纯粹的视图?
【发布时间】:2012-04-30 21:42:24
【问题描述】:

我一直在玩弄“你确定吗?”应该放在哪里?在我的 MVVM WPF 应用程序中键入提示。

我倾向于认为这些纯粹是视图的一部分。如果 ViewModel 公开了DeleteCommand,那么我希望该命令会立即删除。

要将此类提示集成到 ViewModel 中,它必须公开一个单独的 RequestDeleteCommand,一个用于绑定提示的 DeletePromptItem 属性,并且它还可以兼作显示提示的触发器。

即使这样,也没有什么可以阻止单元测试直接调用 DeleteCommand,除非我在 ViewModel 中放置特定逻辑以要求 DeletePromptItem 匹配作为参数提供给 DeleteCommand 的项目。

但是,在我看来,这一切都只是 ViewModel 中的噪音。提示更多的是一个用户界面问题,以防止误点击等。对我来说,这表明它应该在视图中,并带有确认提示调用 DeleteCommand。

有什么想法吗?

【问题讨论】:

  • “这个文件已经存在。你想覆盖它吗?”什么样的提示?
  • 好问题。我不知道。将不得不考虑这一点。
  • 正如我在回答中所说的那样。将应用程序逻辑放在视图模型中并使用服务来显示您的对话框。
  • 怎么样...不使用提示?它们是经典的 UX 失败之一;首先他们惹恼了你的用户,然后用户训练自己只是点击“是”。伊克。
  • @NicolasRepiquet 我遇到过这种错误:stackoverflow.com/questions/21805743/…

标签: c# wpf mvvm


【解决方案1】:

然而,在我看来,这一切都只是 ViewModel 中的噪音。提示更多的是一个用户界面问题,以防止误点击等。对我来说,这表明它应该在视图中,并带有确认提示调用 DeleteCommand。

我同意;像这样的提示应该在视图中处理,因为最终视图是用户正在查看和交互的内容,而不是视图模型。一旦您的视图从用户那里获得了应该调用 DeleteCommand 的确认,然后继续在您的视图模型中调用它。

在我看来,单元测试实际上与用户交互没有任何关系,除非您正在测试视图本身。

【讨论】:

  • 我想我改变了主意。对于有限的特定示例,我给出了仅视图方法可以正常工作,但对于任何基于逻辑的交互,例如 Nicolas 的评论,则需要涉及 ViewModel。
【解决方案2】:

我建议通过管理模式窗口的服务来执行此操作。我很久以前也遇到过这个问题。 This 博文对我帮助很大。

尽管它是一篇 Silverlight 帖子,但与 wpf 相比,它应该不会有太大差异。

【讨论】:

    【解决方案3】:

    我个人认为这只是视图的一部分,因为没有数据

    【讨论】:

      【解决方案4】:

      prompts 绝对不应该是 ViewModel 的一部分,但这并不一定意味着最好的解决方案是将它们硬编码在 View 中(尽管这是一种非常合理的第一种方法)。

      据我所知,有两种方法可以减少 View 和 ViewModel 之间的耦合:使用交互服务和触发交互请求。两者都解释得很好here;你可能想看看。

      一般的想法是你抽象异步交互是如何完成的,并使用更类似于基于事件的逻辑的东西工作同时允许 ViewModel 表达它想要与用户交互作为一个操作;最终结果是您可以记录此交互并对其进行单元测试。

      编辑:我应该补充一点,我已经探索过在原型项目中使用 Prism 4 处理交互请求,我对结果非常满意(有点框架您甚至可以完全在 XAML 中指定特定交互请求会发生什么!)。

      【讨论】:

      • +1 链接。 Prism 的方法似乎是我发现的最接近纯 MVVM 的方法,谢谢。逻辑保留在 ViewModel 中,所有界面内容都保留在同一个视图中。
      【解决方案5】:

      我通过使用EventAggregator 模式解决了这类问题。

      你可以看到它解释here

      【讨论】:

        【解决方案6】:

        我认为这取决于提示,但一般来说,需要提示用户的代码逻辑通常在视图模型中,例如用户按下按钮删除列表项,命令被触发虚拟机,逻辑运行,很明显这可能会影响另一个实体,然后用户必须选择他们想要做的事情,此时您应该无法要求视图提示用户,所以我看不到任何其他选择但要在VM中处理它。这是我一直感到不安的事情,但我只是在我的基础 VM 中编写了一个 Confirm 方法,该方法调用一个对话服务来 dsiplay 提示并返回 true 或 false:

            /// <summary>
            /// A method to ask a confirmation question.
            /// </summary>
            /// <param name="messageText">The text to you the user.</param>
            /// <param name="showAreYouSureText">Optional Parameter which determines whether to prefix the message 
            /// text with "Are you sure you want to {0}?".</param>
            /// <returns>True if the user selected "Yes", otherwise false.</returns>
            public Boolean Confirm(String messageText, Boolean? showAreYouSureText = false)
            {
                String message;
                if (showAreYouSureText.HasValue && showAreYouSureText.Value)
                    message = String.Format(Resources.AreYouSureMessage, messageText);
                else
                    message = messageText;
        
                return DialogService.ShowMessageBox(this, message, MessageBoxType.Question) == MessageBoxResult.Yes;
            }
        

        对我来说,这是我有时无法在 MVVM 中得到明确答案的灰色交叉领域之一,因此我对其他人的方法很感兴趣。

        【讨论】:

          【解决方案7】:

          看看这个:

          MVVM and Confirmation Dialogs

          我在我的视图模型中使用了类似的技术,因为我相信它是视图模型的一部分,询问它是否会继续删除,而不是任何可视对象或视图。使用所描述的技术,您的模型不会引用任何我不喜欢的视觉引用,而是引用某种调用确认对话框或消息框或其他任何东西的服务。

          【讨论】:

            【解决方案8】:

            在我看来,提示用户包括两部分:

            1. 确定是否应显示提示以及应如何处理结果的逻辑
            2. 实际显示提示的代码

            第 2 部分显然不属于 ViewModel。
            但第 1 部分确实属于那里。

            为了使这种分离成为可能,我使用了一个可供 ViewModel 使用的服务,并且我可以为此提供一个特定于我所处环境(WPF、Silverlight、WP7)的实现。

            这导致如下代码:

            interface IMessageBoxManager
            {
                MessageBoxResult ShowMessageBox(string text, string title,
                                                MessageBoxButtons buttons);
            }
            
            class MyViewModel
            {
                IMessageBoxManager _messageBoxManager;
            
                // ...
            
                public void Close()
                {
                    if(HasUnsavedChanges)
                    {
                        var result = _messageBoxManager.ShowMessageBox(
                                         "Unsaved changes, save them before close?", 
                                         "Confirmation", MessageBoxButtons.YesNoCancel);
                        if(result == MessageBoxResult.Yes)
                            Save();
                        else if(result == MessageBoxResult.Cancel)
                            return; // <- Don't close window
                        else if(result == MessageBoxResult.No)
                            RevertUnsavedChanges();
                    }
            
                    TryClose(); // <- Infrastructure method from Caliburn Micro
                }
            }
            

            这种方法不仅可以很容易地用于显示消息框,还可以用于显示其他窗口,如this answer 中所述。

            【讨论】:

            • 我不确定这是否会单独关注。您现在在 ViewModel 中有一个硬编码的消息框提示。如果视图想要以其他方式进行确认,比如拖放,甚至是输入响应,该怎么办。 ViewModel 应该对这些东西一无所知。
            • 我不认为我理解你的问题。消息框是一个抽象结构。 ViewModel 没有定义它是如何实现的。 WP7 的具体实现可以显示文本并要求您将文本拖动到三个框之一上。其中一个框的含义是“是”,一个是“否”,一个是“取消”。它仍然具有相同的语义。
            • @GazTheDestroyer:重点是:(1)有条件地显示消息框 - 仅当 ViewModel 有未保存的更改时 - 以及(2) ViewModel 需要接收三种可能的结果之一并采取行动相应地。
            • 是的,我知道 ShowMessageBox 可以按照您的意愿实现,但我的观点是主视图无法控制它,因为它是从 ViewModel 调用的,因此无法修改它。我可能在挑剔,因为通常它只是一个 MessageBox,但感觉就像从视图中删除了显示选项。
            • @GazTheDestroyer:但事实并非如此。同样,ViewModel 并没有强制执行该接口的特定实现。您甚至可以为不同的 ViewModel 注入不同的实现。
            【解决方案9】:

            我过去处理它的方式是在需要显示对话框时触发的 ViewModel 中放置一个事件。 View 挂钩事件并处理显示确认对话框,并通过其 EventArgs 将结果返回给调用者。

            【讨论】:

              【解决方案10】:

              我想“你确定吗?”提示属于视图模型,因为它的应用程序逻辑,而不是像动画等纯 ui 的东西。

              所以最好的选择是在 deletecommand 执行方法中调用“你确定”service dialog

              编辑:ViewModel 代码

                  IMessageBox _dialogService;//come to the viewmodel with DI
              
                  public ICommand DeleteCommand
                  {
                      get
                      {
                          return this._cmdDelete ?? (this._cmdDelete = new DelegateCommand(this.DeleteCommandExecute, this.CanDeleteCommandExecute));
                      }
                  }
              

              把逻辑放在execute方法中

                  private void DeleteCommandExecute()
                  {
                    if (!this.CanDeleteCommandExecute())
                       return;
              
                      var result = this.dialogService.ShowDialog("Are you sure prompt window?", YesNo);
              
                      //check result
                      //go on with delete when yes
                   } 
              

              对话服务可以是您想要的任何东西,但删除前要检查的应用程序逻辑在您的视图模型中。

                      

              【讨论】:

                【解决方案11】:

                在将旧的 WinForms 应用程序移植到 WPF 时遇到了这个问题。我认为要记住的重要一点是,WPF 通过在视图模型和带有事件的视图(即INotifyPropertyChanged.PropertyChangedINotifyDataErrorInfo.ErrorsChanged 等)之间发出信号来完成它在后台所做的很多事情。我对这个问题的解决方案是拿那个例子并用它来运行。在我的视图模型中:

                /// <summary>
                /// Occurs before the record is deleted
                /// </summary>
                public event CancelEventHandler DeletingRecord;
                
                /// <summary>
                /// Occurs before record changes are discarded (i.e. by a New or Close operation)
                /// </summary>
                public event DiscardingChangesEvent DiscardingChanges;
                

                然后视图可以侦听这些事件,在需要时提示用户并在指示时取消事件。

                请注意,CancelEventHandler 是由框架为您定义的。但是,对于DiscardingChanges,您需要一个三态结果来指示您希望如何处理操作(即保存更改、放弃更改或取消您正在执行的操作)。为此,我自己做了一个:

                public delegate void DiscardingChangesEvent(object sender, DiscardingChangesEventArgs e);
                
                public class DiscardingChangesEventArgs
                    {
                        public DiscardingChangesOperation Operation { get; set; } = DiscardingChangesOperation.Cancel;
                    }
                
                public enum DiscardingChangesOperation
                    {
                        Save,
                        Discard,
                        Cancel
                    }
                

                我试图想出一个更好的命名约定,但这是我能想到的最好的。

                因此,将其付诸实践看起来像这样:

                ViewModel(这实际上是我基于 CRUD 的视图模型的基类):

                protected virtual void New()
                {
                    // handle case when model is dirty
                    if (ModelIsDirty)
                    {
                        var args = new DiscardingChangesEventArgs();    // defaults to cancel, so someone will need to handle the event to signal discard/save
                        DiscardingChanges?.Invoke(this, args);
                        switch (args.Operation)
                        {
                            case DiscardingChangesOperation.Save:
                                if (!SaveInternal()) 
                                    return;
                                break;
                            case DiscardingChangesOperation.Cancel:
                                return;
                        }
                    }
                
                    // continue with New operation
                }
                
                protected virtual void Delete()
                {
                    var args = new CancelEventArgs();
                    DeletingRecord?.Invoke(this, args);
                    if (args.Cancel)
                        return;
                
                    // continue delete operation
                }
                

                查看:

                <UserControl.DataContext>
                    <vm:CompanyViewModel DeletingRecord="CompanyViewModel_DeletingRecord" DiscardingChanges="CompanyViewModel_DiscardingChanges"></vm:CompanyViewModel>
                </UserControl.DataContext>
                

                查看代码隐藏:

                private void CompanyViewModel_DeletingRecord(object sender, System.ComponentModel.CancelEventArgs e)
                {
                    App.HandleRecordDeleting(sender, e);
                }
                
                private void CompanyViewModel_DiscardingChanges(object sender, DiscardingChangesEventArgs e)
                {
                    App.HandleDiscardingChanges(sender, e);
                }
                

                还有几个静态方法,它们是每个视图都可以使用的 App 类的一部分:

                public static void HandleDiscardingChanges(object sender, DiscardingChangesEventArgs e)
                {
                    switch (MessageBox.Show("Save changes?", "Save", MessageBoxButton.YesNoCancel))
                    {
                        case MessageBoxResult.Yes:
                            e.Operation = DiscardingChangesOperation.Save;
                            break;
                        case MessageBoxResult.No:
                            e.Operation = DiscardingChangesOperation.Discard;
                            break;
                        case MessageBoxResult.Cancel:
                            e.Operation = DiscardingChangesOperation.Cancel;
                            break;
                        default:
                            throw new InvalidEnumArgumentException("Invalid MessageBoxResult returned from MessageBox.Show");
                    }
                }
                
                public static void HandleRecordDeleting(object sender, CancelEventArgs e)
                {
                    e.Cancel = MessageBox.Show("Delete current record?", "Delete", MessageBoxButton.YesNo) == MessageBoxResult.No;
                }
                

                将对话框集中在这些静态方法中可以让我们稍后轻松地将它们换成自定义对话框。

                【讨论】:

                  猜你喜欢
                  • 2016-12-30
                  • 2011-08-14
                  • 2015-09-01
                  • 1970-01-01
                  • 2011-04-18
                  • 1970-01-01
                  • 2021-12-14
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多