Manipulando o Evento de Fechamento de Janela com o WPF / MVVM Light Toolkit

145

Eu gostaria de lidar com o Closingevento (quando um usuário clicar no botão 'X' superior direito) da minha janela para, eventualmente, exibir uma mensagem de confirmação ou / e cancelar o fechamento.

Eu sei como fazer isso no code-behind: assine o Closingevento da janela e use a CancelEventArgs.Cancelpropriedade

Mas como estou usando o MVVM, não tenho certeza de que seja uma boa abordagem.

Eu acho que a boa abordagem seria vincular o Closingevento a Commandno meu ViewModel.

Eu tentei isso:

<i:Interaction.Triggers>
    <i:EventTrigger EventName="Closing">
        <cmd:EventToCommand Command="{Binding CloseCommand}" />
    </i:EventTrigger>
</i:Interaction.Triggers>

Com um associado RelayCommandno meu ViewModel, mas ele não funciona (o código do comando não é executado).

Olivier Payen
fonte
3
Também interessado em boa resposta para responder a isso.
Sekhat 27/09/10
3
Eu baixei o código do codeplex e a depuração revelou: "Não foi possível converter o objeto do tipo 'System.ComponentModel.CancelEventArgs' para o tipo 'System.Windows.RoutedEventArgs'." Ele funciona muito bem se você não quer que os CancelEventArgs mas isso não responder à sua pergunta ...
David Hollinshead
Suponho que seu código não funcione porque o controle ao qual você conectou o gatilho não possui um evento de fechamento. Seu contexto de dados não é uma janela ... Provavelmente é um modelo de dados com uma grade ou algo assim, que não possui nenhum evento de fechamento. Portanto, a resposta do dbkk é a melhor resposta neste caso. No entanto, prefiro a abordagem Interaction / EventTrigger quando o evento está disponível.
NielW
O código que você possui funcionará bem em um evento Loaded, por exemplo.
NielW 11/0314

Respostas:

126

Eu simplesmente associaria o manipulador no construtor View:

MyWindow() 
{
    // Set up ViewModel, assign to DataContext etc.
    Closing += viewModel.OnWindowClosing;
}

Em seguida, adicione o manipulador ao ViewModel:

using System.ComponentModel;

public void OnWindowClosing(object sender, CancelEventArgs e) 
{
   // Handle closing logic, set e.Cancel as needed
}

Nesse caso, você não obtém exatamente nada, exceto a complexidade, usando um padrão mais elaborado e mais indireto (5 linhas extras de XAML mais Command padrão).

O mantra "zero code-behind" não é o objetivo em si; o objetivo é desacoplar o ViewModel da View . Mesmo quando o evento é vinculado no code-behind da View, ViewModelisso não depende da View e a lógica de fechamento pode ser testada em unidade .

dbkk
fonte
4
I como esta solução: basta ligar para um botão escondido :)
Benjol
3
Para iniciantes do mvvm que não usam o MVVMLight e pesquisam como informar o ViewModel sobre o evento Closing, os links como configurar o dataContext corretamente e como obter o objeto viewModel no View podem ser interessantes. Como obter uma referência ao ViewModel no modo de exibição? e Como definir um ViewModel em uma janela no xaml usando a propriedade datacontext ... Levei várias horas, como um simples evento de fechamento de janela poderia ser tratado no ViewModel.
precisa saber é o seguinte
18
Esta solução é irrelevante no ambiente MVVM. O código por trás não deve saber sobre o ViewModel.
Jacob
2
@ Jacob Acho que o problema é mais que você recebe um manipulador de eventos de formulário no seu ViewModel, que associa o ViewModel a uma implementação específica da interface do usuário. Se eles usarem o código por trás, devem verificar CanExecute e chamar Execute () em uma propriedade ICommand.
Mal Pigeon
14
@ Jacob O code-behind pode saber sobre os membros do ViewModel muito bem, assim como o código XAML faz. Ou o que você pensa que está fazendo quando cria uma propriedade Binding to a ViewModel? Essa solução é perfeita para o MVVM, desde que você não lide com a lógica de fechamento no code-behind, mas no ViewModel (embora usar um ICommand, como o EvilPigeon sugere, possa ser uma boa idéia, pois você também pode vincular )
almulo 14/04
81

Este código funciona bem:

ViewModel.cs:

public ICommand WindowClosing
{
    get
    {
        return new RelayCommand<CancelEventArgs>(
            (args) =>{
                     });
    }
}

e em XAML:

<i:Interaction.Triggers>
    <i:EventTrigger EventName="Closing">
        <command:EventToCommand Command="{Binding WindowClosing}" PassEventArgsToCommand="True" />
    </i:EventTrigger>
</i:Interaction.Triggers>

assumindo que:

  • O ViewModel é atribuído a um DataContextdos contêineres principais.
  • xmlns:command="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.SL5"
  • xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Stas
fonte
1
Esqueceu-se: para obter argumentos de eventos no comando use PassEventArgsToCommand = "True"
Stas
2
+1 abordagem simples e convencional. Seria ainda melhor ir ao PRISM.
Tri Q Tran
16
Este é um cenário que destaca os buracos no WPF e MVVM.
Damien
1
Seria realmente útil mencionar o que está identro <i:Interaction.Triggers>e como obtê-lo.
Andrii Muzychuk
1
@Chiz, é um espaço para nome que você deve declarar no elemento raiz assim: xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
Stas 16/03
34

Essa opção é ainda mais fácil e talvez seja adequada para você. No construtor View Model, você pode assinar o evento de fechamento da janela principal assim:

Application.Current.MainWindow.Closing += new CancelEventHandler(MainWindow_Closing);

void MainWindow_Closing(object sender, CancelEventArgs e)
{
            //Your code to handle the event
}

Muito bem sucedida.

PILuaces
fonte
Essa é a melhor solução entre as outras mencionadas nesta edição. Obrigado !
Jacob
Isto é o que eu estava procurando. Obrigado!
Nikki Punjabi
20
... e isso cria um forte acoplamento entre o ViewModel e o View. -1.
PiotrK
6
Esta não é a melhor resposta. Quebra o MVVM.
Safiron
1
@ Craig Requer uma referência rígida à janela principal ou à janela para a qual está sendo usada. É muito mais fácil, mas significa que o modelo de exibição não está dissociado. Não se trata de satisfazer ou não os nerds do MVVM, mas se o padrão do MVVM precisar ser quebrado para que funcione, não há sentido em usá-lo.
19418 Alex
16

Aqui está uma resposta de acordo com o padrão MVVM, se você não quiser saber sobre a Janela (ou qualquer um de seus eventos) no ViewModel.

public interface IClosing
{
    /// <summary>
    /// Executes when window is closing
    /// </summary>
    /// <returns>Whether the windows should be closed by the caller</returns>
    bool OnClosing();
}

No ViewModel, adicione a interface e implementação

public bool OnClosing()
{
    bool close = true;

    //Ask whether to save changes och cancel etc
    //close = false; //If you want to cancel close

    return close;
}

Na janela, adiciono o evento Closing. Esse código por trás não quebra o padrão MVVM. O View pode saber sobre o viewmodel!

void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    IClosing context = DataContext as IClosing;
    if (context != null)
    {
        e.Cancel = !context.OnClosing();
    }
}
AxdorphCoder
fonte
Simples, claro e limpo. O ViewModel não precisa conhecer detalhes específicos da exibição, portanto, as preocupações permanecem separadas.
Bernhard Hiller
o contexto é sempre nulo!
Shahid Od
@ ShahidOd Seu ViewModel precisa implementar a IClosinginterface, não apenas implementar o OnClosingmétodo. Caso contrário, o DataContext as IClosingelenco falhará e retornaránull
Erik White
10

Nossa, parece que muito código está acontecendo aqui para isso. Stas acima tiveram a abordagem correta para o mínimo esforço. Aqui está minha adaptação (usando MVVMLight, mas deve ser reconhecível) ... Ah, e o PassEventArgsToCommand = "True" é definitivamente necessário, conforme indicado acima.

(crédito para Laurent Bugnion http://blog.galasoft.ch/archive/2009/10/18/clean-shutdown-in-silverlight-and-wpf-applications.aspx )

   ... MainWindow Xaml
   ...
   WindowStyle="ThreeDBorderWindow" 
    WindowStartupLocation="Manual">



<i:Interaction.Triggers>
    <i:EventTrigger EventName="Closing">
        <cmd:EventToCommand Command="{Binding WindowClosingCommand}" PassEventArgsToCommand="True" />
    </i:EventTrigger>
</i:Interaction.Triggers> 

No modelo de exibição:

///<summary>
///  public RelayCommand<CancelEventArgs> WindowClosingCommand
///</summary>
public RelayCommand<CancelEventArgs> WindowClosingCommand { get; private set; }
 ...
 ...
 ...
        // Window Closing
        WindowClosingCommand = new RelayCommand<CancelEventArgs>((args) =>
                                                                      {
                                                                          ShutdownService.MainWindowClosing(args);
                                                                      },
                                                                      (args) => CanShutdown);

no ShutdownService

    /// <summary>
    ///   ask the application to shutdown
    /// </summary>
    public static void MainWindowClosing(CancelEventArgs e)
    {
        e.Cancel = true;  /// CANCEL THE CLOSE - let the shutdown service decide what to do with the shutdown request
        RequestShutdown();
    }

RequestShutdown se parece com o seguinte, mas basicamenteRequestShutdown ou o nome que ele decide decide se o aplicativo deve ou não ser encerrado (o que fecha a janela de qualquer maneira):

...
...
...
    /// <summary>
    ///   ask the application to shutdown
    /// </summary>
    public static void RequestShutdown()
    {

        // Unless one of the listeners aborted the shutdown, we proceed.  If they abort the shutdown, they are responsible for restarting it too.

        var shouldAbortShutdown = false;
        Logger.InfoFormat("Application starting shutdown at {0}...", DateTime.Now);
        var msg = new NotificationMessageAction<bool>(
            Notifications.ConfirmShutdown,
            shouldAbort => shouldAbortShutdown |= shouldAbort);

        // recipients should answer either true or false with msg.execute(true) etc.

        Messenger.Default.Send(msg, Notifications.ConfirmShutdown);

        if (!shouldAbortShutdown)
        {
            // This time it is for real
            Messenger.Default.Send(new NotificationMessage(Notifications.NotifyShutdown),
                                   Notifications.NotifyShutdown);
            Logger.InfoFormat("Application has shutdown at {0}", DateTime.Now);
            Application.Current.Shutdown();
        }
        else
            Logger.InfoFormat("Application shutdown aborted at {0}", DateTime.Now);
    }
    }
AllenM
fonte
8

O solicitante deve usar a resposta STAS, mas para os leitores que usam prisma e sem galasoft / mvvmlight, eles podem tentar o que eu usei:

Na definição na parte superior da janela ou controle do usuário, etc, defina o espaço para nome:

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"

E logo abaixo dessa definição:

<i:Interaction.Triggers>
        <i:EventTrigger EventName="Closing">
            <i:InvokeCommandAction Command="{Binding WindowClosing}" CommandParameter="{Binding}" />
        </i:EventTrigger>
</i:Interaction.Triggers>

Propriedade em seu viewmodel:

public ICommand WindowClosing { get; private set; }

Anexe o comando delegat no seu construtor viewmodel:

this.WindowClosing = new DelegateCommand<object>(this.OnWindowClosing);

Por fim, seu código que você deseja acessar ao fechar o controle / janela / o que for:

private void OnWindowClosing(object obj)
        {
            //put code here
        }
Chris
fonte
3
Isso não dá acesso ao CancelEventArgs, necessário para cancelar o evento de fechamento. O objeto passado é o modelo de visualização, que tecnicamente é o mesmo modelo de visualização do qual o comando WindowClosing está sendo executado.
stephenbayer
4

Eu ficaria tentado a usar um manipulador de eventos no seu arquivo App.xaml.cs que permitirá que você decida se deseja fechar o aplicativo ou não.

Por exemplo, você pode ter algo como o seguinte código no arquivo App.xaml.cs:

protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);
    // Create the ViewModel to attach the window to
    MainWindow window = new MainWindow();
    var viewModel = new MainWindowViewModel();

    // Create the handler that will allow the window to close when the viewModel asks.
    EventHandler handler = null;
    handler = delegate
    {
        //***Code here to decide on closing the application****
        //***returns resultClose which is true if we want to close***
        if(resultClose == true)
        {
            viewModel.RequestClose -= handler;
            window.Close();
        }
    }
    viewModel.RequestClose += handler;

    window.DataContaxt = viewModel;

    window.Show();

}

Em seu código MainWindowViewModel, você pode ter o seguinte:

#region Fields
RelayCommand closeCommand;
#endregion

#region CloseCommand
/// <summary>
/// Returns the command that, when invoked, attempts
/// to remove this workspace from the user interface.
/// </summary>
public ICommand CloseCommand
{
    get
    {
        if (closeCommand == null)
            closeCommand = new RelayCommand(param => this.OnRequestClose());

        return closeCommand;
    }
}
#endregion // CloseCommand

#region RequestClose [event]

/// <summary>
/// Raised when this workspace should be removed from the UI.
/// </summary>
public event EventHandler RequestClose;

/// <summary>
/// If requested to close and a RequestClose delegate has been set then call it.
/// </summary>
void OnRequestClose()
{
    EventHandler handler = this.RequestClose;
    if (handler != null)
    {
        handler(this, EventArgs.Empty);
    }
}

#endregion // RequestClose [event]
ChrisBD
fonte
1
Obrigado pela resposta detalhada. No entanto, não acho que isso resolva meu problema: preciso lidar com o fechamento da janela quando o usuário clicar no botão 'X' superior direito. Seria fácil fazer isso no code-behind (basta vincular o evento Closing e definir CancelEventArgs.Cancel como true of false), mas eu gostaria de fazer isso no estilo MVVM. Desculpem a confusão #
1144 Olivier Payen
1

Basicamente, o evento da janela não pode ser atribuído ao MVVM. Em geral, o botão Fechar mostra uma caixa de diálogo para solicitar ao usuário "salvar: sim / não / cancelar", e isso pode não ser alcançado pelo MVVM.

Você pode manter o manipulador de eventos OnClosing, onde chama Model.Close.CanExecute () e define o resultado booleano na propriedade event. Portanto, após a chamada CanExecute (), se verdadeira, OU no evento OnClosed, chame o Model.Close.Execute ()

Echtelion
fonte
1

Eu não fiz muitos testes com isso, mas parece funcionar. Aqui está o que eu vim com:

namespace OrtzIRC.WPF
{
    using System;
    using System.Windows;
    using OrtzIRC.WPF.ViewModels;

    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App : Application
    {
        private MainViewModel viewModel = new MainViewModel();
        private MainWindow window = new MainWindow();

        protected override void OnStartup(StartupEventArgs e)
        {
            base.OnStartup(e);

            viewModel.RequestClose += ViewModelRequestClose;

            window.DataContext = viewModel;
            window.Closing += Window_Closing;
            window.Show();
        }

        private void ViewModelRequestClose(object sender, EventArgs e)
        {
            viewModel.RequestClose -= ViewModelRequestClose;
            window.Close();
        }

        private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            window.Closing -= Window_Closing;
            viewModel.RequestClose -= ViewModelRequestClose; //Otherwise Close gets called again
            viewModel.CloseCommand.Execute(null);
        }
    }
}
Brian Ortiz
fonte
1
O que acontecerá aqui no cenário em que a VM deseja cancelar o fechamento?
Tri Q Tran
1

Usando o MVVM Light Toolkit:

Supondo que exista um comando Exit no modelo de exibição:

ICommand _exitCommand;
public ICommand ExitCommand
{
    get
    {
        if (_exitCommand == null)
            _exitCommand = new RelayCommand<object>(call => OnExit());
        return _exitCommand;
    }
}

void OnExit()
{
     var msg = new NotificationMessageAction<object>(this, "ExitApplication", (o) =>{});
     Messenger.Default.Send(msg);
}

Isso é recebido na exibição:

Messenger.Default.Register<NotificationMessageAction<object>>(this, (m) => if (m.Notification == "ExitApplication")
{
     Application.Current.Shutdown();
});

Por outro lado, trato de Closingevento MainWindowusando a instância do ViewModel:

private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{ 
    if (((ViewModel.MainViewModel)DataContext).CancelBeforeClose())
        e.Cancel = true;
}

CancelBeforeClose verifica o estado atual do modelo de exibição e retorna true se o fechamento deve ser interrompido.

Espero que ajude alguém.

Ron
fonte
-2
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
    {
        MessageBox.Show("closing");
    }
Mattias Sturebrand
fonte
Olá, adicione um pouco de explicação junto com o código, pois ajuda a entender seu código. Código únicas respostas são malvistos
Bhargav Rao
O op afirmou explicitamente que ele não estava interessado em usar o código de evento code-behind para isso.
Fer García