“你确定吗?”提示。部分ViewModel还是纯粹的视图?

时间:2012-04-19 11:25:39

标签: c# wpf mvvm

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

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

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

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

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

有什么想法吗?

11 个答案:

答案 0 :(得分:15)

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

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

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

编辑:我应该补充一点,我已经在原型项目中探索过使用Prism 4和交互请求,我对非常高兴的结果(带有一点框架)您可以使用XAML完全指定特定交互请求中的内容!)。

答案 1 :(得分:8)

  

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

我同意;这样的提示应该在视图中处理,因为最终视图是用户看到和交互的内容,而不是视图模型。一旦您的视图获得了用户确认应调用DeleteCommand的信息,请继续并在视图模型中调用它。

我看到它的方式,单元测试与用户交互并没有任何关系,除非您正在测试视图本身。

答案 2 :(得分:6)

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

  1. 确定是否应显示提示以及应对结果执行的操作的逻辑
  2. 实际显示提示的代码
  3. 第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中所述。

答案 3 :(得分:3)

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

尽管它是一个银光灯,但与wpf相比,它不应该有太大差异。

答案 4 :(得分:1)

我认为这取决于提示,但一般来说,需要提示用户的代码逻辑通常在视图模型中,例如用户按下按钮删除列表项,命令被触发VM,逻辑运行,很明显,这可能会影响另一个实体,用户必须选择他们想做的事情,此时你不应该要求View提示用户,所以我看不到任何其他选择但要在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中得到一个明确的答案,所以我对其他poeples方法感兴趣。

答案 5 :(得分:1)

看看这个:

MVVM and Confirmation Dialogs

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

答案 6 :(得分:1)

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

答案 7 :(得分:1)

我认为“你确定吗?”提示属于viewmodel,因为它的应用程序逻辑而不是像动画等纯粹的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
     } 

对话框服务可以是您想要的任何内容,但删除之前要检查的应用程序逻辑位于您的viewmodel中。

答案 8 :(得分:0)

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

答案 9 :(得分:0)

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

您可以看到它解释here

答案 10 :(得分:0)

在将旧的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;
}

通过这些静态方法集中对话框,我们以后可以轻松地将其换出以用于自定义对话框。