ViewModel应该如何关闭表单?

268

我正在尝试学习WPF和MVVM模式,但遇到了一个问题。这个问题类似于此问题(在WPF中处理对话框),但并不完全相同...

我使用MVVM模式编写了一个“登录”表单。

该表单有一个ViewModel来保存用户名和密码,并使用正常的数据绑定将它们绑定到XAML视图上。还有一个“Login”命令,使用正常的数据绑定将其绑定到表单上的“Login”按钮。

当“Login”命令触发时,它会调用ViewModel中的一个函数,该函数会发送数据经过网络进行登录。当该函数完成时,会出现两种情况:

  1. 登录无效-我们只需弹出一个消息框就可以了

  2. 登录有效,我们需要关闭登录窗口并返回true作为它的DialogResult

问题是,ViewModel对实际视图一无所知,那么它如何关闭视图并告诉它返回特定的DialogResult??我可以在CodeBehind中添加一些代码,或者将视图传递给ViewModel,但这似乎会完全破坏MVVM的整个目的...


更新

最终,我打破了MVVM模式的“纯洁性”,使View发布了一个Closed事件,并公开了一个Close方法。然后ViewModel只需调用view.Close. View仅通过接口知道,并通过IOC容器连接,因此没有失去可测试性或可维护性。

看起来,被接受的答案获得-5票似乎相当愚蠢!虽然我十分清楚在保持“纯粹”的同时解决问题所带来的好感,但我肯定不是唯一一个认为200行事件、命令和行为只为了避免使用一个一行代码的方法而使用“模式”和“纯洁性”有点荒谬的人...


我使用附加行为来关闭窗口。 将 ViewModel 上的“signal”属性绑定到附加行为上(我实际上使用触发器)。 当它设置为 true 时,该行为会关闭窗口。http://adammills.wordpress.com/2009/07/01/window-close-from-xaml/ - Adam Mills
25个回答

342
我受到Thejuan's answer的启发,写了一个更简单的附加属性。没有样式,没有触发器;相反,你只需要这样做:
<Window ...
        xmlns:xc="clr-namespace:ExCastle.Wpf"
        xc:DialogCloser.DialogResult="{Binding DialogResult}">

这几乎和WPF团队一开始就把DialogResult作为依赖属性做对一样干净。只需在您的ViewModel上放置一个bool? DialogResult属性并实现INotifyPropertyChanged,然后您的ViewModel就可以通过设置属性来关闭窗口(并设置其DialogResult)。这就是MVVM应该的样子。
以下是DialogCloser的代码:
using System.Windows;
 
namespace ExCastle.Wpf
{
    public static class DialogCloser
    {
        public static readonly DependencyProperty DialogResultProperty =
            DependencyProperty.RegisterAttached(
                "DialogResult",
                typeof(bool?),
                typeof(DialogCloser),
                new PropertyMetadata(DialogResultChanged));
 
        private static void DialogResultChanged(
            DependencyObject d,
            DependencyPropertyChangedEventArgs e)
        {
            var window = d as Window;
            if (window != null)
                window.DialogResult = e.NewValue as bool?;
        }
        public static void SetDialogResult(Window target, bool? value)
        {
            target.SetValue(DialogResultProperty, value);
        }
    }
}

我也在我的博客上发布了这个链接。

你好,博客文章永远下线了吗? - niks

71

在我看来,这个问题非常好,因为相同的方法不仅可以用于“登录”窗口,也可以用于任何类型的窗口。我已经审查了很多建议,但没有一个让我满意。请查看我从MVVM设计模式文章中引用的建议。

每个ViewModel类都应该继承自WorkspaceViewModel,该类具有RequestClose事件和ICommand类型的CloseCommand属性。 CloseCommand属性的默认实现将触发RequestClose事件。

为了关闭窗口,应该重写你的窗口的OnLoaded方法:

void CustomerWindow_Loaded(object sender, RoutedEventArgs e)
{
    CustomerViewModel customer = CustomerViewModel.GetYourCustomer();
    DataContext = customer;
    customer.RequestClose += () => { Close(); };
}

或者在您的应用程序的OnStartup方法中:

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);

        MainWindow window = new MainWindow();
        var viewModel = new MainWindowViewModel();
        viewModel.RequestClose += window.Close;
        window.DataContext = viewModel;

        window.Show();
    }

我认为WorkspaceViewModel中的RequestClose事件和CloseCommand属性实现已经很清楚了,但我将展示它们的一致性:

public abstract class WorkspaceViewModel : ViewModelBase
// There's nothing interesting in ViewModelBase as it only implements the INotifyPropertyChanged interface
{
    RelayCommand _closeCommand;
    public ICommand CloseCommand
    {
        get
        {
            if (_closeCommand == null)
            {
                _closeCommand = new RelayCommand(
                   param => Close(),
                   param => CanClose()
                   );
            }
            return _closeCommand;
        }
    }

    public event Action RequestClose;

    public virtual void Close()
    {
        if ( RequestClose != null )
        {
            RequestClose();
        }
    }

    public virtual bool CanClose()
    {
        return true;
    }
}

并且这是 RelayCommand 的源代码:

public class RelayCommand : ICommand
{
    #region Constructors

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;
    }
    #endregion // Constructors

    #region ICommand Members

    [DebuggerStepThrough]
    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

    #endregion // ICommand Members

    #region Fields

    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;

    #endregion // Fields
}

附言:不要因为那些资源对我不好!如果昨天我有这些资源,那将为我节省几个小时...

再附言:欢迎任何意见或建议。


15

这里有很多评论在讨论MVVM的优缺点。对我而言,我同意Nir的看法;它取决于适当使用该模式,而MVVM并不总是适合的。人们似乎已经愿意牺牲软件设计中最重要的原则,仅仅为了让MVVM适应。

话虽如此...我认为你的情况可能需要进行一些重构才能达到良好的匹配。

在我遇到的大多数情况下,WPF使您能够在不使用多个Window的情况下完成。也许您可以尝试使用FramePage代替带有DialogResult的窗口。

在您的情况下,我的建议是让LoginFormViewModel处理LoginCommand,如果登录无效,则将LoginFormViewModel上的属性设置为适当的值(false或某个枚举值,如UserAuthenticationStates.FailedAuthentication)。对于成功的登录,您也可以做同样的操作(true或其他枚举值)。然后,您可以使用DataTrigger响应各种用户身份验证状态,并可以使用简单的Setter更改FrameSource属性。

我认为您混淆了登录窗口返回DialogResult的概念;那个DialogResult实际上是ViewModel的一个属性。在我有限的WPF经验中,如果某些事情感觉不对,通常是因为我在考虑如何以WinForms相同的方式完成同样的事情。

希望这可以帮助您。


10

假设您的登录对话框是创建的第一个窗口,请尝试在您的LoginViewModel类中使用以下代码:

    void OnLoginResponse(bool loginSucceded)
    {
        if (loginSucceded)
        {
            Window1 window = new Window1() { DataContext = new MainWindowViewModel() };
            window.Show();

            App.Current.MainWindow.Close();
            App.Current.MainWindow = window;
        }
        else
        {
            LoginError = true;
        }
    }

9
这是一个简单清晰的解决方案 - 你可以在ViewModel中添加一个事件,并指示窗口在该事件触发时关闭自身。
更多细节请参阅我的博客文章,从ViewModel关闭窗口
XAML:
<Window
  x:Name="this"
  xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"  
  xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions">
  <i:Interaction.Triggers>
    <i:EventTrigger SourceObject="{Binding}" EventName="Closed">
      <ei:CallMethodAction
        TargetObject="{Binding ElementName=this}"
        MethodName="Close"/>
    </i:EventTrigger>
  </i:Interaction.Triggers>
<Window>

视图模型:

private ICommand _SaveAndCloseCommand;
public ICommand SaveAndCloseCommand
{
  get
  {
    return _SaveAndCloseCommand ??
      (_SaveAndCloseCommand = new DelegateCommand(SaveAndClose));
  }
}
private void SaveAndClose()
{
  Save();
  Close();
}

public event EventHandler Closed;
private void Close()
{
  if (Closed != null) Closed(this, EventArgs.Empty);
}

注意:这个示例使用了 Prism 的 DelegateCommand(请参见Prism: Commanding),但实际上可以使用任何 ICommand 实现。
您可以使用来自this官方包的行为。

你能否请编辑一下这个答案并详细解释一下?我对WPF还很陌生,我想要完全理解这个。我似乎无法理解它,但是由于这个逻辑,我的代码现在可以工作了。如果您只是在“MethodName = Close”、“EventHandler Closed”和方法Close上添加一些注释,那也没关系。(关于您指出的博客文章,它与这里的答案是一样的) - paraJdox1
请更新您的博客链接,当前链接已损坏。 - sorosh_sabz

6

我处理这个问题的方法是在我的ViewModel中添加一个事件处理程序。当用户成功登录时,我会触发该事件。在我的View中,我会附加到这个事件,并在它触发时关闭窗口。


4
好的,这个问题已经有将近6年了,但我仍然找不到我认为是正确答案的东西,所以请允许我分享我的"两分钱"...
实际上,我有两种方法来做这件事,第一种是简单的...第二种是正确的方法,所以如果你正在寻找正确的方法,就跳过#1,直接进入#2:
1. 快速简单(但不完整)
如果我只有一个小项目,有时我会在ViewModel中创建一个CloseWindowAction:
        public Action CloseWindow { get; set; } // In MyViewModel.cs

无论是谁创建了视图,或者在视图的代码后台中设置了方法,该动作将被调用:

(请记住,MVVM 是关于视图和视图模型的分离...视图的代码后台仍然是视图,只要有适当的分离,就不会违反这种模式)

如果某个视图模型创建了一个新窗口:

private void CreateNewView()
{
    MyView window = new MyView();
    window.DataContext = new MyViewModel
                             {
                                 CloseWindow = window.Close,
                             }; 
    window.ShowDialog();
}

或者,如果你想要它显示在主窗口中,只需将其放置在视图构造函数下方即可。
public MyView()
{
    InitializeComponent();           
    this.DataContext = new MainViewModel
                           {
                                CloseWindow = this.Close
                           };
}

当你想关闭窗口时,只需在你的ViewModel上调用该行为。

2. 正确的方法

现在正确的做法是使用Prism(个人意见),关于它的一切都可以在这里找到

你可以创建一个交互请求,将需要的数据填充进去,启动它,关闭它,甚至接收返回的数据。所有这些都封装在MVVM中,并且得到认可。你甚至可以获取窗口关闭的状态,比如用户是取消还是接受(点击确定按钮),以及如果需要的话,返回的数据。这比答案#1要复杂一些,但更加完整,也是微软推荐的模式。

我给出的链接中包含了所有的代码片段和示例,所以我不会在这里放置任何代码,只需阅读文章或下载Prism快速入门并运行它,它非常简单易懂,只是稍微冗长一些以使其工作,但好处远大于仅仅关闭一个窗口。


4
public partial class MyWindow: Window
{
    public ApplicationSelection()
    {
      InitializeComponent();

      MyViewModel viewModel = new MyViewModel();

      DataContext = viewModel;

      viewModel.RequestClose += () => { Close(); };

    }
}

public class MyViewModel
{

  //...Your code...

  public event Action RequestClose;

  public virtual void Close()
  {
    if (RequestClose != null)
    {
      RequestClose();
    }
  }

  public void SomeFunction()
  {
     //...Do something...
     Close();
  }
}

4

以下是我最初的做法,虽然可行,但看起来有点冗长丑陋(全局静态任何东西都不好)

1:App.xaml.cs

public partial class App : Application
{
    // create a new global custom WPF Command
    public static readonly RoutedUICommand LoggedIn = new RoutedUICommand();
}

2: LoginForm.xaml

// bind the global command to a local eventhandler
<CommandBinding Command="client:App.LoggedIn" Executed="OnLoggedIn" />

3: LoginForm.xaml.cs

// implement the local eventhandler in codebehind
private void OnLoggedIn( object sender, ExecutedRoutedEventArgs e )
{
    DialogResult = true;
    Close();
}

4:LoginFormViewModel.cs

// fire the global command from the viewmodel
private void OnRemoteServerReturnedSuccess()
{
    App.LoggedIn.Execute(this, null);
}

后来,我删除了所有这些代码,只让 LoginFormViewModel 在它的视图上调用 Close 方法。这样做更加简单易懂。在我看来,设计模式的目的是让人们更容易理解你的应用程序在做什么,但在这种情况下,MVVM 使得应用程序更难理解,反而成为一种反面模式。


3

您可以使ViewModel公开事件,供View注册。当ViewModel决定关闭视图时,它会触发该事件以导致视图关闭。如果您需要传回特定的结果值,则可以在ViewModel中设置相应的属性。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接