C# ViewModel 应该如何关闭表单?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/501886/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-04 05:50:18  来源:igfitidea点击:

How should the ViewModel close the form?

c#wpfmvvm

提问by Orion Edwards

I'm trying to learn WPF and the MVVM problem, but have hit a snag. This question is similar but not quite the same as this one (handling-dialogs-in-wpf-with-mvvm)...

我正在尝试学习 WPF 和 MVVM 问题,但遇到了障碍。这个问题与这个问题相似但不完全相同(handling-dialogs-in-wpf-with-mvvm)......

I have a "Login" form written using the MVVM pattern.

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

This form has a ViewModel which holds the Username and Password, which are bound to the view in the XAML using normal data bindings. It also has a "Login" command which is bound to the "Login" button on the form, agan using normal databinding.

这个表单有一个保存用户名和密码的 ViewModel,它们使用普通数据绑定绑定到 XAML 中的视图。它还有一个“登录”命令,它绑定到表单上的“登录”按钮,使用普通数据绑定。

When the "Login" command fires, it invokes a function in the ViewModel which goes off and sends data over the network to log in. When this function completes, there are 2 actions:

当“登录”命令触发时,它调用 ViewModel 中的一个函数,该函数关闭并通过网络发送数据以登录。当这个函数完成时,有两个动作:

  1. The login was invalid - we just show a MessageBox and all is fine

  2. The login was valid, we need to close the Login form and have it return true as its DialogResult...

  1. 登录无效 - 我们只显示一个 MessageBox,一切正常

  2. 登录是有效的,我们需要关闭登录表单并让它返回 true 作为它的DialogResult......

The problem is, the ViewModel knows nothing about the actual view, so how can it close the view and tell it to return a particular DialogResult?? I could stick some code in the CodeBehind, and/or pass the View through to the ViewModel, but that seems like it would defeat the whole point of MVVM entirely...

问题是,ViewModel 对实际视图一无所知,那么它如何关闭视图并告诉它返回特定的 DialogResult 呢?我可以在 CodeBehind 中粘贴一些代码,和/或将 View 传递给 ViewModel,但这似乎会完全打败 MVVM 的全部意义......



Update

更新

In the end I just violated the "purity" of the MVVM pattern and had the View publish a Closedevent, and expose a Closemethod. The ViewModel would then just call view.Close. The view is only known via an interface and wired up via an IOC container, so no testability or maintainability is lost.

最后我只是违反了 MVVM 模式的“纯洁性”,让 View 发布了一个Closed事件,并公开了一个Close方法。然后 ViewModel 只会调用view.Close. 该视图仅通过接口已知并通过 IOC 容器连接,因此不会丢失可测试性或可维护性。

It seems rather silly that the accepted answer is at -5 votes! While I'm well aware of the good feelings that one gets by solving a problem while being "pure", Surely I'm not the only one that thinks that 200 lines of events, commands and behaviors just to avoid a one line method in the name of "patterns" and "purity" is a bit ridiculous....

接受的答案是 -5 票,这似乎很愚蠢!虽然我很清楚一个人在“纯粹”的情况下解决问题所获得的好感,当然我并不是唯一一个认为 200 行事件、命令和行为只是为了避免在“图案”和“纯度”的名字有点可笑......

回答by Orion Edwards

Here's what I initially did, which does work, however it seems rather long-winded and ugly (global static anything is never good)

这是我最初所做的,它确实有效,但它似乎相当冗长和丑陋(全局静态任何东西都永远不会好)

1: App.xaml.cs

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

2:登录表单.xaml

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

3: LoginForm.xaml.cs

3:登录表单.xaml.cs

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

4: LoginFormViewModel.cs

4:LoginFormViewModel.cs

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


I later on then removed all this code, and just had the LoginFormViewModelcall the Close method on it's view. It ended up being much nicer and easier to follow. IMHO the point of patterns is to give people an easier way to understand what your app is doing, and in this case, MVVM was making it far harder to understand than if I hadn't used it, and was now an anti-pattern.

后来我删除了所有这些代码,只是LoginFormViewModel在它的视图上调用了 Close 方法。它最终变得更好,更容易遵循。恕我直言,模式的重点是让人们更容易理解你的应用程序在做什么,在这种情况下,MVVM 比我没有使用它更难理解,现在是一种模式。

回答by EightyOne Unite

There are a lot of comments arguing the pros and cons of MVVM here. For me, I agree with Nir; it's a matter of using the pattern appropriately and MVVM doesn't always fit. People seems to have become willing to sacrifice all of the most important principles of software design JUST to get it to fit MVVM.

有很多评论在这里争论 MVVM 的优缺点。对我来说,我同意 Nir;这是适当使用模式的问题,MVVM 并不总是适合。人们似乎愿意牺牲软件设计的所有最重要的原则,只是为了让它适合 MVVM。

That said,..i think your case could be a good fit with a bit of refactoring.

也就是说,..我认为您的案例可能非常适合进行一些重构。

In most cases I've come across, WPF enables you to get by WITHOUT multiple Windows. Maybe you could try using Frames and Pages instead of Windows with DialogResults.

在我遇到的大多数情况下,WPF 使您无需多个Windows。也许您可以尝试使用Frames 和Pages 而不是带有DialogResults的 Windows 。

In your case my suggestion would be have LoginFormViewModelhandle the LoginCommandand if the login is invalid, set a property on LoginFormViewModelto an appropriate value (falseor some enum value like UserAuthenticationStates.FailedAuthentication). You'd do the same for a successful login (trueor some other enum value). You'd then use a DataTriggerwhich responds to the various user authentication states and could use a simple Setterto change the Sourceproperty of the Frame.

在您的情况下,我的建议是LoginFormViewModel处理LoginCommand,如果登录无效,请将属性设置为LoginFormViewModel适当的值(false或某些枚举值,如UserAuthenticationStates.FailedAuthentication)。您可以为成功登录(true或其他一些枚举值)执行相同的操作。然后你会使用DataTrigger它响应各种用户的认证状态并可以使用一个简单的Setter更改Source的属性Frame

Having your login Window return a DialogResulti think is where you're getting confused; that DialogResultis really a property of your ViewModel. In my, admittedly limited experience with WPF, when something doesn't feel right it usually because I'm thinking in terms of how i would've done the same thing in WinForms.

让您的登录窗口返回一个DialogResult我认为是您感到困惑的地方;这DialogResult确实是您的 ViewModel 的一个属性。在我对 WPF 的公认有限的经验中,当某些事情感觉不对时,通常是因为我在考虑如何在 WinForms 中做同样的事情。

Hope that helps.

希望有帮助。

回答by EightyOne Unite

The way I would handle it is to add an event handler in my ViewModel. When the user was successfully logged in I would fire the event. In my View I would attach to this event and when it fired I would close the window.

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

回答by EightyOne Unite

FYI, I ran into this same problem and I think I figured out a work around that doesn't require globals or statics, although it may not be the best answer. I let the you guys decide that for yourself.

仅供参考,我遇到了同样的问题,我想我想出了一个不需要全局变量或静态变量的解决方法,尽管它可能不是最好的答案。我让你们自己决定。

In my case, the ViewModel that instantiates the Window to be displayed (lets call it ViewModelMain) also knows about the LoginFormViewModel (using the situation above as an example).

在我的例子中,实例化要显示的窗口的 ViewModel(我们称之为 ViewModelMain)也知道 LoginFormViewModel(以上面的情况为例)。

So what I did was to create a property on the LoginFormViewModel that was of type ICommand (Lets call it CloseWindowCommand). Then, before I call .ShowDialog() on the Window, I set the CloseWindowCommand property on the LoginFormViewModel to the window.Close() method of the Window I instantiated. Then inside the LoginFormViewModel all I have to do is call CloseWindowCommand.Execute() to close the window.

所以我所做的是在 LoginFormViewModel 上创建一个 ICommand 类型的属性(我们称之为 CloseWindowCommand)。然后,在我在 Window 上调用 .ShowDialog() 之前,我将 LoginFormViewModel 上的 CloseWindowCommand 属性设置为我实例化的 Window 的 window.Close() 方法。然后在 LoginFormViewModel 中,我所要做的就是调用 CloseWindowCommand.Execute() 来关闭窗口。

It is a bit of a workaround/hack I suppose, but it works well without really breaking the MVVM pattern.

我认为这是一种解决方法/黑客攻击,但它运行良好而不会真正破坏 MVVM 模式。

Feel free to critique this process as much as you like, I can take it! :)

随意评论这个过程,我可以接受!:)

回答by Jose

This is probably very late, but I came across the same problem and I found a solution that works for me.

这可能很晚了,但我遇到了同样的问题,我找到了一个适合我的解决方案。

I can't figure out how to create an app without dialogs(maybe it's just a mind block). So I was at an impasse with MVVM and showing a dialog. So I came across this CodeProject article:

我不知道如何创建没有对话框的应用程序(也许这只是一个思维障碍)。所以我与 MVVM 陷入了僵局并显示了一个对话框。所以我遇到了这篇 CodeProject 文章:

http://www.codeproject.com/KB/WPF/XAMLDialog.aspx

http://www.codeproject.com/KB/WPF/XAMLDialog.aspx

Which is a UserControl that basically allows a window to be within the visual tree of another window(not allowed in xaml). It also exposes a boolean DependencyProperty called IsShowing.

这是一个 UserControl,它基本上允许一个窗口位于另一个窗口的可视化树中(在 xaml 中不允许)。它还公开了一个名为 IsShowing 的布尔 DependencyProperty。

You can set a style like,typically in a resourcedictionary, that basically displays the dialog whenever the Content property of the control != null via triggers:

您可以设置一种样式,通常在资源字典中,只要控件的 Content 属性 != null 通过触发器基本上显示对话框:

<Style TargetType="{x:Type d:Dialog}">
    <Style.Triggers>
        <Trigger Property="HasContent"  Value="True">
            <Setter Property="Showing" Value="True" />
        </Trigger>
    </Style.Triggers>
</Style>

In the view where you want to display the dialog simply have this:

在要显示对话框的视图中,只需要有以下内容:

<d:Dialog Content="{Binding Path=DialogViewModel}"/>

And in your ViewModel all you have to do is set the property to a value(Note: the ViewModel class must support INotifyPropertyChanged for the view to know something happened ).

在您的 ViewModel 中,您所要做的就是将属性设置为一个值(注意:ViewModel 类必须支持 INotifyPropertyChanged 以便视图知道发生了什么)。

like so:

像这样:

DialogViewModel = new DisplayViewModel();

To match the ViewModel with the View you should have something like this in a resourcedictionary:

要将 ViewModel 与 View 匹配,您应该在资源字典中有这样的内容:

<DataTemplate DataType="{x:Type vm:DisplayViewModel}">
    <vw:DisplayView/>
</DataTemplate>

With all of that you get a one-liner code to show dialog. The problem you get is you can't really close the dialog with just the above code. So that's why you have to put in an event in a ViewModel base class which DisplayViewModel inherits from and instead of the code above, write this

有了所有这些,您将获得一个单行代码来显示对话框。你得到的问题是你不能真正用上面的代码关闭对话框。所以这就是为什么你必须在 DisplayViewModel 继承的 ViewModel 基类中放置一个事件,而不是上面的代码,写这个

        var vm = new DisplayViewModel();
        vm.RequestClose += new RequestCloseHandler(DisplayViewModel_RequestClose);
        DialogViewModel = vm;

Then you can handle the result of the dialog via the callback.

然后您可以通过回调处理对话框的结果。

This may seem a little complex, but once the groundwork is laid, it's pretty straightforward. Again this is my implementation, I'm sure there are others :)

这可能看起来有点复杂,但是一旦奠定了基础,就非常简单了。这又是我的实现,我确定还有其他实现:)

Hope this helps, it saved me.

希望这会有所帮助,它救了我。

回答by Adam Mills

I used attached behaviours to close the window. Bind a "signal" property on your ViewModel to the attached behaviour (I actually use a trigger) When it's set to true, the behaviour closes the window.

我使用附加行为来关闭窗口。将 ViewModel 上的“信号”属性绑定到附加的行为(我实际上使用触发器)当它设置为 true 时,该行为将关闭窗口。

http://adammills.wordpress.com/2009/07/01/window-close-from-xaml/

http://adammills.wordpress.com/2009/07/01/window-close-from-xaml/

回答by Jim Wallace

Assuming your login dialog is the first window that gets created, try this inside your LoginViewModel class:

假设您的登录对话框是创建的第一个窗口,请在您的 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;
        }
    }

回答by Budda

From my perspective the question is pretty good as the same approach would be used not only for the "Login" window, but for any kind of window. I've reviewed a lot of suggestions and none are OK for me. Please review my suggestion that was taken from the MVVM design pattern article.

从我的角度来看,这个问题非常好,因为同样的方法不仅可以用于“登录”窗口,还可以用于任何类型的窗口。我已经审查了很多建议,但没有一个适合我。请查看我从MVVM 设计模式文章中提出的建议。

Each ViewModel class should inherit from WorkspaceViewModelthat has the RequestCloseevent and CloseCommandproperty of the ICommandtype. The default implementation of the CloseCommandproperty will raise the RequestCloseevent.

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

In order to get the window closed, the OnLoadedmethod of your window should be overridden:

为了关闭窗口,OnLoaded您的窗口方法应该被覆盖:

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

or OnStartupmethod of you app:

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();
    }

I guess that RequestCloseevent and CloseCommandproperty implementation in the WorkspaceViewModelare pretty clear, but I will show them to be consistent:

我想RequestClose事件和CloseCommand属性的实现WorkspaceViewModel很清楚,但我会证明它们是一致的:

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

And the source code of the RelayCommand:

和的源代码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
}


P.S.Don't treat me badly for those sources! If I had them yesterday that would have saved me a few hours...

PS不要因为这些消息来源对我不好!如果我昨天有它们,那会节省我几个小时......

P.P.S.Any comments or suggestions are welcome.

PPS欢迎任何意见或建议。

回答by Joe White

I was inspired by Thejuan's answerto write a simpler attached property. No styles, no triggers; instead, you can just do this:

我受到Thejuan 的回答的启发,写了一个更简单的附加属性。没有样式,没有触发器;相反,你可以这样做:

<Window ...
        xmlns:xc="clr-namespace:ExCastle.Wpf"
        xc:DialogCloser.DialogResult="{Binding DialogResult}">

This is almost as clean as if the WPF team had gotten it right and made DialogResult a dependency property in the first place. Just put a bool? DialogResultproperty on your ViewModel and implement INotifyPropertyChanged, and voilà, your ViewModel can close the Window (and set its DialogResult) just by setting a property. MVVM as it should be.

这几乎就像 WPF 团队已经做对并首先将 DialogResult 设为依赖属性一样干净。只需bool? DialogResult在您的 ViewModel 上放置一个属性并实现 INotifyPropertyChanged,瞧,您的 ViewModel 只需设置一个属性即可关闭窗口(并设置其 DialogResult)。MVVM 应该如此。

Here's the code for DialogCloser:

这是 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);
        }
    }
}

I've also posted this on my blog.

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

回答by chrislarson

Why not just pass the window as a command parameter?

为什么不将窗口作为命令参数传递?

C#:

C#:

 private void Cancel( Window window )
  {
     window.Close();
  }

  private ICommand _cancelCommand;
  public ICommand CancelCommand
  {
     get
     {
        return _cancelCommand ?? ( _cancelCommand = new Command.RelayCommand<Window>(
                                                      ( window ) => Cancel( window ),
                                                      ( window ) => ( true ) ) );
     }
  }

XAML:

XAML:

<Window x:Class="WPFRunApp.MainWindow"
        x:Name="_runWindow"
...
   <Button Content="Cancel"
           Command="{Binding Path=CancelCommand}"
           CommandParameter="{Binding ElementName=_runWindow}" />