Search code examples
c#wpfinterfacewindowmef

WPF Window closing confirmation in c# MEF context


I am tasked with implementing a simple confirmation dialog for trying to close the main window of my application, but only while a specific process is running. Closing the application means aborting that process. If that process is not running no confirmation is needed.

At first I just created an interface to get Application.Current.MainWindow and used the Closing event of that, but my teacher came up with something else, I just can't find the right way to finish it.

There already is a CanClose method in the code base I use, which is empty by default. It's in the AppInit class where the MainWindow is opened. What I did in the constructor is this:

[ImportingConstructor]
    public AppInit(MainWindow mainWindow, [ImportMany] IEnumerable<IClosingValidator> closingValidator)
    {
        this.mainWindow = mainWindow;
        this.closingValidator = closingValidator;
    }

That was the idea of my teacher. Now in the CanClose method I can iterate through these closing validations and return true or false, like so:

public Boolean CanClose()
    {
        foreach (var validation in this.closingValidator)
        {
            var result = validation.CanClose();

            if (result == false)
            {
                return false;
            }
        }

        return true;
    }

The ClosingValidator currently looks like the following, which I think is wrong, but I need to build the dialog somewhere. The whole thing works, but the flag indicating whether the process in question is running is in the ViewModel of another project, meaning this confirmation dialog is always shown.

 [Export(typeof(IClosingValidator))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class ClosingValidator : IClosingValidator
{
    private readonly IMessageDialog messageDialog;
    private readonly IOverlayEffect overlayEffect;

    [ImportingConstructor]
    public ClosingValidator(IMessageDialog messageDialog, IOverlayEffect overlayEffect)
    {
        this.messageDialog = messageDialog;
        this.overlayEffect = overlayEffect;
    }

    public Boolean CanClose()
    {
        using (this.overlayEffect.Apply())
        {
            var result = this.messageDialog.ShowDialog(
                new MessageDialogArgs(Resources.Resources.ConfirmClosingApplicationTitle,
                                      Resources.Resources.ConfirmClosingApplicationMessage,
                                      MessageBoxButton.YesNo,
                                      MessageBoxImage.SystemQuestion));

            if (result == MessageBoxResult.Yes)
            {
                return true;
            }
        }

        return false;
    }

I think my question comes down to this: Where do I build my dialog and how do I use a boolean flag from the ViewModel to determine whether to show the dialog in the first place? I'm relatively new to MEF, so sorry anything's unclear.

Edit: I think the idea was that I can use that interface to implement further closing validations at some point in the future.

EDIT 3:

Simplified ViewModel implementation (The actual viewModel has too many parameters):

[Export]
public class TocViewModel
{
    [ImportingConstructor]
    public TocViewModel(MeasurementSequenceExecution measurementSequenceExecution)
    {
        this.measurementSequenceExecution = measurementSequenceExecution;
    }

        public Boolean CanApplicationClose
    {
        get { return !this.measurementSequenceExecution.IsMeasurementSequenceRunning; }
        set
        {
            this.measurementSequenceExecution.IsMeasurementSequenceRunning = !value;
        }
    }

The IClosingValidator implementation:

[Export(typeof(IClosingValidator))]
[PartCreationPolicy(CreationPolicy.NonShared)]
public class ClosingValidator : IClosingValidator
{
    [ImportingConstructor]
    public ClosingValidator()
    {
    }

    public Boolean CanApplicationClose { get; }

The IClosingValidator interface:

public interface IClosingValidator
{
    Boolean CanApplicationClose { get; }
}

The class handling the closing:

[Export(typeof(IApp))]
public class AppInit : IApp
{
    [ImportingConstructor]
    public AppInit(MainWindow mainWindow,
                   [ImportMany(typeof(IClosingValidator))] IEnumerable<IClosingValidator> closingValidatorClients,
                   IOverlayEffect overlayEffect,
                   IMessageDialog messageDialog)
    {
        this.mainWindow = mainWindow;
        this.closingValidatorClients = closingValidatorClients;
        this.overlayEffect = overlayEffect;
        this.messageDialog = messageDialog;
    }


    public Boolean CanClose()
    {
        if (this.closingValidatorClients != null)
        {
            foreach (var client in this.closingValidatorClients)
            {
                if (!client.CanApplicationClose)
                {
                    using (this.overlayEffect.Apply())
                    {
                        var result = this.messageDialog.ShowDialog(
                            new MessageDialogArgs(Resources.Resources.ConfirmClosingApplicationTitle,
                                                  Resources.Resources.ConfirmClosingApplicationMessage,
                                                  MessageBoxButton.YesNo,
                                                  MessageBoxImage.SystemQuestion));

                        if (result == MessageBoxResult.Yes)
                        {
                            return true;
                        }

                        return false;
                    }
                }
            }
        }

        return true;
    }

private readonly IEnumerable<IClosingValidator> closingValidatorClients;
    private readonly MainWindow mainWindow;
    private readonly IMessageDialog messageDialog;
    private readonly IOverlayEffect overlayEffect;

UPDATE I made it work, the suggestion to Export the ViewModel with typeof(IClosingValidator) was right, I just had to add a second Export, not replace the default one, didn't know you could have 2 of them. So now with 2 ExportAttributes it works!


Solution

  • I think your implementation of IClosingValidator is at the wrong place. I have a similar project and what I do is the following:

    I have a Interface like you IClosingValidator:

    public interface IConfirmShellClosing
    {
        /// <summary>
        /// Gets a value that indicates whether the shell window can be closed.
        /// </summary>
        bool CanShellClose { get; }
    }
    

    This interface is implemented by all ViewModels which should be asked, if the shell can be closed. In your case all ViewModels where the process can be running should implement this interface. So each ViewModel for itself will implement the CanShellClose Property and decide if the process is running in this context. Only if all ViewModels return true for this, the Window can be closed.

    Then, in your instance of the window, you can subscribe to the WindowClosing Event and ask all registered ViewModels, if the Window can be closed. This implementation goes to your ViewModel of the Window (or the code behind file):

    [ImportMany(typeof(Lazy<IConfirmShellClosing>))]
    private IEnumerable<Lazy<IConfirmShellClosing>> _confirmShellClosingClients;
    
        private void ExecuteWindowClosing(CancelEventArgs args)
            {
                if (_confirmShellClosingClients != null)
                {
                    foreach (var client in _confirmShellClosingClients)
                    {
                        if (!client.Value.CanShellClose)
                        {
                            // Show your Dialog here and handle the answer
                        }
                    }
                }
            }
    

    I hope this will help.

    EDIT:

    Okay, you still have a few mistakes in the implementation of your ViewModel and the Validator.

    First of all, the Class ClosingValidator is not needed anymore. You want to give the responsibility to the ViewModels, and not to a central validator class.

    After that, we need to change the ViewModel like this:

        [Export]
        public class TocViewModel : IClosingValidator
        {
            [ImportingConstructor]
            public TocViewModel(MeasurementSequenceExecution measurementSequenceExecution)
            {
                this.measurementSequenceExecution = measurementSequenceExecution;
            }
    
                public Boolean CanApplicationClose
            {
                get { return !this.measurementSequenceExecution.IsMeasurementSequenceRunning; }
                set
                {
                    this.measurementSequenceExecution.IsMeasurementSequenceRunning = !value;
                }
            }
    

    What is happening now? The ViewModel implements the IClosingValidator Interface, so it has to implement the CanShellClose Property. In this property, you can define the logic, in which this ViewModel can decide if the Shell can be closed or not. If you ever add an other ViewModel, it can implement the interface as well and has a different logic for the closing.

    In the Importing in the Application itself, all classes which are implementing the IClosingValidator interface are imported and then asked, if the porperty is true or false.

    EDIT 2:

    I have 3 .dll (HelpingProject, InterfaceProject and ViewModelProject) All 3 should be in the directory where the compositionContainer is searching. In my case, I built the container like this:

    var catalog = new AggregateCatalog();
            catalog.Catalogs.Add(new DirectoryCatalog(@"C:\Projects\HelpingSolution\HelpingWpfProject\bin\Debug"));
            var container = new CompositionContainer(catalog);
            container.ComposeParts(this);
    

    So the Debug folder will be scanned to find matching exports. I would recommend you search for the this code in your codebase and have a look at where your container is looking for the exports. This does not have to be only one folder, but can also be a plugin folder or different locations. You can find a pretty good introduction here.