从ViewModel关闭窗口

117
我正在创建一个登录功能,使用一个窗口控件来允许用户登录到我正在创建的WPF应用程序。
到目前为止,我已经创建了一个方法,用于检查用户是否在登录界面的文本框中输入了正确的用户名和密码,并将其绑定到两个属性。
我通过创建一个布尔方法来实现这一点,就像这样;
public bool CheckLogin()
{
    var user = context.Users.Where(i => i.Username == this.Username).SingleOrDefault();

    if (user == null)
    {
        MessageBox.Show("Unable to Login, incorrect credentials.");
        return false;
    }
    else if (this.Username == user.Username || this.Password.ToString() == user.Password)
    {
        MessageBox.Show("Welcome " + user.Username + ", you have successfully logged in.");

        return true;
    }
    else
    {
        MessageBox.Show("Unable to Login, incorrect credentials.");
        return false;
    }
}

public ICommand ShowLoginCommand
{
    get
    {
        if (this.showLoginCommand == null)
        {
            this.showLoginCommand = new RelayCommand(this.LoginExecute, null);
        }
        return this.showLoginCommand;
    }
}

private void LoginExecute()
{
    this.CheckLogin();
} 

我还有一个在xaml中绑定到按钮上的命令。
<Button Name="btnLogin" IsDefault="True" Content="Login"
        Command="{Binding ShowLoginCommand}" />

当我输入用户名和密码时,它会执行相应的代码,无论是正确还是错误。但是当用户名和密码都正确时,我该如何从ViewModel中关闭这个窗口呢?
我之前尝试过使用一个“对话框模态”,但效果不太好。此外,在我的app.xaml中,我做了类似以下的操作,首先加载登录页面,然后一旦验证通过,再加载实际的应用程序。
private void ApplicationStart(object sender, StartupEventArgs e)
{
    Current.ShutdownMode = ShutdownMode.OnExplicitShutdown;

    var dialog = new UserView();

    if (dialog.ShowDialog() == true)
    {
        var mainWindow = new MainWindow();
        Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
        Current.MainWindow = mainWindow;
        mainWindow.Show();
    }
    else
    {
        MessageBox.Show("Unable to load application.", "Error", MessageBoxButton.OK);
        Current.Shutdown(-1);
    }
}

问题:我如何从ViewModel关闭登录窗口控件?
18个回答

184
您可以使用CommandParameter将窗口传递给您的ViewModel。请参阅下面的示例。
我实现了一个CloseWindow方法,它以Windows为参数并关闭它。通过CommandParameter将窗口传递给ViewModel。请注意,您需要为应该关闭的窗口定义一个x:Name。在我的XAML窗口中,我通过Command调用此方法,并使用CommandParameter将窗口本身作为参数传递给ViewModel。
Command="{Binding CloseWindowCommand, Mode=OneWay}" 
CommandParameter="{Binding ElementName=TestWindow}"

ViewModel

public RelayCommand<Window> CloseWindowCommand { get; private set; }

public MainViewModel()
{
    this.CloseWindowCommand = new RelayCommand<Window>(this.CloseWindow);
}

private void CloseWindow(Window window)
{
    if (window != null)
    {
       window.Close();
    }
}

查看

<Window x:Class="ClientLibTestTool.ErrorView"
        x:Name="TestWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:localization="clr-namespace:ClientLibTestTool.ViewLanguages"
        DataContext="{Binding Main, Source={StaticResource Locator}}"
        Title="{x:Static localization:localization.HeaderErrorView}"
        Height="600" Width="800"
        ResizeMode="NoResize"
        WindowStartupLocation="CenterScreen">
    <Grid> 
        <Button Content="{x:Static localization:localization.ButtonClose}" 
                Height="30" 
                Width="100" 
                Margin="0,0,10,10" 
                IsCancel="True" 
                VerticalAlignment="Bottom" 
                HorizontalAlignment="Right" 
                Command="{Binding CloseWindowCommand, Mode=OneWay}" 
                CommandParameter="{Binding ElementName=TestWindow}"/>
    </Grid>
</Window>

请注意,我正在使用MVVM light框架,但是这个原则适用于每个WPF应用程序。
这个解决方案违反了MVVM模式,因为视图模型不应该知道任何关于UI实现的事情。如果您想严格遵循MVVM编程范例,则必须使用接口来抽象视图的类型。
MVVM符合解决方案:
在评论部分,用户Crono提到了一个有效的观点:
将Window对象传递给视图模型会破坏MVVM模式,因为它会强制您的vm知道它正在被查看的内容。
您可以通过引入包含关闭方法的接口来解决这个问题。
接口:
public interface ICloseable
{
    void Close();
}

您重构后的 ViewModel 将如下所示:

视图模型

public RelayCommand<ICloseable> CloseWindowCommand { get; private set; }

public MainViewModel()
{
    this.CloseWindowCommand = new RelayCommand<IClosable>(this.CloseWindow);
}

private void CloseWindow(ICloseable window)
{
    if (window != null)
    {
        window.Close();
    }
}

您需要在视图中引用并实现 ICloseable 接口。

视图(代码后台)

public partial class MainWindow : Window, ICloseable
{
    public MainWindow()
    {
        InitializeComponent();
    }
}

原问题的答案:(之前的编辑1)

您的登录按钮(添加了CommandParameter):

<Button Name="btnLogin" IsDefault="True" Content="Login" Command="{Binding ShowLoginCommand}" CommandParameter="{Binding ElementName=LoginWindow}"/>

您的代码:

 public RelayCommand<Window> CloseWindowCommand { get; private set; } // the <Window> is important for your solution!

 public MainViewModel() 
 {
     //initialize the CloseWindowCommand. Again, mind the <Window>
     //you don't have to do this in your constructor but it is good practice, thought
     this.CloseWindowCommand = new RelayCommand<Window>(this.CloseWindow);
 }

 public bool CheckLogin(Window loginWindow) //Added loginWindow Parameter
 {
    var user = context.Users.Where(i => i.Username == this.Username).SingleOrDefault();

    if (user == null)
    {
        MessageBox.Show("Unable to Login, incorrect credentials.");
        return false;
    }
    else if (this.Username == user.Username || this.Password.ToString() == user.Password)
    {
        MessageBox.Show("Welcome "+ user.Username + ", you have successfully logged in.");
        this.CloseWindow(loginWindow); //Added call to CloseWindow Method
        return true;
    }
    else
    {
        MessageBox.Show("Unable to Login, incorrect credentials.");
        return false;
    }
 }

 //Added CloseWindow Method
 private void CloseWindow(Window window)
 {
     if (window != null)
     {
         window.Close();
     }
 }

让我们在聊天中继续这个讨论:http://chat.stackoverflow.com/rooms/28870/discussion-between-joel-and-wpfnoob - Joel
18
如果您不喜欢为窗口命名,也可以像这样绑定参数:CommandParameter="{Binding RelativeSource={RelativeSource AncestorType={x:Type Window}}}" - Jacco Dieleman
41
在我看来,将Window对象传递给视图模型会破坏MVVM模式,因为这会强制你的vm知道它正在被查看的内容。如果视图是一个MDI界面中的停靠选项卡,那该怎么办呢?我认为正确的方法是传递某种实现了关闭方法的IUIHost接口,并让任何想要显示vm的视图来实现它。 - Crono
我不清楚为什么将Window抽象化为IClosable接口会让它突然变得符合MVVM。无论如何,您仍然会传递对Window的引用,实际上并没有改变任何东西。如果这样更好,请解释一下为什么。 - DonBoitnott
3
这是因为接口将具体实现隐藏到了ViewModel中。ViewModel对视图一无所知,除了它实现了Close()方法。因此,视图可以是任何东西:WPF窗口、WinForms表格、UWP应用程序甚至是WPF网格。这将视图与视图模型解耦。 - Joel
显示剩余3条评论

73

通常情况下,当我需要这样做时,我会在视图模型上放置一个事件,然后在将视图模型绑定到窗口时将其连接到Window.Close()

public class LoginViewModel
{
    public event EventHandler OnRequestClose;

    private void Login()
    {
        // Login logic here
        OnRequestClose(this, new EventArgs());
    }
}

当创建登录窗口时

var vm = new LoginViewModel();
var loginWindow = new LoginWindow
{
    DataContext = vm
};
vm.OnRequestClose += (s, e) => loginWindow.Close();

loginWindow.ShowDialog(); 

13
这个答案比被接受的回答好得多,因为它不会破坏MVVM框架。 - Spook
2
简单、明显和有效。这对我很有效。 - High Plains Grifter
1
非常优雅的解决方案,这应该被接受为答案。 - Lucy82

45

就MVVM而言,我认为使用来自 Blend SDK (System.Windows.Interactivity) 的 Behaviors 或者 Prism 中的自定义交互请求都非常适合这种情况。

如果选择使用 Behavior,这是一般的想法:

public class CloseWindowBehavior : Behavior<Window>
{
    public bool CloseTrigger
    {
        get { return (bool)GetValue(CloseTriggerProperty); }
        set { SetValue(CloseTriggerProperty, value); }
    }

    public static readonly DependencyProperty CloseTriggerProperty =
        DependencyProperty.Register("CloseTrigger", typeof(bool), typeof(CloseWindowBehavior), new PropertyMetadata(false, OnCloseTriggerChanged));

    private static void OnCloseTriggerChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var behavior = d as CloseWindowBehavior;

        if (behavior != null)
        {
            behavior.OnCloseTriggerChanged();
        }
    }

    private void OnCloseTriggerChanged()
    {
        // when closetrigger is true, close the window
        if (this.CloseTrigger)
        {
            this.AssociatedObject.Close();
        }
    }
}

然后在你的窗口中,你只需要将CloseTrigger绑定到一个布尔值上,当你想要关闭窗口时,该值会被设置。

<Window x:Class="TestApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
        xmlns:local="clr-namespace:TestApp"
        Title="MainWindow" Height="350" Width="525">
    <i:Interaction.Behaviors>
        <local:CloseWindowBehavior CloseTrigger="{Binding CloseTrigger}" />
    </i:Interaction.Behaviors>

    <Grid>

    </Grid>
</Window>

最后,您的DataContext / ViewModel将具有一个属性,当您希望窗口关闭时,您将设置该属性,如下所示:

public class MainWindowViewModel : INotifyPropertyChanged
{
    private bool closeTrigger;

    /// <summary>
    /// Gets or Sets if the main window should be closed
    /// </summary>
    public bool CloseTrigger
    {
        get { return this.closeTrigger; }
        set
        {
            this.closeTrigger = value;
            RaisePropertyChanged(nameof(CloseTrigger));
        }
    }

    public MainWindowViewModel()
    {
        // just setting for example, close the window
        CloseTrigger = true;
    }

    protected void RaisePropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

(将您的Window.DataContext设置为new MainWindowViewModel())


我认为这是最好的逻辑,因为它避免了在代码后台中设置DataContext的需要(是的,您也可以仅在XAML中定义VM DataContext)。在这里,您从这种方法中受益。我不喜欢必须完全触及代码后台文件(xaml.cs)。谢谢你。 - Martin Braun
注意:Microsoft.Xaml.Behaviors.Wpf包仅支持.NET Framework 4.5及以上版本。 - demoncrate
以下软件包今天也将 .Net 5 列为支持的版本:https://www.nuget.org/packages/Microsoft.Xaml.Behaviors.Wpf如果用户只需要该 Behavior 类在其他框架中使用,源代码也是可用的: https://github.com/microsoft/XamlBehaviorsWpf/blob/master/src/Microsoft.Xaml.Behaviors/Behavior.cs - tobster

31

可能有点晚了,但这是我的答案。

foreach (Window item in Application.Current.Windows)
{
    if (item.DataContext == this) item.Close();
}

15

好的,这是我在几个项目中使用过的东西。它可能看起来像一个 hack,但它很好用。

public class AttachedProperties : DependencyObject //adds a bindable DialogResult to window
{
    public static readonly DependencyProperty DialogResultProperty = 
        DependencyProperty.RegisterAttached("DialogResult", typeof(bool?), typeof(AttachedProperties), 
        new PropertyMetaData(default(bool?), OnDialogResultChanged));

    public bool? DialogResult
    {
        get { return (bool?)GetValue(DialogResultProperty); }
        set { SetValue(DialogResultProperty, value); }
    }

    private static void OnDialogResultChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var window = d as Window;
        if (window == null)
            return;

        window.DialogResult = (bool?)e.NewValue;
    }
}

现在您可以将DialogResult绑定到VM并设置其属性的值。当该值被设置时,Window将会关闭。

<!-- Assuming that the VM is bound to the DataContext and the bound VM has a property DialogResult -->
<Window someNs:AttachedProperties.DialogResult={Binding DialogResult} />

这是我们生产环境正在运行的摘要

<Window x:Class="AC.Frontend.Controls.DialogControl.Dialog"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:DialogControl="clr-namespace:AC.Frontend.Controls.DialogControl" 
        xmlns:hlp="clr-namespace:AC.Frontend.Helper"
        MinHeight="150" MinWidth="300" ResizeMode="NoResize" SizeToContent="WidthAndHeight"
        WindowStartupLocation="CenterScreen" Title="{Binding Title}"
        hlp:AttachedProperties.DialogResult="{Binding DialogResult}" WindowStyle="ToolWindow" ShowInTaskbar="True"
        Language="{Binding UiCulture, Source={StaticResource Strings}}">
        <!-- A lot more stuff here -->
</Window>

正如您所看到的,我首先声明命名空间xmlns:hlp="clr-namespace:AC.Frontend.Helper",然后是绑定hlp:AttachedProperties.DialogResult="{Binding DialogResult}"

AttachedProperty看起来像这样。虽然不同于我昨天发布的那个,但在我看来应该没有影响。

public class AttachedProperties
{
    #region DialogResult

    public static readonly DependencyProperty DialogResultProperty =
        DependencyProperty.RegisterAttached("DialogResult", typeof (bool?), typeof (AttachedProperties), new PropertyMetadata(default(bool?), OnDialogResultChanged));

    private static void OnDialogResultChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var wnd = d as Window;
        if (wnd == null)
            return;

        wnd.DialogResult = (bool?) e.NewValue;
    }

    public static bool? GetDialogResult(DependencyObject dp)
    {
        if (dp == null) throw new ArgumentNullException("dp");

        return (bool?)dp.GetValue(DialogResultProperty);
    }

    public static void SetDialogResult(DependencyObject dp, object value)
    {
        if (dp == null) throw new ArgumentNullException("dp");

        dp.SetValue(DialogResultProperty, value);
    }

    #endregion
}

13

简单的方法

public interface IRequireViewIdentification
{
    Guid ViewID { get; }
}

将其实现到 ViewModel 中

public class MyViewVM : IRequireViewIdentification
{
    private Guid _viewId;

    public Guid ViewID
    {
        get { return _viewId; }
    }

    public MyViewVM()
    {
        _viewId = Guid.NewGuid();
    }
}

添加通用窗口管理器帮助程序

public static class WindowManager
{
    public static void CloseWindow(Guid id)
    {
        foreach (Window window in Application.Current.Windows)
        {
            var w_id = window.DataContext as IRequireViewIdentification;
            if (w_id != null && w_id.ViewID.Equals(id))
            {
                window.Close();
            }
        }
    }
}

在viewmodel中这样关闭它

WindowManager.CloseWindow(ViewID);

5

我知道这是一篇旧帖子,可能没有人会滚到这里,我知道我没有。所以,在尝试了几个小时不同的方法后,我发现了这个博客,作者真的很厉害。这是最简单的方法,我试过了,它非常好用。

博客

在ViewModel中:

...

public bool CanClose { get; set; }

private RelayCommand closeCommand;
public ICommand CloseCommand
{
    get
    {
        if(closeCommand == null)
        (
            closeCommand = new RelayCommand(param => Close(), param => CanClose);
        )
    }
}

public void Close()
{
    this.Close();
}

...

在ViewModel中添加一个Action属性,但是从View的代码后台文件中定义它。这将使我们动态地定义一个指向视图的ViewModel引用。

在ViewModel上,我们只需添加:

public Action CloseAction { get; set; }

在视图中,我们将其定义如下:

public View()
{
    InitializeComponent() // this draws the View
    ViewModel vm = new ViewModel(); // this creates an instance of the ViewModel
    this.DataContext = vm; // this sets the newly created ViewModel as the DataContext for the View
    if ( vm.CloseAction == null )
        vm.CloseAction = new Action(() => this.Close());
}

4

以下是一个使用MVVM Light Messenger而不是事件的简单示例。当点击按钮时,视图模型发送关闭消息:

    public MainViewModel()
    {
        QuitCommand = new RelayCommand(ExecuteQuitCommand);
    }

    public RelayCommand QuitCommand { get; private set; }

    private void ExecuteQuitCommand() 
    {
        Messenger.Default.Send<CloseMessage>(new CloseMessage());
    }

然后,它在窗口的代码后台中被接收。

    public Main()
    {   
        InitializeComponent();
        Messenger.Default.Register<CloseMessage>(this, HandleCloseMessage);
    }

    private void HandleCloseMessage(CloseMessage closeMessage)
    {
        Close();
    }

4

这个怎么样?这里有更多相关的IT技术内容。

ViewModel:

class ViewModel
{
    public Action CloseAction { get; set; }
    private void Stuff()
    {
       // Do Stuff
       CloseAction(); // closes the window
    }
}

在您的ViewModel中使用CloseAction()关闭窗口,就像上面的示例一样。
视图:
public View()
{
    InitializeComponent();
    ViewModel vm = new ViewModel (); // this creates an instance of the ViewModel
    this.DataContext = vm; // this sets the newly created ViewModel as the DataContext for the View
    if (vm.CloseAction == null)
        vm.CloseAction = new Action(() => this.Close());
}

2
您可以像这样在ViewModel中创建新的事件处理程序。
public event EventHandler RequestClose;

    protected void OnRequestClose()
    {
        if (RequestClose != null)
            RequestClose(this, EventArgs.Empty);
    }

然后为ExitCommand定义RelayCommand。
private RelayCommand _CloseCommand;
    public ICommand CloseCommand
    {
        get
        {
            if(this._CloseCommand==null)
                this._CloseCommand=new RelayCommand(CloseClick);
            return this._CloseCommand;
        }
    }

    private void CloseClick(object obj)
    {
        OnRequestClose();
    }

接下来,在 XAML 文件中设置

<Button Command="{Binding CloseCommand}" />

在xaml.cs文件中设置DataContext并订阅我们创建的事件。
public partial class MainWindow : Window
{
    private ViewModel mainViewModel = null;
    public MainWindow()
    {
        InitializeComponent();
        mainViewModel = new ViewModel();
        this.DataContext = mainViewModel;
        mainViewModel.RequestClose += delegate(object sender, EventArgs args) { this.Close(); };
    }
}

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