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

3
FYI,我也遇到了这个问题,并想出了一种方法来解决它,不需要全局变量或静态变量,虽然可能不是最佳答案。让你们自己决定吧。
在我的情况下,实例化要显示的窗口(假设我们称之为ViewModelMain)的ViewModel也知道LoginFormViewModel(使用上面的情况作为示例)。
所以我创建了一个名为CloseWindowCommand的属性,类型为ICommand,在LoginFormViewModel上。然后,在调用Window的.ShowDialog()之前,我将LoginFormViewModel上的CloseWindowCommand属性设置为我实例化的Window的window.Close()方法。然后在LoginFormViewModel内部,我只需要调用CloseWindowCommand.Execute()就可以关闭窗口。
我认为这是一个解决方法,虽然有点绕。但它能很好地工作而不会真正破坏MVVM模式。
请随意批评这个过程,我能接受的! :)

3
这可能有点晚了,但我遇到了同样的问题,找到了适合我的解决方案。
我无法想象如何创建一个没有对话框的应用程序(也许只是心理障碍)。所以我在MVVM和显示对话框方面陷入了僵局。然后我看到了这篇CodeProject文章:

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

这是一个UserControl,基本上允许一个窗口在另一个窗口的可视树中存在(在xaml中不允许)。它还公开了一个名为IsShowing的布尔DependencyProperty。
您可以设置一个样式,通常在资源字典中,通过触发器显示对话框,只要控件的Content属性!= null。
<Style TargetType="{x:Type d:Dialog}">
    <Style.Triggers>
        <Trigger Property="HasContent"  Value="True">
            <Setter Property="Showing" Value="True" />
        </Trigger>
    </Style.Triggers>
</Style>

在您想要显示对话框的视图中,只需添加以下内容:
<d:Dialog Content="{Binding Path=DialogViewModel}"/>

在您的ViewModel中,您只需要将属性设置为一个值(注意:ViewModel类必须支持INotifyPropertyChanged,以便视图知道某些事情发生了)。
像这样:
DialogViewModel = new DisplayViewModel();

为了将ViewModel与View匹配,您应该在资源字典中添加类似以下内容的代码:
<DataTemplate DataType="{x:Type vm:DisplayViewModel}">
    <vw:DisplayView/>
</DataTemplate>

使用以上代码,您可以获得一个单行代码来显示对话框。但问题在于,您不能仅使用上述代码关闭对话框。因此,您必须在ViewModel基类中放置一个事件,DisplayViewModel继承自该基类,而不是使用上述代码,编写以下代码。
        var vm = new DisplayViewModel();
        vm.RequestClose += new RequestCloseHandler(DisplayViewModel_RequestClose);
        DialogViewModel = vm;

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

这可能看起来有点复杂,但是一旦奠定了基础,就很简单了。再次强调,这是我的实现方式,我相信还有其他的实现方式 :)

希望这可以帮助你,也帮助过我。


1

在众多答案中,我想补充一点。假设您的ViewModel上有一个ICommand,并且您希望该命令关闭其窗口(或执行任何其他操作),则可以使用以下代码:

var windows = Application.Current.Windows;
for (var i=0;i< windows.Count;i++ )
    if (windows[i].DataContext == this)
        windows[i].Close();

这并不完美,而且可能很难测试(因为很难模拟/存根静态),但它比其他解决方案更清晰(在我看来)。

Erick


1

行为是最方便的方法。

  • 一方面,它可以绑定到给定的视图模型(可以发出“关闭表单!”的信号)

  • 另一方面,它可以访问表单本身,因此可以订阅必要的特定于表单的事件,或显示确认对话框,或执行其他任何操作。

编写必要的行为可能会看起来很无聊。但是,从现在开始,您可以通过精确的一行XAML代码片段在每个需要的表单上重复使用它。如果需要,您可以将其提取为单独的程序集,以便包含在您想要的任何下一个项目中。


1

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

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:

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

1
我实施了Joe White的解决方案,但遇到了偶尔出现的“DialogResult can be set only after Window is created and shown as dialog”错误。
我在关闭视图后仍然保留ViewModel,并且偶尔稍后使用相同的VM打开新视图。似乎在旧视图被垃圾回收之前关闭新视图会导致DialogResultChanged尝试在已关闭的窗口上设置DialogResult属性,从而引发错误。
我的解决方案是将DialogResultChanged更改为检查窗口的IsLoaded属性:
private static void DialogResultChanged(
    DependencyObject d,
    DependencyPropertyChangedEventArgs e)
{
    var window = d as Window;
    if (window != null && window.IsLoaded)
        window.DialogResult = e.NewValue as bool?;
}

在进行此更改后,关闭对话框的任何附件都将被忽略。


1
我最终将Joe White的答案Adam Mills的答案中的一些代码混合在一起,因为我需要在程序创建的窗口中显示用户控件。因此,DialogCloser不需要在窗口上,它可以在用户控件本身上。
<UserControl ...
    xmlns:xw="clr-namespace:Wpf"
    xw:DialogCloser.DialogResult="{Binding DialogResult}">

如果用户控件未附加到窗口本身,则DialogCloser将查找用户控件的窗口。
namespace 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.GetWindow();
      if (window != null)
        window.DialogResult = e.NewValue as bool?;
    }

    public static void SetDialogResult(DependencyObject target, bool? value)
    {
      target.SetValue(DialogResultProperty, value);
    }
  }

  public static class Extensions
  {
    public static Window GetWindow(this DependencyObject sender_)
    {
      Window window = sender_ as Window;        
      return window ?? Window.GetWindow( sender_ );
    }
  }
}

0
在你的 View/任何 UserControl(或你想关闭的 Window) 中创建一个依赖属性(Dependency Property),就像下面这样:
 public bool CloseTrigger
        {
            get { return (bool)GetValue(CloseTriggerProperty); }
            set { SetValue(CloseTriggerProperty, value); }
        }

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

        private static void OnCloseTriggerChanged(DependencyObject dp, DependencyPropertyChangedEventArgs e)
        {
            //write Window Exit Code
        }

然后从您的ViewModel属性绑定它:

<Window x:Class="WpfStackOverflowTempProject.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"  Width="525"
        CloseTrigger="{Binding Path=CloseWindow,Mode=TwoWay}"

ViewModel中的属性:

private bool closeWindow;

    public bool CloseWindow
    {
        get { return closeWindow; }
        set 
        { 
            closeWindow = value;
            RaiseChane("CloseWindow");
        }
    }

现在通过更改ViewModel中的CloseWindow值来触发关闭操作。 :)


0
另一个解决方案是在视图模型中创建具有INotifyPropertyChanged属性的属性,例如DialogResult,然后在代码后台编写以下内容:
public class SomeWindow: ChildWindow
{
    private SomeViewModel _someViewModel;

    public SomeWindow()
    {
        InitializeComponent();

        this.Loaded += SomeWindow_Loaded;
        this.Closed += SomeWindow_Closed;
    }

    void SomeWindow_Loaded(object sender, RoutedEventArgs e)
    {
        _someViewModel = this.DataContext as SomeViewModel;
        _someViewModel.PropertyChanged += _someViewModel_PropertyChanged;
    }

    void SomeWindow_Closed(object sender, System.EventArgs e)
    {
        _someViewModel.PropertyChanged -= _someViewModel_PropertyChanged;
        this.Loaded -= SomeWindow_Loaded;
        this.Closed -= SomeWindow_Closed;
    }

    void _someViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == SomeViewModel.DialogResultPropertyName)
        {
            this.DialogResult = _someViewModel.DialogResult;
        }
    }
}

最重要的片段是_someViewModel_PropertyChangedDialogResultPropertyName可以是SomeViewModel中的一些公共常量字符串。
我使用这种技巧来在视图控件中进行一些更改,以防在ViewModel中难以实现。在ViewModel中的OnPropertyChanged,您可以在View中执行任何想要的操作。ViewModel仍然是“可单元测试的”,而代码后台中的一些小行代码并没有什么区别。

0

虽然这并没有回答如何通过viewmodel实现这个问题,但是它展示了如何只使用XAML和Blend SDK来完成它。

我选择从Blend SDK中下载并使用两个文件,你可以通过NuGet从Microsoft获取这两个文件:

System.Windows.Interactivity.dll 和 Microsoft.Expression.Interactions.dll

Microsoft.Expression.Interactions.dll为您提供了诸如在viewmodel或其他目标上设置属性或调用方法的能力,并且还有其他内部小部件。

一些XAML:

<Window x:Class="Blah.Blah.MyWindow"
    ...
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
  ...>
 <StackPanel>
    <Button x:Name="OKButton" Content="OK">
       <i:Interaction.Triggers>
          <i:EventTrigger EventName="Click">
             <ei:ChangePropertyAction
                      TargetObject="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
                      PropertyName="DialogResult"
                      Value="True"
                      IsEnabled="{Binding SomeBoolOnTheVM}" />                                
          </i:EventTrigger>
    </Button>
    <Button x:Name="CancelButton" Content="Cancel">
       <i:Interaction.Triggers>
          <i:EventTrigger EventName="Click">
             <ei:ChangePropertyAction
                      TargetObject="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
                      PropertyName="DialogResult"
                      Value="False" />                                
          </i:EventTrigger>
    </Button>

    <Button x:Name="CloseButton" Content="Close">
       <i:Interaction.Triggers>
                <i:EventTrigger EventName="Click">
                    <!-- method being invoked should be void w/ no args -->
                    <ei:CallMethodAction
                        TargetObject="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}"
                        MethodName="Close" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
    </Button>
 <StackPanel>
</Window>

请注意,如果您只需要简单的“确定/取消”行为,则可以使用IsDefault和IsCancel属性,并且窗口显示时可以轻松实现。
我个人遇到了一个问题,即当页面加载时,设置了IsDefault属性为true的按钮被隐藏。在它显示后,它似乎不想友好地工作,所以我只是像上面那样设置了Window.DialogResult属性,这对我很有效。

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