From 4c8384db5f429e59eb72edead3e3cb08d7b27113 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 11 Apr 2016 23:22:42 +0200 Subject: [PATCH 01/16] Failing unit test for issue #500. --- .../Perspex.Markup.Xaml/Data/Binding.cs | 16 ++++ .../Primitives/SelectingItemsControlTests.cs | 74 +++++++++++++++++++ 2 files changed, 90 insertions(+) diff --git a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs index 588cc12f0e..a99582cea7 100644 --- a/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Perspex.Markup.Xaml/Data/Binding.cs @@ -16,6 +16,22 @@ namespace Perspex.Markup.Xaml.Data /// public class Binding : IBinding { + /// + /// Initializes a new instance of the class. + /// + public Binding() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The binding path. + public Binding(string path) + { + Path = path; + } + /// /// Gets or sets the to use. /// diff --git a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index ab8a7090e7..333f4e9411 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.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 System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using Perspex.Collections; @@ -8,6 +9,8 @@ using Perspex.Controls.Presenters; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; using Perspex.Interactivity; +using Perspex.Markup.Xaml.Data; +using Perspex.UnitTests; using Xunit; namespace Perspex.Controls.UnitTests.Primitives @@ -487,6 +490,56 @@ namespace Perspex.Controls.UnitTests.Primitives Assert.True(called); } + [Fact] + public void Changing_DataContext_Should_Not_Clear_Nested_ViewModel_SelectedItem() + { + var items = new[] + { + new Item(), + new Item(), + }; + + var vm = new MasterViewModel + { + Child = new ChildViewModel + { + Items = items, + SelectedItem = items[1], + } + }; + + var target = new SelectingItemsControl { DataContext = vm }; + var itemsBinding = new Binding("Child.Items"); + var selectedBinding = new Binding("Child.SelectedItem"); + + target.Bind(SelectingItemsControl.ItemsProperty, itemsBinding); + target.Bind(SelectingItemsControl.SelectedItemProperty, selectedBinding); + + Assert.Equal(1, target.SelectedIndex); + Assert.Same(vm.Child.SelectedItem, target.SelectedItem); + + items = new[] + { + new Item(), + new Item(), + new Item(), + }; + + vm = new MasterViewModel + { + Child = new ChildViewModel + { + Items = items, + SelectedItem = items[2], + } + }; + + target.DataContext = vm; + + Assert.Equal(2, target.SelectedIndex); + Assert.Same(vm.Child.SelectedItem, target.SelectedItem); + } + private FuncControlTemplate Template() { return new FuncControlTemplate(control => @@ -502,5 +555,26 @@ namespace Perspex.Controls.UnitTests.Primitives { public bool IsSelected { get; set; } } + + private class MasterViewModel : NotifyingBase + { + private ChildViewModel _child; + + public ChildViewModel Child + { + get { return _child; } + set + { + _child = value; + RaisePropertyChanged(); + } + } + } + + private class ChildViewModel : NotifyingBase + { + public IList Items { get; set; } + public Item SelectedItem { get; set; } + } } } From 8cccc846091f6a9c9cd83b667906d00df1c1e767 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 12 Apr 2016 10:35:28 +0200 Subject: [PATCH 02/16] Added (failing) init test for SelectingItemsControl. --- .../Primitives/SelectingItemsControlTests.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 333f4e9411..7f8893415a 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -490,6 +490,21 @@ namespace Perspex.Controls.UnitTests.Primitives Assert.True(called); } + [Fact] + public void Order_Of_Setting_Items_And_SelectedIndex_During_Initialization_Should_Not_Matter() + { + var items = new[] { "Foo", "Bar" }; + var target = new SelectingItemsControl(); + + ((ISupportInitialize)target).BeginInit(); + target.SelectedIndex = 1; + target.Items = items; + ((ISupportInitialize)target).EndInit(); + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + } + [Fact] public void Changing_DataContext_Should_Not_Clear_Nested_ViewModel_SelectedItem() { From f7f4f6492cca9c43bec0af7921be2f25f90e3075 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 12 Apr 2016 21:38:03 +0200 Subject: [PATCH 03/16] Fix #500. When updating DataContext, defer changes to SelectedIndex/Items until after DataContext has finished changing. Use the same method between BeginInit and EndInit meaning that [DependsOn] is no longer needed. --- src/Perspex.Controls/Control.cs | 52 +++---- .../Primitives/SelectingItemsControl.cs | 147 +++++++++++------- .../Primitives/SelectingItemsControlTests.cs | 22 ++- 3 files changed, 136 insertions(+), 85 deletions(-) diff --git a/src/Perspex.Controls/Control.cs b/src/Perspex.Controls/Control.cs index 6ff38d08f1..74899a336a 100644 --- a/src/Perspex.Controls/Control.cs +++ b/src/Perspex.Controls/Control.cs @@ -337,19 +337,13 @@ namespace Perspex.Controls IStyleHost IStyleHost.StylingParent => (IStyleHost)InheritanceParent; /// - void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) - { - this.OnDetachedFromLogicalTreeCore(e); - } - - /// - void ISupportInitialize.BeginInit() + public virtual void BeginInit() { ++_initCount; } /// - void ISupportInitialize.EndInit() + public virtual void EndInit() { if (_initCount == 0) { @@ -373,14 +367,10 @@ namespace Perspex.Controls } } - /// - /// Gets a value which indicates whether a change to the is in - /// the process of being notified. - /// - protected bool IsDataContextChanging + /// + void ILogical.NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) { - get; - private set; + this.OnDetachedFromLogicalTreeCore(e); } /// @@ -548,6 +538,21 @@ namespace Perspex.Controls base.OnDetachedFromVisualTreeCore(e); } + /// + /// Called before the property changes. + /// + protected virtual void OnDataContextChanging() + { + } + + /// + /// Called after the property changes. + /// + protected virtual void OnDataContextChanged() + { + DataContextChanged?.Invoke(this, EventArgs.Empty); + } + /// protected override void OnGotFocus(GotFocusEventArgs e) { @@ -598,15 +603,6 @@ namespace Perspex.Controls } } - /// - /// Called when the is changed and all subscribers to that change - /// have been notified. - /// - protected virtual void OnDataContextChanged() - { - DataContextChanged?.Invoke(this, EventArgs.Empty); - } - /// /// Called when the property begins and ends being notified. /// @@ -618,9 +614,11 @@ namespace Perspex.Controls if (control != null) { - control.IsDataContextChanging = notifying; - - if (!notifying) + if (notifying) + { + control.OnDataContextChanging(); + } + else { control.OnDataContextChanged(); } diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index 4d9482e7de..f409d72792 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -107,7 +107,9 @@ namespace Perspex.Controls.Primitives private IList _selectedItems; private bool _ignoreContainerSelectionChanged; private bool _syncingSelectedItems; - private IList _clearSelectedItemsAfterDataContextChanged; + private int _updateCount; + private int _updateSelectedIndex; + private object[] _updateSelectedItems; /// /// Initializes static members of the class. @@ -117,13 +119,6 @@ namespace Perspex.Controls.Primitives IsSelectedChangedEvent.AddClassHandler(x => x.ContainerSelectionChanged); } - /// - /// Initializes a new instance of the class. - /// - public SelectingItemsControl() - { - } - /// /// Occurs when the control's selection changes. /// @@ -145,7 +140,6 @@ namespace Perspex.Controls.Primitives /// /// Gets or sets the index of the selected item. /// - [DependsOn(nameof(Items))] public int SelectedIndex { get @@ -155,14 +149,22 @@ namespace Perspex.Controls.Primitives set { - var old = SelectedIndex; - var effective = (value >= 0 && value < Items?.Cast().Count()) ? value : -1; + if (_updateCount == 0) + { + var old = SelectedIndex; + var effective = (value >= 0 && value < Items?.Cast().Count()) ? value : -1; - if (old != effective) + if (old != effective) + { + _selectedIndex = effective; + RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue); + SelectedItem = ElementAt(Items, effective); + } + } + else { - _selectedIndex = effective; - RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue); - SelectedItem = ElementAt(Items, effective); + _updateSelectedIndex = value; + _updateSelectedItems = null; } } } @@ -170,7 +172,6 @@ namespace Perspex.Controls.Primitives /// /// Gets or sets the selected item. /// - [DependsOn(nameof(Items))] public object SelectedItem { get @@ -180,45 +181,39 @@ namespace Perspex.Controls.Primitives set { - var old = SelectedItem; - var index = IndexOf(Items, value); - var effective = index != -1 ? value : null; - - if (!object.Equals(effective, old)) + if (_updateCount == 0) { - _selectedItem = effective; - RaisePropertyChanged(SelectedItemProperty, old, effective, BindingPriority.LocalValue); - SelectedIndex = index; + var old = SelectedItem; + var index = IndexOf(Items, value); + var effective = index != -1 ? value : null; - if (effective != null) + if (!object.Equals(effective, old)) { - if (SelectedItems.Count != 1 || SelectedItems[0] != effective) + _selectedItem = effective; + RaisePropertyChanged(SelectedItemProperty, old, effective, BindingPriority.LocalValue); + SelectedIndex = index; + + if (effective != null) { - _syncingSelectedItems = true; - SelectedItems.Clear(); - SelectedItems.Add(effective); - _syncingSelectedItems = false; + if (SelectedItems.Count != 1 || SelectedItems[0] != effective) + { + _syncingSelectedItems = true; + SelectedItems.Clear(); + SelectedItems.Add(effective); + _syncingSelectedItems = false; + } } - } - else if (SelectedItems.Count > 0) - { - if (!IsDataContextChanging) + else if (SelectedItems.Count > 0) { SelectedItems.Clear(); } - else - { - // The DataContext is changing, and it's quite possible that our - // selection is being cleared because both Items and SelectedItems - // are bound to something on the DataContext. However, if we clear - // the collection now, we may be clearing a the SelectedItems from - // the DataContext which is being unbound, so do it after DataContext - // has notified all interested parties, in - // the OnDataContextFinishedChanging method. - _clearSelectedItemsAfterDataContextChanged = SelectedItems; - } } } + else + { + _updateSelectedItems = new[] { value }; + _updateSelectedIndex = int.MinValue; + } } } @@ -260,6 +255,25 @@ namespace Perspex.Controls.Primitives /// protected bool AlwaysSelected => (SelectionMode & SelectionMode.AlwaysSelected) != 0; + /// + public override void BeginInit() + { + base.BeginInit(); + ++_updateCount; + _updateSelectedIndex = int.MinValue; + } + + /// + public override void EndInit() + { + base.EndInit(); + + if (--_updateCount == 0) + { + UpdateFinished(); + } + } + /// /// Tries to get the container that was the source of an event. /// @@ -279,13 +293,16 @@ namespace Perspex.Controls.Primitives { base.ItemsChanged(e); - if (SelectedIndex != -1) - { - SelectedIndex = IndexOf((IEnumerable)e.NewValue, SelectedItem); - } - else if (AlwaysSelected && Items != null && Items.Cast().Any()) + if (_updateCount == 0) { - SelectedIndex = 0; + if (SelectedIndex != -1) + { + SelectedIndex = IndexOf((IEnumerable)e.NewValue, SelectedItem); + } + else if (AlwaysSelected && Items != null && Items.Cast().Any()) + { + SelectedIndex = 0; + } } } @@ -354,15 +371,22 @@ namespace Perspex.Controls.Primitives } } + /// + protected override void OnDataContextChanging() + { + base.OnDataContextChanging(); + ++_updateCount; + } + /// protected override void OnDataContextChanged() { - if (_clearSelectedItemsAfterDataContextChanged == SelectedItems) + base.OnDataContextChanged(); + + if (--_updateCount == 0) { - _clearSelectedItemsAfterDataContextChanged.Clear(); + UpdateFinished(); } - - _clearSelectedItemsAfterDataContextChanged = null; } /// @@ -695,6 +719,7 @@ namespace Perspex.Controls.Primitives if (SelectedItems.Count > 0) { + _selectedItem = null; SelectedItemsAdded(SelectedItems); added = SelectedItems; } @@ -800,5 +825,17 @@ namespace Perspex.Controls.Primitives incc.CollectionChanged -= SelectedItemsCollectionChanged; } } + + private void UpdateFinished() + { + if (_updateSelectedIndex != int.MinValue) + { + SelectedIndex = _updateSelectedIndex; + } + else if (_updateSelectedItems != null) + { + SelectedItems = _updateSelectedItems; + } + } } } diff --git a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 7f8893415a..f472740b5f 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -505,6 +505,21 @@ namespace Perspex.Controls.UnitTests.Primitives Assert.Equal("Bar", target.SelectedItem); } + [Fact] + public void Order_Of_Setting_Items_And_SelectedItem_During_Initialization_Should_Not_Matter() + { + var items = new[] { "Foo", "Bar" }; + var target = new SelectingItemsControl(); + + ((ISupportInitialize)target).BeginInit(); + target.SelectedItem = "Bar"; + target.Items = items; + ((ISupportInitialize)target).EndInit(); + + Assert.Equal(1, target.SelectedIndex); + Assert.Equal("Bar", target.SelectedItem); + } + [Fact] public void Changing_DataContext_Should_Not_Clear_Nested_ViewModel_SelectedItem() { @@ -535,9 +550,9 @@ namespace Perspex.Controls.UnitTests.Primitives items = new[] { - new Item(), - new Item(), - new Item(), + new Item { Value = "Item1" }, + new Item { Value = "Item2" }, + new Item { Value = "Item3" }, }; vm = new MasterViewModel @@ -568,6 +583,7 @@ namespace Perspex.Controls.UnitTests.Primitives private class Item : Control, ISelectable { + public string Value { get; set; } public bool IsSelected { get; set; } } From 30266420c4634e402047387cadb9adb3d06eb49e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 14 Apr 2016 00:09:26 +0200 Subject: [PATCH 04/16] Don't assign an array to SelectedItems. And add a check should the user try to do that. --- .../Primitives/SelectingItemsControl.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index f409d72792..485bcceefe 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -109,7 +109,7 @@ namespace Perspex.Controls.Primitives private bool _syncingSelectedItems; private int _updateCount; private int _updateSelectedIndex; - private object[] _updateSelectedItems; + private IList _updateSelectedItems; /// /// Initializes static members of the class. @@ -211,7 +211,7 @@ namespace Perspex.Controls.Primitives } else { - _updateSelectedItems = new[] { value }; + _updateSelectedItems = new PerspexList(value); _updateSelectedIndex = int.MinValue; } } @@ -235,6 +235,12 @@ namespace Perspex.Controls.Primitives set { + if (value.IsFixedSize || value.IsReadOnly) + { + throw new NotSupportedException( + "Cannot use a fixed size or read-only collection as SelectedItems."); + } + UnsubscribeFromSelectedItems(); _selectedItems = value ?? new PerspexList(); SubscribeToSelectedItems(); From d1fcb2f5a643b34bfad230f16f5b1bcde5e0311b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 14 Apr 2016 00:16:38 +0200 Subject: [PATCH 05/16] Fixed failing tests. --- src/Perspex.Controls/Primitives/SelectingItemsControl.cs | 2 +- .../Primitives/SelectingItemsControlTests_Multiple.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index 485bcceefe..a141444754 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -235,7 +235,7 @@ namespace Perspex.Controls.Primitives set { - if (value.IsFixedSize || value.IsReadOnly) + if (value?.IsFixedSize == true || value?.IsReadOnly == true) { throw new NotSupportedException( "Cannot use a fixed size or read-only collection as SelectedItems."); diff --git a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index 21cce03055..89d1c4fb72 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -57,7 +57,7 @@ namespace Perspex.Controls.UnitTests.Primitives }; target.ApplyTemplate(); - target.SelectedItems = new[] { "bar" }; + target.SelectedItems = new PerspexList("bar"); Assert.Equal(1, target.SelectedIndex); } @@ -536,7 +536,7 @@ namespace Perspex.Controls.UnitTests.Primitives target.ApplyTemplate(); target.Presenter.ApplyTemplate(); - target.SelectedItems = new[] { "foo", "baz" }; + target.SelectedItems = new PerspexList("foo", "baz"); Assert.True(called); } From 06b9d3885821b78f6509ade13f0a806e047f0df6 Mon Sep 17 00:00:00 2001 From: Jason Jarvis Date: Wed, 13 Apr 2016 15:39:08 -0700 Subject: [PATCH 06/16] Subsystems can now be selected via Application extension methods. --- src/Gtk/Perspex.Cairo/CairoPlatform.cs | 12 ++++++++++++ src/Gtk/Perspex.Gtk/GtkPlatform.cs | 12 ++++++++++++ src/Skia/Perspex.Skia/SkiaPlatform.cs | 12 ++++++++++++ src/Windows/Perspex.Direct2D1/Direct2D1Platform.cs | 12 ++++++++++++ src/Windows/Perspex.Win32/Win32Platform.cs | 12 ++++++++++++ 5 files changed, 60 insertions(+) diff --git a/src/Gtk/Perspex.Cairo/CairoPlatform.cs b/src/Gtk/Perspex.Cairo/CairoPlatform.cs index 2c72811bbb..36078a1196 100644 --- a/src/Gtk/Perspex.Cairo/CairoPlatform.cs +++ b/src/Gtk/Perspex.Cairo/CairoPlatform.cs @@ -7,6 +7,18 @@ using Perspex.Cairo.Media.Imaging; using Perspex.Media; using Perspex.Platform; +namespace Perspex +{ + public static class GtkApplicationExtensions + { + public static AppT UseCairo(this AppT app) where AppT : Application + { + Perspex.Cairo.CairoPlatform.Initialize(); + return app; + } + } +} + namespace Perspex.Cairo { using System.IO; diff --git a/src/Gtk/Perspex.Gtk/GtkPlatform.cs b/src/Gtk/Perspex.Gtk/GtkPlatform.cs index b8a6d5c511..b3de47f4eb 100644 --- a/src/Gtk/Perspex.Gtk/GtkPlatform.cs +++ b/src/Gtk/Perspex.Gtk/GtkPlatform.cs @@ -10,6 +10,18 @@ using Perspex.Input; using Perspex.Platform; using Perspex.Shared.PlatformSupport; +namespace Perspex +{ + public static class GtkApplicationExtensions + { + public static AppT UseGtkSubsystem(this AppT app) where AppT : Application + { + Perspex.Gtk.GtkPlatform.Initialize(); + return app; + } + } +} + namespace Perspex.Gtk { using Gtk = global::Gtk; diff --git a/src/Skia/Perspex.Skia/SkiaPlatform.cs b/src/Skia/Perspex.Skia/SkiaPlatform.cs index 00c5494308..2161397dd9 100644 --- a/src/Skia/Perspex.Skia/SkiaPlatform.cs +++ b/src/Skia/Perspex.Skia/SkiaPlatform.cs @@ -3,6 +3,18 @@ using System.Collections.Generic; using System.Text; using Perspex.Platform; +namespace Perspex +{ + public static class SkiaApplicationExtensions + { + public static AppT UseSkia(this AppT app) where AppT : Application + { + Perspex.Skia.SkiaPlatform.Initialize(); + return app; + } + } +} + namespace Perspex.Skia { public static class SkiaPlatform diff --git a/src/Windows/Perspex.Direct2D1/Direct2D1Platform.cs b/src/Windows/Perspex.Direct2D1/Direct2D1Platform.cs index 93c820b26d..fa6be34844 100644 --- a/src/Windows/Perspex.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Perspex.Direct2D1/Direct2D1Platform.cs @@ -7,6 +7,18 @@ using Perspex.Direct2D1.Media; using Perspex.Media; using Perspex.Platform; +namespace Perspex +{ + public static class Direct2DApplicationExtensions + { + public static AppT UseDirect2D(this AppT app) where AppT : Application + { + Perspex.Direct2D1.Direct2D1Platform.Initialize(); + return app; + } + } +} + namespace Perspex.Direct2D1 { public class Direct2D1Platform : IPlatformRenderInterface diff --git a/src/Windows/Perspex.Win32/Win32Platform.cs b/src/Windows/Perspex.Win32/Win32Platform.cs index 5fb0a7c4b7..c2f4943e28 100644 --- a/src/Windows/Perspex.Win32/Win32Platform.cs +++ b/src/Windows/Perspex.Win32/Win32Platform.cs @@ -16,6 +16,18 @@ using Perspex.Shared.PlatformSupport; using Perspex.Win32.Input; using Perspex.Win32.Interop; +namespace Perspex +{ + public static class Win32ApplicationExtensions + { + public static AppT UseWin32Subsystem(this AppT app) where AppT : Application + { + Perspex.Win32.Win32Platform.Initialize(); + return app; + } + } +} + namespace Perspex.Win32 { public class Win32Platform : IPlatformThreadingInterface, IPlatformSettings, IWindowingPlatform From 97388703b3e986baf1d4ce1a7cc80f96cb70a714 Mon Sep 17 00:00:00 2001 From: Jason Jarvis Date: Wed, 13 Apr 2016 21:44:12 -0700 Subject: [PATCH 07/16] Adding various other runtime application configuration extensions to improve boilerplate main startup. Updated most Sample applications to use simpler bootstrapping extensions. --- samples/BindingTest/App.xaml.cs | 24 +- samples/BindingTest/MainWindow.xaml.cs | 5 +- samples/ControlCatalog/App.xaml.cs | 52 +- samples/ControlCatalog/ControlCatalog.csproj | 1 + samples/ControlCatalog/MainWindow.xaml.cs | 7 +- samples/ControlCatalog/Program.cs | 127 ++++ samples/TestApplication/Program.cs | 29 +- samples/TestApplicationShared/App.cs | 46 +- samples/XamlTestApplication/App.cs | 23 +- samples/XamlTestApplication/Program.cs | 44 +- samples/XamlTestApplicationPcl/XamlTestApp.cs | 15 +- .../Perspex.Markup.Xaml/PerspexXamlLoader.cs | 12 + src/Perspex.Controls/Window.cs | 631 +++++++++--------- src/Perspex.Diagnostics/DevTools.xaml.cs | 13 +- 14 files changed, 573 insertions(+), 456 deletions(-) create mode 100644 samples/ControlCatalog/Program.cs diff --git a/samples/BindingTest/App.xaml.cs b/samples/BindingTest/App.xaml.cs index 5ffac249e9..38b9aea430 100644 --- a/samples/BindingTest/App.xaml.cs +++ b/samples/BindingTest/App.xaml.cs @@ -13,30 +13,20 @@ namespace BindingTest public App() { RegisterServices(); - InitializeSubsystems((int)Environment.OSVersion.Platform); - InitializeComponent(); - InitializeLogging(); - } - - public static void AttachDevTools(Window window) - { - DevTools.Attach(window); } private static void Main() { - var app = new App(); - var window = new MainWindow(); - window.Show(); - app.Run(window); - } + InitializeLogging(); - private void InitializeComponent() - { - PerspexXamlLoader.Load(this); + new App() + .UseWin32Subsystem() + .UseDirect2D() + .LoadFromXaml() + .RunWithMainWindow(); } - private void InitializeLogging() + private static void InitializeLogging() { #if DEBUG SerilogLogger.Initialize(new LoggerConfiguration() diff --git a/samples/BindingTest/MainWindow.xaml.cs b/samples/BindingTest/MainWindow.xaml.cs index a8e64485cb..61b1e0ffb8 100644 --- a/samples/BindingTest/MainWindow.xaml.cs +++ b/samples/BindingTest/MainWindow.xaml.cs @@ -1,4 +1,5 @@ using BindingTest.ViewModels; +using Perspex; using Perspex.Controls; using Perspex.Markup.Xaml; @@ -10,12 +11,12 @@ namespace BindingTest { this.InitializeComponent(); this.DataContext = new MainWindowViewModel(); - App.AttachDevTools(this); + this.AttachDevTools(); } private void InitializeComponent() { - PerspexXamlLoader.Load(this); + this.LoadFromXaml(); } } } diff --git a/samples/ControlCatalog/App.xaml.cs b/samples/ControlCatalog/App.xaml.cs index 2930fa746a..f5cc993e2c 100644 --- a/samples/ControlCatalog/App.xaml.cs +++ b/samples/ControlCatalog/App.xaml.cs @@ -9,58 +9,14 @@ using Serilog; namespace ControlCatalog { + // Eventually we should move this into a PCL library so we can access + // from mobile platforms + // class App : Application { public App() { RegisterServices(); - InitializeSubsystems(GetPlatformId()); - InitializeLogging(); - InitializeComponent(); } - - public static void AttachDevTools(Window window) - { -#if DEBUG - DevTools.Attach(window); -#endif - } - - static void Main(string[] args) - { - var app = new App(); - var window = new MainWindow(); - window.Show(); - app.Run(window); - } - - private void InitializeComponent() - { - PerspexXamlLoader.Load(this); - } - - private void InitializeLogging() - { -#if DEBUG - SerilogLogger.Initialize(new LoggerConfiguration() - .MinimumLevel.Warning() - .WriteTo.Trace(outputTemplate: "{Area}: {Message}") - .CreateLogger()); -#endif - } - - private int GetPlatformId() - { - var args = Environment.GetCommandLineArgs(); - - if (args.Contains("--gtk")) - { - return (int)PlatformID.Unix; - } - else - { - return (int)Environment.OSVersion.Platform; - } - } - } + } } diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 087998c805..36238efc7c 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -103,6 +103,7 @@ SliderPage.xaml + diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 5a5b7d4303..67b88dc71f 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -1,4 +1,5 @@ -using Perspex.Controls; +using Perspex; +using Perspex.Controls; using Perspex.Markup.Xaml; namespace ControlCatalog @@ -8,12 +9,12 @@ namespace ControlCatalog public MainWindow() { this.InitializeComponent(); - App.AttachDevTools(this); + this.AttachDevTools(); } private void InitializeComponent() { - PerspexXamlLoader.Load(this); + this.LoadFromXaml(); } } } diff --git a/samples/ControlCatalog/Program.cs b/samples/ControlCatalog/Program.cs new file mode 100644 index 0000000000..0627e77019 --- /dev/null +++ b/samples/ControlCatalog/Program.cs @@ -0,0 +1,127 @@ +using Perspex.Logging.Serilog; +using Serilog; +using System; +using System.Linq; +using Perspex; + +namespace ControlCatalog +{ + internal class Program + { + static void Main(string[] args) + { + InitializeLogging(); + + new App() + .ConfigureRenderSystem(args) + .LoadFromXaml() + .RunWithMainWindow(); + } + + // This will be made into a runtime configuration extension soon! + private static void InitializeLogging() + { +#if DEBUG + SerilogLogger.Initialize(new LoggerConfiguration() + .MinimumLevel.Warning() + .WriteTo.Trace(outputTemplate: "{Area}: {Message}") + .CreateLogger()); +#endif + } + + } + + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + // Experimental: Would like to move this into a shared location once I figure out the best place for it + // considering all common libraries are PCL and do not have access to Environment.OSVersion.Platform + // nor do they have access to the platform specific render/subsystem extensions. + // + // Perhaps via DI we register each system with a priority/rank + // + public static class RenderSystemExtensions + { + [Flags] + enum RenderSystem + { + None = 0, + GTK = 1, + Skia = 2, + Direct2D = 4 + }; + + /// + /// Default (Optimal) render system for a particular platform + /// + /// + private static RenderSystem DefaultRenderSystem() + { + switch (Environment.OSVersion.Platform) + { + case PlatformID.MacOSX: + return RenderSystem.GTK; + + case PlatformID.Unix: + return RenderSystem.GTK; + + case PlatformID.Win32Windows: + return RenderSystem.Direct2D; + } + + return RenderSystem.None; + } + + /// + /// Returns an array of avalidable rendering systems in priority order + /// + /// + private static RenderSystem[] AvailableRenderSystems() + { + switch (Environment.OSVersion.Platform) + { + case PlatformID.MacOSX: + return new RenderSystem[] { RenderSystem.GTK, RenderSystem.Skia }; + + case PlatformID.Unix: + return new RenderSystem[] { RenderSystem.GTK, RenderSystem.Skia }; + + case PlatformID.Win32Windows: + return new RenderSystem[] { RenderSystem.Direct2D, RenderSystem.Skia, RenderSystem.GTK }; + } + + return new RenderSystem[0]; + } + + /// + /// Selects the optimal render system for desktop platforms. Supports cmd line overrides + /// + /// + /// + public static AppT ConfigureRenderSystem(this AppT app, string[] args) where AppT : Application + { + // So this all works great under Windows where it can support + // ALL configurations. But on OSX/Unix we cannot use Direct2D + // + if (args.Contains("--gtk") || DefaultRenderSystem() == RenderSystem.GTK) + { + app.UseGtkSubsystem(); + app.UseCairo(); + } + else + { + app.UseWin32Subsystem(); + + // not available until we do the SkiaSharp merge + //if (args.Contains("--skia") || DefaultRenderSystem() == RenderSystem.Skia) + //{ + // app.UseSkia(); + //} + //else + { + app.UseDirect2D(); + } + } + + return app; + } + } +} diff --git a/samples/TestApplication/Program.cs b/samples/TestApplication/Program.cs index 7aa433bfa8..f7169290a7 100644 --- a/samples/TestApplication/Program.cs +++ b/samples/TestApplication/Program.cs @@ -31,11 +31,30 @@ namespace TestApplication // The version of ReactiveUI currently included is for WPF and so expects a WPF // dispatcher. This makes sure it's initialized. System.Windows.Threading.Dispatcher foo = System.Windows.Threading.Dispatcher.CurrentDispatcher; - new App(); - MainWindow.RootNamespace = "TestApplication"; - var wnd = MainWindow.Create(); - DevTools.Attach(wnd); - Application.Current.Run(wnd); + + var app = new App(); + + if (args.Contains("--gtk")) + { + app.UseGtkSubsystem(); + app.UseCairo(); + } + else + { + app.UseWin32Subsystem(); + + // not available until we do the SkiaSharp merge + //if (args.Contains("--skia")) + //{ + // app.UseSkia(); + //} + //else + { + app.UseDirect2D(); + } + } + + app.Run(); } } } diff --git a/samples/TestApplicationShared/App.cs b/samples/TestApplicationShared/App.cs index e4211ffb82..83987d26df 100644 --- a/samples/TestApplicationShared/App.cs +++ b/samples/TestApplicationShared/App.cs @@ -8,6 +8,9 @@ using Perspex.Controls.Templates; using Perspex.Markup.Xaml; using Perspex.Styling; using Perspex.Themes.Default; +using Perspex.Diagnostics; +using Perspex.Platform; +using Perspex.Shared.PlatformSupport; namespace TestApplication { @@ -15,22 +18,35 @@ namespace TestApplication { public App() { + // TODO: I believe this has to happen before we select sub systems. Can we + // move this safely into Application itself? Is there anything in here + // that is platform specific?? + // RegisterServices(); - InitializeSubsystems((int)Environment.OSVersion.Platform); - Styles.Add(new DefaultTheme()); + } - var loader = new PerspexXamlLoader(); - var baseLight = (IStyle)loader.Load( - new Uri("resm:Perspex.Themes.Default.Accents.BaseLight.xaml?assembly=Perspex.Themes.Default")); - Styles.Add(baseLight); + public void Run() + { + Styles.Add(new DefaultTheme()); - Styles.Add(new SampleTabStyle()); - DataTemplates = new DataTemplates - { - new FuncTreeDataTemplate( - x => new TextBlock {Text = x.Name}, - x => x.Children), - }; - } - } + var loader = new PerspexXamlLoader(); + var baseLight = (IStyle)loader.Load( + new Uri("resm:Perspex.Themes.Default.Accents.BaseLight.xaml?assembly=Perspex.Themes.Default")); + Styles.Add(baseLight); + + Styles.Add(new SampleTabStyle()); + DataTemplates = new DataTemplates + { + new FuncTreeDataTemplate( + x => new TextBlock {Text = x.Name}, + x => x.Children), + }; + + MainWindow.RootNamespace = "TestApplication"; + var wnd = MainWindow.Create(); + DevTools.Attach(wnd); + + Run(wnd); + } + } } diff --git a/samples/XamlTestApplication/App.cs b/samples/XamlTestApplication/App.cs index 01ac91299c..8841a02c46 100644 --- a/samples/XamlTestApplication/App.cs +++ b/samples/XamlTestApplication/App.cs @@ -7,26 +7,5 @@ using Serilog; namespace XamlTestApplication { - public class App : XamlTestApp - { - public App() - { - InitializeLogging(); - } - - protected override void RegisterPlatform() - { - InitializeSubsystems((int)Environment.OSVersion.Platform); - } - - private void InitializeLogging() - { -#if DEBUG - SerilogLogger.Initialize(new LoggerConfiguration() - .MinimumLevel.Warning() - .WriteTo.Trace(outputTemplate: "{Area}: {Message}") - .CreateLogger()); -#endif - } - } + // No longer needed! } diff --git a/samples/XamlTestApplication/Program.cs b/samples/XamlTestApplication/Program.cs index 76d1aa7efb..339cd1ae30 100644 --- a/samples/XamlTestApplication/Program.cs +++ b/samples/XamlTestApplication/Program.cs @@ -1,39 +1,39 @@ // 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 System; +using System.Linq; using System.Diagnostics; using System.Windows.Threading; using Perspex; -using Perspex.Collections; -using Perspex.Controls; -using Perspex.Controls.Templates; -using ReactiveUI; -using XamlTestApplication.Views; +using Serilog; +using Perspex.Logging.Serilog; namespace XamlTestApplication { internal class Program { - private static void Main() + private static void Main(string[] args) { - var sw = new Stopwatch(); - sw.Start(); - + // this sucks. Can we fix this? Do we even need it anymore? var foo = Dispatcher.CurrentDispatcher; - App application = new App - { - - }; - - var window = new MainWindow(); - window.Show(); + InitializeLogging(); - sw.Stop(); - Debug.WriteLine($"Startup: {sw.ElapsedMilliseconds}ms"); - - Application.Current.Run(window); + new XamlTestApp() + .UseWin32Subsystem() + .UseDirect2D() + .LoadFromXaml() + .RunWithMainWindow(); } - } + + private static void InitializeLogging() + { +#if DEBUG + SerilogLogger.Initialize(new LoggerConfiguration() + .MinimumLevel.Warning() + .WriteTo.Trace(outputTemplate: "{Area}: {Message}") + .CreateLogger()); +#endif + } + } } \ No newline at end of file diff --git a/samples/XamlTestApplicationPcl/XamlTestApp.cs b/samples/XamlTestApplicationPcl/XamlTestApp.cs index a4d4ac2e0e..936b35699b 100644 --- a/samples/XamlTestApplicationPcl/XamlTestApp.cs +++ b/samples/XamlTestApplicationPcl/XamlTestApp.cs @@ -1,23 +1,14 @@ using Perspex; using Perspex.Markup.Xaml; +using XamlTestApplication.Views; namespace XamlTestApplication { - public abstract class XamlTestApp : Application + public class XamlTestApp : Application { - protected abstract void RegisterPlatform(); - public XamlTestApp() { RegisterServices(); - RegisterPlatform(); - InitializeComponent(); - } - - private void InitializeComponent() - { - var loader = new PerspexXamlLoader(); - loader.Load(typeof(XamlTestApp), this); } - } + } } diff --git a/src/Markup/Perspex.Markup.Xaml/PerspexXamlLoader.cs b/src/Markup/Perspex.Markup.Xaml/PerspexXamlLoader.cs index 389e646215..1bbff1c886 100644 --- a/src/Markup/Perspex.Markup.Xaml/PerspexXamlLoader.cs +++ b/src/Markup/Perspex.Markup.Xaml/PerspexXamlLoader.cs @@ -187,3 +187,15 @@ namespace Perspex.Markup.Xaml } } } + +namespace Perspex +{ + public static class XamlObjectExtensions + { + public static ObjectT LoadFromXaml(this ObjectT obj) + { + Markup.Xaml.PerspexXamlLoader.Load(obj); + return obj; + } + } +} diff --git a/src/Perspex.Controls/Window.cs b/src/Perspex.Controls/Window.cs index 7692f22e01..cf6a3c7190 100644 --- a/src/Perspex.Controls/Window.cs +++ b/src/Perspex.Controls/Window.cs @@ -14,314 +14,327 @@ using System.Collections.Generic; namespace Perspex.Controls { - /// - /// Determines how a will size itself to fit its content. - /// - public enum SizeToContent - { - /// - /// The window will not automatically size itself to fit its content. - /// - Manual, - - /// - /// The window will size itself horizontally to fit its content. - /// - Width, - - /// - /// The window will size itself vertically to fit its content. - /// - Height, - - /// - /// The window will size itself horizontally and vertically to fit its content. - /// - WidthAndHeight, - } - - /// - /// A top-level window. - /// - public class Window : TopLevel, IStyleable, IFocusScope, ILayoutRoot, INameScope - { - private static IList s_windows = new List(); - - /// - /// Retrieves an enumeration of all Windows in the currently running application. - /// - public static IList OpenWindows => s_windows; - - /// - /// Defines the property. - /// - public static readonly StyledProperty SizeToContentProperty = - PerspexProperty.Register(nameof(SizeToContent)); - - /// - /// Enables of disables system window decorations (title bar, buttons, etc) - /// - public static readonly StyledProperty HasSystemDecorationsProperty = - PerspexProperty.Register(nameof(HasSystemDecorations), true); - - /// - /// Defines the property. - /// - public static readonly StyledProperty TitleProperty = - PerspexProperty.Register(nameof(Title), "Window"); - - private readonly NameScope _nameScope = new NameScope(); - private object _dialogResult; - private readonly Size _maxPlatformClientSize; - - /// - /// Initializes static members of the class. - /// - static Window() - { - BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); - TitleProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl.SetTitle((string)e.NewValue)); - HasSystemDecorationsProperty.Changed.AddClassHandler( + /// + /// Determines how a will size itself to fit its content. + /// + public enum SizeToContent + { + /// + /// The window will not automatically size itself to fit its content. + /// + Manual, + + /// + /// The window will size itself horizontally to fit its content. + /// + Width, + + /// + /// The window will size itself vertically to fit its content. + /// + Height, + + /// + /// The window will size itself horizontally and vertically to fit its content. + /// + WidthAndHeight, + } + + /// + /// A top-level window. + /// + public class Window : TopLevel, IStyleable, IFocusScope, ILayoutRoot, INameScope + { + private static IList s_windows = new List(); + + /// + /// Retrieves an enumeration of all Windows in the currently running application. + /// + public static IList OpenWindows => s_windows; + + /// + /// Defines the property. + /// + public static readonly StyledProperty SizeToContentProperty = + PerspexProperty.Register(nameof(SizeToContent)); + + /// + /// Enables of disables system window decorations (title bar, buttons, etc) + /// + public static readonly StyledProperty HasSystemDecorationsProperty = + PerspexProperty.Register(nameof(HasSystemDecorations), true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty TitleProperty = + PerspexProperty.Register(nameof(Title), "Window"); + + private readonly NameScope _nameScope = new NameScope(); + private object _dialogResult; + private readonly Size _maxPlatformClientSize; + + /// + /// Initializes static members of the class. + /// + static Window() + { + BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); + TitleProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl.SetTitle((string)e.NewValue)); + HasSystemDecorationsProperty.Changed.AddClassHandler( (s, e) => s.PlatformImpl.SetSystemDecorations((bool) e.NewValue)); - } - - /// - /// Initializes a new instance of the class. - /// - public Window() - : base(PlatformManager.CreateWindow()) - { - _maxPlatformClientSize = this.PlatformImpl.MaxClientSize; - } - - /// - event EventHandler INameScope.Registered - { - add { _nameScope.Registered += value; } - remove { _nameScope.Registered -= value; } - } - - /// - event EventHandler INameScope.Unregistered - { - add { _nameScope.Unregistered += value; } - remove { _nameScope.Unregistered -= value; } - } - - /// - /// Gets the platform-specific window implementation. - /// - public new IWindowImpl PlatformImpl => (IWindowImpl)base.PlatformImpl; - - /// - /// Gets or sets a value indicating how the window will size itself to fit its content. - /// - public SizeToContent SizeToContent - { - get { return GetValue(SizeToContentProperty); } - set { SetValue(SizeToContentProperty, value); } - } - - /// - /// Gets or sets the title of the window. - /// - public string Title - { - get { return GetValue(TitleProperty); } - set { SetValue(TitleProperty, value); } - } - - /// - /// Enables of disables system window decorations (title bar, buttons, etc) - /// - /// - public bool HasSystemDecorations - { - get { return GetValue(HasSystemDecorationsProperty); } - set { SetValue(HasSystemDecorationsProperty, value); } - } - - /// - /// Gets or sets the minimized/maximized state of the window. - /// - public WindowState WindowState - { - get { return this.PlatformImpl.WindowState; } - set { this.PlatformImpl.WindowState = value; } - } - - /// - Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize; - - /// - Type IStyleable.StyleKey => typeof(Window); - - /// - /// Closes the window. - /// - public void Close() - { - s_windows.Remove(this); - PlatformImpl.Dispose(); - } - - protected override void HandleApplicationExiting() - { - base.HandleApplicationExiting(); - Close(); - } - - /// - /// Closes a dialog window with the specified result. - /// - /// The dialog result. - /// - /// When the window is shown with the method, the - /// resulting task will produce the value when the window - /// is closed. - /// - public void Close(object dialogResult) - { - _dialogResult = dialogResult; - Close(); - } - - /// - /// Hides the window but does not close it. - /// - public void Hide() - { - using (BeginAutoSizing()) - { - PlatformImpl.Hide(); - } - } - - /// - /// Shows the window. - /// - public void Show() - { - s_windows.Add(this); - - EnsureInitialized(); - LayoutManager.Instance.ExecuteInitialLayoutPass(this); - - using (BeginAutoSizing()) - { - PlatformImpl.Show(); - } - } - - /// - /// Shows the window as a dialog. - /// - /// - /// A task that can be used to track the lifetime of the dialog. - /// - public Task ShowDialog() - { - return ShowDialog(); - } - - /// - /// Shows the window as a dialog. - /// - /// - /// The type of the result produced by the dialog. - /// - /// . - /// A task that can be used to retrive the result of the dialog when it closes. - /// - public Task ShowDialog() - { - s_windows.Add(this); - - EnsureInitialized(); - LayoutManager.Instance.ExecuteInitialLayoutPass(this); - - using (BeginAutoSizing()) - { - var modal = PlatformImpl.ShowDialog(); - var result = new TaskCompletionSource(); - - Observable.FromEventPattern(this, nameof(Closed)) - .Take(1) - .Subscribe(_ => - { - modal.Dispose(); - result.SetResult((TResult)_dialogResult); - }); - - return result.Task; - } - } - - /// - void INameScope.Register(string name, object element) - { - _nameScope.Register(name, element); - } - - /// - object INameScope.Find(string name) - { - return _nameScope.Find(name); - } - - /// - void INameScope.Unregister(string name) - { - _nameScope.Unregister(name); - } - - /// - protected override Size MeasureOverride(Size availableSize) - { - var sizeToContent = SizeToContent; - var size = ClientSize; - var desired = base.MeasureOverride(availableSize.Constrain(_maxPlatformClientSize)); - - switch (sizeToContent) - { - case SizeToContent.Width: - size = new Size(desired.Width, ClientSize.Height); - break; - case SizeToContent.Height: - size = new Size(ClientSize.Width, desired.Height); - break; - case SizeToContent.WidthAndHeight: - size = new Size(desired.Width, desired.Height); - break; - case SizeToContent.Manual: - size = ClientSize; - break; - default: - throw new InvalidOperationException("Invalid value for SizeToContent."); - } - - return size; - } - - /// - protected override void HandleResized(Size clientSize) - { - if (!AutoSizing) - { - SizeToContent = SizeToContent.Manual; - } - - base.HandleResized(clientSize); - } - - private void EnsureInitialized() - { - if (!this.IsInitialized) - { - var init = (ISupportInitialize)this; - init.BeginInit(); - init.EndInit(); - } - } - } + } + + /// + /// Initializes a new instance of the class. + /// + public Window() + : base(PlatformManager.CreateWindow()) + { + _maxPlatformClientSize = this.PlatformImpl.MaxClientSize; + } + + /// + event EventHandler INameScope.Registered + { + add { _nameScope.Registered += value; } + remove { _nameScope.Registered -= value; } + } + + /// + event EventHandler INameScope.Unregistered + { + add { _nameScope.Unregistered += value; } + remove { _nameScope.Unregistered -= value; } + } + + /// + /// Gets the platform-specific window implementation. + /// + public new IWindowImpl PlatformImpl => (IWindowImpl)base.PlatformImpl; + + /// + /// Gets or sets a value indicating how the window will size itself to fit its content. + /// + public SizeToContent SizeToContent + { + get { return GetValue(SizeToContentProperty); } + set { SetValue(SizeToContentProperty, value); } + } + + /// + /// Gets or sets the title of the window. + /// + public string Title + { + get { return GetValue(TitleProperty); } + set { SetValue(TitleProperty, value); } + } + + /// + /// Enables of disables system window decorations (title bar, buttons, etc) + /// + /// + public bool HasSystemDecorations + { + get { return GetValue(HasSystemDecorationsProperty); } + set { SetValue(HasSystemDecorationsProperty, value); } + } + + /// + /// Gets or sets the minimized/maximized state of the window. + /// + public WindowState WindowState + { + get { return this.PlatformImpl.WindowState; } + set { this.PlatformImpl.WindowState = value; } + } + + /// + Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize; + + /// + Type IStyleable.StyleKey => typeof(Window); + + /// + /// Closes the window. + /// + public void Close() + { + s_windows.Remove(this); + PlatformImpl.Dispose(); + } + + protected override void HandleApplicationExiting() + { + base.HandleApplicationExiting(); + Close(); + } + + /// + /// Closes a dialog window with the specified result. + /// + /// The dialog result. + /// + /// When the window is shown with the method, the + /// resulting task will produce the value when the window + /// is closed. + /// + public void Close(object dialogResult) + { + _dialogResult = dialogResult; + Close(); + } + + /// + /// Hides the window but does not close it. + /// + public void Hide() + { + using (BeginAutoSizing()) + { + PlatformImpl.Hide(); + } + } + + /// + /// Shows the window. + /// + public void Show() + { + s_windows.Add(this); + + EnsureInitialized(); + LayoutManager.Instance.ExecuteInitialLayoutPass(this); + + using (BeginAutoSizing()) + { + PlatformImpl.Show(); + } + } + + /// + /// Shows the window as a dialog. + /// + /// + /// A task that can be used to track the lifetime of the dialog. + /// + public Task ShowDialog() + { + return ShowDialog(); + } + + /// + /// Shows the window as a dialog. + /// + /// + /// The type of the result produced by the dialog. + /// + /// . + /// A task that can be used to retrive the result of the dialog when it closes. + /// + public Task ShowDialog() + { + s_windows.Add(this); + + EnsureInitialized(); + LayoutManager.Instance.ExecuteInitialLayoutPass(this); + + using (BeginAutoSizing()) + { + var modal = PlatformImpl.ShowDialog(); + var result = new TaskCompletionSource(); + + Observable.FromEventPattern(this, nameof(Closed)) + .Take(1) + .Subscribe(_ => + { + modal.Dispose(); + result.SetResult((TResult)_dialogResult); + }); + + return result.Task; + } + } + + /// + void INameScope.Register(string name, object element) + { + _nameScope.Register(name, element); + } + + /// + object INameScope.Find(string name) + { + return _nameScope.Find(name); + } + + /// + void INameScope.Unregister(string name) + { + _nameScope.Unregister(name); + } + + /// + protected override Size MeasureOverride(Size availableSize) + { + var sizeToContent = SizeToContent; + var size = ClientSize; + var desired = base.MeasureOverride(availableSize.Constrain(_maxPlatformClientSize)); + + switch (sizeToContent) + { + case SizeToContent.Width: + size = new Size(desired.Width, ClientSize.Height); + break; + case SizeToContent.Height: + size = new Size(ClientSize.Width, desired.Height); + break; + case SizeToContent.WidthAndHeight: + size = new Size(desired.Width, desired.Height); + break; + case SizeToContent.Manual: + size = ClientSize; + break; + default: + throw new InvalidOperationException("Invalid value for SizeToContent."); + } + + return size; + } + + /// + protected override void HandleResized(Size clientSize) + { + if (!AutoSizing) + { + SizeToContent = SizeToContent.Manual; + } + + base.HandleResized(clientSize); + } + + private void EnsureInitialized() + { + if (!this.IsInitialized) + { + var init = (ISupportInitialize)this; + init.BeginInit(); + init.EndInit(); + } + } + } +} + +namespace Perspex +{ + public static class WindowApplicationExtensions + { + public static void RunWithMainWindow(this Application app) where WindowT : Perspex.Controls.Window, new() + { + var window = new WindowT(); + window.Show(); + app.Run(window); + } + } } diff --git a/src/Perspex.Diagnostics/DevTools.xaml.cs b/src/Perspex.Diagnostics/DevTools.xaml.cs index a6e4e83533..5d4d962f15 100644 --- a/src/Perspex.Diagnostics/DevTools.xaml.cs +++ b/src/Perspex.Diagnostics/DevTools.xaml.cs @@ -13,9 +13,20 @@ using Perspex.Markup.Xaml; using Perspex.VisualTree; using ReactiveUI; +namespace Perspex +{ + public static class WindowExtensions + { + public static void AttachDevTools(this Window window) + { + Perspex.Diagnostics.DevTools.Attach(window); + } + } +} + namespace Perspex.Diagnostics { - public class DevTools : UserControl + public class DevTools : UserControl { private static Dictionary s_open = new Dictionary(); private IDisposable _keySubscription; From 2e9b80d3ff357a5d27c985967da628303c49c067 Mon Sep 17 00:00:00 2001 From: Jason Jarvis Date: Wed, 13 Apr 2016 22:02:29 -0700 Subject: [PATCH 08/16] Fixed tabs in Window.cs --- src/Perspex.Controls/Window.cs | 637 ++++++++++++++++----------------- 1 file changed, 318 insertions(+), 319 deletions(-) diff --git a/src/Perspex.Controls/Window.cs b/src/Perspex.Controls/Window.cs index cf6a3c7190..84478f7bb7 100644 --- a/src/Perspex.Controls/Window.cs +++ b/src/Perspex.Controls/Window.cs @@ -14,327 +14,326 @@ using System.Collections.Generic; namespace Perspex.Controls { - /// - /// Determines how a will size itself to fit its content. - /// - public enum SizeToContent - { - /// - /// The window will not automatically size itself to fit its content. - /// - Manual, - - /// - /// The window will size itself horizontally to fit its content. - /// - Width, - - /// - /// The window will size itself vertically to fit its content. - /// - Height, - - /// - /// The window will size itself horizontally and vertically to fit its content. - /// - WidthAndHeight, - } - - /// - /// A top-level window. - /// - public class Window : TopLevel, IStyleable, IFocusScope, ILayoutRoot, INameScope - { - private static IList s_windows = new List(); - - /// - /// Retrieves an enumeration of all Windows in the currently running application. - /// - public static IList OpenWindows => s_windows; - - /// - /// Defines the property. - /// - public static readonly StyledProperty SizeToContentProperty = - PerspexProperty.Register(nameof(SizeToContent)); - - /// - /// Enables of disables system window decorations (title bar, buttons, etc) - /// - public static readonly StyledProperty HasSystemDecorationsProperty = - PerspexProperty.Register(nameof(HasSystemDecorations), true); - - /// - /// Defines the property. - /// - public static readonly StyledProperty TitleProperty = - PerspexProperty.Register(nameof(Title), "Window"); - - private readonly NameScope _nameScope = new NameScope(); - private object _dialogResult; - private readonly Size _maxPlatformClientSize; - - /// - /// Initializes static members of the class. - /// - static Window() - { - BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); - TitleProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl.SetTitle((string)e.NewValue)); - HasSystemDecorationsProperty.Changed.AddClassHandler( + /// + /// Determines how a will size itself to fit its content. + /// + public enum SizeToContent + { + /// + /// The window will not automatically size itself to fit its content. + /// + Manual, + + /// + /// The window will size itself horizontally to fit its content. + /// + Width, + + /// + /// The window will size itself vertically to fit its content. + /// + Height, + + /// + /// The window will size itself horizontally and vertically to fit its content. + /// + WidthAndHeight, + } + + /// + /// A top-level window. + /// + public class Window : TopLevel, IStyleable, IFocusScope, ILayoutRoot, INameScope + { + private static IList s_windows = new List(); + + /// + /// Retrieves an enumeration of all Windows in the currently running application. + /// + public static IList OpenWindows => s_windows; + + /// + /// Defines the property. + /// + public static readonly StyledProperty SizeToContentProperty = + PerspexProperty.Register(nameof(SizeToContent)); + + /// + /// Enables of disables system window decorations (title bar, buttons, etc) + /// + public static readonly StyledProperty HasSystemDecorationsProperty = + PerspexProperty.Register(nameof(HasSystemDecorations), true); + + /// + /// Defines the property. + /// + public static readonly StyledProperty TitleProperty = + PerspexProperty.Register(nameof(Title), "Window"); + + private readonly NameScope _nameScope = new NameScope(); + private object _dialogResult; + private readonly Size _maxPlatformClientSize; + + /// + /// Initializes static members of the class. + /// + static Window() + { + BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White); + TitleProperty.Changed.AddClassHandler((s, e) => s.PlatformImpl.SetTitle((string)e.NewValue)); + HasSystemDecorationsProperty.Changed.AddClassHandler( (s, e) => s.PlatformImpl.SetSystemDecorations((bool) e.NewValue)); - } - - /// - /// Initializes a new instance of the class. - /// - public Window() - : base(PlatformManager.CreateWindow()) - { - _maxPlatformClientSize = this.PlatformImpl.MaxClientSize; - } - - /// - event EventHandler INameScope.Registered - { - add { _nameScope.Registered += value; } - remove { _nameScope.Registered -= value; } - } - - /// - event EventHandler INameScope.Unregistered - { - add { _nameScope.Unregistered += value; } - remove { _nameScope.Unregistered -= value; } - } - - /// - /// Gets the platform-specific window implementation. - /// - public new IWindowImpl PlatformImpl => (IWindowImpl)base.PlatformImpl; - - /// - /// Gets or sets a value indicating how the window will size itself to fit its content. - /// - public SizeToContent SizeToContent - { - get { return GetValue(SizeToContentProperty); } - set { SetValue(SizeToContentProperty, value); } - } - - /// - /// Gets or sets the title of the window. - /// - public string Title - { - get { return GetValue(TitleProperty); } - set { SetValue(TitleProperty, value); } - } - - /// - /// Enables of disables system window decorations (title bar, buttons, etc) - /// - /// - public bool HasSystemDecorations - { - get { return GetValue(HasSystemDecorationsProperty); } - set { SetValue(HasSystemDecorationsProperty, value); } - } - - /// - /// Gets or sets the minimized/maximized state of the window. - /// - public WindowState WindowState - { - get { return this.PlatformImpl.WindowState; } - set { this.PlatformImpl.WindowState = value; } - } - - /// - Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize; - - /// - Type IStyleable.StyleKey => typeof(Window); - - /// - /// Closes the window. - /// - public void Close() - { - s_windows.Remove(this); - PlatformImpl.Dispose(); - } - - protected override void HandleApplicationExiting() - { - base.HandleApplicationExiting(); - Close(); - } - - /// - /// Closes a dialog window with the specified result. - /// - /// The dialog result. - /// - /// When the window is shown with the method, the - /// resulting task will produce the value when the window - /// is closed. - /// - public void Close(object dialogResult) - { - _dialogResult = dialogResult; - Close(); - } - - /// - /// Hides the window but does not close it. - /// - public void Hide() - { - using (BeginAutoSizing()) - { - PlatformImpl.Hide(); - } - } - - /// - /// Shows the window. - /// - public void Show() - { - s_windows.Add(this); - - EnsureInitialized(); - LayoutManager.Instance.ExecuteInitialLayoutPass(this); - - using (BeginAutoSizing()) - { - PlatformImpl.Show(); - } - } - - /// - /// Shows the window as a dialog. - /// - /// - /// A task that can be used to track the lifetime of the dialog. - /// - public Task ShowDialog() - { - return ShowDialog(); - } - - /// - /// Shows the window as a dialog. - /// - /// - /// The type of the result produced by the dialog. - /// - /// . - /// A task that can be used to retrive the result of the dialog when it closes. - /// - public Task ShowDialog() - { - s_windows.Add(this); - - EnsureInitialized(); - LayoutManager.Instance.ExecuteInitialLayoutPass(this); - - using (BeginAutoSizing()) - { - var modal = PlatformImpl.ShowDialog(); - var result = new TaskCompletionSource(); - - Observable.FromEventPattern(this, nameof(Closed)) - .Take(1) - .Subscribe(_ => - { - modal.Dispose(); - result.SetResult((TResult)_dialogResult); - }); - - return result.Task; - } - } - - /// - void INameScope.Register(string name, object element) - { - _nameScope.Register(name, element); - } - - /// - object INameScope.Find(string name) - { - return _nameScope.Find(name); - } - - /// - void INameScope.Unregister(string name) - { - _nameScope.Unregister(name); - } - - /// - protected override Size MeasureOverride(Size availableSize) - { - var sizeToContent = SizeToContent; - var size = ClientSize; - var desired = base.MeasureOverride(availableSize.Constrain(_maxPlatformClientSize)); - - switch (sizeToContent) - { - case SizeToContent.Width: - size = new Size(desired.Width, ClientSize.Height); - break; - case SizeToContent.Height: - size = new Size(ClientSize.Width, desired.Height); - break; - case SizeToContent.WidthAndHeight: - size = new Size(desired.Width, desired.Height); - break; - case SizeToContent.Manual: - size = ClientSize; - break; - default: - throw new InvalidOperationException("Invalid value for SizeToContent."); - } - - return size; - } - - /// - protected override void HandleResized(Size clientSize) - { - if (!AutoSizing) - { - SizeToContent = SizeToContent.Manual; - } - - base.HandleResized(clientSize); - } - - private void EnsureInitialized() - { - if (!this.IsInitialized) - { - var init = (ISupportInitialize)this; - init.BeginInit(); - init.EndInit(); - } - } - } + } + + /// + /// Initializes a new instance of the class. + /// + public Window() + : base(PlatformManager.CreateWindow()) + { + _maxPlatformClientSize = this.PlatformImpl.MaxClientSize; + } + + /// + event EventHandler INameScope.Registered + { + add { _nameScope.Registered += value; } + remove { _nameScope.Registered -= value; } + } + + /// + event EventHandler INameScope.Unregistered + { + add { _nameScope.Unregistered += value; } + remove { _nameScope.Unregistered -= value; } + } + + /// + /// Gets the platform-specific window implementation. + /// + public new IWindowImpl PlatformImpl => (IWindowImpl)base.PlatformImpl; + + /// + /// Gets or sets a value indicating how the window will size itself to fit its content. + /// + public SizeToContent SizeToContent + { + get { return GetValue(SizeToContentProperty); } + set { SetValue(SizeToContentProperty, value); } + } + + /// + /// Gets or sets the title of the window. + /// + public string Title + { + get { return GetValue(TitleProperty); } + set { SetValue(TitleProperty, value); } + } + + /// + /// Enables of disables system window decorations (title bar, buttons, etc) + /// + /// + public bool HasSystemDecorations + { + get { return GetValue(HasSystemDecorationsProperty); } + set { SetValue(HasSystemDecorationsProperty, value); } + } + + /// + /// Gets or sets the minimized/maximized state of the window. + /// + public WindowState WindowState + { + get { return this.PlatformImpl.WindowState; } + set { this.PlatformImpl.WindowState = value; } + } + + /// + Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize; + + /// + Type IStyleable.StyleKey => typeof(Window); + + /// + /// Closes the window. + /// + public void Close() + { + s_windows.Remove(this); + PlatformImpl.Dispose(); + } + + protected override void HandleApplicationExiting() + { + base.HandleApplicationExiting(); + Close(); + } + + /// + /// Closes a dialog window with the specified result. + /// + /// The dialog result. + /// + /// When the window is shown with the method, the + /// resulting task will produce the value when the window + /// is closed. + /// + public void Close(object dialogResult) + { + _dialogResult = dialogResult; + Close(); + } + + /// + /// Hides the window but does not close it. + /// + public void Hide() + { + using (BeginAutoSizing()) + { + PlatformImpl.Hide(); + } + } + + /// + /// Shows the window. + /// + public void Show() + { + s_windows.Add(this); + + EnsureInitialized(); + LayoutManager.Instance.ExecuteInitialLayoutPass(this); + + using (BeginAutoSizing()) + { + PlatformImpl.Show(); + } + } + + /// + /// Shows the window as a dialog. + /// + /// + /// A task that can be used to track the lifetime of the dialog. + /// + public Task ShowDialog() + { + return ShowDialog(); + } + + /// + /// Shows the window as a dialog. + /// + /// + /// The type of the result produced by the dialog. + /// + /// . + /// A task that can be used to retrive the result of the dialog when it closes. + /// + public Task ShowDialog() + { + s_windows.Add(this); + + EnsureInitialized(); + LayoutManager.Instance.ExecuteInitialLayoutPass(this); + + using (BeginAutoSizing()) + { + var modal = PlatformImpl.ShowDialog(); + var result = new TaskCompletionSource(); + + Observable.FromEventPattern(this, nameof(Closed)) + .Take(1) + .Subscribe(_ => + { + modal.Dispose(); + result.SetResult((TResult)_dialogResult); + }); + + return result.Task; + } + } + + /// + void INameScope.Register(string name, object element) + { + _nameScope.Register(name, element); + } + + /// + object INameScope.Find(string name) + { + return _nameScope.Find(name); + } + + /// + void INameScope.Unregister(string name) + { + _nameScope.Unregister(name); + } + + /// + protected override Size MeasureOverride(Size availableSize) + { + var sizeToContent = SizeToContent; + var size = ClientSize; + var desired = base.MeasureOverride(availableSize.Constrain(_maxPlatformClientSize)); + + switch (sizeToContent) + { + case SizeToContent.Width: + size = new Size(desired.Width, ClientSize.Height); + break; + case SizeToContent.Height: + size = new Size(ClientSize.Width, desired.Height); + break; + case SizeToContent.WidthAndHeight: + size = new Size(desired.Width, desired.Height); + break; + case SizeToContent.Manual: + size = ClientSize; + break; + default: + throw new InvalidOperationException("Invalid value for SizeToContent."); + } + + return size; + } + + /// + protected override void HandleResized(Size clientSize) + { + if (!AutoSizing) + { + SizeToContent = SizeToContent.Manual; + } + + base.HandleResized(clientSize); + } + + private void EnsureInitialized() + { + if (!this.IsInitialized) + { + var init = (ISupportInitialize)this; + init.BeginInit(); + init.EndInit(); + } + } + } } - namespace Perspex { - public static class WindowApplicationExtensions - { - public static void RunWithMainWindow(this Application app) where WindowT : Perspex.Controls.Window, new() - { - var window = new WindowT(); - window.Show(); - app.Run(window); - } - } + public static class WindowApplicationExtensions + { + public static void RunWithMainWindow(this Application app) where WindowT : Perspex.Controls.Window, new() + { + var window = new WindowT(); + window.Show(); + app.Run(window); + } + } } From 79f30b5a8c647832fb44ab4ba8691472002c2e57 Mon Sep 17 00:00:00 2001 From: Jason Jarvis Date: Wed, 13 Apr 2016 22:03:17 -0700 Subject: [PATCH 09/16] Adding LF --- src/Perspex.Controls/Window.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Perspex.Controls/Window.cs b/src/Perspex.Controls/Window.cs index 84478f7bb7..3d59ce24a3 100644 --- a/src/Perspex.Controls/Window.cs +++ b/src/Perspex.Controls/Window.cs @@ -325,6 +325,7 @@ namespace Perspex.Controls } } } + namespace Perspex { public static class WindowApplicationExtensions From 6716cc90ef89c4a85c2c6ca250eafc4083e348a2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 14 Apr 2016 18:34:44 +0200 Subject: [PATCH 10/16] Added failing test for #503 --- .../Xaml/BindingTests.cs | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Xaml/BindingTests.cs b/tests/Perspex.Markup.Xaml.UnitTests/Xaml/BindingTests.cs index 0224584cdb..7f01d3251f 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Xaml/BindingTests.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/Xaml/BindingTests.cs @@ -16,8 +16,8 @@ namespace Perspex.Markup.Xaml.UnitTests.Xaml { var xaml = @" + xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' + xmlns:local='clr-namespace:Perspex.Markup.Xaml.UnitTests.Xaml;assembly=Perspex.Markup.Xaml.UnitTests'>