CloseStrategy doesn't work when closing the application

Topics: UI Architecture
Jan 12, 2012 at 2:40 PM
Hello,

I have a question about the CloseStrategy. I have used the code from the HelloScreens example. This works great when closing indivual screens, but when I close the whole application and there are still screens with dirty data, the application closes whithout any warning. But I want to show warnings for every screen which contains dirty data, such that the user can cancel the closing.

I have used the ApplicationCloseStrategy and ApplicationClosCheck classes from the example.

I have a shellviewmodel with a conductor which conducts IWorkspace instances, none of these IWorkspace instances implement a Conductor.  I haven't overriden the CanClose in my shellviewmodel, do I need to override this function in my shellviewmodel?

The shellviewmodel signature is:

    [Export(typeof(IShell))]
    public class ShellViewModel : Conductor<IWorkspace>.Collection.OneActiveIShell

The IWorkspace instances have the following signature:

    [Export(typeof(CustomerViewModel))]
    [ExportWorkspace(WorkspaceType = WorkspaceTypes.CustomerViewModel)]
    [PartCreationPolicy(CreationPolicy.NonShared)]
    public class CustomerViewModel : WorkspaceBase<Customer>, IWorkspace

The WorkspaceBase class contains the close logic, such that all the IWorkspace instances will inherit that.

    public abstract class WorkspaceBase<T> : ValidationBaseSingleEntity, IHaveShutdownTask
        where T : EntityObject, IEntity
    {
        public IResult GetShutdownTask()
        {
            return IsDirty ? new ApplicationCloseCheck(this, DoCloseCheck) : null;
        }

        protected virtual void DoCloseCheck(IDialogManager dialogs, Action<bool> callback)
        {
            Dialogs.ShowMessageBox(
                "The screen has unsaved data!",
                MessageBoxKind.Warning,
                "Unsaved Data",
                MessageBoxOptions.YesNo,
                box => callback(box.WasSelected(MessageBoxOptions.Yes))
                );
        }

        public override void CanClose(Action<bool> callback)
        {
            if (IsDirty)
                DoCloseCheck(Dialogs, callback);
            else
                callback(true);
        }
        // More code			
}

What am I doing wrong here?

Marcel

Coordinator
Jan 13, 2012 at 1:28 PM

You need to add some additional code to your bootstrapper, per the sample:

 

Window mainWindow;
bool actuallyClosing;

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

    if(Application.IsRunningOutOfBrowser) {
        mainWindow = Application.MainWindow;
        mainWindow.Closing += MainWindowClosing;
    }
}

void MainWindowClosing(object sender, ClosingEventArgs e) {
    if (actuallyClosing)
        return;

    e.Cancel = true;

    Execute.OnUIThread(() => {
        var shell = IoC.Get<IShell>();

        shell.CanClose(result => {
            if(result) {
                actuallyClosing = true;
                mainWindow.Close();
            }
        });
    });
}

Two things to note here. First, you must hold on to a reference to the Window as I have shown, or it won't work due to a garbage collection bug on the Closing event handlers in Silverlight. Second, this only works in out of browser mode. You can't detect application close otherwise.

Jan 16, 2012 at 6:34 AM

@Rob:

I have put the code above, into my bootstrapper but it still doesn't work.

You mentioned Silverlight, but I am building a WPF application, does this functionality also works on WPF?

Marcel

Jan 16, 2012 at 8:25 AM

I found the solution, my application has a slightly different architecture than the HelloScreens example.

I have changed the ApplicationCloseStrategy a little bit and now it works.

Thanks for the support Rob!

May 8, 2012 at 3:51 PM

Marcel - 

I am looking at a similar issue with CM and a WPF application based on the Hello Screens Application. What changes did you make to the ApplicationCloseStrategy?

 

Thanks

 

Chuck

May 8, 2012 at 8:04 PM

Hello Chuck,

 

I changed the ApplicationCloseStrategy class, in the HelloScreens example it only looks at workspaces which are conductors, so workspaces which do no inherit from the Conductor class are skipped.

The HelloScreens code is below:

 

        public void Execute(IEnumerable<IWorkspace> toClose, Action<bool, IEnumerable<IWorkspace>> callback) {
            enumerator = toClose.GetEnumerator();
            this.callback = callback;
            finalResult = true;

            Evaluate(finalResult);
        }

        void Evaluate(bool result)
        {
            finalResult = finalResult && result;

            if (!enumerator.MoveNext() || !result)
                callback(finalResult, new List<IWorkspace>());
            else
            {
                var current = enumerator.Current;
                var conductor = current as IConductor;
                if (conductor != null)
                {
                    var tasks = conductor.GetChildren()
                        .OfType<IHaveShutdownTask>()
                        .Select(x => x.GetShutdownTask())
                        .Where(x => x != null);

                    var sequential = new SequentialResult(tasks.GetEnumerator());
                    sequential.Completed += (s, e) => {
                        if(!e.WasCancelled)
                        Evaluate(!e.WasCancelled);
                    };
                    sequential.Execute(new ActionExecutionContext());
                }
                else Evaluate(true);
            }
        }
    }

 

I changed this into:

        public void Execute(IEnumerable<IWorkspace> toClose, Action<bool, IEnumerable<IWorkspace>> callback)
        {
            enumerator = toClose.GetEnumerator();
            this.callback = callback;
            finalResult = true;

            Evaluate(finalResult);
        }

        private void Evaluate(bool result)
        {
            finalResult = finalResult && result;

            if (!enumerator.MoveNext() || !result)
                callback(finalResult, new List<IWorkspace>());
            else
            {
                var current = enumerator.Current;
                var conductor = current as IConductor;
                // If Workspace is a conductor
                if (conductor != null)
                {
                    var tasks = conductor.GetChildren()
                        .OfType<IHaveShutdownTask>()
                        .Select(x => x.GetShutdownTask())
                        .Where(x => x != null);

                    var sequential = new SequentialResult(tasks.GetEnumerator());
                    sequential.Completed += (s, e) =>
                    {
                        if (!e.WasCancelled)
                            Evaluate(!e.WasCancelled);
                    };
                    sequential.Execute(new ActionExecutionContext());
                }
                // *** ADDED *** If Workspace is NOT a conductor
                else
                {

                    var item = new List<IWorkspace>();
                    item.Add(enumerator.Current);

                    var task = item.AsEnumerable()
                                .OfType<IHaveShutdownTask>()
                                .Select(x => x.GetShutdownTask())
                                .Where(x => x != null);

                    var sequential = new SequentialResult(task.GetEnumerator());
                    sequential.Completed += (s, e) =>
                    {
                        if (!e.WasCancelled)
                            Evaluate(!e.WasCancelled);
                    };

                    sequential.Execute(new ActionExecutionContext());
                }

             }

 

I hope this helps.

 

Marcel

May 9, 2012 at 12:31 PM

Marcel - 

Thanks. I am assuming you also needed the code that Rob referenced above, or, was that only for OOB SL?

Chuck

May 9, 2012 at 3:13 PM
Edited May 9, 2012 at 3:13 PM

Chuck,

 

Yes you also need that code in your bootstrapper, and need to change the OnStartup function and mainWindow_Closing signature, because it does not work in WPF.

I changed it to:

 

        protected override void OnStartup(object sender, StartupEventArgs e)
        {
            _mainWindow = Application.MainWindow;
            _mainWindow.Closing += new CancelEventHandler(_mainWindow_Closing);

        }

 

and:

 

        void _mainWindow_Closing(object sender, CancelEventArgs e)
        {
            if (_actuallyClosing)
                return;

            e.Cancel = true;

            Execute.OnUIThread(() =>
            {
                var shell = IoC.Get<IShell>();

                shell.CanClose(result =>
                {
                    if (result)
                    {
                        _actuallyClosing = true;
                        ShutdownApplication();
                    }
                });
            });
        }

 

Marcel

May 10, 2012 at 1:42 PM
Edited May 10, 2012 at 4:47 PM

Tally Ho!

I thought that my problem lies with the code:

_mainWindow = Application.MainWindow;

because _mainWindow is null. NullReferenceExcpetion when setting the handler.

After researching how Main Window is set (e.g., Automatically), I realized that I was not passing the OnStartup call to the Base class.

I need to walk through my code some more. When I click the [X] it looks like the contents of the Shell Closes. However, the Window remains open, and the content is Black (so, I have a black Square). Nice.

When I click the [X] again, the application closes.

Chuck

May 11, 2012 at 4:56 PM

I was able to re-write the Application Close Strategy for my purposes. 

My scenario was pretty simple, I just needed to present a "Are you sure" message without having to iterate through items that implemented the IHaveShutDownTask.

In the end, I did not need the code that was in the HelloScreens example to handle the Silver Light Out Of Browser. Caliburn Micro's Window Manager engaged the Application Close Strategy without this code.

Cheers

Chuck