39

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

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

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

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

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

有什么想法吗?

4

11 回答 11

16

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

我知道有两种选择可以减少 View 和 ViewModel 之间的耦合:使用交互服务和触发交互请求。两者在这里都得到了很好的解释;你可能想看看。

总体思路是抽象异步交互是如何完成的,并使用更类似于基于事件的逻辑的方式工作,同时允许 ViewModel 表达它希望与用户交互作为操作的一部分;最终结果是您可以记录此交互并对其进行单元测试。

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

于 2012-04-19T11:32:52.380 回答
8

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

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

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

于 2012-04-19T11:27:59.707 回答
6

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

  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
    }
}

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

于 2012-04-19T11:43:23.943 回答
3

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

即使它是一个 Silverlight 帖子,与 wpf 相比,它也不应该有太大差异。

于 2012-04-19T11:28:24.173 回答
1

我认为这取决于提示,但一般来说,需要提示用户的代码逻辑通常在视图模型中,例如用户按下按钮删除列表项,在 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 中得到明确答案的灰色交叉领域之一,因此我对其他人的方法很感兴趣。

于 2012-04-19T11:36:41.077 回答
1

看看这个:

MVVM 和确认对话框

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

于 2012-04-19T11:39:03.870 回答
1

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

于 2012-04-19T12:15:43.087 回答
1

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

所以最好的选择是在 deletecommand 执行方法中调用“你确定”服务对话框

编辑:视图模型代码

    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
     } 

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

           

于 2012-04-19T12:35:48.193 回答
0

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

于 2012-04-19T11:28:26.517 回答
0

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

你可以在这里看到它的解释

于 2012-04-19T11:33:32.170 回答
0

在将旧的 WinForms 应用程序移植到 WPF 时遇到了这个问题。我认为要记住的重要一点是,WPF 通过在视图模型和带有事件的视图(即,等)之间发出信号来完成它在后台所做的很多INotifyPropertyChanged.PropertyChanged事情INotifyDataErrorInfo.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;
}

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

于 2018-12-19T16:52:12.260 回答