diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.cs b/samples/XamlTestApplicationPcl/Views/MainWindow.cs index d18985a898..5003af3ecd 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.cs +++ b/samples/XamlTestApplicationPcl/Views/MainWindow.cs @@ -1,6 +1,7 @@ // Copyright (c) The Perspex Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Perspex; using Perspex.Controls; using Perspex.Diagnostics; using Perspex.Markup.Xaml; @@ -10,6 +11,8 @@ namespace XamlTestApplication.Views { public class MainWindow : Window { + private MenuItem _exitMenu; + public MainWindow() { InitializeComponent(); @@ -20,6 +23,8 @@ namespace XamlTestApplication.Views private void InitializeComponent() { PerspexXamlLoader.Load(this); + _exitMenu = this.FindControl("exitMenu"); + _exitMenu.Click += (s, e) => Application.Current.Exit(); } } } \ No newline at end of file diff --git a/samples/XamlTestApplicationPcl/Views/MainWindow.paml b/samples/XamlTestApplicationPcl/Views/MainWindow.paml index e5155b0b95..1dc8575178 100644 --- a/samples/XamlTestApplicationPcl/Views/MainWindow.paml +++ b/samples/XamlTestApplicationPcl/Views/MainWindow.paml @@ -17,7 +17,7 @@ - + diff --git a/src/Perspex.Application/Application.cs b/src/Perspex.Application/Application.cs index 1a245a8d9f..06f0b6ddeb 100644 --- a/src/Perspex.Application/Application.cs +++ b/src/Perspex.Application/Application.cs @@ -32,7 +32,7 @@ namespace Perspex /// method. /// - Tracks the lifetime of the application. /// - public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot + public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IApplicationLifecycle { static Action _platformInitializationCallback; @@ -56,6 +56,7 @@ namespace Perspex } PerspexLocator.CurrentMutable.BindToSelf(this); + OnExit += OnExiting; } /// @@ -138,10 +139,35 @@ namespace Perspex public void Run(ICloseable closable) { var source = new CancellationTokenSource(); + closable.Closed += OnExiting; closable.Closed += (s, e) => source.Cancel(); Dispatcher.UIThread.MainLoop(source.Token); } + /// + /// Exits the application + /// + public void Exit() + { + OnExit?.Invoke(this, EventArgs.Empty); + } + + /// + /// Sent when the application is exiting. + /// + public event EventHandler OnExit; + + + /// + /// Called when the application is exiting. + /// + /// + /// + protected virtual void OnExiting(object sender, EventArgs e) + { + + } + /// /// Register's the services needed by Perspex. /// @@ -160,7 +186,8 @@ namespace Perspex .Bind().ToTransient() .Bind().ToConstant(_styler) .Bind().ToSingleton() - .Bind().ToTransient(); + .Bind().ToTransient() + .Bind().ToConstant(this); } /// diff --git a/src/Perspex.Controls/IApplicationLifecycle.cs b/src/Perspex.Controls/IApplicationLifecycle.cs new file mode 100644 index 0000000000..38487bb9fc --- /dev/null +++ b/src/Perspex.Controls/IApplicationLifecycle.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Perspex.Controls +{ + /// + /// Sends events about the application lifecycle. + /// + public interface IApplicationLifecycle + { + /// + /// Sent when the application is exiting. + /// + event EventHandler OnExit; + + /// + /// Exits the application. + /// + void Exit(); + } +} diff --git a/src/Perspex.Controls/Perspex.Controls.csproj b/src/Perspex.Controls/Perspex.Controls.csproj index 3d4b29e489..ee90633945 100644 --- a/src/Perspex.Controls/Perspex.Controls.csproj +++ b/src/Perspex.Controls/Perspex.Controls.csproj @@ -50,6 +50,7 @@ + diff --git a/src/Perspex.Controls/TopLevel.cs b/src/Perspex.Controls/TopLevel.cs index b82d3da078..24d3770f98 100644 --- a/src/Perspex.Controls/TopLevel.cs +++ b/src/Perspex.Controls/TopLevel.cs @@ -48,6 +48,7 @@ namespace Perspex.Controls private readonly IInputManager _inputManager; private readonly IAccessKeyHandler _accessKeyHandler; private readonly IKeyboardNavigationHandler _keyboardNavigationHandler; + private readonly IApplicationLifecycle _applicationLifecycle; private Size _clientSize; private bool _isActive; @@ -92,6 +93,7 @@ namespace Perspex.Controls _inputManager = TryGetService(dependencyResolver); _keyboardNavigationHandler = TryGetService(dependencyResolver); _renderQueueManager = TryGetService(dependencyResolver); + _applicationLifecycle = TryGetService(dependencyResolver); (TryGetService(dependencyResolver) ?? new DefaultTopLevelRenderer()).Attach(this); PlatformImpl.SetInputRoot(this); @@ -112,6 +114,11 @@ namespace Perspex.Controls .Select( x => (x as InputElement)?.GetObservable(CursorProperty) ?? Observable.Empty()) .Switch().Subscribe(cursor => PlatformImpl.SetCursor(cursor?.PlatformCursor)); + + if (_applicationLifecycle != null) + { + _applicationLifecycle.OnExit += OnApplicationExiting; + } } /// @@ -347,6 +354,19 @@ namespace Perspex.Controls private void HandleClosed() { Closed?.Invoke(this, EventArgs.Empty); + _applicationLifecycle.OnExit -= OnApplicationExiting; + } + + private void OnApplicationExiting(object sender, EventArgs args) + { + HandleApplicationExiting(); + } + + /// + /// Handles the application exiting, either from the last window closing, or a call to . + /// + protected virtual void HandleApplicationExiting() + { } /// diff --git a/src/Perspex.Controls/Window.cs b/src/Perspex.Controls/Window.cs index 22307ab97e..ae12a4d6ed 100644 --- a/src/Perspex.Controls/Window.cs +++ b/src/Perspex.Controls/Window.cs @@ -156,6 +156,12 @@ namespace Perspex.Controls PlatformImpl.Dispose(); } + protected override void HandleApplicationExiting() + { + base.HandleApplicationExiting(); + Close(); + } + /// /// Closes a dialog window with the specified result. /// diff --git a/tests/Perspex.Controls.UnitTests/TopLevelTests.cs b/tests/Perspex.Controls.UnitTests/TopLevelTests.cs index f03737c728..d121ad06ff 100644 --- a/tests/Perspex.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Perspex.Controls.UnitTests/TopLevelTests.cs @@ -272,6 +272,19 @@ namespace Perspex.Controls.UnitTests } } + [Fact] + public void Exiting_Application_Notifies_Top_Level() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var impl = new Mock(); + impl.SetupAllProperties(); + var target = new TestTopLevel(impl.Object); + UnitTestApplication.Current.Exit(); + Assert.True(target.IsClosed); + } + } + private FuncControlTemplate CreateTemplate() { return new FuncControlTemplate(x => @@ -284,10 +297,18 @@ namespace Perspex.Controls.UnitTests private class TestTopLevel : TopLevel { + public bool IsClosed { get; private set; } + public TestTopLevel(ITopLevelImpl impl) : base(impl) { } + + protected override void HandleApplicationExiting() + { + base.HandleApplicationExiting(); + IsClosed = true; + } } } } diff --git a/tests/Perspex.UnitTests/UnitTestApplication.cs b/tests/Perspex.UnitTests/UnitTestApplication.cs index 3fe4af0b59..04fd5daf68 100644 --- a/tests/Perspex.UnitTests/UnitTestApplication.cs +++ b/tests/Perspex.UnitTests/UnitTestApplication.cs @@ -6,6 +6,7 @@ using Perspex.Input; using Perspex.Layout; using Perspex.Platform; using Perspex.Styling; +using Perspex.Controls; namespace Perspex.UnitTests { @@ -47,7 +48,8 @@ namespace Perspex.UnitTests .Bind().ToConstant(Services.ThreadingInterface) .Bind().ToConstant(Services.StandardCursorFactory) .Bind().ToConstant(Services.Styler) - .Bind().ToConstant(Services.WindowingPlatform); + .Bind().ToConstant(Services.WindowingPlatform) + .Bind().ToConstant(this); } } }