从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个回答

1

我的首选方式是在ViewModel中声明事件,并使用Blend的InvokeMethodAction,如下所示。

示例ViewModel

public class MainWindowViewModel : BindableBase, ICloseable
{
    public DelegateCommand SomeCommand { get; private set; }
    #region ICloseable Implementation
    public event EventHandler CloseRequested;        

    public void RaiseCloseNotification()
    {
        var handler = CloseRequested;
        if (handler != null)
        {
            handler.Invoke(this, EventArgs.Empty);
        }
    }
    #endregion

    public MainWindowViewModel()
    {
        SomeCommand = new DelegateCommand(() =>
        {
            //when you decide to close window
            RaiseCloseNotification();
        });
    }
}

ICloseable接口如下所示,但不需要执行此操作。 ICloseable将有助于创建通用视图服务,因此,如果您通过依赖注入构建视图和ViewModel,则可以执行以下操作:

internal interface ICloseable
{
    event EventHandler CloseRequested;
}

使用ICloseable
var viewModel = new MainWindowViewModel();
        // As service is generic and don't know whether it can request close event
        var window = new Window() { Content = new MainView() };
        var closeable = viewModel as ICloseable;
        if (closeable != null)
        {
            closeable.CloseRequested += (s, e) => window.Close();
        }

以下是Xaml代码,即使您没有实现界面,也可以使用此Xaml,它只需要您的视图模型引发CloseRquested事件。
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WPFRx"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions" 
xmlns:ViewModels="clr-namespace:WPFRx.ViewModels" x:Name="window" x:Class="WPFRx.MainWindow"
    mc:Ignorable="d"
    Title="MainWindow" Height="350" Width="525" 
d:DataContext="{d:DesignInstance {x:Type ViewModels:MainWindowViewModel}}">

<i:Interaction.Triggers>
    <i:EventTrigger SourceObject="{Binding Mode=OneWay}" EventName="CloseRequested" >
        <ei:CallMethodAction TargetObject="{Binding ElementName=window}" MethodName="Close"/>
    </i:EventTrigger>
</i:Interaction.Triggers>

<Grid>
    <Button Content="Some Content" Command="{Binding SomeCommand}" Width="100" Height="25"/>
</Grid>


1
你可以使用 MVVMLight 工具包中的 Messenger。在你的 ViewModel 中像这样发送消息:
Messenger.Default.Send(new NotificationMessage("Close"));
然后在你的窗口代码后台,在 InitializeComponent 后,像这样注册该消息:
Messenger.Default.Register<NotificationMessage>(this, m=>{
    if(m.Notification == "Close") 
    {
        this.Close();
    }
   });

你可以在这里找到更多关于MVVMLight工具包的信息: Codeplex上的MVVMLight工具包 请注意,在MVVM中没有“完全无代码后台规则”,您可以在视图的代码后台注册消息。

1
你可以将窗口视为服务(例如UI服务),并通过接口将其传递给视图模型,如下所示:

public interface IMainWindowAccess
{
    void Close(bool result);
}

public class MainWindow : IMainWindowAccess
{
    // (...)
    public void Close(bool result)
    {
        DialogResult = result;
        Close();
    }
}

public class MainWindowViewModel
{
    private IMainWindowAccess access;

    public MainWindowViewModel(IMainWindowAccess access)
    {
        this.access = access;
    }

    public void DoClose()
    {
        access.Close(true);
    }
}

这个解决方案在不破坏MVVM的情况下,具有将视图本身传递给视图模型的大多数优点,因为虽然物理上将视图传递给视图模型,但后者仍然不知道前者,它只看到一些IMainWindowAccess。例如,如果我们想要将此解决方案迁移到其他平台,只需要为另一个Activity正确实现IMainWindowAccess即可。我在此发布该解决方案,以提出与事件不同的方法(尽管实际上非常相似),因为它似乎比事件更容易实现(附加/分离等),但仍然与MVVM模式对齐。

0

很简单。 您可以为登录创建自己的ViewModel类 - LoginViewModel。 您可以在LoginViewModel中创建视图变量dialog = new UserView();。 然后,您可以将LoginCommand设置到按钮上。

<Button Name="btnLogin" IsDefault="True" Content="Login" Command="{Binding LoginCommand}" />

<Button Name="btnCancel" IsDefault="True" Content="Login" Command="{Binding CancelCommand}" />

ViewModel类:

public class LoginViewModel
{
    Window dialog;
    public bool ShowLogin()
    {
       dialog = new UserView();
       dialog.DataContext = this; // set up ViewModel into View
       if (dialog.ShowDialog() == true)
       {
         return true;
       }

       return false;
    }

    ICommand _loginCommand
    public ICommand LoginCommand
    {
        get
        {
            if (_loginCommand == null)
                _loginCommand = new RelayCommand(param => this.Login());

            return _loginCommand;
        }
    }

    public void CloseLoginView()
    {
            if (dialog != null)
          dialog.Close();
    }   

    public void Login()
    {
        if(CheckLogin()==true)
        {
            CloseLoginView();         
        }
        else
        {
          // write error message
        }
    }

    public bool CheckLogin()
    {
      // ... check login code
      return true;
    }
}

3
是的,这也是一种有效的解决方案。但是,如果你想坚持MVVM模式和VMs与views的解耦,那么你将会打破这个模式。 - DHN

0
这是我相当简单地做的一个方法:

YourWindow.xaml.cs

//In your constructor
public YourWindow()
{
    InitializeComponent();
    DataContext = new YourWindowViewModel(this);
}

YourWindowViewModel.cs

private YourWindow window;//so we can kill the window

//In your constructor
public YourWindowViewModel(YourWindow window)
{
    this.window = window;
}

//to close the window
public void CloseWindow()
{
    window.Close();
}

我认为你选择的答案没有问题,只是我认为这可能是一种更简单的方法来完成它!


8
这需要您的ViewModel知道并引用您的View。 - AndrewS
@AndrewS 这为什么不好? - thestephenstanton
9
遵循MVVM模式,ViewModel不应该了解View。 - MetalMikester
1
进一步说,MVVM 的目的是使大部分 GUI 代码可单元测试。视图具有许多依赖项,这使它们无法进行单元测试。ViewModel 应该可以进行单元测试,但如果您给它们直接依赖于视图,则不能进行单元测试。 - ILMTitan
进一步扩展,正确编写的MVVM可以轻松地将解决方案迁移到不同的平台。特别是,您应该能够在不进行任何更改的情况下重用您的视图模型。如果您将解决方案移动到Android上,则无法工作,因为Android没有窗口的概念。这种破坏MVVM的解决方案得分-1。 - Spook

0
在MVVM WPF中,我通常将我的View设计为UserControl。这只是关于你想如何显示它的问题。如果你想要它在一个窗口中,那么你可以创建一个WindowService类:
public class WindowService
{
   //...

   public void Show_window(object viewModel, int height, int width, string title)
   {
     var window = new Window
     {
       Content = viewModel,
       Title = title,
       Height = height,
       Width = width,
       WindowStartupLocation = WindowStartupLocation.CenterOwner,
       Owner = Application.Current.MainWindow,
       Style = (Style)Application.Current.FindResource("Window_style") //even style can be added
      };

      //If you own custom window style, then you can bind close/minimize/maxmize/restore buttons like this 
      window.CommandBindings.Add(new CommandBinding(SystemCommands.CloseWindowCommand, OnCloseWindow));
      window.CommandBindings.Add(new CommandBinding(SystemCommands.MaximizeWindowCommand, OnMaximizeWindow, OnCanResizeWindow));
      window.CommandBindings.Add(new CommandBinding(SystemCommands.MinimizeWindowCommand, OnMinimizeWindow, OnCanMinimizeWindow));
      window.CommandBindings.Add(new CommandBinding(SystemCommands.RestoreWindowCommand, OnRestoreWindow, OnCanResizeWindow));
                  
      window.ShowDialog();
   }

   public void Close_window(object viewmodel)
   {
       //Close window
       foreach (Window item in Application.Current.Windows)
       {
          if (item.Content == viewmodel) item.Close();
       }
    }
}

使用我的方法非常简单。通常,当窗口中发生某些事情时,您希望关闭该窗口。所以,当它发生时,只需从相应的ViewModel - 这是一个DataContext的UserControl,在窗口中显示。请看下面的示例:

1.) 我们从某个ViewModel打开窗口:

public class MyViewModel // ViewModel where you open window
{
   private readonly WindowService _windowservice // or inject/inherit from Base

   public MyViewModel()
   {
     _windowservice = new WindowService();
   }  

   private void Example_method()
   {
        //...Open window
        _windowservice.Show_window(new WindowViewModel(),100,100,"Test window");
   }
  
}

2.) 我们的窗口已经打开,现在我们想要关闭它:

 public class WindowViewModel // ViewModel which is your Window content!
 {
     private readonly WindowService _windowservice // or inject/inherit from Base

     public MyViewModel()
     {
       _windowservice = new WindowService();
     }  

     private void Example_method()
     {
          //Close window
          _windowservice.Close(this); //Pass a reference of viewmodel to method
     }
    
  }

这个解决方案可能不如其他被接受的答案优雅,但对我来说很有效。我在项目中广泛使用它,到目前为止没有出现任何问题。但我相信总会有人会说“这违反了MVVM原则”。


-1

你可以使用以下代码关闭当前窗口:

Application.Current.Windows[0].Close();

-7
在视图模型中使用 System.Environment.Exit(0); 即可生效。

6
不会。它会退出应用程序,而不是关闭当前窗口。 - Tilak

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