¿Cómo debe cerrar ViewModel el formulario?

247

Estoy tratando de aprender WPF y el problema MVVM, pero he encontrado un obstáculo. Esta pregunta es similar pero no es exactamente la misma que esta (manejo-diálogos-en-wpf-con-mvvm) ...

Tengo un formulario de "Inicio de sesión" escrito con el patrón MVVM.

Este formulario tiene un ViewModel que contiene el Nombre de usuario y la Contraseña, que están vinculados a la vista en XAML utilizando enlaces de datos normales. También tiene un comando "Iniciar sesión" que está vinculado al botón "Iniciar sesión" en el formulario, de nuevo usando el enlace de datos normal.

Cuando se activa el comando "Iniciar sesión", invoca una función en ViewModel que se apaga y envía datos a través de la red para iniciar sesión. Cuando se completa esta función, hay 2 acciones:

  1. El inicio de sesión no era válido: solo mostramos un MessageBox y todo está bien

  2. El inicio de sesión fue válido, debemos cerrar el formulario de inicio de sesión y hacer que regrese verdadero como su DialogResult...

El problema es que ViewModel no sabe nada sobre la vista real, entonces, ¿cómo puede cerrar la vista y decirle que devuelva un DialogResult particular? Podría pegar algo de código en CodeBehind, y / o pasar la vista al ViewModel, pero parece que podría derrotar por completo el punto de MVVM ...


Actualizar

Al final, simplemente violé la "pureza" del patrón MVVM e hice que View publicara un Closedevento y exponga un Closemétodo. ViewModel simplemente llamaría view.Close. La vista solo se conoce a través de una interfaz y se conecta a través de un contenedor IOC, por lo que no se pierde capacidad de prueba ni mantenimiento.

¡Parece bastante tonto que la respuesta aceptada sea de -5 votos! Si bien soy consciente de los buenos sentimientos que uno tiene al resolver un problema mientras es "puro", seguramente no soy el único que piensa que 200 líneas de eventos, comandos y comportamientos solo para evitar un método de una línea en el nombre de "patrones" y "pureza" es un poco ridículo ...

Orion Edwards
fuente
2
No rechacé la respuesta aceptada, pero supongo que la razón de los votos negativos es que no es útil en general, incluso si podría funcionar en un caso. Lo dijiste tú mismo en otro comentario: "Si bien el formulario de inicio de sesión es un cuadro de diálogo de 'dos ​​campos', tengo muchos otros que son mucho más complejos (y por lo tanto justifican MVVM), pero aún necesitan cerrarse ..."
Joe Blanco
1
Entiendo su punto de vista, pero personalmente creo que incluso para el caso general, un Closemétodo simple sigue siendo la mejor solución. Todo lo demás en los otros diálogos más complejos es MVVM y databound, pero parecía una tontería implementar las enormes "soluciones" aquí en lugar de solo un método simple ...
Orion Edwards
2
Puede consultar el siguiente enlace para ver el resultado del diálogo asimsajjad.blogspot.com/2010/10/… , que devolverá el diálogo y cerrará la vista desde viewModel
Asim Sajjad
3
Por favor cambie la respuesta aceptada a esta pregunta. Hay muchas buenas soluciones que son mucho mejores que alguien que cuestiona el uso de MVVM para esta funcionalidad. Esa no es una respuesta, eso es evitar.
ScottCher
2
@OrionEdwards Creo que tienes razón al romper el patrón aquí. El propósito principal de un patrón de diseño es acelerar los ciclos de desarrollo, aumentar la capacidad de mantenimiento y simplificar su código haciendo que todo el equipo siga las mismas reglas. Esto no se logra agregando dependencias en bibliotecas externas e implementando cientos de líneas de código para realizar una tarea, ignorando totalmente que hay una solución mucho más simple, solo porque uno es terco para sacrificar la "pureza" del patrón. Sólo asegúrese de documentar lo your've hecho y BESAR su código ( k eep i t s hort y s imple).
M463

Respuestas:

324

Me inspiró la respuesta de Thejuan para escribir una propiedad adjunta más simple. Sin estilos, sin desencadenantes; en cambio, puedes hacer esto:

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

Esto es casi tan limpio como si el equipo de WPF lo hubiera hecho bien e hiciera de DialogResult una propiedad de dependencia en primer lugar. Simplemente coloque una bool? DialogResultpropiedad en su ViewModel e implemente INotifyPropertyChanged, y listo, su ViewModel puede cerrar la Ventana (y establecer su DialogResult) simplemente configurando una propiedad. MVVM como debería ser.

Aquí está el código para 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);
        }
    }
}

También publiqué esto en mi blog .

Joe White
fuente
3
¡Esta es la respuesta que más me gusta! Buen trabajo escribiendo esa propiedad adjunta.
Jorge Vargas
2
Buena opción, pero hay un error sutil en esta solución. Si el modelo de vista para el cuadro de diálogo es un singleton, el valor DialogResult se transfiere al siguiente uso del cuadro de diálogo. Eso significa que se cancelará o aceptará instantáneamente antes de mostrarse para que el diálogo no se muestre por segunda vez.
Gone Coding
13
@HiTech Magic, parece que el error está en usar un ViewModel singleton en primer lugar. (sonríe) En serio, ¿por qué querrías un ViewModel singleton? Es una mala idea mantener el estado mutable en variables globales. Hace que las pruebas sean una pesadilla, y las pruebas son una de las razones por las que usaría MVVM en primer lugar.
Joe White
3
¿No es el punto de MVVM no acoplar estrechamente su lógica a una interfaz de usuario específica? En este caso, bool? Ciertamente no es utilizable por otra interfaz de usuario como WinForm, y DialogCloser es específico de WPF. Entonces, ¿cómo encaja esto como una solución? Además, ¿por qué escribir código 2x-10x solo para cerrar una ventana a través de un enlace?
David Anderson
2
@DavidAnderson, no probaría MVVM con WinForms en ningún caso; su soporte de enlace de datos es demasiado débil y MVVM se basa en un sistema de enlace bien pensado. Y no está cerca del código 2x-10x. Escribe ese código una vez , no una vez para cada ventana. Después de eso, es un enlace de una línea más una propiedad de notificación, usando el mismo mecanismo que ya está usando para todo lo demás en su vista (por lo tanto, por ejemplo, no necesita inyectar una interfaz de vista adicional solo para manejar el cierre del ventana). Puede hacer otras compensaciones, pero en general me parece un buen negocio.
Joe White,
64

Desde mi punto de vista, la pregunta es bastante buena, ya que el mismo enfoque se usaría no solo para la ventana "Iniciar sesión", sino para cualquier tipo de ventana. He revisado muchas sugerencias y ninguna está bien para mí. Revise mi sugerencia que se tomó del artículo del patrón de diseño MVVM .

Cada clase ViewModel debe heredar de WorkspaceViewModelque tiene el RequestCloseevento y la CloseCommandpropiedad del ICommandtipo. La implementación predeterminada de la CloseCommandpropiedad generará el RequestCloseevento.

Para cerrar la OnLoadedventana, debe anularse el método de su ventana:

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

o OnStartupmétodo de su aplicación:

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

Supongo que la implementación de RequestCloseeventos y CloseCommandpropiedades en el WorkspaceViewModeles bastante clara, pero les mostraré que son consistentes:

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

Y el código fuente de 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
}

PD: ¡No me traten mal por esas fuentes! Si los tuviera ayer eso me habría ahorrado unas horas ...

PPS Cualquier comentario o sugerencia es bienvenida.

Budda
fuente
2
Umm, el hecho de que hayas conectado el controlador de eventos customer.RequestCloseen el código detrás de tu archivo XAML no viola el patrón MVVM? En primer lugar, también podría vincularse al Clickcontrolador de eventos en su botón de cierre al ver que de todos modos tocó el código e hizo un this.Close()! ¿Correcto?
GONeale
1
No tengo demasiados problemas con el enfoque del evento, pero no me gusta la palabra RequestClose porque para mí todavía implica mucho conocimiento sobre la implementación de la Vista. Prefiero exponer propiedades como IsCancelled que tienden a ser más significativas dado el contexto e implican menos sobre lo que se supone que debe hacer la vista en respuesta.
jpierson
18

Usé comportamientos adjuntos para cerrar la ventana. Enlace una propiedad de "señal" en su ViewModel al comportamiento adjunto (en realidad uso un disparador) Cuando se establece en verdadero, el comportamiento cierra la ventana.

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

Adam Mills
fuente
Esta es la única respuesta hasta el momento que no requiere ningún código detrás de la ventana (y en realidad cierra una ventana modal, en lugar de sugerir otro enfoque). Lástima que requiera tanta complejidad, con Style and Trigger y toda esa basura, parece que esto realmente debería ser posible con un comportamiento adjunto de una línea.
Joe White el
44
Ahora es factible con un comportamiento adjunto de una línea. Vea mi respuesta: stackoverflow.com/questions/501886/…
Joe White el
15

Hay muchos comentarios que discuten los pros y los contras de MVVM aquí. Para mí, estoy de acuerdo con Nir; es cuestión de usar el patrón de manera adecuada y MVVM no siempre encaja. La gente parece estar dispuesta a sacrificar todos los principios más importantes del diseño de software SOLO para que se ajuste a MVVM.

Dicho esto, creo que su caso podría encajar bien con un poco de refactorización.

En la mayoría de los casos que he encontrado, WPF le permite pasar SIN múltiples correos electrónicos Window. Tal vez usted podría tratar de usar Frames y Pages en lugar de Windows con DialogResults.

En su caso, mi sugerencia sería LoginFormViewModelmanejar el LoginCommandy, si el inicio de sesión no es válido, establezca una propiedad en LoginFormViewModelun valor apropiado ( falseo algún valor de enumeración como UserAuthenticationStates.FailedAuthentication). Haría lo mismo para un inicio de sesión exitoso ( trueo algún otro valor de enumeración). Luego usaría un DataTriggerque responde a los diversos estados de autenticación de usuario y podría usar un simple Setterpara cambiar la Sourcepropiedad del Frame.

Hacer que su ventana de inicio de sesión regrese DialogResultcreo que es donde se está confundiendo; eso DialogResultes realmente una propiedad de su ViewModel. En mi experiencia, ciertamente limitada, con WPF, cuando algo no se siente bien, generalmente porque estoy pensando en términos de cómo habría hecho lo mismo en WinForms.

Espero que ayude.

Ochenta y uno
fuente
10

Suponiendo que su diálogo de inicio de sesión es la primera ventana que se crea, intente esto dentro de su clase 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;
        }
    }
Jim Wallace
fuente
Hombres, esto es simple y funciona muy bien. Actualmente estoy usando este enfoque.
Erre Efe el
Funciona solo para la ventana PRINCIPAL. Así que no lo use para ninguna otra ventana.
Oleksii
7

Esta es una solución simple y limpia: agrega un evento a ViewModel e indica a la ventana que se cierre cuando se activa ese evento.

Para obtener más detalles, consulte mi publicación de blog, Cerrar ventana de 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>

ViewModel:

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

Nota: El ejemplo usa Prism's DelegateCommand(ver Prism: Command ), pero cualquier ICommandimplementación puede usarse para ese asunto.

Puede usar comportamientos de este paquete oficial.

Shimmy Weitzhandler
fuente
2
+1, pero debe proporcionar más detalles en la propia respuesta, por ejemplo, que esta solución requiere referencia al ensamblaje de Expression Blend Interactivity.
surfen
6

La forma en que lo manejaría es agregar un controlador de eventos en mi ViewModel. Cuando el usuario iniciaba sesión correctamente, activaba el evento. En mi vista, me adjuntaría a este evento y, cuando se disparara, cerraría la ventana.


fuente
2
Eso es lo que suelo hacer también. Aunque eso parece un poco sucio teniendo en cuenta todas esas cosas novedosas que controlan wpf.
Botz3000
4

Esto es lo que hice inicialmente, que funciona, sin embargo, parece bastante largo y feo (todo estático global nunca es bueno)

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

Más tarde, eliminé todo este código y acabo de LoginFormViewModelllamar al método Cerrar en su vista. Terminó siendo mucho más agradable y fácil de seguir. En mi humilde opinión, el objetivo de los patrones es dar a las personas una forma más fácil de entender lo que está haciendo su aplicación, y en este caso, MVVM estaba haciendo que sea mucho más difícil de entender que si no la hubiera usado, y ahora era un anti- patrón.

Orion Edwards
fuente
3

Para su información, me encontré con este mismo problema y creo que descubrí una solución que no requiere estáticos ni estadísticas, aunque puede que no sea la mejor respuesta. Dejo que ustedes decidan eso por ustedes mismos.

En mi caso, el ViewModel que crea una instancia de la ventana que se mostrará (llamémoslo ViewModelMain) también conoce el LoginFormViewModel (usando la situación anterior como ejemplo).

Entonces, lo que hice fue crear una propiedad en el LoginFormViewModel que era del tipo ICommand (vamos a llamarlo CloseWindowCommand). Luego, antes de llamar a .ShowDialog () en la ventana, configuro la propiedad CloseWindowCommand en LoginFormViewModel en el método window.Close () de la ventana que ejecuté. Luego, dentro de LoginFormViewModel, todo lo que tengo que hacer es llamar a CloseWindowCommand.Execute () para cerrar la ventana.

Supongo que es un poco como una solución / truco, pero funciona bien sin realmente romper el patrón MVVM.

No dudes en criticar este proceso tanto como quieras, ¡puedo soportarlo! :)


fuente
No estoy seguro de haberlo asimilado por completo, pero ¿no significa esto que su MainWindow debe ser instanciada antes de su LoginWindow? Eso es algo que me gustaría evitar si es posible
Orion Edwards
3

Probablemente sea muy tarde, pero me encontré con el mismo problema y encontré una solución que me funciona.

No puedo entender cómo crear una aplicación sin diálogos (tal vez es solo un bloqueo mental). Así que estaba en un punto muerto con MVVM y mostrando un diálogo. Entonces me encontré con este artículo de CodeProject:

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

Que es un UserControl que básicamente permite que una ventana esté dentro del árbol visual de otra ventana (no permitido en xaml). También expone una propiedad de dependencia booleana llamada IsShowing.

Puede establecer un estilo como, generalmente en una biblioteca de recursos, que básicamente muestra el cuadro de diálogo siempre que la propiedad Contenido del control! = Nulo a través de activadores:

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

En la vista donde desea mostrar el cuadro de diálogo, simplemente tenga esto:

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

Y en su ViewModel todo lo que tiene que hacer es establecer la propiedad en un valor (Nota: la clase ViewModel debe admitir INotifyPropertyChanged para que la vista sepa que algo sucedió).

al igual que:

DialogViewModel = new DisplayViewModel();

Para hacer coincidir el modelo de vista con la vista, debe tener algo como esto en una biblioteca de recursos:

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

Con todo eso, obtienes un código de una línea para mostrar el diálogo. El problema que obtienes es que realmente no puedes cerrar el diálogo solo con el código anterior. Es por eso que debe poner un evento en una clase base ViewModel de la cual DisplayViewModel hereda y, en lugar del código anterior, escriba esto

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

Luego puede manejar el resultado del diálogo a través de la devolución de llamada.

Esto puede parecer un poco complejo, pero una vez que se sientan las bases, es bastante sencillo. Nuevamente, esta es mi implementación, estoy seguro de que hay otros :)

Espero que esto ayude, me salvó.

Jose
fuente
3

Ok, entonces esta pregunta tiene casi 6 años y todavía no puedo encontrar aquí lo que creo que es la respuesta correcta, así que permítanme compartir mis "2 centavos" ...

De hecho, tengo 2 formas de hacerlo, la primera es la simple ... la segunda en la correcta, por lo que si está buscando la correcta, omita el n. ° 1 y salte al n. ° 2 :

1. Rápido y fácil (pero no completo)

Si solo tengo un pequeño proyecto, a veces solo creo una CloseWindowAction en ViewModel:

        public Action CloseWindow { get; set; } // In MyViewModel.cs

Y quien crea la Vista, o en el código de la Vista detrás, acabo de configurar el Método que la Acción llamará:

(recuerde que MVVM se trata de la separación de la vista y el modelo de vista ... el código de la vista detrás sigue siendo la vista y siempre que haya una separación adecuada, no está violando el patrón)

Si algún ViewModel crea una nueva ventana:

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

O si lo desea en su ventana principal, simplemente colóquelo debajo del constructor de su vista:

public MyView()
{
    InitializeComponent();           
    this.DataContext = new MainViewModel
                           {
                                CloseWindow = this.Close
                           };
}

cuando desee cerrar la ventana, simplemente llame a la Acción en su ViewModel.


2. La forma correcta

Ahora, la forma correcta de hacerlo es utilizando Prism (en mi humilde opinión), y todo se puede encontrar aquí .

Puede realizar una Solicitud de interacción , completarla con los datos que necesitará en su nueva ventana, almorzarla, cerrarla e incluso recibir datos nuevamente . Todo esto encapsulado y MVVM aprobado. Incluso obtiene un estado de cómo se cerró la ventana , como si el usuario Canceledo Accepted(botón OK) la ventana y los datos vuelvan si lo necesita . Es un poco más complicado y la respuesta n. ° 1, pero es mucho más completo y es un patrón recomendado por Microsoft.

El enlace que proporcioné tiene todos los fragmentos de código y ejemplos, por lo que no me molestaré en colocar ningún código aquí, solo lea el artículo de descarga del Prism Quick Start y ejecútelo, es realmente simple entender un poco más detallado hacer que funcione, pero los beneficios son mayores que simplemente cerrar una ventana.

mFeinstein
fuente
Buena manera, pero la resolución y asignación de ViewModels no puede ser tan sencilla siempre. ¿Qué pasa si el mismo modelo de vista es DataContext de muchos Windows?
Kylo Ren
Entonces supongo que tendría que cerrar todas las ventanas a la vez, recuerde que una Acción puede activar muchos delegados a la vez, solo use +=para agregar un delegado y llame a la Acción, los activará a todos ... O lo hará tiene que hacer una lógica especial en su VM para que sepa qué ventana cerrar (tal vez tenga una colección de Acciones de cierre) ... Pero creo que tener varias vistas vinculadas a una VM no es la mejor práctica, es es mejor administrar tener una vista y una instancia de máquina virtual, unidas entre sí y tal vez una máquina virtual principal que administre todas las máquinas virtuales secundarias que están vinculadas a todas las vistas.
mFeinstein
3
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();
  }
}
Amir Touitou
fuente
2

Puede hacer que ViewModel exponga un evento en el que la Vista se registre. Luego, cuando ViewModel decide su hora de cerrar la vista, dispara ese evento que hace que la vista se cierre. Si desea que se devuelva un valor de resultado específico, entonces tendría una propiedad en ViewModel para eso.

Abdulla Al-Qawasmeh
fuente
Estoy de acuerdo con esto: la simplicidad es valiosa. Tengo que pensar en lo que sucede cuando el próximo desarrollador junior es contratado para hacerse cargo de este proyecto. Supongo que tendrá una oportunidad mucho mejor de hacer esto bien como lo describe. ¿A menos que pienses que vas a mantener este código para siempre? +1
Dean
2

Solo para agregar a la gran cantidad de respuestas, quiero agregar lo siguiente. Suponiendo que tiene un ICommand en su ViewModel y desea que ese comando cierre su ventana (o cualquier otra acción), puede usar algo como lo siguiente.

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

No es perfecto, y puede ser difícil de probar (ya que es difícil burlarse de una estática) pero es más limpio (en mi humilde opinión) que las otras soluciones.

Erick

Erick T
fuente
¡Me puse muy feliz cuando vi tu respuesta simple! ¡pero tampoco funciona! Necesito abrir y cerrar con Visual Basic. ¿Conoces la equivalencia de (windows [i] .DataContext == this) en VB?
Ehsan
¡Lo tengo finalmente! :) Gracias. Si windows (i) .DataContext Soy yo
Ehsan
¿Conoces la misma forma simple de abrir una ventana también? Necesito enviar y recibir algunos datos también en el modelo de vista infantil y viceversa.
Ehsan
1

Implementé la solución de Joe White, pero me encontré con problemas ocasionales " DialogResult solo se puede configurar después de que se crea la ventana y se muestra como errores de diálogo ".

Mantuve el ViewModel alrededor después de que se cerró la Vista y ocasionalmente abrí una nueva Vista usando la misma VM. Parece que al cerrar la nueva Vista antes de que la Vista anterior se hubiera recolectado basura, DialogResultChanged intentó establecer la propiedad DialogResult en la ventana cerrada, provocando así el error.

Mi solución fue cambiar DialogResultChanged para verificar la propiedad IsLoaded de la ventana :

private static void DialogResultChanged(
    DependencyObject d,
    DependencyPropertyChangedEventArgs e)
{
    var window = d as Window;
    if (window != null && window.IsLoaded)
        window.DialogResult = e.NewValue as bool?;
}

Después de hacer este cambio, se ignoran los archivos adjuntos a los diálogos cerrados.

Jim Hansen
fuente
Gracias Señor. Tuve el mismo problema
DJ Burb
1

Terminé combinando la respuesta de Joe White y algo de código de la respuesta de Adam Mills , ya que necesitaba mostrar un control de usuario en una ventana creada mediante programación. Por lo tanto, DialogCloser no necesita estar en la ventana, puede estar en el control del usuario.

<UserControl ...
    xmlns:xw="clr-namespace:Wpf"
    xw:DialogCloser.DialogResult="{Binding DialogResult}">

Y DialogCloser encontrará la ventana del control del usuario si no estaba conectada a la ventana misma.

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_ );
    }
  }
}
Anuroopa Shenoy
fuente
1

El comportamiento es la forma más conveniente aquí.

  • Por un lado, se puede vincular al modelo de vista dado (que puede indicar "¡cierra el formulario!")

  • Por otro lado, tiene acceso al formulario en sí mismo, por lo que puede suscribirse a los eventos específicos del formulario necesarios, o mostrar el diálogo de confirmación, o cualquier otra cosa.

Escribir el comportamiento necesario puede verse aburrido desde la primera vez. Sin embargo, a partir de ahora, puede reutilizarlo en cada formulario que necesite mediante un fragmento XAML de una sola línea. Y si es necesario, puede extraerlo como un ensamblaje separado para que pueda incluirse en el próximo proyecto que desee.

Yury Schkatula
fuente
0

¿Por qué no simplemente pasar la ventana como parámetro de comando?

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}" />
chrislarson
fuente
No creo que sea una buena idea restringir la VM a un tipo de ventana.
Shimmy Weitzhandler
2
No creo que sea una buena idea restringir la VM a un Windowtipo que no sea MVVM "puro". Vea esta respuesta, donde la VM no está restringida a un Windowobjeto.
Shimmy Weitzhandler
De esta manera, la dependencia se pone en un botón que seguramente no puede ser la situación siempre. También pasar el tipo de IU a ViewModel es una mala práctica.
Kylo Ren el
0

Otra solución es crear una propiedad con INotifyPropertyChanged en View Model como DialogResult, y luego en Code Behind escriba esto:

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

El fragmento más importante es _someViewModel_PropertyChanged. DialogResultPropertyNamepuede ser una cadena const pública SomeViewModel.

Utilizo este tipo de truco para hacer algunos cambios en los controles de vista en caso de que sea difícil hacerlo en ViewModel. OnPropertyChanged en ViewModel puede hacer lo que quiera en View. ViewModel todavía es 'comprobable por unidad' y algunas pequeñas líneas de código en el código detrás no hacen ninguna diferencia.

sliwinski.lukas
fuente
0

Yo iría de esta manera:

using GalaSoft.MvvmLight;
using GalaSoft.MvvmLight.Command;    
using GalaSoft.MvvmLight.Messaging; 

// View

public partial class TestCloseWindow : Window
{
    public TestCloseWindow() {
        InitializeComponent();
        Messenger.Default.Register<CloseWindowMsg>(this, (msg) => Close());
    }
}

// View Model

public class MainViewModel: ViewModelBase
{
    ICommand _closeChildWindowCommand;

    public ICommand CloseChildWindowCommand {
        get {
            return _closeChildWindowCommand?? (_closeChildWindowCommand = new RelayCommand(() => {
                Messenger.Default.Send(new CloseWindowMsg());
        }));
        }
    }
}

public class CloseWindowMsg
{
}
romanoza
fuente
0

He leído todas las respuestas, pero debo decir que la mayoría de ellas no son lo suficientemente buenas o incluso peores.

Puede manejar esto hermosamente con la clase DialogService , cuya responsabilidad es mostrar la ventana de diálogo y devolver el resultado del diálogo. He creado un proyecto de muestra que demuestra su implementación y uso.

Aquí están las partes más importantes:

//we will call this interface in our viewmodels
public interface IDialogService
{
    bool? ShowDialog(object dialogViewModel, string caption);
}

//we need to display logindialog from mainwindow
public class MainWindowViewModel : ViewModelBase
{
    public string Message {get; set;}
    public void ShowLoginCommandExecute()
    {
        var loginViewModel = new LoginViewModel();
        var dialogResult = this.DialogService.ShowDialog(loginViewModel, "Please, log in");

        //after dialog is closed, do someting
        if (dialogResult == true && loginViewModel.IsLoginSuccessful)
        {
            this.Message = string.Format("Hello, {0}!", loginViewModel.Username);
        }
    }
}


public class DialogService : IDialogService
{
    public bool? ShowDialog(object dialogViewModel, string caption)
    {
        var contentView = ViewLocator.GetView(dialogViewModel);
        var dlg = new DialogWindow
        {
            Title = caption
        };
        dlg.PART_ContentControl.Content = contentView;

        return dlg.ShowDialog();
    }
}

¿No es esto más simple? ¿Más directo, más legible y, por último, pero no menos fácil de depurar que EventAggregator u otras soluciones similares?

como puede ver, en mis modelos de vista he usado el primer enfoque de ViewModel descrito en mi publicación aquí: Mejores prácticas para llamar a View desde ViewModel en WPF

Por supuesto, en el mundo real, DialogService.ShowDialogdeben tener más opciones para configurar el diálogo, por ejemplo, botones y comandos que deben ejecutar. Hay diferentes formas de hacerlo, pero está fuera de alcance :)

Liero
fuente
0

Si bien esto no responde a la pregunta de cómo hacer esto a través del modelo de vista, sí muestra cómo hacerlo usando solo XAML + el SDK de mezcla.

Elegí descargar y usar dos archivos del SDK de Blend, que puedes usar como paquete de Microsoft a través de NuGet. Los archivos son:

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

Microsoft.Expression.Interactions.dll le brinda buenas capacidades, como la capacidad de establecer propiedades o invocar un método en su modelo de vista u otro objetivo y también tiene otros widgets dentro.

Algunos 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>

Tenga en cuenta que si solo desea un comportamiento simple de Aceptar / Cancelar, puede escapar usando las propiedades IsDefault e IsCancel siempre que la ventana se muestre con Window.ShowDialog ().
Personalmente tuve problemas con un botón que tenía la propiedad IsDefault establecida en verdadero, pero estaba oculta cuando se cargaba la página. No parecía querer jugar bien después de que se mostró, así que solo estoy configurando la propiedad Window.DialogResult como se muestra arriba y funciona para mí.

Wes
fuente
0

Aquí está la solución simple libre de errores (con código fuente), está funcionando para mí.

  1. Derive su ViewModel de INotifyPropertyChanged

  2. Crear una propiedad observable CloseDialog en ViewModel

    public void Execute()
    {
        // Do your task here
    
        // if task successful, assign true to CloseDialog
        CloseDialog = true;
    }
    
    private bool _closeDialog;
    public bool CloseDialog
    {
        get { return _closeDialog; }
        set { _closeDialog = value; OnPropertyChanged(); }
    }
    
    public event PropertyChangedEventHandler PropertyChanged;
    
    private void OnPropertyChanged([CallerMemberName]string property = "")
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(property));
        }
    }

    }

  3. Adjunte un controlador en la vista para este cambio de propiedad

        _loginDialogViewModel = new LoginDialogViewModel();
        loginPanel.DataContext = _loginDialogViewModel;
        _loginDialogViewModel.PropertyChanged += OnPropertyChanged;
  4. Ahora ya casi has terminado. En caso de que el controlador hagaDialogResult = true

    protected void OnPropertyChanged(object sender, PropertyChangedEventArgs args)
    {
        if (args.PropertyName == "CloseDialog")
        {
            DialogResult = true;
        }
    }
Anil8753
fuente
0

Cree un Dependency Propertyen su View/ any UserControl(o Windowdesea cerrar). Como abajo:

 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
        }

Y vincúlelo desde la propiedad de su 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}"

Propiedad en VeiwModel:

private bool closeWindow;

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

Ahora active la operación de cierre cambiando el CloseWindowvalor en ViewModel. :)

Kylo Ren
fuente
-2

Cuando necesite cerrar la ventana, simplemente ponga esto en el modelo de vista:

ta-da

  foreach (Window window in Application.Current.Windows)
        {
            if (window.DataContext == this)
            {
                window.Close();
                return;
            }
        }
Cătălin Rădoi
fuente
Un modelo de vista no debe contener un elemento UIE de ninguna manera, ya que esto puede crear errores
WiiMaxx
¿Qué sucede si DataContext se hereda en varias ventanas?
Kylo Ren
ta-da, esto no es totalmente MVVM.
Alexandru Dicu
-10
Application.Current.MainWindow.Close() 

¡Eso es suficiente!

Alexey
fuente
3
-1 sólo es cierto si la ventana que desea cerrar la ventana principal es ... Muy improbable supuesto de diálogo Inicio de sesión ...
surfen